Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,7 @@ This makes it easy to connect Alice to MCP-capable assistants and development en
See:

- [docs/integrations/mcp.md](docs/integrations/mcp.md)
- [docs/integrations/hermes.md](docs/integrations/hermes.md)

### OpenClaw

Expand Down
81 changes: 61 additions & 20 deletions apps/api/src/alicebot_api/mcp_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import json
import os
import sys
from typing import Any, BinaryIO
from typing import Any, BinaryIO, Literal
from uuid import UUID

from alicebot_api import __version__
Expand All @@ -22,6 +22,9 @@
_MCP_PROTOCOL_VERSION = "2024-11-05"
_MCP_SERVER_NAME = "alice-core-mcp"
_DEFAULT_MCP_USER_ID = "00000000-0000-0000-0000-000000000001"
_TRANSPORT_CONTENT_LENGTH = "content-length"
_TRANSPORT_JSON_LINE = "json-line"
_TransportMode = Literal["content-length", "json-line"]


def _parse_uuid(value: str) -> UUID:
Expand All @@ -46,14 +49,27 @@ def _build_runtime_context(args: argparse.Namespace) -> MCPRuntimeContext:
return MCPRuntimeContext(database_url=database_url, user_id=user_id)


def _read_message(stream: BinaryIO) -> dict[str, Any] | None:
headers: dict[str, str] = {}
def _parse_json_rpc_payload(raw_payload: str) -> dict[str, Any]:
payload = json.loads(raw_payload)
if not isinstance(payload, dict):
raise ValueError("JSON-RPC payload must be an object")
return payload

while True:
line = stream.readline()
if line == b"":
return None

def _read_message(stream: BinaryIO) -> tuple[dict[str, Any], _TransportMode] | None:
first_line = stream.readline()
if first_line == b"":
return None

# MCP SDK >=1.0 stdio transport sends one JSON-RPC message per line.
stripped_first_line = first_line.strip()
if stripped_first_line.startswith(b"{"):
payload = _parse_json_rpc_payload(stripped_first_line.decode("utf-8"))
return payload, _TRANSPORT_JSON_LINE

headers: dict[str, str] = {}
line = first_line
while True:
if line in {b"\r\n", b"\n"}:
break

Expand All @@ -63,6 +79,10 @@ def _read_message(stream: BinaryIO) -> dict[str, Any] | None:
key, value = decoded.split(":", 1)
headers[key.strip().lower()] = value.strip()

line = stream.readline()
if line == b"":
return None

content_length_raw = headers.get("content-length")
if content_length_raw is None:
raise ValueError("missing Content-Length header")
Expand All @@ -76,17 +96,23 @@ def _read_message(stream: BinaryIO) -> dict[str, Any] | None:
body = stream.read(content_length)
if len(body) != content_length:
return None
payload = json.loads(body.decode("utf-8"))
if not isinstance(payload, dict):
raise ValueError("JSON-RPC payload must be an object")
return payload
payload = _parse_json_rpc_payload(body.decode("utf-8"))
return payload, _TRANSPORT_CONTENT_LENGTH


def _write_message(stream: BinaryIO, message: dict[str, Any]) -> None:
def _write_message(
stream: BinaryIO,
message: dict[str, Any],
*,
transport_mode: _TransportMode,
) -> None:
encoded = json.dumps(message, separators=(",", ":"), sort_keys=True).encode("utf-8")
header = f"Content-Length: {len(encoded)}\r\n\r\n".encode("ascii")
stream.write(header)
stream.write(encoded)
if transport_mode == _TRANSPORT_JSON_LINE:
stream.write(encoded + b"\n")
else:
header = f"Content-Length: {len(encoded)}\r\n\r\n".encode("ascii")
stream.write(header)
stream.write(encoded)
stream.flush()


Expand Down Expand Up @@ -114,6 +140,7 @@ def __init__(self, *, context: MCPRuntimeContext, input_stream: BinaryIO, output
self._context = context
self._input_stream = input_stream
self._output_stream = output_stream
self._transport_mode: _TransportMode = _TRANSPORT_CONTENT_LENGTH

def _handle_request(self, request: dict[str, Any]) -> dict[str, Any] | None:
if request.get("jsonrpc") != _JSONRPC_VERSION:
Expand Down Expand Up @@ -202,22 +229,36 @@ def _handle_request(self, request: dict[str, Any]) -> dict[str, Any] | None:
def run(self) -> int:
while True:
try:
request = _read_message(self._input_stream)
framed_request = _read_message(self._input_stream)
except json.JSONDecodeError as exc:
response = _response_error(None, code=-32700, message=f"parse error: {exc.msg}")
_write_message(self._output_stream, response)
_write_message(
self._output_stream,
response,
transport_mode=self._transport_mode,
)
continue
except ValueError as exc:
response = _response_error(None, code=-32600, message=str(exc))
_write_message(self._output_stream, response)
_write_message(
self._output_stream,
response,
transport_mode=self._transport_mode,
)
continue

if request is None:
if framed_request is None:
return 0

request, transport_mode = framed_request
self._transport_mode = transport_mode
response = self._handle_request(request)
if response is not None:
_write_message(self._output_stream, response)
_write_message(
self._output_stream,
response,
transport_mode=self._transport_mode,
)


def build_parser() -> argparse.ArgumentParser:
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
17 changes: 17 additions & 0 deletions docs/integrations/assets/hermes/hermes-mcp-test.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@

Testing 'alice_core'...
Transport: stdio → npx
Auth: none
✓ Connected (653ms)
✓ Tools discovered: 9

alice_capture Capture continuity input into deterministic continuity ...
alice_recall Recall continuity objects with deterministic ranking an...
alice_resume Compile continuity resumption brief for decisions, open...
alice_open_loops List continuity open loops grouped by deterministic pos...
alice_recent_decisions List most recent continuity decisions in deterministic ...
alice_recent_changes List recent continuity changes from the shipped resumpt...
alice_memory_review List correction review queue or fetch review detail for...
alice_memory_correct Apply deterministic continuity correction actions and r...
alice_context_pack Assemble a deterministic continuity context pack for sc...

Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions docs/integrations/assets/hermes/hermes-runtime-smoke.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"open_loop_count":1,"recall_items":2,"registered_tools":["mcp_alice_core_alice_open_loops","mcp_alice_core_alice_recall","mcp_alice_core_alice_resume"],"resume_last_decision_title":"Decision: Keep Alice MCP local-first for Hermes verification."}
162 changes: 162 additions & 0 deletions docs/integrations/hermes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
# Hermes MCP Integration

This guide connects Hermes Agent to Alice MCP and verifies the exact tool path
for:

- `alice_recall`
- `alice_resume`
- `alice_open_loops`

## Prerequisites

- Hermes Agent with MCP support (`hermes mcp --help` works).
- Alice local runtime is available (`./.venv/bin/python -m alicebot_api.mcp_server --help` works).
- Postgres is reachable from the machine where Hermes runs.

## Config (`~/.hermes/config.yaml`)

Use `mcp_servers` in Hermes config.

### Option A: local command (direct Python)

```yaml
mcp_servers:
alice_core:
command: "/ABS/PATH/TO/AliceBot/.venv/bin/python"
args: ["-m", "alicebot_api.mcp_server"]
env:
DATABASE_URL: "postgresql://alicebot_app:alicebot_app@localhost:5432/alicebot"
ALICEBOT_AUTH_USER_ID: "00000000-0000-0000-0000-000000000001"
PYTHONPATH: "/ABS/PATH/TO/AliceBot/apps/api/src:/ABS/PATH/TO/AliceBot/workers"
tools:
include: [alice_recall, alice_resume, alice_open_loops]
resources: false
prompts: false
```

### Option B: `npx` command (via `alice-cli` package)

```yaml
mcp_servers:
alice_core:
command: "npx"
args: ["-y", "--package", "/ABS/PATH/TO/AliceBot/packages/alice-cli", "alice", "mcp"]
env:
NPM_CONFIG_CACHE: "/tmp/alice-npm-cache"
ALICEBOT_PYTHON: "/ABS/PATH/TO/AliceBot/.venv/bin/python"
DATABASE_URL: "postgresql://alicebot_app:alicebot_app@localhost:5432/alicebot"
ALICEBOT_AUTH_USER_ID: "00000000-0000-0000-0000-000000000001"
PYTHONPATH: "/ABS/PATH/TO/AliceBot/apps/api/src:/ABS/PATH/TO/AliceBot/workers"
tools:
include: [alice_recall, alice_resume, alice_open_loops]
resources: false
prompts: false
```

`alice mcp` shells out to `${ALICEBOT_PYTHON} -m alicebot_api.mcp_server`.

If you have a published CLI version with `mcp` support, you can replace args
with:

```yaml
args: ["-y", "@aliceos/alice-cli", "mcp"]
```

## Verify Connection

```bash
hermes mcp test alice_core
```

Expected:

- `Connected`
- `Tools discovered`
- includes `alice_recall`, `alice_resume`, `alice_open_loops`

## Verify Tool Calls (Hermes Runtime Path)

Run the smoke script:

```bash
./scripts/run_hermes_mcp_smoke.py
```

Expected JSON output includes:

- `registered_tools` containing:
- `mcp_alice_core_alice_recall`
- `mcp_alice_core_alice_resume`
- `mcp_alice_core_alice_open_loops`
- non-zero `recall_items`
- `open_loop_count` >= `1`

## Sample Hermes Prompts

Hermes prefixes MCP tools as `mcp_<server>_<tool>`. With server name
`alice_core`, the names are:

- `mcp_alice_core_alice_recall`
- `mcp_alice_core_alice_resume`
- `mcp_alice_core_alice_open_loops`

Prompts:

```text
Use mcp_alice_core_alice_recall with {"query":"Hermes docs","limit":5} and summarize the top 3 memories.
```

```text
Use mcp_alice_core_alice_resume with {"thread_id":"aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa","max_recent_changes":5,"max_open_loops":5}. Return only decisions, next action, and blockers.
```

```text
Use mcp_alice_core_alice_open_loops with {"thread_id":"aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa","limit":10}. Group results by waiting_for, blocker, stale, next_action.
```

## Troubleshooting

### `Connection failed` in `hermes mcp test`

- Confirm `command` points to an existing executable.
- Use absolute paths for `command` and `PYTHONPATH`.
- Run the server command directly:
- `"/ABS/PATH/TO/AliceBot/.venv/bin/python" -m alicebot_api.mcp_server --help`

### Tool list is missing `alice_recall`/`alice_resume`/`alice_open_loops`

- Check `tools.include` values are unprefixed tool names:
- `alice_recall`, `alice_resume`, `alice_open_loops`
- Run `/reload-mcp` in Hermes after config changes.
- Re-run `hermes mcp test alice_core`.

### Tools register but calls fail at runtime

- Validate `DATABASE_URL` is reachable and points to a migrated DB.
- Validate `ALICEBOT_AUTH_USER_ID` is a UUID string.
- Run `./scripts/run_hermes_mcp_smoke.py` to isolate server/runtime issues.

### `npx` path fails

- Check `npx --version`.
- Ensure `args` contains a valid local package path or a published package.
- If npm cache permissions are locked down, set `NPM_CONFIG_CACHE` to a writable path.
- If `npx` is blocked in your environment, use Option A (local command).

## Demo Screenshots

`hermes mcp test` against Alice:

![Hermes MCP test with Alice](assets/hermes/hermes-mcp-test.png)

Hermes runtime tool-call smoke result:

![Hermes runtime smoke result](assets/hermes/hermes-runtime-smoke.png)

## Test Record

Validated on `2026-04-09`:

- `HERMES_HOME=/tmp/alice-hermes-home ./.venv/bin/hermes mcp test alice_core` (local command config)
- `HERMES_HOME=/tmp/alice-hermes-home ./.venv/bin/hermes mcp test alice_core` (`npx --package ... alice mcp` config)
- `./scripts/run_hermes_mcp_smoke.py`
6 changes: 6 additions & 0 deletions docs/integrations/mcp.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,12 @@ MCP uses the same local runtime scope as CLI:
}
```

## Hermes

For Hermes Agent-specific setup, prompts, and troubleshooting:

- `docs/integrations/hermes.md`

## Contract Guardrails

- tool set is intentionally narrow and stable
Expand Down
19 changes: 19 additions & 0 deletions packages/alice-cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,25 @@ npm install -g @aliceos/alice-cli

```bash
alice hello
alice mcp --help
alice --help
alice --version
```

## MCP passthrough

`alice mcp` launches the Python Alice MCP server:

```bash
ALICEBOT_PYTHON=/ABS/PATH/TO/AliceBot/.venv/bin/python alice mcp --help
```

For `npx` usage:

```bash
ALICEBOT_PYTHON=/ABS/PATH/TO/AliceBot/.venv/bin/python npx -y @aliceos/alice-cli mcp --help
```

Prerequisite: the selected Python runtime must be able to import
`alicebot_api.mcp_server` (for example, run from this repository after editable
install).
Loading