From 08876f694db5e4aeace94a94e845fa2e4a04a7e9 Mon Sep 17 00:00:00 2001 From: David Kaya Date: Sat, 28 Mar 2026 20:00:53 +0100 Subject: [PATCH] fix: handle MCP tools with complex JSON schema \ in outputSchema The SDK's listTools() calls cacheToolMetadata() which compiles outputSchema validators. Servers with \ in their schemas (e.g., icm-mcp) cause a JSON schema resolution error. Fall back to a raw JSON-RPC tools/list request that skips schema compilation, since we only need tool names and descriptions for the approval pill. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/main/services/mcpToolProber.ts | 33 +++++++++++++++++++++++++++--- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/src/main/services/mcpToolProber.ts b/src/main/services/mcpToolProber.ts index 3f2f069..1029fca 100644 --- a/src/main/services/mcpToolProber.ts +++ b/src/main/services/mcpToolProber.ts @@ -123,12 +123,39 @@ async function probeWithTransport( try { await client.connect(transport); - const result = await client.listTools(); - return (result.tools ?? []) + // Use listTools() which validates schemas. If a tool has a complex + // outputSchema with $ref that the SDK can't resolve, fall back to + // a raw JSON-RPC request that skips schema compilation. + let rawTools: Array<{ name?: string; description?: string }>; + try { + const result = await client.listTools(); + rawTools = result.tools ?? []; + } catch { + // listTools failed (likely schema validation of outputSchema $ref). + // Send raw JSON-RPC and extract tool names without validation. + const response = await new Promise<{ tools?: Array<{ name?: string; description?: string }> }>((resolve, reject) => { + const id = Math.random().toString(36).slice(2); + const onMessage = (msg: { id?: string; result?: unknown; error?: unknown }) => { + if (msg.id !== id) return; + transport.onmessage = undefined; + if (msg.error) reject(new Error(JSON.stringify(msg.error))); + else resolve((msg.result ?? {}) as { tools?: Array<{ name?: string; description?: string }> }); + }; + const prevHandler = transport.onmessage; + transport.onmessage = (msg) => { + onMessage(msg as { id?: string; result?: unknown; error?: unknown }); + if (prevHandler) (prevHandler as (msg: unknown) => void)(msg); + }; + transport.send({ jsonrpc: '2.0', id, method: 'tools/list', params: {} }).catch(reject); + }); + rawTools = response.tools ?? []; + } + + return rawTools .filter((tool) => typeof tool.name === 'string' && tool.name.trim().length > 0) .map((tool) => ({ - name: tool.name.trim(), + name: tool.name!.trim(), description: typeof tool.description === 'string' && tool.description.trim().length > 0 ? tool.description.trim() : undefined,