tRPC to MCP package
Overview
You can skip this part if you know exactly why you're turning your tRPC endpoints into MCP tools. At its core, the Model Context Protocol (MCP) gives language models a context to interact with external systems by exposing well-defined tools. This isn't about wrapping API endpoints- it's about enabling AI to perform tasks with clarity and purpose. This library was created to convert only the tRPC procedures you explicitly choose into MCP tools. By doing so, it ensures that only the functionalities intended for controlled AI interaction are exposed, rather than indiscriminately converting all endpoints. Remember, for an endpoint to be effectively transformed into an MCP tool, it should be meticulously documented with clear descriptions, meaningful names, and well-defined inputs/outputs. This careful design is crucial because MCP tools are more than mere API endpoints - they form the operational contexts that enable language models to function reliably.
README
trpc-to-mcp
Turn the tRPC procedures you choose into MCP tools.
- Works with tRPC v11 and
@modelcontextprotocol/serverv2, the MCP TypeScript SDK - Serve tools over stdio or HTTP
- Use the tRPC context and middlewares on every tool call
- Transform outputs into text, images, or other MCP content
- Return structured content from procedures with an output schema
- Mount on Next.js and other frameworks with mcp-handler
Install
pnpm add trpc-to-mcp @modelcontextprotocol/server
@trpc/server 11 and @modelcontextprotocol/server 2 are peer dependencies. Install mcp-handler 2 only if you use the mcp-handler adapter. Node.js 20 or later is required.
Usage
- Add
McpMetato your tRPC instance.
import { initTRPC } from "@trpc/server";
import { type McpMeta } from "trpc-to-mcp";
const t = initTRPC.context().meta().create();
- Enable
mcpon the procedures you want as tools.
import { z } from "zod";
export const appRouter = t.router({
hello: t.procedure
.meta({
mcp: {
enabled: true,
description: "Say hello to someone",
// Optional. Defaults to the procedure path with dots replaced by
// underscores, e.g. "hello" or "user_byId".
name: "say_hello",
},
})
.input(z.object({ name: z.string().describe("Who to greet") }))
.query(({ input }) => `Hello ${input.name}`),
});
Inputs must be object schemas from a validator that implements Standard JSON Schema, such as zod 4.2 or later, ArkType, or Valibot with toStandardJsonSchema(). Chained .input() calls are merged into one tool input schema. Subscriptions can’t be tools.
tRPC validates input on every call, so your zod transforms and refinements run as usual. Errors thrown by a procedure, including TRPCError, come back to the client as a tool result with isError: true.
meta.mcp also takes title and annotations. Queries get readOnlyHint: true unless you set it yourself.
Serve your tools
createMcpServer returns an McpServer with your tools registered. You can register more tools, prompts, and resources on it.
Over stdio:
import { serveStdio } from "@modelcontextprotocol/server/stdio";
import { createMcpServer } from "trpc-to-mcp";
serveStdio(() =>
createMcpServer({ name: "my-app", version: "1.0.0" }, appRouter, { db }),
);
Over HTTP, with the SDK’s createMcpHandler on any runtime with web-standard Request and Response:
import { createMcpHandler } from "@modelcontextprotocol/server";
import { createMcpServer } from "trpc-to-mcp";
const handler = createMcpHandler(() =>
createMcpServer({ name: "my-app", version: "1.0.0" }, appRouter, { db }),
);
export default { fetch: handler.fetch };
To add tools to an McpServer you created yourself, call registerTrpcTools(server, appRouter, ctx).
Context
The third argument is the tRPC context. Pass a value, or a function that creates the context on each tool call. The function receives the MCP request context, so it can read the HTTP request and the auth info from your auth middleware.
createMcpServer({ name: "my-app", version: "1.0.0" }, appRouter, (ctx) => ({
db,
headers: ctx.http?.req?.headers,
auth: ctx.http?.authInfo,
}));
Tool calls run through your tRPC middlewares, so auth checks in a protectedProcedure apply to MCP clients too.
Transform outputs
By default a tool returns the procedure output as JSON text. To return something easier for a model to read, wrap the procedure with transformMcpProcedure and return MCP content blocks. The output argument is typed from the procedure.
import { transformMcpProcedure } from "trpc-to-mcp";
export const appRouter = t.router({
weather: transformMcpProcedure(
t.procedure
.meta({
mcp: { enabled: true, description: "Current weather in a city" },
})
.input(z.object({ city: z.string() }))
.query(({ input }) => getWeather(input.city)),
(output) => [
{ type: "text", text: `${output.city}: ${output.temperature}°C` },
{ type: "image", data: output.radarPng, mimeType: "image/png" },
],
),
});
The content types are text, image, audio, resource, and resource_link. The procedure still returns its normal output to tRPC clients.
Structured output
If a procedure has an .output() validator that describes an object, the tool advertises it as an outputSchema and returns the output as structuredContent next to the text content.
t.procedure
.meta({ mcp: { enabled: true, description: "Look up a user" } })
.input(z.object({ id: z.string() }))
.output(z.object({ id: z.string(), name: z.string() }))
.query(({ input }) => db.user.find(input.id));
mcp-handler
mcp-handler turns an MCP server into a (Request) => Promise handler for Next.js, Nuxt, SvelteKit, Hono, and other frameworks. Install mcp-handler 2, then:
// app/api/mcp/route.ts
import { trpcToMcpHandler } from "trpc-to-mcp/adapters/vercel-mcp-adapter";
const handler = trpcToMcpHandler(appRouter, createContext, {
// mcp-handler options
serverOptions: {
serverInfo: { name: "my-app", version: "1.0.0" },
verboseLogs: process.env.NODE_ENV === "development",
},
// Register extra tools, prompts, or resources
callback: (server) => {},
});
export { handler as GET, handler as POST };
mcp-handler serves requests without sessions, so there is no Redis setup and no basePath. Mount the handler at any route.
Use with better-auth
The @better-auth/mcp plugin makes your better-auth app an OAuth authorization server for MCP clients. Put your handler behind requireMcpAuth, and read the user ID from the access token claims.
// app/api/mcp/route.ts
import { requireMcpAuth } from "@better-auth/mcp";
import { createMcpHandler } from "@modelcontextprotocol/server";
import { createMcpServer } from "trpc-to-mcp";
export const POST = requireMcpAuth(auth, (request, claims) => {
const handler = createMcpHandler(
() =>
createMcpServer({ name: "my-app", version: "1.0.0" }, appRouter, {
db,
userId: claims.sub,
}),
// @better-auth/mcp recommends serving only the 2026-07-28 protocol.
{ legacy: "reject" },
);
return handler.fetch(request);
});
List tools without a server
extractToolsFromProcedures returns the tool definitions, including JSON input schemas, without creating a server.
import { extractToolsFromProcedures } from "trpc-to-mcp";
const tools = extractToolsFromProcedures(appRouter);
// [{ name: "say_hello", description: "...", path: "hello", type: "query", inputSchema: {...} }]
API
| Export | Import from | Description |
|---|---|---|
createMcpServer(info, router, ctx, options?) |
trpc-to-mcp |
Creates an McpServer with the router’s tools registered. |
registerTrpcTools(server, router, ctx) |
trpc-to-mcp |
Registers the router’s tools on an existing McpServer. |
extractToolsFromProcedures(router) |
trpc-to-mcp |
Returns the tool definitions without a server. |
transformMcpProcedure(procedure, transform) |
trpc-to-mcp |
Sets the MCP content a procedure’s tool returns. |
trpcToMcpHandler(router, ctx, options?) |
trpc-to-mcp/adapters/vercel-mcp-adapter |
Creates an mcp-handler request handler. |
McpMeta, McpContext, McpTool |
trpc-to-mcp |
Types for procedure meta, the context argument, and tool definitions. |
Upgrading from 1.x
- Replace
@modelcontextprotocol/sdkwith@modelcontextprotocol/server2, and upgrademcp-handlerto 2 if you use the adapter. Node.js 20 or later is required. - In
trpcToMcpHandler, removeconfig. MoveverboseLogsintoserverOptions, and mount the handler at the route you want. - Use zod 4.2 or later, or another validator with Standard JSON Schema support. zod is no longer a peer dependency.
- Replace
setRequestHandler(server.server, tools, caller)withregisterTrpcTools(server, appRouter, ctx).mergeInputsis removed. - In
McpTool,pathInRouteris nowpath, a dotted string, andtransformMcpProcedureis nowtransform. - Set output transforms with the
transformMcpProcedure()helper.meta.mcp.transformMcpProcedureis removed. - An enabled subscription, or an enabled procedure whose input is not an object, now throws when the server starts.
See the 2.0.0 release notes for the full list.
Development
pnpm install
pnpm test # vitest
pnpm lint # oxlint with type-aware rules
pnpm typecheck # tsc
pnpm format # oxfmt
pnpm build # tsdown
CI runs format:check, lint, typecheck, test, and build on every push.
To release, add a changeset with pnpm changeset in your pull request. After it merges, the publish workflow opens a “Version Packages” pull request that bumps the version and updates CHANGELOG.md. Merging that pull request publishes to npm with trusted publishing and creates a GitHub release. No npm token is stored in the repository.
Result
Now you can chill and add MCP to your VC-backed startup so your investors won’t worry, and profit
Twitter / X
https://x.com/iboughtbed - cracked 17 year old engineer from Kazakhstan :)
Install
This server does not publish a one-line install command.
Open the repository installation guide New to MCP? Start with the mcpskill.org guide →