Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
55d5d95
feat(mcp): add `aelf mcp` subcommand + python -m fallback for FastMCP…
robotrocketscience May 9, 2026
7a65829
feat(mcp): docstrings on all 12 @mcp.tool wrappers + AST regression g…
robotrocketscience May 9, 2026
ce9b689
docs(mcp): document `aelf mcp` entrypoint + fix stale aelf-mcp/upgrad…
robotrocketscience May 9, 2026
91b14c1
gate: phase 1 — MCP server is now startable + discoverable
robotrocketscience May 9, 2026
8775888
feat(mcp): annotations on every @mcp.tool — readOnly/destructive/idem…
robotrocketscience May 9, 2026
aa2016c
feat(mcp): server instructions= overview + fix stale 9-vs-12 tools co…
robotrocketscience May 9, 2026
9b32bbf
fix(mcp): tool_lock returns structured error instead of AssertionErro…
robotrocketscience May 9, 2026
d6bafcb
feat(mcp): Pydantic Field constraints on every tool param via Annotated
robotrocketscience May 9, 2026
930256d
feat(mcp): cursor pagination on tool_locked / aelf_locked
robotrocketscience May 9, 2026
5faab70
feat(mcp): response_format='markdown' on read tools (search/locked/st…
robotrocketscience May 9, 2026
d051efd
test(mcp): wrapper-layer tests via static AST + fastmcp shim
robotrocketscience May 9, 2026
7224e2c
gate: mcp-server-properly-built — all four phases shipped, branch rea…
robotrocketscience May 9, 2026
327a4d0
docs(mcp): scrub banned-vocab additions from MCP-branch diff
robotrocketscience May 9, 2026
31fd316
test(slash): register 'mcp' subcommand in HIDDEN_SUBCOMMANDS closed-w…
robotrocketscience May 9, 2026
75f66af
build(mcp): declare pydantic>=2 in [mcp] extra to satisfy deptry DEP001
robotrocketscience May 9, 2026
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
3 changes: 2 additions & 1 deletion docs/COMMANDS.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,11 @@ DB resolves from `$AELFRICE_DB`, then `<git-common-dir>/aelfrice/memory.db` when
|---|---|
| `setup` | Install the `UserPromptSubmit` hook + statusline notifier. Auto-detects scope (`project` if `cwd/.venv` matches the active interpreter, else `user`). Idempotent + atomic. Optional flags: `--transcript-ingest`, `--commit-ingest`, `--session-start`, `--rebuilder`. |
| `unsetup` | Remove the hook and our statusline contribution. Composed statuslines are surgically unwrapped. Mirrors `setup` flags. |
| `upgrade [--check]` | Print the pip-upgrade command for the active env (venv / pipx / system). Includes wheel SHA-256 for hash-pinned installs. Does not run pip. |
| `upgrade-cmd [--check]` | Print the install-method-aware upgrade command (uv tool / pipx / venv / system). Includes wheel SHA-256 for hash-pinned installs. Does not run the upgrade itself — replacing the running interpreter mid-process is unreliable. (Bare `upgrade` remains as a deprecated alias for one minor.) |
| `uninstall (--keep-db \| --archive PATH \| --purge)` | Tear down aelfrice. One disposition flag required. `--purge` has three confirmation gates. `--archive` writes a Fernet-encrypted file then deletes the original. |
| `migrate [--from P] [--apply] [--all]` | Port beliefs from the legacy global DB into the active project's per-project DB. Dry-run by default. Read-only on the source. |
| `statusline` | Emit the update-banner snippet (or empty). Reads cache only, no network. |
| `mcp` | Start the FastMCP stdio server exposing the 12 memory tools. Requires the `[mcp]` extra (`pip install 'aelfrice[mcp]'`). Blocks; SIGINT exits cleanly. Hosts can also use `python -m aelfrice.mcp_server`. See [MCP](MCP.md). |
| `ingest-transcript [PATH \| --batch DIR] [--since DATE]` | Ingest one `turns.jsonl` file or batch-walk a directory. Auto-detects aelfrice and Claude Code formats. Idempotent. |
| `rebuild [--transcript PATH] [--n N] [--budget N]` | Manual context-rebuilder run (alpha; normally fires on `PreCompact`). Prints the rebuild block to stdout. |
| `project-warm <path> [--debounce N]` | CwdChanged hook entry point. Resolves `<path>` to a project root (git work-tree or `~/.aelfrice/projects/<id>/`-provisioned ancestor), pre-loads the SQLite + OS page cache, and writes a sentinel under `~/.aelfrice/projects/<id>/.last_warm`. Silent no-op for unknown paths, denied paths (default deny: `/tmp/**`, `/var/folders/**`, `~/Downloads/**`, `~/Desktop/**` — override via `~/.aelfrice/config.json` `project_warm.deny_globs`), and any call inside the 60-second debounce window. Always exits 0; never writes to stdout. |
Expand Down
35 changes: 31 additions & 4 deletions docs/MCP.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,50 @@

aelfrice exposes twelve memory tools through a [Model Context Protocol](https://modelcontextprotocol.io) server. The agent calls them mid-turn; you don't have to invoke them yourself.

Lifecycle commands (`setup`, `unsetup`, `migrate`, `doctor`, `upgrade`, `uninstall`) are CLI-only.
Lifecycle commands (`setup`, `unsetup`, `migrate`, `doctor`, `upgrade-cmd`, `uninstall`) are CLI-only.

## Install + run

The MCP server ships in every install of aelfrice, but the FastMCP runtime is gated behind the `[mcp]` extra:

```bash
# pip
pip install "aelfrice[mcp]"
uv run python -m aelfrice.mcp_server # or just `aelf-mcp` after install

# uv tool
uv tool install --with fastmcp aelfrice
```

Two equivalent ways to start the server (both speak stdio):

```bash
aelf mcp # console-script entry (preferred)
python -m aelfrice.mcp_server # module-exec fallback
```

If `fastmcp` is missing, `aelf mcp` exits 1 with an actionable message (`error: fastmcp is not installed. Install with: pip install aelfrice[mcp]`) — no traceback, no half-started server.

Host config — any MCP-capable host:

```json
{
"mcpServers": {
"aelfrice": {
"command": "aelf",
"args": ["mcp"]
}
}
}
```

Host config — Claude Code, Codex, any MCP-capable host:
Working from a source checkout instead? Point the host at `uv` so it picks up the project's local interpreter:

```json
{
"mcpServers": {
"aelfrice": {
"command": "uv",
"args": ["run", "--project", "/abs/path/to/aelfrice", "python", "-m", "aelfrice.mcp_server"]
"args": ["run", "--project", "/abs/path/to/aelfrice", "aelf", "mcp"]
}
}
}
Expand Down
7 changes: 7 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,13 @@ aelf-session-start-hook = "aelfrice.hook:main_session_start"
[project.optional-dependencies]
mcp = [
"fastmcp>=0.2.0",
# Used directly via `from pydantic import Field` inside `serve()`
# for tool-parameter constraints (Annotated[..., Field(...)]).
# The import is lazy and gated to fastmcp availability, so pydantic
# only needs to be present when the [mcp] extra is installed —
# which is also when fastmcp pulls it in transitively. Declared
# explicitly to satisfy deptry DEP001.
"pydantic>=2",
]
onboard-llm = [
# Required only for `aelf onboard --llm-classify` (or
Expand Down
43 changes: 43 additions & 0 deletions src/aelfrice/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -2292,6 +2292,40 @@ def _cmd_statusline(args: argparse.Namespace, out: object) -> int:
return 0


def _cmd_mcp(args: argparse.Namespace, out: object) -> int:
"""Start the FastMCP stdio server exposing the aelfrice tool surface.

Requires the `[mcp]` extra: `pip install 'aelfrice[mcp]'` (or
`uv tool install --with fastmcp aelfrice`). Blocks until the host
closes the stdio pipes; SIGINT exits cleanly with status 0.

stdio MCP servers must never write to stdout — that channel carries
the JSON-RPC protocol. The aelfrice tool handlers return dicts and
never print; fastmcp itself respects the boundary.
"""
_ = (args, out)
try:
from aelfrice.mcp_server import serve
except ImportError as exc: # pragma: no cover — defensive
print(
f"error: aelfrice.mcp_server import failed: {exc}",
file=sys.stderr,
)
return 1
try:
serve()
except RuntimeError as exc:
# serve() raises RuntimeError when fastmcp is not installed —
# the message includes the install hint.
print(f"error: {exc}", file=sys.stderr)
return 1
except KeyboardInterrupt:
# Clean stop on Ctrl-C; hosts may signal shutdown via SIGINT
# and a traceback would clutter their logs.
return 0
return 0


_UPGRADE_CONTEXT_NOTE: dict[str, str] = {
"uv_tool": "installed via uv tool — use uv to upgrade",
"pipx": "installed via pipx — use pipx to upgrade",
Expand Down Expand Up @@ -4427,6 +4461,15 @@ def build_parser(*, show_advanced: bool = False) -> argparse.ArgumentParser:
p_statusline = sub.add_parser("statusline", help=argparse.SUPPRESS)
p_statusline.set_defaults(func=_cmd_statusline)

# `aelf mcp`: start the FastMCP stdio server. Visible in --help so
# MCP-capable hosts configuring a server entry can discover it;
# the [mcp] extra must be installed for it to actually run.
p_mcp = sub.add_parser(
"mcp",
help="start the FastMCP stdio server (requires aelfrice[mcp])",
)
p_mcp.set_defaults(func=_cmd_mcp)

# Hidden: the orange statusline banner already prompts users when an
# update is pending — direct CLI invocation is auxiliary.
#
Expand Down
Loading
Loading