import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { z } from 'zod'; import { ApiClient } from './api-client.js'; // ---------- Types ---------- /** Standard tool definition — maps directly to an API endpoint */ export interface ToolDef { name: string; description: string; inputSchema: Record; method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'; path: string; // e.g., '/api/campaigns/:id' tier: 1 | 2; } /** Composite tool — custom handler that chains multiple API calls */ export interface CompositeToolDef { name: string; description: string; inputSchema: Record; handler: (args: Record, apiClient: ApiClient) => Promise; tier: 3; } export type AnyToolDef = ToolDef | CompositeToolDef; /** Tool pack metadata */ export interface ToolPack { name: string; description: string; tools: AnyToolDef[]; } // ---------- Handler Factory ---------- /** * Create a generic handler for a standard ToolDef. * Substitutes path params (`:id` → `args.id`), then sends remaining * fields as query params (GET/DELETE) or body (POST/PUT/PATCH). */ function createHandler(tool: ToolDef, apiClient: ApiClient) { return async (args: Record): Promise => { // Clone args so we can remove path params without mutating const remaining = { ...args }; // Substitute path parameters let path = tool.path; const pathParamMatches = tool.path.match(/:(\w+)/g) || []; for (const match of pathParamMatches) { const key = match.slice(1); if (remaining[key] !== undefined) { path = path.replace(match, String(remaining[key])); delete remaining[key]; } else { throw new Error(`Missing required path parameter: ${key}`); } } // Strip undefined values from remaining params const cleanParams: Record = {}; for (const [k, v] of Object.entries(remaining)) { if (v !== undefined) cleanParams[k] = v; } const hasParams = Object.keys(cleanParams).length > 0; const result = await apiClient.request( tool.method, path, hasParams ? cleanParams : undefined ); return JSON.stringify(result, null, 2); }; } // ---------- Pack Manager ---------- export class ToolRegistry { private server: McpServer; private apiClient: ApiClient; private activePacks = new Set(); private availablePacks = new Map(); private registeredTools = new Set(); constructor(server: McpServer, apiClient: ApiClient) { this.server = server; this.apiClient = apiClient; } /** Register a batch of tools (Tier 1 core tools) */ registerTools(tools: AnyToolDef[]): void { for (const tool of tools) { this.registerSingleTool(tool); } } /** Register a tool pack (available for on-demand activation) */ registerPack(pack: ToolPack): void { this.availablePacks.set(pack.name, pack); } /** Activate a tool pack — registers its tools and notifies the client */ enablePack(packName: string): { success: boolean; message: string } { if (this.activePacks.has(packName)) { return { success: true, message: `Pack "${packName}" is already active` }; } const pack = this.availablePacks.get(packName); if (!pack) { const available = Array.from(this.availablePacks.keys()).join(', '); return { success: false, message: `Unknown pack "${packName}". Available: ${available}`, }; } for (const tool of pack.tools) { this.registerSingleTool(tool); } this.activePacks.add(packName); return { success: true, message: `Enabled "${packName}" pack — ${pack.tools.length} tools added: ${pack.tools.map((t) => t.name).join(', ')}`, }; } /** Get status of all available packs */ getPackStatus(): Array<{ name: string; description: string; active: boolean; toolCount: number }> { return Array.from(this.availablePacks.entries()).map(([name, pack]) => ({ name, description: pack.description, active: this.activePacks.has(name), toolCount: pack.tools.length, })); } // ---------- Internal ---------- private registerSingleTool(tool: AnyToolDef): void { if (this.registeredTools.has(tool.name)) return; // Prevent duplicate registration if (tool.tier === 3) { // Composite tool — custom handler const composite = tool as CompositeToolDef; const apiClient = this.apiClient; this.server.tool( composite.name, composite.description, composite.inputSchema, async (args) => ({ content: [{ type: 'text' as const, text: await composite.handler(args as Record, apiClient) }], }) ); } else { // Standard tool — generic HTTP handler const standard = tool as ToolDef; const handler = createHandler(standard, this.apiClient); this.server.tool( standard.name, standard.description, standard.inputSchema, async (args) => ({ content: [{ type: 'text' as const, text: await handler(args as Record) }], }) ); } this.registeredTools.add(tool.name); } }