Skip to content
Open
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
eae3d77
feat(mcp): @orpc/mcp — serve one oRPC router as an MCP server
mi3lix9 Jun 27, 2026
f80544a
test(mcp): unit + adapter coverage for @orpc/mcp (72 tests)
mi3lix9 Jun 27, 2026
5d0cab7
feat(mcp): wire MCP into the Next playground + docs
mi3lix9 Jun 27, 2026
65b87ef
test(mcp): satisfy strict tsc in test files (noUncheckedIndexedAccess…
mi3lix9 Jun 27, 2026
19dc1a8
test(mcp): e2e spec-compliance with the official @modelcontextprotoco…
mi3lix9 Jun 27, 2026
2676629
fix(mcp): omit structuredContent on tool errors (MCP outputSchema val…
mi3lix9 Jun 27, 2026
0db6a51
refactor(mcp)!: rebuild MCPHandler on StandardHandler + auto-register…
mi3lix9 Jun 28, 2026
1261c32
refactor(mcp): single namespaced meta API — mcp.tool/resource/prompt …
mi3lix9 Jun 28, 2026
c4e931c
feat(mcp): catalog pagination for list methods + auth/result-paginati…
mi3lix9 Jun 28, 2026
2884a01
docs(mcp): rewrite the integration page in the standard docs style
mi3lix9 Jun 28, 2026
dec3d6e
fix(mcp): address code-review findings (security, robustness, correct…
mi3lix9 Jun 29, 2026
b6c283c
Merge origin/main into feat/mcp-integration
mi3lix9 Jun 29, 2026
6a654af
refactor(mcp): remove unused getMessageId export (dead code)
mi3lix9 Jun 29, 2026
cf318e9
chore(mcp)!: rename package to @orpc/experimental-mcp
mi3lix9 Jul 6, 2026
1ff32dd
fix(mcp): parse JSON-RPC envelope once, independent of request identity
mi3lix9 Jul 6, 2026
56a323e
refactor(mcp): use ORPCError internally, map to JSON-RPC at one boundary
mi3lix9 Jul 6, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ You can read the documentation [here](https://orpc.dev).

**Framework & ecosystem integrations**

- [@orpc/mcp](https://www.npmjs.com/package/@orpc/mcp): Serve your router as a [Model Context Protocol](https://modelcontextprotocol.io) server.
- [@orpc/next](https://www.npmjs.com/package/@orpc/next): Integrate with [Next.js Server Functions](https://nextjs.org/docs/app/getting-started/mutating-data).
- [@orpc/tanstack-query](https://www.npmjs.com/package/@orpc/tanstack-query): Integrate with [TanStack Query](https://tanstack.com/query/latest).
- [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect): Integrate with [Effect](https://effect.website/).
Expand Down
1 change: 1 addition & 0 deletions apps/content/.vitepress/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,7 @@ export default withMermaid(defineConfig({
items: [
{ text: 'Effect', link: '/docs/integrations/effect' },
{ text: 'Evlog', link: '/docs/integrations/evlog' },
{ text: 'MCP', link: '/docs/integrations/mcp' },
{ text: 'NestJS', link: '/docs/integrations/nest' },
{ text: 'Next.js', link: '/docs/integrations/next' },
{ text: 'OpenTelemetry', link: '/docs/integrations/opentelemetry' },
Expand Down
209 changes: 209 additions & 0 deletions apps/content/docs/integrations/mcp.md
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
Comment thread
pullfrog[bot] marked this conversation as resolved.
Outdated
```

```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
Comment thread
mi3lix9 marked this conversation as resolved.

- 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.
75 changes: 75 additions & 0 deletions packages/mcp/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
{
"name": "@orpc/mcp",
Comment thread
mi3lix9 marked this conversation as resolved.
Outdated
"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"
}
}
1 change: 1 addition & 0 deletions packages/mcp/src/adapters/fetch/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './mcp-handler'
Loading
Loading