feat(agents): the Luke MCP server, its bearer and one tool
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 inpackages/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: Beareris hashed with SHA-256 (hex) and looked up through the newreadRunnerTokenConversation(digest)incoding-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 withWWW-Authenticate: Bearer, before the body is read.
- The presented
- Index: an additive migration
conversations_runner_token_hash_indexadds a unique partial index onrunner_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_inboxuses 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.inboxmoved verbatim tocoder-host/inbox-door.ts(runAgentInbox, plus theInboxCalltype, re-exported fromhost.ts). The host's method delegates to it, and the MCP registry calls the same function. This was needed because the web function cannot importcoder-host/host.ts, which pulls in eve's sandbox and tools: the first Vercel build failed resolvingmicrosandbox/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/listandtools/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:
initializeanswers with the client'sprotocolVersionif it is one of2025-11-25,2025-06-18or2025-03-26, the versions that define streamable HTTP. Otherwise it answers with the newest of those.- An
MCP-Protocol-Versionheader 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.tsdrives the route over PGlite with the SDK's ownClientandStreamableHTTPClientTransport:- 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 client connects, lists
- The SDK is a devDependency pinned at
1.32.1, the version the pinned@anthropic-ai/claude-agent-sdk@0.3.304resolves its^1.29.0MCP 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 onmainas 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