-
-
Notifications
You must be signed in to change notification settings - Fork 152
feat(mcp): @orpc/mcp — serve one oRPC router as an MCP server (tools/resources/prompts) #1604
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
mi3lix9
wants to merge
13
commits into
middleapi:main
Choose a base branch
from
mi3lix9:feat/mcp-integration
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
13 commits
Select commit
Hold shift + click to select a range
eae3d77
feat(mcp): @orpc/mcp — serve one oRPC router as an MCP server
mi3lix9 f80544a
test(mcp): unit + adapter coverage for @orpc/mcp (72 tests)
mi3lix9 5d0cab7
feat(mcp): wire MCP into the Next playground + docs
mi3lix9 65b87ef
test(mcp): satisfy strict tsc in test files (noUncheckedIndexedAccess…
mi3lix9 19dc1a8
test(mcp): e2e spec-compliance with the official @modelcontextprotoco…
mi3lix9 2676629
fix(mcp): omit structuredContent on tool errors (MCP outputSchema val…
mi3lix9 0db6a51
refactor(mcp)!: rebuild MCPHandler on StandardHandler + auto-register…
mi3lix9 1261c32
refactor(mcp): single namespaced meta API — mcp.tool/resource/prompt …
mi3lix9 c4e931c
feat(mcp): catalog pagination for list methods + auth/result-paginati…
mi3lix9 2884a01
docs(mcp): rewrite the integration page in the standard docs style
mi3lix9 dec3d6e
fix(mcp): address code-review findings (security, robustness, correct…
mi3lix9 b6c283c
Merge origin/main into feat/mcp-integration
mi3lix9 6a654af
refactor(mcp): remove unused getMessageId export (dead code)
mi3lix9 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
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
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
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,209 @@ | ||
| # MCP Integration | ||
|
|
||
| [Model Context Protocol (MCP)](https://modelcontextprotocol.io) is an open standard for connecting AI applications to external tools and data. This integration exposes your oRPC router as an MCP server, so the **same** procedures you already serve over RPC and OpenAPI become MCP **tools**, **resources**, and **prompts** — usable by clients like Claude, ChatGPT, and IDEs, with the same types, validation, and middleware. | ||
|
|
||
| ::: warning | ||
| This guide assumes you are familiar with [MCP](https://modelcontextprotocol.io). The integration targets protocol revision `2025-11-25`. | ||
| ::: | ||
|
|
||
| ## Installation | ||
|
|
||
| ::: code-group | ||
|
|
||
| ```sh [npm] | ||
| npm install @orpc/mcp@beta | ||
| ``` | ||
|
|
||
| ```sh [yarn] | ||
| yarn add @orpc/mcp@beta | ||
| ``` | ||
|
|
||
| ```sh [pnpm] | ||
| pnpm add @orpc/mcp@beta | ||
| ``` | ||
|
|
||
| ```sh [bun] | ||
| bun add @orpc/mcp@beta | ||
| ``` | ||
|
|
||
| ```sh [deno] | ||
| deno add npm:@orpc/mcp@beta | ||
| ``` | ||
|
|
||
| ::: | ||
|
|
||
| ## Setup | ||
|
|
||
| Exposing a procedure to MCP is **opt-in**: annotate it with `mcp.tool`, `mcp.resource`, or `mcp.prompt`. MCP metadata is independent of any [`openapi`](/docs/openapi/routing) meta, so a single procedure can be served over REST and MCP at the same time. | ||
|
|
||
| ```ts twoslash | ||
| import { mcp } from '@orpc/mcp' | ||
| import { os } from '@orpc/server' | ||
| import * as z from 'zod' | ||
|
|
||
| export const createPlanet = os | ||
| .meta(mcp.tool({ description: 'Create a new planet' })) | ||
| .input(z.object({ name: z.string() })) | ||
| .output(z.object({ id: z.string(), name: z.string() })) | ||
| .handler(({ input }) => ({ id: crypto.randomUUID(), name: input.name })) | ||
|
|
||
| export const router = { createPlanet } | ||
| ``` | ||
|
|
||
| Then [serve the router](#serving) with one of the `MCPHandler` adapters. | ||
|
|
||
| ## Tools | ||
|
|
||
| Tools are functions the model can call. A procedure's `.input()` becomes the tool's JSON Schema, its return value becomes the result, and its `.output()` adds an output schema plus structured content. Thrown [typed errors](/docs/error-handling) are reported back to the model as in-band tool errors, so it can react to them. | ||
|
|
||
| ```ts | ||
| export const createPlanet = os | ||
| .meta(mcp.tool({ | ||
| description: 'Create a new planet', | ||
| annotations: { destructiveHint: false }, | ||
| })) | ||
| .input(CreatingPlanetSchema) | ||
| .output(PlanetSchema) | ||
| .handler(({ input }) => create(input)) | ||
| ``` | ||
|
|
||
| Behavior hints — `readOnlyHint`, `destructiveHint`, `idempotentHint`, `openWorldHint` — go in `annotations`. | ||
|
|
||
| ## Resources | ||
|
|
||
| Resources expose read-only data addressed by a URI. Use a fixed `uri` for a single resource, or a `uriTemplate` whose variables map to the procedure's input. | ||
|
|
||
| ```ts | ||
| // Static resource | ||
| export const appConfig = os | ||
| .meta(mcp.resource({ uri: 'config://app', mimeType: 'application/json' })) | ||
| .output(ConfigSchema) | ||
| .handler(() => getConfig()) | ||
|
|
||
| // Templated resource — `{id}` is read from the input | ||
| export const planet = os | ||
| .meta(mcp.resource({ uriTemplate: 'planet://{id}', mimeType: 'application/json' })) | ||
| .input(z.object({ id: z.string() })) | ||
| .output(PlanetSchema) | ||
| .handler(({ input }) => findPlanet(input.id)) | ||
| ``` | ||
|
|
||
| ::: tip | ||
| Only annotate read-only, side-effect-free procedures as resources. | ||
| ::: | ||
|
|
||
| ## Prompts | ||
|
|
||
| Prompts are reusable templates a user can invoke. The arguments are derived from the procedure's `.input()`, and the handler returns the prompt messages. | ||
|
|
||
| ```ts | ||
| export const planTrip = os | ||
| .meta(mcp.prompt({ description: 'Plan a vacation' })) | ||
| .input(z.object({ destination: z.string() })) | ||
| .output(z.object({ | ||
| messages: z.array(z.object({ | ||
| role: z.enum(['user', 'assistant']), | ||
| content: z.object({ type: z.literal('text'), text: z.string() }), | ||
| })), | ||
| })) | ||
| .handler(({ input }) => ({ | ||
| messages: [{ role: 'user', content: { type: 'text', text: `Plan a trip to ${input.destination}` } }], | ||
| })) | ||
| ``` | ||
|
|
||
| ## Serving | ||
|
|
||
| `MCPHandler` speaks the MCP protocol over the [Streamable HTTP](https://modelcontextprotocol.io/specification/2025-11-25/basic/transports) transport (Fetch or Node.js) or over stdio. Pass the schema converter for your validation library — the same converters used by [`@orpc/openapi`](/docs/openapi/specification). | ||
|
|
||
| It is built on oRPC's standard request/response flow, so tool, resource, and prompt calls run through your [middleware](/docs/middleware), validation, and context, and any handler plugin (CORS, body limit, OpenTelemetry) composes as usual. | ||
|
|
||
| ### Fetch | ||
|
|
||
| ```ts | ||
| import { MCPHandler } from '@orpc/mcp/fetch' | ||
| import { ZodToJsonSchemaConverter } from '@orpc/zod' | ||
|
|
||
| const handler = new MCPHandler(router, { | ||
| serverInfo: { name: 'planets', version: '1.0.0' }, | ||
| converters: [new ZodToJsonSchemaConverter()], | ||
| }) | ||
|
|
||
| export async function POST(request: Request) { | ||
| const { response } = await handler.handle(request, { context: {} }) | ||
| return response ?? new Response('Not found', { status: 404 }) | ||
| } | ||
| ``` | ||
|
|
||
| ### Node.js | ||
|
|
||
| ```ts | ||
| import { createServer } from 'node:http' | ||
| import { MCPHandler } from '@orpc/mcp/node' | ||
| import { ZodToJsonSchemaConverter } from '@orpc/zod' | ||
|
|
||
| const handler = new MCPHandler(router, { converters: [new ZodToJsonSchemaConverter()] }) | ||
|
|
||
| createServer((req, res) => handler.handle(req, res, { context: {} })).listen(3000) | ||
| ``` | ||
|
|
||
| ### stdio | ||
|
|
||
| For clients that launch your server as a subprocess (Claude Desktop, IDEs): | ||
|
|
||
| ```ts | ||
| import { MCPHandler } from '@orpc/mcp/stdio' | ||
| import { ZodToJsonSchemaConverter } from '@orpc/zod' | ||
|
|
||
| await new MCPHandler(router, { converters: [new ZodToJsonSchemaConverter()] }) | ||
| .listen({ context: {} }) | ||
| ``` | ||
|
|
||
| ## Authorization | ||
|
|
||
| Authentication and authorization are your application's responsibility — the integration stays unopinionated about tokens, scopes, and OAuth. Supply request-derived values as `context` when calling the handler, then enforce them with ordinary [middleware](/docs/middleware), which runs for every tool, resource, and prompt call. | ||
|
|
||
| ```ts | ||
| export const authed = os.use(({ context, next, errors }) => { | ||
| const user = verifyToken(context.authToken) | ||
| if (!user) | ||
| throw errors.UNAUTHORIZED() | ||
| return next({ context: { user } }) | ||
| }) | ||
|
|
||
| export const deletePlanet = authed | ||
| .meta(mcp.tool({ description: 'Delete a planet' })) | ||
| .handler(({ context }) => remove(context.user)) | ||
| ``` | ||
|
|
||
| A thrown `UNAUTHORIZED` reaches the model as an in-band tool error, or a resource/prompt request as a protocol error. | ||
|
|
||
| ## Security | ||
|
|
||
| For HTTP servers reachable by browsers, enable Origin and Host validation to guard against DNS-rebinding attacks. A missing `Origin` header still passes, so non-browser clients are unaffected. | ||
|
|
||
| ```ts | ||
| export const handler = new MCPHandler(router, { | ||
| converters: [new ZodToJsonSchemaConverter()], | ||
| enableDnsRebindingProtection: true, | ||
| allowedOrigins: ['https://your-app.example'], | ||
| allowedHosts: ['your-app.example'], | ||
| }) | ||
| ``` | ||
|
|
||
| ## One Router, Every Surface | ||
|
|
||
| Because MCP exposure lives in procedure metadata, a single router can be mounted on multiple handlers at once — RPC, OpenAPI, and MCP — over the same instance: | ||
|
|
||
| ```ts | ||
| export const handlers = { | ||
| rpc: new RPCHandler(router), // typed oRPC clients | ||
| openapi: new OpenAPIHandler(router), // REST + OpenAPI | ||
| mcp: new MCPHandler(router, { converters: [new ZodToJsonSchemaConverter()] }), // MCP tools / resources / prompts | ||
| } | ||
| ``` | ||
|
|
||
| ## Limitations | ||
|
|
||
| - Targets MCP revision `2025-11-25`; older revisions are accepted during negotiation. | ||
| - One JSON-RPC message per request — batching is not supported. | ||
| - Server-initiated streaming (the `GET` SSE channel), `listChanged`/`subscribe` notifications, and sessions are not implemented. These are being removed or replaced in the next MCP revision, so the stateless request/response design is intentional. |
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,75 @@ | ||
| { | ||
| "name": "@orpc/mcp", | ||
| "type": "module", | ||
| "version": "2.0.0-beta.5", | ||
| "license": "MIT", | ||
| "homepage": "https://orpc.dev", | ||
| "repository": { | ||
| "type": "git", | ||
| "url": "git+https://github.com/middleapi/orpc.git", | ||
| "directory": "packages/mcp" | ||
| }, | ||
| "keywords": [ | ||
| "orpc", | ||
| "mcp", | ||
| "model-context-protocol" | ||
| ], | ||
| "sideEffects": false, | ||
| "publishConfig": { | ||
| "exports": { | ||
| "./package.json": "./package.json", | ||
| ".": { | ||
| "types": "./dist/index.d.mts", | ||
| "import": "./dist/index.mjs", | ||
| "default": "./dist/index.mjs" | ||
| }, | ||
| "./standard": { | ||
| "types": "./dist/adapters/standard/index.d.mts", | ||
| "import": "./dist/adapters/standard/index.mjs", | ||
| "default": "./dist/adapters/standard/index.mjs" | ||
| }, | ||
| "./fetch": { | ||
| "types": "./dist/adapters/fetch/index.d.mts", | ||
| "import": "./dist/adapters/fetch/index.mjs", | ||
| "default": "./dist/adapters/fetch/index.mjs" | ||
| }, | ||
| "./node": { | ||
| "types": "./dist/adapters/node/index.d.mts", | ||
| "import": "./dist/adapters/node/index.mjs", | ||
| "default": "./dist/adapters/node/index.mjs" | ||
| }, | ||
| "./stdio": { | ||
| "types": "./dist/adapters/stdio/index.d.mts", | ||
| "import": "./dist/adapters/stdio/index.mjs", | ||
| "default": "./dist/adapters/stdio/index.mjs" | ||
| } | ||
| } | ||
| }, | ||
| "exports": { | ||
| "./package.json": "./package.json", | ||
| ".": "./src/index.ts", | ||
| "./standard": "./src/adapters/standard/index.ts", | ||
| "./fetch": "./src/adapters/fetch/index.ts", | ||
| "./node": "./src/adapters/node/index.ts", | ||
| "./stdio": "./src/adapters/stdio/index.ts" | ||
| }, | ||
| "files": [ | ||
| "dist" | ||
| ], | ||
| "scripts": { | ||
| "build": "unbuild", | ||
| "type:check": "tsc -b" | ||
| }, | ||
| "dependencies": { | ||
| "@orpc/client": "workspace:*", | ||
| "@orpc/contract": "workspace:*", | ||
| "@orpc/json-schema": "workspace:*", | ||
| "@orpc/server": "workspace:*", | ||
| "@orpc/shared": "workspace:*" | ||
| }, | ||
| "devDependencies": { | ||
| "@modelcontextprotocol/sdk": "^1.29.0", | ||
| "@orpc/zod": "workspace:*", | ||
| "zod": "^4.4.3" | ||
| } | ||
| } | ||
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| export * from './mcp-handler' |
Oops, something went wrong.
Oops, something went wrong.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we should use the name
@orpc/experimental-mcp. There's no way this will be stable anytime soon.