Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
```

```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.
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",

Copy link
Copy Markdown
Member

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.

"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