armillary mcp-serve exposes your indexed project portfolio to AI
coding agents (Claude Code, Cursor, Codex) as Model Context
Protocol tools. This document covers
the runtime model, wire protocol, and what actually happens when the
agent calls a tool.
┌────────────────────┐ stdio ┌──────────────────────────┐
│ Claude Code │ JSON-RPC 2.0 │ armillary mcp-serve │
│ (or Cursor / │ ◀──────────────────────▶ (FastMCP over stdio) │
│ Codex session) │ newline-framed │ │
└────────────────────┘ └─────────────┬────────────┘
│
▼
┌──────────────────────────┐
│ SQLite cache │
│ <platform cache>/cache │
│ .db (override with │
│ ARMILLARY_CACHE_DB) │
└──────────────────────────┘
- Transport is stdio.
mcp-servedoes not open a port and does not listen on HTTP. The subprocess reads messages from stdin and writes responses to stdout using the MCP wire protocol (JSON-RPC 2.0, newline-delimited). - The agent owns the process lifetime. Claude Code launches
armillary mcp-serveas a subprocess at session start (per your~/.claude/mcp.json), keeps it alive for the duration of the session, and sends itSIGTERMon exit. - The dashboard is independent.
armillary startlaunches a Streamlit server;armillary mcp-serveruns separately. Both read the same SQLite cache; neither depends on the other.
armillary config --init writes the Claude Code MCP entry when
~/.claude/ already exists and you accept the bridge prompt — the
init flow skips it otherwise, and it does not touch Cursor or Codex
configuration files at all. For Cursor / Codex, and for the Claude
Code case where --init didn't write the entry, configure manually as
shown below:
{
"mcpServers": {
"armillary": {
"command": "/absolute/path/to/venv/bin/armillary",
"args": ["mcp-serve"]
}
}
}The file location depends on the agent:
- Claude Code —
~/.claude/mcp.json - Cursor —
~/.cursor/mcp.json(or project-local.cursor/mcp.json) - Codex —
~/.codex/config.tomlunder[mcp_servers.armillary]
Five tools are registered via @mcp.tool() in src/armillary/mcp_server.py:
Every tool returns str. On success the string is JSON-encoded (except
armillary_pulse, which is a human-readable summary); on error or when
a precondition is missing (empty cache, ripgrep not installed, project
name ambiguous, no matches) the tool returns a plain-text message
instead of JSON. Consumers should attempt json.loads and fall back to
treating the return as literal text.
| Tool | Signature | Typical latency |
|---|---|---|
armillary_next |
() → str |
<50 ms |
armillary_context |
(project_name: str) → str |
<500 ms (git log runs) |
armillary_search |
(query: str, max_results: int = 20) → str |
<50 ms per repo hit |
armillary_projects |
(status_filter: str | None) → str |
<20 ms |
armillary_pulse |
() → str |
<30 ms |
armillary_steal |
(query: str, limit: int = 5, language: str | None) → str |
<100 ms |
armillary_revive |
(project_path: str) → str |
<500 ms (revive subprocess + steal) |
Schemas are introspected from the Python function signatures; the agent
receives them in the tools/list response during the MCP handshake.
- Handshake. Claude Code sends
initializeas the first message on stdin. FastMCP replies with the server's name, version, and capabilities, then emitsinitialized. - Tool discovery. The agent immediately calls
tools/list. FastMCP returns the seven schemas so the agent knows what it can invoke. Any system-level prompt instructions declared on theFastMCP()constructor ride along here — armillary's say "ALWAYS callarmillary_nextat the very start of every conversation," which is why a fresh Claude Code session surfaces your portfolio without you asking. - Idle. The process blocks on
stdin.readline(). No CPU, no disk I/O, ~30–50 MB resident Python overhead. - Tool call. The agent sends
{"method":"tools/call","params":{"name":"armillary_next", ...}}. FastMCP dispatches to the decorated function, which opens aCache()context manager, runs its work, returns a JSON string. FastMCP wraps the result in a JSON-RPC response and writes it to stdout. - Repeat. Each call goes back to idle afterward. Cache connection is closed between calls — no persistent state in the process memory.
- Shutdown. Agent closes stdin or sends SIGTERM; FastMCP's run loop exits and the process dies.
Tool responses mix two sources:
- Live git data —
armillary_contextrunsgit logandgit statuson each call, so recent commits and dirty-file counts are always current. - Cached metadata —
work_hours,readme_excerpt,statuscome from the SQLite cache, which is only refreshed byarmillary scan,armillary start(which runs an incremental pre-scan), or the dashboard's Scan now button.
Practical consequence: if you rename a project's README and immediately
ask Claude Code for that project's description, the cached excerpt may
still be the old one. Run armillary scan (or refresh the dashboard)
after changes that should propagate.
MCP is silent by design — the subprocess's stdout is consumed by the
agent, and stderr is typically swallowed. To debug a misbehaving tool
call, feed the server a valid handshake followed by a real request.
Spec-compliant clients must send initialize (and the
notifications/initialized acknowledgement) before any tools/*
method; the same holds when you drive the server by hand:
{
printf '%s\n' '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"debug","version":"0"}}}'
printf '%s\n' '{"jsonrpc":"2.0","method":"notifications/initialized"}'
printf '%s\n' '{"jsonrpc":"2.0","id":2,"method":"tools/list"}'
} | .venv/bin/armillary mcp-serveThat prints the registered tool schemas straight to your terminal. For
richer traces across a running session, pipe mcp-serve through tee
in the mcp.json command field and tail the log.
armillary uses mcp.server.fastmcp.FastMCP — the implementation that
ships with the official MCP Python SDK (pip install mcp). This is not
the standalone fastmcp package. Anthropic internalized that codebase
into the official SDK, so armillary's only MCP dependency is mcp>=1.0.