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 *_SHORT form 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 index and the MCP index tool share index_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.

Closes #643 (closed).

Testing

  • New mcp_roundtrip harness + 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 existing cli-integration-test CI 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 multi-statement run_sql
Codex 0.139.0 multi-statement 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/index paths 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.version now uses compile-time env!("ORBIT_VERSION") (main's build.rs bakes CI_COMMIT_TAG/git describe), replacing the runtime env lookup. Same authoritative source as orbit --version.
  • The per-request list_tools/get_tool description-override hack is gone. OrbitLocalServer::new() rebuilds the macro router once with descriptions injected (ToolRoute.attr is 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::error payloads, the MCP-recommended shape for model-recoverable failures.
  • One blocking_tool wrapper replaces three copies of the spawn_blocking/match boilerplate.

Alternatives considered.

  • A list_repos tool (mirroring orbit list) was dropped to keep the surface minimal: the server instructions point agents at SELECT * FROM _orbit_manifest, which covers the use case with zero new code.
  • ProtocolVersion::V_2024_11_05 is 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.

Merge request reports

Loading