Skip to content
You are reading the v2 docs, currently in beta.V1 docs
oRPC
Esc
↑↓navigate↵open⌘Jpreview
On this page

MCP Integration

Expose oRPC procedures as Model Context Protocol tools, reusing their OpenAPI metadata, schemas, middleware, and error handling.

Installation

npm install @orpc/mcp@beta @modelcontextprotocol/server
pnpm add @orpc/mcp@beta @modelcontextprotocol/server
yarn add @orpc/mcp@beta @modelcontextprotocol/server
bun add @orpc/mcp@beta @modelcontextprotocol/server
nub add @orpc/mcp@beta @modelcontextprotocol/server
aube add @orpc/mcp@beta @modelcontextprotocol/server

Creating a Registrar

McpRegistrar registers every procedure in a router as an MCP tool. Tool calls run through the same standard handler as the RPC and OpenAPI handlers.

import { McpServer } from '@modelcontextprotocol/server'
import { McpRegistrar } from '@orpc/mcp'

const registrar = new McpRegistrar(router)

const server = new McpServer({ name: 'planets', version: '1.0.0' })
await registrar.register(server, {
  context: {}, // provide initial context if needed
})

Connect each server to a transport as described in the MCP TypeScript SDK. register can be called on any number of servers, such as one per request.

Tool Definitions

Tool definitions derive from OpenAPI metadata:

Tool field Derived from
name operationId, otherwise the router path joined by _, such as planet_list
title summary
description description, otherwise summary
annotations method: GET, HEAD, and QUERY imply readOnlyHint, PUT and DELETE imply idempotentHint
import { openapi } from '@orpc/openapi'

const listPlanets = os
  .meta(openapi({
    method: 'GET',
    operationId: 'listPlanets',
    summary: 'List planets',
    description: 'List planets, newest first, optionally limited to a number of results.',
  }))
  .input(z.object({ limit: z.number().int().min(1).max(100).optional() }))
  .output(z.array(PlanetSchema))
  .handler(async ({ input }) => {
    // ...
  })

Tool Meta

Use mcp.tool metadata to override derived fields or set MCP-only ones:

import { mcp } from '@orpc/mcp'

const deletePlanet = os
  .meta(mcp.tool({
    name: 'delete_planet',
    title: 'Delete a planet',
    description: 'Permanently delete a planet. Ask the user to confirm first.',
    annotations: { destructiveHint: true, openWorldHint: false },
    icons: [{ src: 'https://example.com/planet.png', mimeType: 'image/png' }],
  }))
  .input(z.object({ id: z.number() }))
  .handler(async ({ input }) => {
    // ...
  })

Filtering Procedures

Use filter to skip procedures:

import { getOpenAPIMeta } from '@orpc/openapi'

const registrar = new McpRegistrar(router, {
  filter: (procedure, path) => !path.includes('internal') && !getOpenAPIMeta(procedure)?.deprecated,
})

Input and Output

JSON Schema Converters

Tools advertise JSON Schemas converted from your input and output schemas. Standard JSON Schema schemas, such as Zod v4.2+, need no converter; add one from the Zod, Valibot, or ArkType integrations for more types, like the OpenAPI Generator:

import { ZodToJsonSchemaConverter } from '@orpc/zod'

const registrar = new McpRegistrar(router, {
  converters: [new ZodToJsonSchemaConverter()],
})

Input

JSON cannot carry types such as Date or BigInt, so add the Smart Coercion Plugin with the same converters to coerce arguments before validation:

import { SmartCoercionHandlerPlugin } from '@orpc/json-schema'
import { ZodToJsonSchemaConverter } from '@orpc/zod'

const converters = [new ZodToJsonSchemaConverter()]

const registrar = new McpRegistrar(router, {
  converters,
  plugins: [new SmartCoercionHandlerPlugin({ converters })],
})

Tool arguments are always an object, so a non-object input, such as z.string(), is nested under an input argument.

Output

Outputs are serialized like OpenAPI responses. Pass your OpenAPI handler’s serializer as serializer to share custom types.

Output Tool result
undefined Empty content
String Text content with the string, and structured content
Other values Text content with the JSON, and structured content
Blob or File Image or audio content by MIME type, otherwise embedded resource content
Other values containing a Blob or File Like other values, with each file also returned as embedded resource content named by its path, such as file:///report/q3.pdf
ReadableStream Like a Blob, typed by the content-type response header, such as one set with the Response Headers Plugin
AsyncIteratorObject The last event, mapped as above, with every event reported as progress when requested
Error Tool execution error

A tool advertises an output schema only when every result has structured content matching it, so not for optional outputs or outputs containing a Blob or File. For an AsyncIteratorObject, its yield schema is advertised, so it must yield at least once.

Error Handling

Errors are returned as tool execution errors the model can read, with the ORPCError JSON as text:

{
  "defined": true,
  "code": "NOT_FOUND",
  "message": "Planet not found",
  "data": { "id": 42 }
}

Other errors become INTERNAL_SERVER_ERROR without their message. Use interceptors to log them.

Context

register takes the initial context for tool calls on that server, as a value or a function of the MCP request context, called on every tool call:

await registrar.register(server, {
  context: async ctx => ({ user: await findUser(ctx) }),
})

Plugins and Interceptors

McpRegistrar accepts the same interceptors and plugins as the RPC and OpenAPI handlers:

import { SmartCoercionHandlerPlugin } from '@orpc/json-schema'
import { onError } from '@orpc/server'
import { ZodToJsonSchemaConverter } from '@orpc/zod'

const converters = [new ZodToJsonSchemaConverter()]

const registrar = new McpRegistrar(router, {
  converters,
  plugins: [new SmartCoercionHandlerPlugin({ converters })],
  clientInterceptors: [
    onError((error) => {
      console.error(error)
    }),
  ],
})

Interceptors see each tool call as a POST to the procedure path, with the arguments as body and only the trace context and baggage as headers. Plugins working on the procedure call, such as Smart Coercion, Timeout, and logging, apply; HTTP plugins, such as CORS or compression, do nothing, and Rethrow is not supported.

Last updated on

Was this page helpful?