feat(api): streamable-HTTP MCP server endpoint (/api/mcp, PAT-authed) #332

Merged
james merged 1 commit from 331-mcp-endpoint into main 2026-06-28 22:06:07 +00:00
Owner

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, single application/json responses (no SSE). Handles initialize / notifications/initialized / ping / tools/list / tools/call. PAT-authed via the existing getAuthIdentity (same pattern as /api/agent/proposals/*); no token → 401. Stateless (no Mcp-Session-Id) — every request re-authenticates.
  • tools/list = the #51 registry (listTools()) + commit_proposal/reject_proposal meta-tools. Each tool's zod schema → JSON Schema via zod v4's z.toJSONSchema (io: "input" so .transform() DTOs represent what a client sends; allOf-of-objects from idSchema.and(bodySchema) flattened to satisfy MCP's inputSchema.type === "object").
  • tools/call — read tools return data; write tools return a ProposedChange and never mutate; commit_proposal is the only mutator (the #51 commit path) and writes the audit event. AgentError/validation failures become MCP isError results, not 500s; unknown tool/method → JSON-RPC -32602/-32601.

Safety

  • Per-user scoping is structural: userId comes only from the PAT, tools take no user_id (asserted by test), so cross-user access is unrepresentable; a cross-user commit_proposal inherits #51's 404 (surfaced as an isError with status: 404).

Decisions

  • Minimal handler over the official SDK@modelcontextprotocol/sdk's StreamableHTTPServerTransport is built on Node http req/res + session bookkeeping that doesn't fit a single Next App Router handler returning a Web Response; the method set is small and fully testable in-process. No new dependencies.
  • /api/mcp is JSON-RPC, not a typed REST route → added to the OpenAPI coverage EXCLUDED_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/api typecheck + lint clean; openapi:check up to date · coverage 108 pairs; @carol/api-client unaffected.
  • Full suite on BOTH engines: 1055 passed / 0 skipped — ran an ephemeral Postgres so the MCP tools/call data-layer paths (via #51's repositories) are proven on Postgres too. New tests/api/mcp.test.ts (14 tests) drives the real PAT Bearer path: 401 without a token, initialize capabilities/protocolVersion, tools/list includes registry + meta-tools with no user_id, read round-trip, write returns a proposal + leaves the DB unchanged, commit_proposal applies + 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-Id or SSE, that's a small follow-up.

Closes #331

🤖 Generated with Claude Code

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, single `application/json` responses (no SSE). Handles `initialize` / `notifications/initialized` / `ping` / `tools/list` / `tools/call`. **PAT-authed** via the existing `getAuthIdentity` (same pattern as `/api/agent/proposals/*`); no token → `401`. Stateless (no `Mcp-Session-Id`) — every request re-authenticates. - **`tools/list`** = the #51 registry (`listTools()`) + `commit_proposal`/`reject_proposal` meta-tools. Each tool's zod schema → JSON Schema via **zod v4's `z.toJSONSchema`** (`io: "input"` so `.transform()` DTOs represent what a client sends; `allOf`-of-objects from `idSchema.and(bodySchema)` flattened to satisfy MCP's `inputSchema.type === "object"`). - **`tools/call`** — read tools return data; **write tools return a `ProposedChange` and never mutate**; `commit_proposal` is the only mutator (the #51 commit path) and writes the audit event. `AgentError`/validation failures become MCP `isError` results, not 500s; unknown tool/method → JSON-RPC `-32602`/`-32601`. ## Safety - Per-user scoping is structural: `userId` comes **only** from the PAT, tools take no `user_id` (asserted by test), so cross-user access is unrepresentable; a cross-user `commit_proposal` inherits #51's **404** (surfaced as an `isError` with `status: 404`). ## Decisions - **Minimal handler over the official SDK** — `@modelcontextprotocol/sdk`'s `StreamableHTTPServerTransport` is built on Node `http` req/res + session bookkeeping that doesn't fit a single Next App Router handler returning a Web `Response`; the method set is small and fully testable in-process. **No new dependencies.** - `/api/mcp` is JSON-RPC, not a typed REST route → added to the OpenAPI coverage `EXCLUDED_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/api` typecheck + lint clean; `openapi:check` up to date · coverage 108 pairs; `@carol/api-client` unaffected. - **Full suite on BOTH engines: 1055 passed / 0 skipped** — ran an ephemeral Postgres so the MCP `tools/call` data-layer paths (via #51's repositories) are proven on Postgres too. New `tests/api/mcp.test.ts` (14 tests) drives the real PAT Bearer path: 401 without a token, `initialize` capabilities/protocolVersion, `tools/list` includes registry + meta-tools with no `user_id`, read round-trip, write returns a proposal + leaves the DB unchanged, `commit_proposal` applies + 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-Id` or SSE, that's a small follow-up. Closes #331 🤖 Generated with [Claude Code](https://claude.com/claude-code)
feat(api): streamable-HTTP MCP server endpoint (/api/mcp, PAT-authed)
All checks were successful
Commits / Conventional Commits (pull_request) Successful in 5s
PR / Static analysis (pull_request) Successful in 2m24s
PR / Client (web export smoke) (pull_request) Successful in 2m30s
PR / Typecheck (pull_request) Successful in 2m44s
PR / pnpm audit (pull_request) Successful in 3m3s
PR / OpenAPI (pull_request) Successful in 3m22s
PR / OSV-Scanner (pull_request) Successful in 1m1s
PR / Build (pull_request) Successful in 3m44s
PR / Lint (pull_request) Successful in 3m51s
PR / Package age policy (soft) (pull_request) Successful in 41s
PR / Test (postgres) (pull_request) Successful in 4m0s
PR / Test (sqlite) (pull_request) Successful in 4m10s
Secrets / gitleaks (pull_request) Successful in 49s
PR / Trivy (image) (pull_request) Successful in 2m16s
PR / Coverage (soft) (pull_request) Successful in 2m11s
PR / E2E (Playwright) (pull_request) Successful in 5m0s
2872ec6b06
Exposes Carol's shared #51 agent tool registry to external agent runtimes
over a streamable-HTTP MCP endpoint at POST /api/mcp, PAT-authed via
getAuthIdentity (#331). JSON-RPC 2.0 with single application/json
responses (no SSE); implements initialize / notifications/initialized /
ping / tools/list / tools/call.

tools/list returns the #51 registry plus commit_proposal/reject_proposal
meta-tools. Read tools return data; write tools return a ProposedChange
and never mutate — commit_proposal is the only mutator and writes the
audit event. Per-user scoping is structural: the userId comes only from
the PAT, tools take no user_id, so cross-user access is unrepresentable
and a cross-user commit returns 404.

Minimal spec-compliant handler chosen over the official SDK (its
Node-http transport doesn't fit a Next Web-Response handler); no new
deps (zod v4's z.toJSONSchema converts tool schemas). Excluded from the
OpenAPI coverage gate (JSON-RPC, not a typed REST route). Tests run on
both DB engines through the #51 data layer.

Closes #331

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

📊 Test coverage

Patch coverage: no testable lines changed.

Overall (app/, lib/, db/, excluding UI per ADR-0019):

Metric Value Soft target
Lines 78.1% ≥ 50%
Branches 69.5% ⚠️ ≥ 75%
Functions 78.5% informational

Soft thresholds per ADR-0019. Coverage is informational and does not block merge.

<!-- coverage-comment --> ## 📊 Test coverage **Patch coverage:** no testable lines changed. **Overall** (`app/`, `lib/`, `db/`, excluding UI per ADR-0019): | Metric | Value | Soft target | |---|---|---| | Lines | 78.1% ✅ | ≥ 50% | | Branches | 69.5% ⚠️ | ≥ 75% | | Functions | 78.5% | informational | Soft thresholds per [ADR-0019](docs/adr/0019-coverage-soft-targets.md). Coverage is informational and does not block merge.
james merged commit 2a17d977cf into main 2026-06-28 22:06:07 +00:00
james deleted branch 331-mcp-endpoint 2026-06-28 22:06:08 +00:00
Sign in to join this conversation.
No description provided.