IT

iboughtbed/trpc-to-mcp

API development
36 stars 1 forks Quality 65 Trend 20MIT

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.

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

  1. Add McpMeta to your tRPC instance.
import { initTRPC } from "@trpc/server";
import { type McpMeta } from "trpc-to-mcp";

const t = initTRPC.context().meta().create();
  1. Enable mcp on 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/sdk with @modelcontextprotocol/server 2, and upgrade mcp-handler to 2 if you use the adapter. Node.js 20 or later is required.
  • In trpcToMcpHandler, remove config. Move verboseLogs into serverOptions, 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) with registerTrpcTools(server, appRouter, ctx). mergeInputs is removed.
  • In McpTool, pathInRouter is now path, a dotted string, and transformMcpProcedure is now transform.
  • Set output transforms with the transformMcpProcedure() helper. meta.mcp.transformMcpProcedure is 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 :)

View the full README on GitHub

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 →