UNPKG

mcp-uni

Version:

A unified gateway for managing multiple MCP servers

153 lines (152 loc) 5.25 kB
import { Client } from '@modelcontextprotocol/sdk/client/index.js'; import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js'; import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'; import z from 'zod'; import { sleep } from './utils/utils.js'; export const transportConfigSchema = z.union([ z.object({ type: z.literal('sse'), url: z.string(), query: z.record(z.string(), z.string()), headers: z.record(z.string(), z.string()), }), z.object({ type: z.literal('stdio'), command: z.string(), args: z.array(z.string()), env: z.record(z.string(), z.string()), cwd: z.string(), }), ]); class McpHost { clients = new Map(); constructor() { this.clients = new Map(); } async addClient(name, transportConfig) { const waitFor = 2500; const retries = 3; let count = 0; let retry = true; while (retry) { const { client, transport } = createClient(name, transportConfig); if (!client || !transport) { break; } try { await client.connect(transport, { timeout: 30000 }); let nextCursor; const tools = []; while (true) { const result = await client.listTools({ cursor: nextCursor }); nextCursor = result.nextCursor; tools.push(...result.tools); if (!nextCursor) { break; } } nextCursor = undefined; const prompts = []; while (true) { const result = await client.listPrompts({ cursor: nextCursor }); nextCursor = result.nextCursor; prompts.push(...result.prompts); if (!nextCursor) { break; } } nextCursor = undefined; const resources = []; while (true) { const result = await client.listResources({ cursor: nextCursor }); nextCursor = result.nextCursor; resources.push(...result.resources); if (!nextCursor) { break; } } nextCursor = undefined; const resourceTemplates = []; while (true) { const result = await client.listResourceTemplates({ cursor: nextCursor, }); nextCursor = result.nextCursor; resourceTemplates.push(...result.resourceTemplates); if (!nextCursor) { break; } } this.clients.set(name, { name, client, tools, resources, prompts, resourceTemplates, cleanup: async () => { await transport.close(); }, }); break; } catch (error) { console.error(`Failed to connect to ${name}:`, error); count++; retry = count < retries; if (retry) { try { await client.close(); } catch { } console.log(`Retry connection to ${name} in ${waitFor}ms (${count}/${retries})`); await sleep(waitFor); } } } } async removeClient(name) { const client = this.clients.get(name); if (client) { await client.cleanup(); this.clients.delete(name); } } getClient(name) { return this.clients.get(name); } getClients() { return Array.from(this.clients.values()); } async cleanup() { for (const client of this.clients.values()) { await client.cleanup(); } this.clients.clear(); } } const createClient = (name, transportConfig) => { const transport = createClientTransport(transportConfig); const client = new Client({ name, version: '' }); return { client, transport }; }; const createClientTransport = (transportConfig) => { if (transportConfig.type === 'sse') { const searchParams = new URLSearchParams(transportConfig.query); return new SSEClientTransport(new URL(`${transportConfig.url}?${searchParams.toString()}`), { requestInit: { headers: { 'Content-Type': 'text/event-stream', ...transportConfig.headers, }, }, }); } return new StdioClientTransport({ command: transportConfig.command, args: transportConfig.args, env: transportConfig.env, cwd: transportConfig.cwd, }); }; export const mcpHost = new McpHost();