feat(agents): the Luke MCP server, its bearer and one tool

Merged
CH
5d ago
+15828-39

P06 of the "Bringing Codex or Claude Code Max plans into Luke" rollout. This adds Luke's MCP server, which Claude Code (mcp__luke__…) and Codex (mcp_servers.luke) will call from inside the sandbox. It authenticates with a per-session bearer and serves one tool, read_inbox, to prove the path. P07 registers the rest.

What changes

  • Route: POST /api/agents/mcp (apps/web/server/routes/agents/mcp.ts). The logic is in packages/service/src/agents/luke-mcp/app.ts. The rewrites are regenerated (api-rewrites.json, vercel.json) and the package export is added. The route runs in the default function group.
  • Bearer (luke-mcp/bearer.ts):
    • The presented Authorization: Bearer is hashed with SHA-256 (hex) and looked up through the new readRunnerTokenConversation(digest) in coding-agent-store.ts.
    • The lookup only matches a coding agent's conversation that is not deleted.
    • A match admits that conversation with its opener (the agent's starter) as the caller. Tools then run as that user, in that conversation's context.
    • A missing, wrong or retired (recordCodingAgentRunnerToken(…, null)) bearer gets a 401 with WWW-Authenticate: Bearer, before the body is read.
  • Index: an additive migration conversations_runner_token_hash_index adds a unique partial index on runner_token_hash (WHERE … IS NOT NULL, IF NOT EXISTS). P02 left it out. Nothing writes the column yet, so the unique constraint can't refuse old code.
  • Tool registry (luke-mcp/tools.ts): a map from tool name to {description, inputSchema, run}. read_inbox uses the eve tool's own description and schema (INBOX_TOOLS.READ) and runs through the door eve's tool reaches. P07 adds one entry per tool.
  • The inbox door, extracted: the body of CoderHost.inbox moved verbatim to coder-host/inbox-door.ts (runAgentInbox, plus the InboxCall type, re-exported from host.ts). The host's method delegates to it, and the MCP registry calls the same function. This was needed because the web function cannot import coder-host/host.ts, which pulls in eve's sandbox and tools: the first Vercel build failed resolving microsandbox/just-bash. P07 extracts each door it registers the same way.

Why the server is hand-written

The @modelcontextprotocol/sdk server's request handlers return promises, and every door is an Effect. Running a door inside such a handler would put an Effect.run* outside tools/anti-slop/effect-edges.json's edges, which would need a new permanent-adaptor entry in AGENTS.md. Instead the server implements, in Effect, the stateless subset of streamable HTTP the tools need:

  • POST JSON-RPC with JSON responses, handling initialize, ping, tools/list and tools/call.
  • Notifications and client responses get 202 with no body.
  • GET and DELETE get 405, since the server keeps no session and offers no stream.

The precedent is packages/service/src/integrations/mcp-tools.ts, which already hand-writes the client side of the same protocol. The orchestrator approved this approach (option B).

Protocol details:

  • initialize answers with the client's protocolVersion if it is one of 2025-11-25, 2025-06-18 or 2025-03-26, the versions that define streamable HTTP. Otherwise it answers with the newest of those.
  • An MCP-Protocol-Version header naming any other version gets a 400.
  • A door's refusal, or a door that failed, comes back as a tool result with isError: true.
  • Protocol faults (parse error, invalid request, unknown method, unknown tool) are JSON-RPC errors.

Tests

  • luke-mcp/app.test.ts drives the route over PGlite with the SDK's own Client and StreamableHTTPClientTransport:
    • The client connects, lists read_inbox, and reads back an inbox thread posted through the door.
    • A wrong bearer, a missing bearer and a retired bearer are each refused.
    • Version negotiation, unknown tool and method, a notification, and an unsupported version header each have a case.
  • The SDK is a devDependency pinned at 1.32.1, the version the pinned @anthropic-ai/claude-agent-sdk@0.3.304 resolves its ^1.29.0 MCP peer to. A client upgrade that breaks compatibility therefore fails here. Codex speaks MCP through its own Rust client, so there is no JS SDK of its to pin.
  • coding-agent-store.test.ts: a held digest finds the conversation under its starter, another digest finds nothing, and a retired digest finds nothing.

Checks

  • Locally green: pnpm check:affected's typecheck, oxlint, biome, knip and discarded-effect, plus the touched test files. In this sandbox they had to run one at a time, because running them in parallel was OOM-killed.
  • prepare-script.test.ts (the Claude Code image test) fails on main as it stands, without this change. That failure is not from this PR.

The developer waived WORKFLOW.md's per-PR Preview/Mac runs for this rollout. This PR merges on green.

Out of scope: the remaining tools (P07), minting the bearer at launch (P08), and the events route (P09). P09 reuses bearer.ts.

Made with Luke


Devin Review