MCP server endpoint (streamable HTTP, PAT-authed) #52

Closed
opened 2026-06-14 19:52:04 +00:00 by james · 1 comment
Owner

Expose the tool registry as a streamable-HTTP MCP server at /api/mcp, authenticated by Personal Access Token, tested end-to-end against at least one real external client (Claude Code).

Scope

  • New route handler at /api/mcp (or /api/mcp/[...path] depending on what the chosen MCP server library expects).
  • Auth: Authorization: Bearer <PAT>. Requests without a valid token return 401. The authenticated user is taken from the PAT lookup and passed to the tool registry the same way the PWA agent does.
  • Tool list, tool call, and tool result follow the MCP spec for streamable HTTP. Write tools surface their confirmation requirement via the standard MCP tool-call flow — they return the proposed-change object the client agent presents to the user, who confirms or rejects through their MCP client. A confirmed proposal hits the commit endpoint.
  • Per-user scoping flows from the PAT all the way through to the repository layer.
  • One reference end-to-end test: a Claude Code instance with the MCP server configured can call a read tool and see results, and call a write tool and see the proposed-change response.
  • The setup recipe (issue PAT → paste into claude_desktop_config.json → verify) is documented in the external-agent-setup docs ticket.

Acceptance criteria

  • /api/mcp is reachable, speaks streamable-HTTP MCP, and rejects unauth'd requests with 401.
  • Claude Code (or another real MCP client) can connect with a Carol PAT, list tools, and successfully call at least one read tool and one write tool.
  • A PAT for user A cannot read user B's data through any MCP tool.
  • Test exercises the full handshake → list tools → call tool round trip.

Part of epic #47. Depends on Personal Access Tokens, the agent-runtime ADR, the tool-granularity ADR, and the Domain tool surface.

Expose the tool registry as a streamable-HTTP MCP server at `/api/mcp`, authenticated by Personal Access Token, tested end-to-end against at least one real external client (Claude Code). ## Scope - New route handler at `/api/mcp` (or `/api/mcp/[...path]` depending on what the chosen MCP server library expects). - Auth: `Authorization: Bearer <PAT>`. Requests without a valid token return 401. The authenticated user is taken from the PAT lookup and passed to the tool registry the same way the PWA agent does. - Tool list, tool call, and tool result follow the MCP spec for streamable HTTP. Write tools surface their confirmation requirement via the standard MCP tool-call flow — they return the proposed-change object the client agent presents to the user, who confirms or rejects through their MCP client. A confirmed proposal hits the commit endpoint. - Per-user scoping flows from the PAT all the way through to the repository layer. - One reference end-to-end test: a Claude Code instance with the MCP server configured can call a read tool and see results, and call a write tool and see the proposed-change response. - The setup recipe (issue PAT → paste into `claude_desktop_config.json` → verify) is documented in the external-agent-setup docs ticket. ## Acceptance criteria - [ ] `/api/mcp` is reachable, speaks streamable-HTTP MCP, and rejects unauth'd requests with 401. - [ ] Claude Code (or another real MCP client) can connect with a Carol PAT, list tools, and successfully call at least one read tool and one write tool. - [ ] A PAT for user A cannot read user B's data through any MCP tool. - [ ] Test exercises the full handshake → list tools → call tool round trip. Part of epic #47. Depends on Personal Access Tokens, the agent-runtime ADR, the tool-granularity ADR, and the Domain tool surface.
Author
Owner

Closing — the deliverable shipped under #331 (initial /api/mcp endpoint) and #339 (refactor onto the shared dispatchRegistryTool), so this epic-linked ticket was never auto-closed.

Acceptance criteria, status:

  • /api/mcp is reachable, speaks streamable-HTTP MCP (JSON-RPC: initialize / notifications/initialized / ping / tools/list / tools/call), and rejects unauth'd requests with 401 (PAT bearer). Covered in apps/api/tests/api/mcp.test.ts.
  • Per-user isolation: a PAT for user A cannot read user B's data through any tool — read tools return only the acting user's rows, and commit_proposal against another user's proposal returns a 404-class isError. Tested.
  • Full handshake → tools/listtools/call round trip, including the write-confirmation flow (create_note returns a ProposedChange and mutates nothing; the commit_proposal meta-tool applies it and writes an audit event). Tested.
  • Reference end-to-end test with a real external client (Claude Code) — the only criterion not satisfiable by automated tests. The server side is fully exercised by the integration tests above; the live-client run is a manual smoke test, with the recipe now documented in docs/agent-setup-guide.md (#360). Left as a maintainer sign-off.

Note: /api/mcp answers with a single application/json response (stateless, no Mcp-Session-Id, no GET/SSE channel) — a valid minimal streamable-HTTP server for request/response tool calls. The manual smoke test is what confirms a real client is happy with that shape; if a gap surfaces there, it'll be its own follow-up.

Closing — the deliverable shipped under #331 (initial `/api/mcp` endpoint) and #339 (refactor onto the shared `dispatchRegistryTool`), so this epic-linked ticket was never auto-closed. **Acceptance criteria, status:** - ✅ `/api/mcp` is reachable, speaks streamable-HTTP MCP (JSON-RPC: `initialize` / `notifications/initialized` / `ping` / `tools/list` / `tools/call`), and rejects unauth'd requests with 401 (PAT bearer). Covered in `apps/api/tests/api/mcp.test.ts`. - ✅ Per-user isolation: a PAT for user A cannot read user B's data through any tool — read tools return only the acting user's rows, and `commit_proposal` against another user's proposal returns a 404-class `isError`. Tested. - ✅ Full handshake → `tools/list` → `tools/call` round trip, including the write-confirmation flow (`create_note` returns a `ProposedChange` and mutates nothing; the `commit_proposal` meta-tool applies it and writes an audit event). Tested. - ⬜ **Reference end-to-end test with a real external client (Claude Code)** — the only criterion not satisfiable by automated tests. The server side is fully exercised by the integration tests above; the live-client run is a manual smoke test, with the recipe now documented in `docs/agent-setup-guide.md` (#360). Left as a maintainer sign-off. Note: `/api/mcp` answers with a single `application/json` response (stateless, no `Mcp-Session-Id`, no GET/SSE channel) — a valid minimal streamable-HTTP server for request/response tool calls. The manual smoke test is what confirms a real client is happy with that shape; if a gap surfaces there, it'll be its own follow-up.
james closed this issue 2026-06-29 13:57:09 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
james/carol#52
No description provided.