Serve the API as an MCP server over streamable HTTP - #6
Open
kamath wants to merge 1 commit into
Open
Conversation
`POST /api/mcp` exposes every operation in the generated OpenAPI document as an MCP tool, verbatim. The tool list is a pure function of the document, so a route that exists is a tool that exists and adding an endpoint needs no work in the bridge; `describeRoute`'s prose becomes the tool description and hono-openapi's derived `operationId` becomes the tool name. The one transformation is shape: MCP hands a tool a single flat `arguments` object while an HTTP operation splits its inputs across path, query and body, so `plan` flattens the three and `rebuild` puts them back. A name arriving in two places throws at startup rather than silently routing a value to the wrong slot. Notes on the design: - It lives on `app` beside `/api/openapi` and `/api/scalar`, not on `routes` — a protocol endpoint belongs in neither the document nor the RPC client's type. Carrying no `describeRoute` is also what stops it generating a tool for itself. - Dispatch is in-process via `app.request`, so the bridge never talks to its own HTTP port. A separate MCP process importing `@repo/api` would be a second PGlite opener, which corrupts the data dir silently. - Stateless: a fresh server and transport per POST, nothing to garbage-collect. A module-level transport registry is the hazard that already forced `db.ts` onto `globalThis`, since Vite re-evaluates on every edit. `enableJsonResponse` keeps replies plain JSON; GET returns 405. - Results are capped at 64 KB, because `getApiTypes` over the whole catalog is 8.6 MB and a response that evicts the conversation it was fetched for is worse than an error — it looks like success. - A failing route is `isError` in the result, never a thrown JSON-RPC error, so a caller can read the failure and correct itself. Only an unadvertised tool name is a protocol error. `@modelcontextprotocol/sdk` is `ssr.external` in the frontend and a direct dependency there, the same treatment PGlite gets and for the same reason: it is Node-targeted and pulls in express and ajv. Co-Authored-By: Claude <noreply@anthropic.com>
kamath
force-pushed
the
mcp-server-streamable-http
branch
from
July 26, 2026 05:44
a5309d2 to
39e14c3
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
POST /api/mcpexposes every operation in the generated OpenAPI document as an MCP tool, verbatim — the tool list is a pure function of the document, so a new route becomes a tool with no work in the bridge, withdescribeRoute's prose as the description and hono-openapi's derivedoperationIdas the name.The only transformation is shape: MCP passes a single flat
argumentsobject, soplanflattens each operation's path, query and body inputs into one schema andrebuildputs them back, throwing at startup if two differently-scoped names would collide.It is stateless (fresh server per POST,
enableJsonResponse, GET → 405) and dispatches in-process viaapp.request— a separate process importing@repo/apiwould be a second PGlite opener — with results capped at 64 KB becausegetApiTypesover the whole catalog is 8.6 MB, and a failing route surfaced asisErrorrather than a thrown JSON-RPC error so a caller can correct itself.Also adds
@modelcontextprotocol/sdkasssr.externalplus a direct frontend dependency (PGlite's treatment, for the same reason),src/mcp.test.tswhose central assertion is that every tool's advertised inputs equal its operation's declared inputs, and aCLAUDE.mdsection.🤖 Generated with Claude Code