Skip to content

Instantly share code, notes, and snippets.

@tunnckoCore
Created March 29, 2025 17:05
Show Gist options
  • Select an option

  • Save tunnckoCore/f98c037f46a23f2da9f3018a08af1355 to your computer and use it in GitHub Desktop.

Select an option

Save tunnckoCore/f98c037f46a23f2da9f3018a08af1355 to your computer and use it in GitHub Desktop.
converting oRPC routers to MCP tools - beginning, it's buggy.. can't figure out how to provide the procedure's inputSchema as mcp tool schema... should be `inputSchema.shape` but.. it errors
interface McpToolResult {
content: Array<{ type: string; text: string }>;
isError?: boolean;
}
interface OrpcProcedureMeta {
meta?: {
tool_name?: string;
};
route: {
summary?: string;
tags?: string[];
};
inputSchema: ZodSchema;
handler: (input: any) => Promise<any>;
}
// Helper to convert procedure result to MCP format
function toMcpResult(result: any): McpToolResult {
return {
content: [{ type: 'text', text: JSON.stringify(result) }],
};
}
// Helper to convert error to MCP format
function toMcpError(error: any): McpToolResult {
return {
content: [{ type: 'text', text: error instanceof Error ? error.message : String(error) }],
isError: true,
};
}
export function convertRouterToMcpTools(orpcRouter: any, options?: any) {
const tools = new Map();
const opts = {
context: {},
name: 'orpc router',
version: '0.0.1',
...options,
};
// Recursively walk through router to find procedures
function walkRouter(node: any, path: string[] = []) {
for (const [key, value] of Object.entries<any>(node)) {
if (value?.['~orpc']) {
// Found a procedure
const procedure = value['~orpc'] as OrpcProcedureMeta;
const toolName = procedure.meta?.tool_name || [...path, key].join('_');
tools.set(toolName, {
name: toolName,
path: (procedure.route as any)?.path?.slice(1),
description: procedure.route.summary || '',
inputSchema: procedure.inputSchema,
handler: async (input: any) => {
try {
const result = await procedure.handler({
context: opts.context,
input,
path: toolName.split('_'),
procedure: value['~orpc'],
});
return toMcpResult(result);
} catch (err) {
return toMcpError(err);
}
},
});
} else if (typeof value === 'object') {
// Continue walking
walkRouter(value, [...path, key]);
}
}
}
walkRouter(orpcRouter);
return { ...opts, tools };
}
// USAGE ---
const mcp = convertRouterToMcpTools(router, {
name: 'orpc-contract-first-playground-demo-mcp-server',
version: '0.1.0',
context: {
db: createFakeDB(),
},
});
// Create server instance
const server = new McpServer({
name: mcp.name,
version: mcp.version,
});
for (const [_, tool] of mcp.tools.entries()) {
server.tool(tool.name, tool.inputSchema.shape, tool.handler);
}
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error('orpc contract first playground mcp server running on stdio');
}
main().catch((err) => {
console.error('Fatal error in main():', err);
process.exit(1);
});
@tunnckoCore
Copy link
Author

As a reference, you may try this one by Cody: unnoq/orpc#300 (reply in thread)

It's made better and supports converting Zod v4 oRPC input schemas to Zod v3 that is required by the MCP SDK.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment