feat(api): streamable-HTTP MCP server endpoint (/api/mcp, PAT-authed) #332
No reviewers
Labels
No labels
area:auth
area:ci
area:db
area:infra
area:native
area:pwa
area:service
epic
feature
foundation
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
james/carol!332
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "331-mcp-endpoint"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Exposes Carol's shared #51 agent tool registry to external agent runtimes over a streamable-HTTP MCP endpoint at
POST /api/mcp(#331), so a user can paste a Carol PAT into Claude Code / opencode / a custom MCP client and read + write their own data. Implements ADR-0029/ADR-0030.What's in it
POST /api/mcp— JSON-RPC 2.0, singleapplication/jsonresponses (no SSE). Handlesinitialize/notifications/initialized/ping/tools/list/tools/call. PAT-authed via the existinggetAuthIdentity(same pattern as/api/agent/proposals/*); no token →401. Stateless (noMcp-Session-Id) — every request re-authenticates.tools/list= the #51 registry (listTools()) +commit_proposal/reject_proposalmeta-tools. Each tool's zod schema → JSON Schema via zod v4'sz.toJSONSchema(io: "input"so.transform()DTOs represent what a client sends;allOf-of-objects fromidSchema.and(bodySchema)flattened to satisfy MCP'sinputSchema.type === "object").tools/call— read tools return data; write tools return aProposedChangeand never mutate;commit_proposalis the only mutator (the #51 commit path) and writes the audit event.AgentError/validation failures become MCPisErrorresults, not 500s; unknown tool/method → JSON-RPC-32602/-32601.Safety
userIdcomes only from the PAT, tools take nouser_id(asserted by test), so cross-user access is unrepresentable; a cross-usercommit_proposalinherits #51's 404 (surfaced as anisErrorwithstatus: 404).Decisions
@modelcontextprotocol/sdk'sStreamableHTTPServerTransportis built on Nodehttpreq/res + session bookkeeping that doesn't fit a single Next App Router handler returning a WebResponse; the method set is small and fully testable in-process. No new dependencies./api/mcpis JSON-RPC, not a typed REST route → added to the OpenAPI coverageEXCLUDED_PATHS(like/api/openapi.json), not the spec. Short "Connect an external agent (MCP)" note added to the README; no new env var.Verification
@carol/apitypecheck + lint clean;openapi:checkup to date · coverage 108 pairs;@carol/api-clientunaffected.tools/calldata-layer paths (via #51's repositories) are proven on Postgres too. Newtests/api/mcp.test.ts(14 tests) drives the real PAT Bearer path: 401 without a token,initializecapabilities/protocolVersion,tools/listincludes registry + meta-tools with nouser_id, read round-trip, write returns a proposal + leaves the DB unchanged,commit_proposalapplies + audits, cross-user commit → 404, invalid args →isError.Caveat for review
The protocol surface is unit-tested in-process but not yet verified against a live Claude Code connection (no client in CI) — worth a manual smoke test (paste a PAT into Claude Code's MCP config) before relying on it. Stateless single-JSON-response mode is spec-compliant; if a specific client requires
Mcp-Session-Idor SSE, that's a small follow-up.Closes #331
🤖 Generated with Claude Code
📊 Test coverage
Patch coverage: no testable lines changed.
Overall (
app/,lib/,db/, excluding UI per ADR-0019):Soft thresholds per ADR-0019. Coverage is informational and does not block merge.