Skip to content

Model Context Protocol (MCP)

@orpc/mcp turns an oRPC router into an MCP server, so the same procedures you already serve over RPC and OpenAPI can be called by MCP clients (Claude, ChatGPT, IDEs, agents).

Exposure is opt-in: only procedures annotated with the mcp() meta are visible to MCP clients. The procedure's .input() schema becomes the tool's JSON Schema inputSchema, and .output() becomes its outputSchema — reusing the same converters as @orpc/openapi.

Installation

sh
npm install @orpc/mcp@latest
sh
pnpm add @orpc/mcp@latest
sh
yarn add @orpc/mcp@latest
sh
bun add @orpc/mcp@latest

Annotate procedures

Use mcp() (or the mcp.tool / mcp.resource / mcp.prompt shorthands). MCP metadata is independent of openapi() — a procedure can carry both.

ts
import { 
os
} from '@orpc/server'
import {
mcp
} from '@orpc/mcp'
import * as
z
from 'zod'
// Tool (the default) — the model can call it export const
createPlanet
=
os
.
meta
(
mcp
.
tool
({
description
: 'Create a new planet',
annotations
: {
destructiveHint
: false },
})) .
input
(
z
.
object
({
name
:
z
.
string
(),
description
:
z
.
string
().
optional
() }))
.
output
(
z
.
object
({
id
:
z
.
string
(),
name
:
z
.
string
() }))
.
handler
(({
input
}) => ({
id
:
crypto
.
randomUUID
(),
name
:
input
.
name
}))
// Resource — read-only data, addressed by a URI template (vars map to input) export const
planet
=
os
.
meta
(
mcp
({
type
: 'resource',
uriTemplate
: 'planet://{id}',
mimeType
: 'application/json' }))
.
input
(
z
.
object
({
id
:
z
.
string
() }))
.
output
(
z
.
object
({
id
:
z
.
string
(),
name
:
z
.
string
() }))
.
handler
(({
input
}) => ({
id
:
input
.
id
,
name
: `Planet ${
input
.
id
}` }))
// Prompt — arguments come from .input(), messages from the handler's return export const
planTrip
=
os
.
meta
(
mcp
({
type
: 'prompt',
description
: 'Plan a vacation' }))
.
input
(
z
.
object
({
destination
:
z
.
string
(),
days
:
z
.
number
() }))
.
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' as
const
,
content
: {
type
: 'text' as
const
,
text
: `Plan ${
input
.
days
} days in ${
input
.
destination
}` } }],
})) export const
router
= {
createPlanet
,
planet
,
planTrip
}

mcp() meta options

FieldApplies toDescription
typeall'tool' (default), 'resource', or 'prompt'.
nameallIdentifier in the server. Defaults to the router path joined by _.
titleallHuman-readable display name.
descriptionallExplanation used by the model to decide when/how to use it.
annotationstoolreadOnlyHint, destructiveHint, idempotentHint, openWorldHint.
outputSchematoolEmit an MCP outputSchema from .output() (default: true when .output() is set).
uriresourceFixed URI of a static resource (e.g. config://app).
uriTemplateresourceTemplated URI (e.g. planet://{id}); variables bind to the procedure input.
mimeTyperesourceMIME type of the resource contents.

Serve it

MCPHandler speaks the MCP protocol (initialize, tools/list, tools/call, resources/*, prompts/*) over the Streamable HTTP transport (fetch/node) or stdio. Pass the schema converters for your validation library.

Streamable HTTP (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
}

Streamable HTTP (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 (local servers)

For MCP 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: {} })

One router, every surface

Because MCP exposure lives in procedure meta, a single router can be mounted on multiple handlers — RPC, OpenAPI, and MCP — at different paths over the same instance:

ts
export const handlers = {
  rpc: new RPCHandler(router), // /rpc  — typed oRPC clients
  openapi: new OpenAPIHandler(router), // /api  — REST + OpenAPI spec
  mcp: new MCPHandler(router), // /mcp  — MCP tools / resources / prompts
}

How it maps

oRPCMCP
.input() schematool inputSchema / prompt arguments / resource template variables
.output() schematool outputSchema (+ structuredContent) / prompt messages / resource contents
handler return valuetool content[] (+ structuredContent), resource contents[], or prompt messages[]
thrown errors.*() (typed)in-band tool result with isError: true (so the model can react)
ORPCError in a resource/promptJSON-RPC protocol error

Notes & limitations

  • Targets MCP protocol revision 2025-11-25 (negotiated at initialize; older revisions are accepted).
  • Server → client streaming (GET SSE), listChanged/subscribe notifications, sessions, and pagination cursors are not implemented yet.
  • Resource handlers should be side-effect free; only annotate read-only procedures as resources.

Released under the MIT License.