UNPKG

codemesh

Version:

Execute TypeScript code against multiple MCP servers, weaving them together into powerful workflows

225 lines (224 loc) โ€ข 8.58 kB
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'; import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'; import { Client } from '@modelcontextprotocol/sdk/client/index.js'; import { createServerObjectName, convertToolName, createSafeFunctionName } from './utils.js'; import { logger } from './logger.js'; export class RuntimeWrapper { connections = new Map(); transports = new Map(); tools = new Map(); serverConfigs = new Map(); constructor() { } /** * Register tools for runtime execution */ async registerTools(tools, serverConfigs) { logger.log(`๐Ÿ”ง Registering ${tools.length} tools for runtime execution...`); // Store server configurations for (const config of serverConfigs) { this.serverConfigs.set(config.id, config); } // Register each tool for (const tool of tools) { const serverConfig = this.serverConfigs.get(tool.serverId); if (!serverConfig) { logger.warn(`โš ๏ธ Server config not found for tool ${tool.name} (server: ${tool.serverId})`); continue; } const functionName = createSafeFunctionName(tool.name, tool.serverId); this.tools.set(functionName, { name: functionName, originalName: tool.name, serverId: tool.serverId, serverConfig, tool, }); logger.log(`โœ… Registered ${tool.name} โ†’ ${functionName}()`); } logger.log(`๐ŸŽฏ Runtime wrapper ready with ${this.tools.size} tools`); } /** * Get or create a connection to an MCP server */ async getConnection(serverId) { if (this.connections.has(serverId)) { return this.connections.get(serverId); } const serverConfig = this.serverConfigs.get(serverId); if (!serverConfig) { throw new Error(`Server configuration not found for ${serverId}`); } logger.log(`๐Ÿ”Œ Connecting to ${serverConfig.name} (${serverConfig.type})...`); let transport; if (serverConfig.type === 'http') { if (!serverConfig.url) { throw new Error(`HTTP server ${serverId} missing URL`); } logger.log(`๐Ÿ“ก HTTP connection to ${serverConfig.url}`); transport = new StreamableHTTPClientTransport(new URL(serverConfig.url)); } else if (serverConfig.type === 'stdio') { if (!serverConfig.command || serverConfig.command.length === 0) { throw new Error(`Stdio server ${serverId} missing command`); } logger.log(`๐Ÿ–ฅ๏ธ Spawning process: ${serverConfig.command.join(' ')}`); transport = new StdioClientTransport({ command: serverConfig.command[0], args: serverConfig.command.slice(1), cwd: serverConfig.cwd || process.cwd(), // SDK handles safe env vars via getDefaultEnvironment() + our config env: serverConfig.env, }); } else { throw new Error(`Unsupported server type: ${serverConfig.type} (server: ${serverId})`); } // Create client const client = new Client({ name: 'codemode-runtime-client', version: '1.0.0', }, { capabilities: { elicitation: {}, }, }); // Connect await client.connect(transport); // Store connections this.connections.set(serverId, client); this.transports.set(serverId, transport); logger.log(`โœ… Connected to ${serverConfig.name}`); return client; } /** * Execute a tool function */ async callTool(functionName, input) { const runtimeTool = this.tools.get(functionName); if (!runtimeTool) { throw new Error(`Tool function '${functionName}' not found. Available tools: ${Array.from(this.tools.keys()).join(', ')}`); } logger.log(`๐Ÿ”ง Executing ${functionName} (${runtimeTool.originalName} on ${runtimeTool.tool.serverName})`); try { // Get connection to the appropriate server const client = await this.getConnection(runtimeTool.serverId); // Call the tool on the MCP server const result = await client.callTool({ name: runtimeTool.originalName, arguments: (input || {}), }); logger.log(`โœ… ${functionName} executed successfully`); // Return the CallToolResult directly (it is now our ToolResult type) return result; } catch (error) { logger.error(`โŒ Error executing ${functionName}:`, error); return { content: [ { type: 'text', text: `Error executing ${functionName}: ${error instanceof Error ? error.message : String(error)}`, }, ], isError: true, }; } } /** * Create a tools object with callable functions (legacy flat API) */ createToolsObject() { const toolsObject = {}; for (const [functionName] of this.tools) { toolsObject[functionName] = async (input) => { return this.callTool(functionName, input); }; } return toolsObject; } /** * Create server objects with namespaced methods (new API) */ createServerObjects() { const serverObjects = {}; // Group tools by server const serverGroups = new Map(); for (const [functionName, runtimeTool] of this.tools) { const serverObjectName = createServerObjectName(runtimeTool.serverId); if (!serverGroups.has(serverObjectName)) { serverGroups.set(serverObjectName, []); } serverGroups.get(serverObjectName).push(runtimeTool); } // Create server objects with methods for (const [serverObjectName, serverTools] of serverGroups) { const serverObject = {}; for (const runtimeTool of serverTools) { const methodName = convertToolName(runtimeTool.originalName); const functionName = runtimeTool.name; // This is the flat function name serverObject[methodName] = async (input) => { return this.callTool(functionName, input); }; } serverObjects[serverObjectName] = serverObject; } return serverObjects; } /** * Create runtime API with namespaced server objects */ createRuntimeApi() { return this.createServerObjects(); } /** * Get registered tool names */ getToolNames() { return Array.from(this.tools.keys()); } /** * Get tool metadata for a specific function */ getToolMetadata(functionName) { return this.tools.get(functionName); } /** * Close all connections */ async cleanup() { logger.log(`๐Ÿงน Cleaning up runtime wrapper...`); // Close all transports for (const [serverId, transport] of this.transports) { try { await transport.close(); logger.log(`๐Ÿ”Œ Disconnected from ${serverId}`); } catch (error) { logger.error(`โŒ Error disconnecting from ${serverId}:`, error); } } // Clear all maps this.connections.clear(); this.transports.clear(); this.tools.clear(); this.serverConfigs.clear(); logger.log(`โœ… Runtime wrapper cleanup complete`); } /** * Get summary of registered tools */ getSummary() { const lines = [ `๐Ÿ”ง Runtime Wrapper Summary`, `๐Ÿ“Š ${this.tools.size} tools registered`, `๐ŸŒ ${this.serverConfigs.size} server configurations`, `๐Ÿ”Œ ${this.connections.size} active connections`, '', 'Registered Tools:', ]; for (const [functionName, runtimeTool] of this.tools) { lines.push(` ๐Ÿ”ง ${functionName}() โ†’ ${runtimeTool.originalName} on ${runtimeTool.tool.serverName}`); } return lines.join('\n'); } }