├── .gitignore ├── LICENSE ├── README.md ├── biome.json ├── package.json ├── pnpm-lock.yaml ├── src ├── example.ts ├── index.ts ├── server.ts ├── tools.ts └── types.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | lib 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Jacob Lauritzen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # tRPC <-> MCP 2 | 3 | Serve tRPC routes via MCP. 4 | 5 | ## Usage 6 | 7 | ### 2. Add to meta 8 | ```ts 9 | import { initTRPC } from '@trpc/server'; 10 | import { type McpMeta } from 'trpc-to-openapi'; 11 | 12 | const t = initTRPC.meta().create(); 13 | ``` 14 | 15 | ### 3. Enable for routes 16 | ```ts 17 | export const appRouter = t.router({ 18 | sayHello: t.procedure 19 | .meta({ openapi: { enabled: true, description: 'Greet the user' } }) 20 | .input(z.object({ name: z.string() })) 21 | .output(z.object({ greeting: z.string() })) 22 | .query(({ input }) => { 23 | return { greeting: `Hello ${input.name}!` }; 24 | }); 25 | }); 26 | ``` 27 | ### 4. Serve 28 | ```ts 29 | import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; 30 | import { createMcpServer } from 'trpc-mcp'; 31 | 32 | const mcpServer = createMcpServer( 33 | { name: 'trpc-mcp-example', version: '0.0.1' }, 34 | appRouter, 35 | ); 36 | 37 | const transport = new StdioServerTransport(); 38 | await mcpServer.connect(transport); 39 | ``` 40 | -------------------------------------------------------------------------------- /biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", 3 | "vcs": { 4 | "enabled": false, 5 | "clientKind": "git", 6 | "useIgnoreFile": false 7 | }, 8 | "files": { 9 | "ignoreUnknown": false, 10 | "ignore": [] 11 | }, 12 | "formatter": { 13 | "enabled": true, 14 | "indentStyle": "space" 15 | }, 16 | "organizeImports": { 17 | "enabled": true 18 | }, 19 | "linter": { 20 | "enabled": true, 21 | "rules": { 22 | "recommended": true 23 | } 24 | }, 25 | "javascript": { 26 | "formatter": { 27 | "quoteStyle": "single" 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "trpc-mcp", 3 | "type": "module", 4 | "volta": { 5 | "node": "22.13.1", 6 | "pnpm": "10.2.0" 7 | }, 8 | "devDependencies": { 9 | "@biomejs/biome": "1.9.4", 10 | "@types/node": "22.13.1", 11 | "typescript": "^5.7.3", 12 | "zod": "^3.24.1" 13 | }, 14 | "peerDependencies": { 15 | "@trpc/server": "11.0.0-rc.502", 16 | "zod": "^3.24.1" 17 | }, 18 | "dependencies": { 19 | "@modelcontextprotocol/sdk": "^1.4.1", 20 | "zod-to-json-schema": "^3.24.1" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /pnpm-lock.yaml: -------------------------------------------------------------------------------- 1 | lockfileVersion: '9.0' 2 | 3 | settings: 4 | autoInstallPeers: true 5 | excludeLinksFromLockfile: false 6 | 7 | importers: 8 | 9 | .: 10 | dependencies: 11 | '@modelcontextprotocol/sdk': 12 | specifier: ^1.4.1 13 | version: 1.4.1 14 | '@trpc/server': 15 | specifier: 11.0.0-rc.502 16 | version: 11.0.0-rc.502 17 | zod-to-json-schema: 18 | specifier: ^3.24.1 19 | version: 3.24.1(zod@3.24.1) 20 | devDependencies: 21 | '@biomejs/biome': 22 | specifier: 1.9.4 23 | version: 1.9.4 24 | '@types/node': 25 | specifier: 22.13.1 26 | version: 22.13.1 27 | typescript: 28 | specifier: ^5.7.3 29 | version: 5.7.3 30 | zod: 31 | specifier: ^3.24.1 32 | version: 3.24.1 33 | 34 | packages: 35 | 36 | '@biomejs/biome@1.9.4': 37 | resolution: {integrity: sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog==} 38 | engines: {node: '>=14.21.3'} 39 | hasBin: true 40 | 41 | '@biomejs/cli-darwin-arm64@1.9.4': 42 | resolution: {integrity: sha512-bFBsPWrNvkdKrNCYeAp+xo2HecOGPAy9WyNyB/jKnnedgzl4W4Hb9ZMzYNbf8dMCGmUdSavlYHiR01QaYR58cw==} 43 | engines: {node: '>=14.21.3'} 44 | cpu: [arm64] 45 | os: [darwin] 46 | 47 | '@biomejs/cli-darwin-x64@1.9.4': 48 | resolution: {integrity: sha512-ngYBh/+bEedqkSevPVhLP4QfVPCpb+4BBe2p7Xs32dBgs7rh9nY2AIYUL6BgLw1JVXV8GlpKmb/hNiuIxfPfZg==} 49 | engines: {node: '>=14.21.3'} 50 | cpu: [x64] 51 | os: [darwin] 52 | 53 | '@biomejs/cli-linux-arm64-musl@1.9.4': 54 | resolution: {integrity: sha512-v665Ct9WCRjGa8+kTr0CzApU0+XXtRgwmzIf1SeKSGAv+2scAlW6JR5PMFo6FzqqZ64Po79cKODKf3/AAmECqA==} 55 | engines: {node: '>=14.21.3'} 56 | cpu: [arm64] 57 | os: [linux] 58 | 59 | '@biomejs/cli-linux-arm64@1.9.4': 60 | resolution: {integrity: sha512-fJIW0+LYujdjUgJJuwesP4EjIBl/N/TcOX3IvIHJQNsAqvV2CHIogsmA94BPG6jZATS4Hi+xv4SkBBQSt1N4/g==} 61 | engines: {node: '>=14.21.3'} 62 | cpu: [arm64] 63 | os: [linux] 64 | 65 | '@biomejs/cli-linux-x64-musl@1.9.4': 66 | resolution: {integrity: sha512-gEhi/jSBhZ2m6wjV530Yy8+fNqG8PAinM3oV7CyO+6c3CEh16Eizm21uHVsyVBEB6RIM8JHIl6AGYCv6Q6Q9Tg==} 67 | engines: {node: '>=14.21.3'} 68 | cpu: [x64] 69 | os: [linux] 70 | 71 | '@biomejs/cli-linux-x64@1.9.4': 72 | resolution: {integrity: sha512-lRCJv/Vi3Vlwmbd6K+oQ0KhLHMAysN8lXoCI7XeHlxaajk06u7G+UsFSO01NAs5iYuWKmVZjmiOzJ0OJmGsMwg==} 73 | engines: {node: '>=14.21.3'} 74 | cpu: [x64] 75 | os: [linux] 76 | 77 | '@biomejs/cli-win32-arm64@1.9.4': 78 | resolution: {integrity: sha512-tlbhLk+WXZmgwoIKwHIHEBZUwxml7bRJgk0X2sPyNR3S93cdRq6XulAZRQJ17FYGGzWne0fgrXBKpl7l4M87Hg==} 79 | engines: {node: '>=14.21.3'} 80 | cpu: [arm64] 81 | os: [win32] 82 | 83 | '@biomejs/cli-win32-x64@1.9.4': 84 | resolution: {integrity: sha512-8Y5wMhVIPaWe6jw2H+KlEm4wP/f7EW3810ZLmDlrEEy5KvBsb9ECEfu/kMWD484ijfQ8+nIi0giMgu9g1UAuuA==} 85 | engines: {node: '>=14.21.3'} 86 | cpu: [x64] 87 | os: [win32] 88 | 89 | '@modelcontextprotocol/sdk@1.4.1': 90 | resolution: {integrity: sha512-wS6YC4lkUZ9QpP+/7NBTlVNiEvsnyl0xF7rRusLF+RsG0xDPc/zWR7fEEyhKnnNutGsDAZh59l/AeoWGwIb1+g==} 91 | engines: {node: '>=18'} 92 | 93 | '@trpc/server@11.0.0-rc.502': 94 | resolution: {integrity: sha512-n6B8Q/UqF+hFXyCTXq9AWSn6EkXBbVo/Bs7/QzZxe5KD5CdnBomC7A1y6Qr+i0eiOWwTHJZQ0az+gJetb2fdxw==} 95 | 96 | '@types/node@22.13.1': 97 | resolution: {integrity: sha512-jK8uzQlrvXqEU91UxiK5J7pKHyzgnI1Qnl0QDHIgVGuolJhRb9EEl28Cj9b3rGR8B2lhFCtvIm5os8lFnO/1Ew==} 98 | 99 | bytes@3.1.2: 100 | resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} 101 | engines: {node: '>= 0.8'} 102 | 103 | content-type@1.0.5: 104 | resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} 105 | engines: {node: '>= 0.6'} 106 | 107 | depd@2.0.0: 108 | resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} 109 | engines: {node: '>= 0.8'} 110 | 111 | eventsource-parser@3.0.0: 112 | resolution: {integrity: sha512-T1C0XCUimhxVQzW4zFipdx0SficT651NnkR0ZSH3yQwh+mFMdLfgjABVi4YtMTtaL4s168593DaoaRLMqryavA==} 113 | engines: {node: '>=18.0.0'} 114 | 115 | eventsource@3.0.5: 116 | resolution: {integrity: sha512-LT/5J605bx5SNyE+ITBDiM3FxffBiq9un7Vx0EwMDM3vg8sWKx/tO2zC+LMqZ+smAM0F2hblaDZUVZF0te2pSw==} 117 | engines: {node: '>=18.0.0'} 118 | 119 | http-errors@2.0.0: 120 | resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} 121 | engines: {node: '>= 0.8'} 122 | 123 | iconv-lite@0.6.3: 124 | resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} 125 | engines: {node: '>=0.10.0'} 126 | 127 | inherits@2.0.4: 128 | resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} 129 | 130 | raw-body@3.0.0: 131 | resolution: {integrity: sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==} 132 | engines: {node: '>= 0.8'} 133 | 134 | safer-buffer@2.1.2: 135 | resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} 136 | 137 | setprototypeof@1.2.0: 138 | resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} 139 | 140 | statuses@2.0.1: 141 | resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} 142 | engines: {node: '>= 0.8'} 143 | 144 | toidentifier@1.0.1: 145 | resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} 146 | engines: {node: '>=0.6'} 147 | 148 | typescript@5.7.3: 149 | resolution: {integrity: sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==} 150 | engines: {node: '>=14.17'} 151 | hasBin: true 152 | 153 | undici-types@6.20.0: 154 | resolution: {integrity: sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==} 155 | 156 | unpipe@1.0.0: 157 | resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} 158 | engines: {node: '>= 0.8'} 159 | 160 | zod-to-json-schema@3.24.1: 161 | resolution: {integrity: sha512-3h08nf3Vw3Wl3PK+q3ow/lIil81IT2Oa7YpQyUUDsEWbXveMesdfK1xBd2RhCkynwZndAxixji/7SYJJowr62w==} 162 | peerDependencies: 163 | zod: ^3.24.1 164 | 165 | zod@3.24.1: 166 | resolution: {integrity: sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==} 167 | 168 | snapshots: 169 | 170 | '@biomejs/biome@1.9.4': 171 | optionalDependencies: 172 | '@biomejs/cli-darwin-arm64': 1.9.4 173 | '@biomejs/cli-darwin-x64': 1.9.4 174 | '@biomejs/cli-linux-arm64': 1.9.4 175 | '@biomejs/cli-linux-arm64-musl': 1.9.4 176 | '@biomejs/cli-linux-x64': 1.9.4 177 | '@biomejs/cli-linux-x64-musl': 1.9.4 178 | '@biomejs/cli-win32-arm64': 1.9.4 179 | '@biomejs/cli-win32-x64': 1.9.4 180 | 181 | '@biomejs/cli-darwin-arm64@1.9.4': 182 | optional: true 183 | 184 | '@biomejs/cli-darwin-x64@1.9.4': 185 | optional: true 186 | 187 | '@biomejs/cli-linux-arm64-musl@1.9.4': 188 | optional: true 189 | 190 | '@biomejs/cli-linux-arm64@1.9.4': 191 | optional: true 192 | 193 | '@biomejs/cli-linux-x64-musl@1.9.4': 194 | optional: true 195 | 196 | '@biomejs/cli-linux-x64@1.9.4': 197 | optional: true 198 | 199 | '@biomejs/cli-win32-arm64@1.9.4': 200 | optional: true 201 | 202 | '@biomejs/cli-win32-x64@1.9.4': 203 | optional: true 204 | 205 | '@modelcontextprotocol/sdk@1.4.1': 206 | dependencies: 207 | content-type: 1.0.5 208 | eventsource: 3.0.5 209 | raw-body: 3.0.0 210 | zod: 3.24.1 211 | zod-to-json-schema: 3.24.1(zod@3.24.1) 212 | 213 | '@trpc/server@11.0.0-rc.502': {} 214 | 215 | '@types/node@22.13.1': 216 | dependencies: 217 | undici-types: 6.20.0 218 | 219 | bytes@3.1.2: {} 220 | 221 | content-type@1.0.5: {} 222 | 223 | depd@2.0.0: {} 224 | 225 | eventsource-parser@3.0.0: {} 226 | 227 | eventsource@3.0.5: 228 | dependencies: 229 | eventsource-parser: 3.0.0 230 | 231 | http-errors@2.0.0: 232 | dependencies: 233 | depd: 2.0.0 234 | inherits: 2.0.4 235 | setprototypeof: 1.2.0 236 | statuses: 2.0.1 237 | toidentifier: 1.0.1 238 | 239 | iconv-lite@0.6.3: 240 | dependencies: 241 | safer-buffer: 2.1.2 242 | 243 | inherits@2.0.4: {} 244 | 245 | raw-body@3.0.0: 246 | dependencies: 247 | bytes: 3.1.2 248 | http-errors: 2.0.0 249 | iconv-lite: 0.6.3 250 | unpipe: 1.0.0 251 | 252 | safer-buffer@2.1.2: {} 253 | 254 | setprototypeof@1.2.0: {} 255 | 256 | statuses@2.0.1: {} 257 | 258 | toidentifier@1.0.1: {} 259 | 260 | typescript@5.7.3: {} 261 | 262 | undici-types@6.20.0: {} 263 | 264 | unpipe@1.0.0: {} 265 | 266 | zod-to-json-schema@3.24.1(zod@3.24.1): 267 | dependencies: 268 | zod: 3.24.1 269 | 270 | zod@3.24.1: {} 271 | -------------------------------------------------------------------------------- /src/example.ts: -------------------------------------------------------------------------------- 1 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; 2 | import { initTRPC } from "@trpc/server"; 3 | import { z } from "zod"; 4 | import { type McpMeta, createMcpServer } from "./index.js"; 5 | 6 | const t = initTRPC.meta().create(); /* 👈 */ 7 | 8 | const appRouter = t.router({ 9 | provideFeedback: t.procedure 10 | .meta({ 11 | mcp: { 12 | enabled: true, 13 | name: "give_feedback", 14 | description: "Give feedback to the tool provider", 15 | }, 16 | }) 17 | .input(z.object({ message: z.string() })) 18 | .query(async ({ input }) => { 19 | return { status: "success" }; 20 | }), 21 | userInfo: { 22 | get: t.procedure 23 | .meta({ mcp: { enabled: true } }) 24 | .input(z.object({ name: z.string() })) 25 | .query(async ({ input }) => { 26 | return { 27 | name: "John Doe", 28 | age: 30, 29 | email: "johndoe@example.com", 30 | }; 31 | }), 32 | address: { 33 | get: t.procedure 34 | .input(z.object({ city: z.string() })) 35 | .query(async ({ input }) => { 36 | return { 37 | city: "New York", 38 | country: "USA", 39 | }; 40 | }), 41 | }, 42 | }, 43 | }); 44 | 45 | const mcpServer = createMcpServer( 46 | { name: "trpc-mcp-example", version: "0.0.1" }, 47 | appRouter, 48 | ); 49 | 50 | const transport = new StdioServerTransport(); 51 | await mcpServer.connect(transport); 52 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./server.js"; 2 | export * from "./types.js"; 3 | -------------------------------------------------------------------------------- /src/server.ts: -------------------------------------------------------------------------------- 1 | import { Server } from "@modelcontextprotocol/sdk/server/index.js"; 2 | import { 3 | CallToolRequestSchema, 4 | type Implementation, 5 | ListToolsRequestSchema, 6 | } from "@modelcontextprotocol/sdk/types.js"; 7 | import type { 8 | AnyProcedure, 9 | AnyRootTypes, 10 | Router, 11 | RouterRecord, 12 | } from "@trpc/server/unstable-core-do-not-import"; 13 | 14 | import { tRcpRouterToMcpToolsList } from "./tools.js"; 15 | 16 | export function createMcpServer< 17 | TRoot extends AnyRootTypes, 18 | TRecord extends RouterRecord, 19 | >( 20 | implementation: Implementation, // awful type naming by Anthropic 21 | appRouter: Router, 22 | options?: { 23 | defaultNameSeparator?: string; 24 | }, 25 | ): Server { 26 | const nameSeparator = options?.defaultNameSeparator ?? "__"; 27 | const tools = tRcpRouterToMcpToolsList(appRouter, nameSeparator); 28 | const caller = appRouter.createCaller({}); 29 | 30 | const server = new Server(implementation, { 31 | capabilities: { 32 | tools: {}, 33 | }, 34 | }); 35 | 36 | // List available tools 37 | server.setRequestHandler(ListToolsRequestSchema, () => ({ tools })); 38 | 39 | // Handle tool execution 40 | server.setRequestHandler(CallToolRequestSchema, async (request) => { 41 | const { name, arguments: args } = request.params; 42 | 43 | // Find tool 44 | const tool = tools.find((t) => t.name === name); 45 | 46 | if (!tool) { 47 | return { content: [{ type: "text", text: "Could not find tool" }] }; 48 | } 49 | 50 | // Find procedure in router 51 | // @ts-expect-error wrangle types later 52 | const procedure: AnyProcedure = tool.pathInRouter.reduce( 53 | // @ts-expect-error wrangle types later 54 | (acc, part) => acc?.[part], 55 | caller, 56 | ); 57 | 58 | // @ts-expect-error wrangle types later 59 | return procedure(args); 60 | }); 61 | 62 | return server; 63 | } 64 | -------------------------------------------------------------------------------- /src/tools.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | AnyRootTypes, 3 | Router, 4 | RouterRecord, 5 | } from "@trpc/server/unstable-core-do-not-import"; 6 | import { 7 | type JsonSchema7ObjectType, 8 | type JsonSchema7StringType, 9 | type JsonSchema7Type, 10 | zodToJsonSchema, 11 | } from "zod-to-json-schema"; 12 | import { McpMeta } from "./types.js"; 13 | import { AnyZodObject, z } from "zod"; 14 | 15 | type Tool = { 16 | name: string; 17 | description: string; 18 | inputSchema?: JsonSchema7Type; 19 | pathInRouter: string[]; 20 | }; 21 | 22 | const mergeInputs = (inputParsers: AnyZodObject[]): AnyZodObject => { 23 | return inputParsers.reduce((acc, inputParser) => { 24 | return acc.merge(inputParser); 25 | }, z.object({})); 26 | }; 27 | 28 | function tRpcRouterRecordToMcpToolsList( 29 | routerRecord: RouterRecord, 30 | nameSeparator: string, 31 | currentPath: string[] = [], 32 | ): Tool[] { 33 | const tools: Tool[] = []; 34 | const procedures = Object.entries(routerRecord); 35 | 36 | for (const [name, value] of procedures) { 37 | if (value._def && "procedure" in value._def) { 38 | // @ts-expect-error The types seems to be incorrect 39 | const inputs = value._def.inputs as AnyZodObject[]; 40 | 41 | const inputSchema = inputs.length >= 2 ? mergeInputs(inputs) : inputs[0]; 42 | const meta = value._def.meta as McpMeta; 43 | if (!meta || !meta.mcp.enabled) { 44 | continue; 45 | } 46 | 47 | const pathInRouter = [...currentPath, name]; 48 | 49 | const tool: Tool = { 50 | name: 51 | meta.mcp.name ?? 52 | pathInRouter.reduce((acc, curr) => acc + nameSeparator + curr), 53 | description: meta.mcp.description ?? "", 54 | pathInRouter, 55 | }; 56 | 57 | if (inputSchema) { 58 | const jsonSchema = zodToJsonSchema(inputSchema) as 59 | | JsonSchema7ObjectType 60 | | JsonSchema7StringType; 61 | 62 | if (jsonSchema.type === "object") { 63 | const { type, properties = {}, required = [] } = jsonSchema; 64 | // MCP apparently only support object input types rn 65 | tool.inputSchema = { type, properties, required }; 66 | tools.push(tool); 67 | } else { 68 | console.error( 69 | "Trying to add MCP server for procedure with non-object input type", 70 | ); 71 | } 72 | } 73 | } else { 74 | const childTools = tRpcRouterRecordToMcpToolsList( 75 | value as RouterRecord, 76 | nameSeparator, 77 | [...currentPath, name], 78 | ); 79 | tools.push(...childTools); 80 | } 81 | } 82 | return tools; 83 | } 84 | 85 | export function tRcpRouterToMcpToolsList< 86 | TRoot extends AnyRootTypes, 87 | TRecord extends RouterRecord, 88 | >(router: Router, nameSeparator: string): Tool[] { 89 | return tRpcRouterRecordToMcpToolsList(router._def.record, nameSeparator); 90 | } 91 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | export const NAME_SEPARATOR = "_"; 2 | 3 | export type McpMeta = { 4 | mcp: { 5 | enabled: boolean; 6 | name?: string; 7 | description?: string; 8 | }; 9 | }; 10 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["ES2023"], 4 | "target": "ES2023", 5 | "outDir": "./lib/", 6 | "moduleResolution": "nodenext", 7 | "module": "nodenext", 8 | "declarationDir": "./lib/types", 9 | "strict": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "esModuleInterop": true, 12 | "skipLibCheck": true, 13 | "declaration": true, 14 | "declarationMap": true 15 | }, 16 | "files": ["./src/index.ts", "./src/example.ts"] 17 | } 18 | --------------------------------------------------------------------------------