import { spawn, type ChildProcess } from "node:child_process"; import { randomUUID } from "node:crypto"; interface McpTool { name: string; description?: string; } interface McpServerConfig { type: "stdio" | "sse" | "http"; command?: string; args?: string[]; env?: Record; url?: string; } interface JsonRpcRequest { jsonrpc: "2.0"; id: string | number; method: string; params?: Record; } interface JsonRpcResponse { jsonrpc: "2.0"; id: string | number; result?: unknown; error?: { code: number; message: string }; } /** * Connect to an MCP server and discover its available tools. * Only supports stdio transport for now. */ export async function discoverMcpTools( config: McpServerConfig ): Promise<{ tools: McpTool[]; error?: string }> { if (config.type !== "stdio") { return { tools: [], error: "Only stdio transport is supported for tool discovery" }; } if (!config.command) { return { tools: [], error: "Command is required for stdio transport" }; } let process: ChildProcess | null = null; let buffer = ""; try { // Spawn the MCP server process process = spawn(config.command, config.args || [], { env: { ...globalThis.process.env, ...config.env }, stdio: ["pipe", "pipe", "pipe"], }); if (!process.stdin || !process.stdout) { return { tools: [], error: "Failed to create process streams" }; } const sendRequest = (request: JsonRpcRequest): void => { const json = JSON.stringify(request); process!.stdin!.write(json + "\n"); }; const waitForResponse = (id: string | number, timeoutMs = 10000): Promise => { return new Promise((resolve, reject) => { const timeout = setTimeout(() => { reject(new Error("Timeout waiting for MCP response")); }, timeoutMs); const onData = (chunk: Buffer) => { buffer += chunk.toString(); // Try to parse complete JSON-RPC messages (newline-delimited) const lines = buffer.split("\n"); buffer = lines.pop() || ""; // Keep incomplete line in buffer for (const line of lines) { if (!line.trim()) continue; try { const response = JSON.parse(line) as JsonRpcResponse; if (response.id === id) { clearTimeout(timeout); process!.stdout!.off("data", onData); resolve(response); return; } } catch { // Not valid JSON, skip } } }; process!.stdout!.on("data", onData); }); }; // Step 1: Initialize const initId = randomUUID(); sendRequest({ jsonrpc: "2.0", id: initId, method: "initialize", params: { protocolVersion: "2024-11-05", capabilities: {}, clientInfo: { name: "claude-flow", version: "1.0.0" }, }, }); const initResponse = await waitForResponse(initId); if (initResponse.error) { return { tools: [], error: `Initialize failed: ${initResponse.error.message}` }; } // Send initialized notification (no response expected) process.stdin.write(JSON.stringify({ jsonrpc: "2.0", method: "notifications/initialized", }) + "\n"); // Step 2: List tools const toolsId = randomUUID(); sendRequest({ jsonrpc: "2.0", id: toolsId, method: "tools/list", params: {}, }); const toolsResponse = await waitForResponse(toolsId); if (toolsResponse.error) { return { tools: [], error: `tools/list failed: ${toolsResponse.error.message}` }; } const result = toolsResponse.result as { tools?: Array<{ name: string; description?: string }> }; const tools: McpTool[] = (result.tools || []).map((t) => ({ name: t.name, description: t.description, })); return { tools }; } catch (err) { return { tools: [], error: String(err) }; } finally { // Clean up if (process) { process.kill(); } } }