feat(mcp): `altimate-code mcp serve` exposes data tools to MCP clients without an LLM

Draft
AN
2h ago
+4412-195

Issue for this PR

Closes # (feature; no linked issue)

Type of change

  • Bug fix
  • New feature
  • Refactor / code improvement
  • Documentation

What does this PR do?

There are two ways to get altimate-code's data tools into another agent today, and both are poor fits:

  • The Claude Code plugin hands the whole task to altimate-code run. That pays for a second model and replaces the client's own reasoning loop.
  • The tools themselves can only be used inside an altimate-code session. altimate-code is an MCP client only.

This PR adds altimate-code mcp serve, a stdio MCP server. Any MCP client, such as Claude Code, can call the data tools directly while its own model does the reasoning. The server never calls a model, so an MCP client's subscription covers the whole LLM cost.

What it exposes: five data tools, reusing the existing implementations, each with a short description:

  • warehouse_list
  • schema_inspect
  • sql_execute
  • sql_check
  • sql_materialize

Added since the first revision:

  • answer_commit (MCP only). The client's model commits its final answer through this tool. It runs the same requested-item and number-provenance checks as the native analyst, and returns a FINAL ANSWER: line for the model to reply with. It returns plain text only: Claude Code shows structuredContent to the model in place of the text, and the model then rewrote the answer as prose.
  • Server instructions: the analyst method, 723 characters, sent in the initialize response.
  • analyze prompt: interactive clients show it as a slash command.
  • Reading question (elicitation): when a commit lists two readings and the client supports elicitation, the server asks once which reading the user meant. If the user picks the other reading, the commit is rejected and the model recomputes under it. The question times out after 20 s and follows the tool call's abort signal, so a client that never answers cannot stall the commit. Decline, cancel and timeout all keep the commit accepted.
  • resource_link: when sql_execute spills a large result to a file, the result also carries a link to that file.

Behaviour:

  • Read-only by default. Writes request the sql_execute_write permission, which the server denies unless it was started with --allow-write. Reads are auto-allowed. sql_materialize still requires a read-only source query and writes only to the server session's temporary scratch warehouse, which is removed on exit.
  • Clean results. A local session is bound at start-up, so results no longer carry the "Not routed through the bound workspace" notice.
  • Cancellation. An MCP request's abort is forwarded into the tool call.
  • Client-neutral hints. Spill-file hints say "open this path with any file reader" instead of naming the agent-only read tool. This applies only on the MCP path, through a context flag, so normal agent output is unchanged.
  • Connections. --connections <file> points at a connections file; without it, the server uses normal connection discovery.
  • Clean protocol. stdout carries only MCP protocol messages; logs go to stderr. The CLI's start-up middleware (banner, telemetry, agent init) is skipped for mcp serve only. Cold start is about 1.5 s.

Shared files touched:

FileChange
cli/cmd/mcp.tsnew serve subcommand (marked)
index.tsmiddleware skip for mcp serve only (marked)
workspace/precedence.tsbindLocalSession / releaseLocalSession helpers
sql-execute.ts, sql-result-format.tsthe mcpClient hint flag

Normal agent behaviour is unchanged.

Docs: a new "Use Altimate Code tools from another MCP client" section in docs/docs/configure/mcp-servers.md.

How did you verify your code works?

  • Unit and integration: bun test on test/altimate/mcp-server plus every suite that references the shared files touched: 2,696 pass, 0 fail. The MCP tests run a real in-process MCP client and a stdio client against a temporary database. They:
    • list the 5 tools;
    • run warehouse_list, schema_inspect and sql_execute;
    • check a mutating sql_execute is refused by default and allowed with --allow-write;
    • check the routing notice is absent;
    • check cancellation works.
  • Typecheck and markers: bun run typecheck is clean, and the marker check script/upstream/analyze.ts --markers --base main --strict passes.
  • End to end with the real Claude Code CLI (2.1.302, Sonnet):
    • Setup: claude -p with --mcp-config pointing at altimate-code mcp serve from this branch, --strict-mcp-config, and Bash disallowed. The database was a 60-row SQLite orders table.
    • The server reported connected.
    • Claude called warehouse_list, schema_inspect and sql_execute, and answered correctly: 30 paid orders totalling 12,555, checked by hand.
    • A requested DELETE was refused with Permission denied: sql_execute_write. Restart with --allow-write to enable writes.
    • Cost: $0.09.
  • Not run: Postgres, Snowflake and other remote warehouses through the MCP path. They reuse the same tool implementations and connection registry.

Screenshots / recordings

Not a UI change.

Checklist

  • I have tested my changes locally
  • I have not included unrelated changes in this PR

[!NOTE]
Medium Risk
New SQL execution surface over MCP with read-only defaults and permission gating, but it reuses core query paths and misconfiguration or --allow-write could allow mutations against configured warehouses.

Overview
Adds altimate-code mcp serve, a stdio MCP server so external MCP clients can call Altimate’s five warehouse tools (warehouse_list, schema_inspect, sql_execute, sql_check, sql_materialize) without starting an agent or calling a model. A thin adapter wires the existing tool implementations with a local session, MCP-specific permissions, and client-neutral truncation/spill hints.

Safety and behavior: Source SQL writes are denied unless --allow-write; otherwise reads propagate forceReadOnly through dispatch so Postgres/SQLite/DuckDB get database-level protection, with stderr/warehouse_list caveats for classification-only engines. Optional --connections loads an isolated connections file via Registry.loadFile. Stdio keeps protocol on stdout; CLI middleware (telemetry/heap) is skipped for mcp serve. Scratch materialization and private spill directories are torn down on shutdown; MCP abort signals cancel in-flight queries.

Supporting changes: Truncate.makeLayer for per-server output dirs; docs for client setup; CI runs MCP server tests in a separate process with real drivers (ALTIMATE_MCP_E2E).

Reviewed by Cursor Bugbot for commit d88dd208359146583f5413730e404b8126ba9e9e. Bugbot is set up for automated code reviews on this repo. Configure here.


Summary by cubic

Adds altimate-code mcp serve, a stdio MCP server that exposes altimate-code's data tools to MCP clients without calling a model, so the client's own model does the reasoning and its subscription covers LLM cost.

  • Exposes exactly warehouse_list, schema_inspect, sql_execute, sql_check, sql_materialize, and answer_commit, reusing existing implementations. schema_inspect without a table lists schemas with table counts (capped at 200) via one bounded catalog aggregate, then up to 500 tables per schema through pushed-down catalog queries; MongoDB uses scalar dbStats counts and a bounded listCollections cursor. Row counts are local SQLite/DuckDB only (at most 50 tables under a 3-second budget); remote engines show catalog estimates, never COUNT(*).
  • warehouse_list adds local tables with columns and a one-row sample (SQLite and local DuckDB base tables only), bounded by table budgets and a 4-second deadline; remote connections run no queries.
  • sql_execute accepts up to 8 read-only queries per call as one labeled block each with a shared byte/line budget and full_text_columns forwarded per query; a failing item does not stop the rest. A result too large to show inline becomes a scratch table result_N (typed like sql_materialize, sequential names) with a 10-row preview and a SELECT ... FROM result_N hint; the full rows spill to a CSV file with a single header row (NULL empty, "" kept), described as a CSV copy since CSV text cannot preserve binary, JSON, or base64: values byte-for-byte.
  • answer_commit requires every requested item to map to its own field (at most two rejections per turn) and, once per commit, rejects a fields result with fewer fields than the request asks for; an unchanged recommit is accepted. MCP commits skip the number-provenance warning, which the server cannot verify for numbers computed in the client; native sessions keep it. Calls without asked behave as before. The user's question is passed verbatim as an MCP-only request argument, stripped before parsing; the compact description tells clients not to collapse a compound request into one field ('the repository and the most copied file' needs two). Complete commits (a rows commit, or a fields commit covering every requested item) return the basis as context above an answer-only FINAL ANSWER: <text> line without structured content; under-asked commits keep the earlier rendering. When a commit records two possible readings it asks once per session which reading was meant, with a 20-second timeout (elicitTimeoutMs) that follows the tool call's signal so a silent client cannot stall the commit; declining, an unsupporting client, or a timeout leaves the commit accepted.
  • Read-only by default; writes need --allow-write. Without it, source queries request database-enforced read-only on Postgres, SQLite, and DuckDB; other engines are classification-only, so the server warns at startup and in warehouse_list to use a read-only database user. sql_materialize reads the source read-only and writes only to a session-local scratch warehouse removed on exit.
  • Spill output goes to a private per-process directory deleted on close; the original file stays as a copy and fallback, with client-neutral hints (no agent-only read or lineage_check text) and a resource_link beside the result. Tool output drops rule separator lines, sql_materialize receipts omit the echoed SQL, and usage hints are shown once per session.
  • Forwards MCP cancellation to tool calls, binds a local session so results lack the routing notice, and sends analyst-method instructions in the initialize response, including an analyze prompt for interactive clients. Advertised tool schemas are compacted for the client (filler unions collapsed, parser-enforced keys dropped) while keeping sql_check.join allowed values, the query_from rule, and the answer_commit compound-request guidance, without changing parsing. Writes only MCP frames to stdout with logs on stderr; --print-logs/--log-level apply and telemetry startup is skipped.
  • --connections <file> is the sole source of connections and rejects the reserved scratch name; otherwise normal discovery applies.
  • The stdio server is driven over raw JSON-RPC in tests so MCP SDK client mocks cannot leak in.
  • The analyst requests high reasoning effort on adaptive-thinking models when neither the request nor agent config chooses one; ALTIMATE_ANALYST_EFFORT overrides it or turns it off.

Summary by CodeRabbit

  • New Features
    • Added a standalone MCP server that gives MCP clients access to five Altimate tools for warehouse discovery, schema inspection, SQL execution, SQL checks, and materialization.
    • Supports an optional connections file. SQL writes are disabled by default; enabling them remains subject to safety checks. Materialization uses a read-only source query and a temporary scratch destination.
    • Large results can be opened from a local file path, with temporary output cleaned up when the client closes.
  • Documentation
    • Added setup instructions, connection-source details, tool descriptions, write-permission guidance, output limits, and notes on read-only enforcement.

Update: answer_commit over MCP (stacked on #native commit 15d2766115)

This branch now merges 15d2766115 (the native feat/analyst-commit-slots-effort commit, no PR of its own) so the server reuses answer-commit-checks.ts instead of a copy. Until that commit lands on main, this PR's diff includes it; the MCP-specific change is the last two commits.

  • New sixth tool answer_commit: asked-item coverage (at most two rejections per turn), one-time number-provenance warning, canonical FINAL ANSWER: <text> result. The adapter keeps session history (tool outputs and rejections) in the shape the shared checks read, and starts a new turn after an accepted commit.
  • The 400-character basis limit is not enforced over MCP: longer text is trimmed to 400 characters instead of rejected. The native tool is unchanged.
  • Server instructions step 7 now points at answer_commit.
  • Help snapshot updated for mcp serve.

Tests: ALTIMATE_MCP_E2E=1 bun test test/altimate/mcp-server 33 pass, 0 fail ("Ran 33 tests across 2 files"); typecheck clean; marker check ok. Full package run: 20221 tests, 19335 pass, 18 fail, 790 skip, 78 todo; after the snapshot fix, the remaining failures (mcp.headers, HttpApi status/experimental, --time-budget run tests, and a few environment-dependent suites) fail identically on the native branch without these commits. Codex read-only review of the MCP diff: no P0/P1.

🤖 Generated with Claude Code