feat(orbit-local): add stateless MCP server (`orbit mcp serve`)
What does this MR do and why?
Adds orbit mcp serve: a stateless MCP server over stdio that exposes the local DuckDB graph to MCP-compatible AI agents. Verified end-to-end with Claude Code, OpenCode, and Codex.
| Tool | Purpose |
|---|---|
run_sql |
Read-only DuckDB SQL. Takes an array of statements, returns one JSON row array per statement at the same index. Results over ~1 MB (Arrow estimate, checked before serialisation) are rejected with a "add LIMIT" error. |
get_graph_schema |
information_schema.columns for the graph DB. |
index |
Index a repo (or directory of repos) into the workspace DB. |
One source of truth across surfaces:
- Tool descriptions live in descriptions.rs: the CLI uses the
*_SHORTform via#[command(about = …)], MCP uses*_MCP(concat!of SHORT + agent hint, prefix asserted by a test). - The open/query path and the schema introspection SQL are shared helpers in sql.rs, used by
orbit sql,orbit schema, and both MCP query tools. orbit indexand the MCPindextool shareindex_collect.
Tool failures return in-band is_error results (not protocol errors), so agents see statement 1: query failed: SELECT …: Catalog Error: … and can self-correct.
Docs: docs/source/local/access/mcp.md returns from roadmap page (!1606 (merged)) to a usage page with per-client config; the planned/not-yet-available markers come off the four pages that carried them.
Related Issues
Closes #643 (closed).
Testing
- New
mcp_roundtripharness + 3 CLI integration tests drive the real binary over JSON-RPC (cli.rs): tool listing, index→schema→query flow, recoverable bad-SQL errors. Run in the existingcli-integration-testCI job. - Verified live against the same indexed graph, identical results from all three clients (
defs=516974 edges=2199476):
| Client | Version | Result |
|---|---|---|
| Claude Code | 2.1.175 | get_graph_schema + multi-statement run_sql |
| OpenCode | 1.15.13 | run_sql |
| Codex | 0.139.0 | run_sql (needs the user to approve MCP calls per its policy) |
mise run test:fast(1903 passed),mise run lint:code,cargo deny check, docs linters green on changed files.
Performance Analysis
- This merge request does not introduce any performance regression. New subcommand only; the CLI
sql/schema/indexpaths are the same code moved behind shared helpers.
Agent context — long-form analysis, file-by-file walkthroughs, profiler output, alternatives considered
History. This MR was reviewed in March (all threads resolved), then drifted 543 commits behind main while orbit-local was restructured (sql.rs/list.rs extraction, build.rs version baking, Debug subcommand removal). Rather than rebase through that, the branch was rebuilt from scratch on latest main, re-applying the reviewed design plus all review outcomes: spawn_blocking everywhere, pre-serialisation size cap, statement-array run_sql, shared descriptions, shared introspection SQL.
What changed vs the reviewed version.
serverInfo.versionnow uses compile-timeenv!("ORBIT_VERSION")(main'sbuild.rsbakesCI_COMMIT_TAG/git describe), replacing the runtime env lookup. Same authoritative source asorbit --version.- The per-request
list_tools/get_tooldescription-override hack is gone.OrbitLocalServer::new()rebuilds the macro router once with descriptions injected (ToolRoute.attris public in rmcp 1.7), and#[tool_handler(router = self.tool_router)]serves it. A unit test asserts every tool carries its shared description. - Tool failures moved from JSON-RPC protocol errors to
CallToolResult::errorpayloads, the MCP-recommended shape for model-recoverable failures. - One
blocking_toolwrapper replaces three copies of the spawn_blocking/match boilerplate.
Alternatives considered.
- A
list_repostool (mirroringorbit list) was dropped to keep the surface minimal: the serverinstructionspoint agents atSELECT * FROM _orbit_manifest, which covers the use case with zero new code. ProtocolVersion::V_2024_11_05is pinned as the lowest still-current revision for client compat; rmcp would otherwise advertise the latest.
Codex note. Codex's approval_policy = "never" auto-rejects MCP tool calls as "user cancelled"; interactive sessions prompt instead. Verified with approvals granted (--dangerously-bypass-approvals-and-sandbox in headless exec). Not a server-side issue: tools/list and instructions load fine either way.