From 044d628a47f063bcbbd9adba7860542156a0c66e Mon Sep 17 00:00:00 2001 From: Clawd Date: Sun, 1 Mar 2026 10:15:14 -0800 Subject: feat(mcp): add tool discovery and per-tool permissions - Add MCP protocol client for tool discovery (initialize + tools/list) - Show discovered tools in settings UI with enable/disable checkboxes - Build explicit allowedTools list from enabled MCP tools - Remove bypassPermissions hack - now uses proper tool allowlisting - Format: mcp__servername__toolname for SDK allowedTools --- src/main/mcp.ts | 151 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 151 insertions(+) create mode 100644 src/main/mcp.ts (limited to 'src/main/mcp.ts') diff --git a/src/main/mcp.ts b/src/main/mcp.ts new file mode 100644 index 0000000..cfd3d26 --- /dev/null +++ b/src/main/mcp.ts @@ -0,0 +1,151 @@ +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(); + } + } +} -- cgit v1.2.3