npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

trpc-to-mcp

v2.0.0

Published

Turn tRPC procedures into MCP tools

Readme

trpc-to-mcp

Turn the tRPC procedures you choose into MCP tools.

An MCP tool is something a language model decides to call on its own, so expose only the procedures you want a model to use. A model picks a tool based on its name, description, and input schema. Give each exposed procedure a clear name, a description of what it does and when to use it, and a .describe() on each input field.

Install

pnpm add trpc-to-mcp @modelcontextprotocol/server

@trpc/server 11 and @modelcontextprotocol/server 2 are peer dependencies. 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<Context>().meta<McpMeta>().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<Response> 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: {...} }]

Upgrading from 1.x

  • Replace @modelcontextprotocol/sdk with @modelcontextprotocol/server 2, and upgrade mcp-handler to 2 if you use the adapter.
  • 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.
  • Replace setRequestHandler(server.server, tools, caller) with registerTrpcTools(server, appRouter, ctx).
  • McpTool.pathInRouter is now McpTool.path, a dotted string.

See CHANGELOG.md for the full list.

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 :)