import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js' import type { ReclaimClient } from '@reclaimprotocol/client/api' import type { IOperationArgs, IOperationId } from '@reclaimprotocol/client/openapi' import { remapErrorAsResponse } from '../remap_errors.ts' import { MCP_TOOLS } from './operations.gen.ts' /** * The generated `MCP_TOOLS` are keyed by OpenAPI `operationId` (PascalCase, * For example, `GetMe`). MCP tools are conventionally snake_case, matching the * hand-written tools in this server. Convert the public tool name here while * still dispatching to the API with the original operationId — doing this in * the (non-generated) registration keeps it from being reverted on regen. */ function toSnakeCase(name: string): string { return name .replace(/([a-z0-9])([A-Z])/g, '$1_$2') .replace(/([A-Z]+)([A-Z][a-z])/g, '$1_$2') .toLowerCase() } export function registerTools(server: McpServer, client: ReclaimClient) { for(const [opId, shape] of Object.entries(MCP_TOOLS)) { // snake_case to match the hand-written tools. A name that collides with // a hand-written tool throws here at startup — intentional: hand-written // and generated names must stay disjoint (see start.ts), so a clash is a // bug to surface, not silently shadow. server.registerTool( toSnakeCase(opId), shape, async(args: unknown) => { try { const result = await client.call( opId as IOperationId, args as IOperationArgs, ) return { content: [{ type: 'text' as const, text: JSON.stringify(result.data, null, 2) }], } } catch(err) { return remapErrorAsResponse(err) } }, ) } }