@openeryc/pi-coding-agent 0.75.54 → 0.75.55

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/cli.d.ts.map +1 -1
  3. package/dist/cli.js +4 -0
  4. package/dist/cli.js.map +1 -1
  5. package/dist/core/agent-session.d.ts.map +1 -1
  6. package/dist/core/agent-session.js +8 -26
  7. package/dist/core/agent-session.js.map +1 -1
  8. package/dist/core/mcp/client.d.ts +16 -7
  9. package/dist/core/mcp/client.d.ts.map +1 -1
  10. package/dist/core/mcp/client.js +135 -107
  11. package/dist/core/mcp/client.js.map +1 -1
  12. package/dist/core/mcp/manager.d.ts +8 -1
  13. package/dist/core/mcp/manager.d.ts.map +1 -1
  14. package/dist/core/mcp/manager.js +56 -33
  15. package/dist/core/mcp/manager.js.map +1 -1
  16. package/dist/core/mcp/types.d.ts +0 -13
  17. package/dist/core/mcp/types.d.ts.map +1 -1
  18. package/dist/core/mcp/types.js.map +1 -1
  19. package/dist/core/package-manager.d.ts.map +1 -1
  20. package/dist/core/package-manager.js +3 -1
  21. package/dist/core/package-manager.js.map +1 -1
  22. package/dist/core/session-manager.d.ts.map +1 -1
  23. package/dist/core/session-manager.js +1 -1
  24. package/dist/core/session-manager.js.map +1 -1
  25. package/dist/modes/interactive/interactive-mode.d.ts +18 -0
  26. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  27. package/dist/modes/interactive/interactive-mode.js +359 -426
  28. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  29. package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
  30. package/dist/modes/rpc/rpc-client.js +5 -1
  31. package/dist/modes/rpc/rpc-client.js.map +1 -1
  32. package/dist/utils/sleep.d.ts.map +1 -1
  33. package/dist/utils/sleep.js +7 -3
  34. package/dist/utils/sleep.js.map +1 -1
  35. package/dist/utils/tools-manager.d.ts.map +1 -1
  36. package/dist/utils/tools-manager.js.map +1 -1
  37. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  38. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  39. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  40. package/examples/extensions/sandbox/package-lock.json +2 -2
  41. package/examples/extensions/sandbox/package.json +1 -1
  42. package/examples/extensions/with-deps/package-lock.json +2 -2
  43. package/examples/extensions/with-deps/package.json +1 -1
  44. package/npm-shrinkwrap.json +12 -12
  45. package/package.json +4 -4
@@ -6,24 +6,33 @@ export declare class MCPClient {
6
6
  private requestId;
7
7
  private pending;
8
8
  private sseEndpoint;
9
+ private sseReader;
9
10
  private timeoutMs;
10
11
  private _connected;
11
- /** Called when the connection is lost unexpectedly */
12
12
  onDisconnect: MCPDisconnectHandler | null;
13
13
  constructor(timeoutMs?: number);
14
14
  get isConnected(): boolean;
15
15
  connect(config: MCPServerConfig): Promise<void>;
16
- private handleDisconnect;
16
+ /** Reset all connection state (called on disconnect or intentional close).
17
+ * Rejects all in-flight pending promises so tool calls don't hang. */
18
+ private resetState;
19
+ private processSseLine;
17
20
  private connectStdio;
18
21
  private connectHttp;
19
- private readSSEEvent;
22
+ private startSseReader;
20
23
  private send;
21
24
  /**
22
- * Wraps a request with timeout and AbortSignal support.
23
- * Registers the pending handler so that response lines resolve it,
24
- * and rejects if the timeout fires or the caller's signal aborts.
25
+ * Register a pending request with timeout + abort signal support.
26
+ * The pending entry stays alive until the response line arrives
27
+ * (via stdio readline or SSE processSseLine), the timeout fires,
28
+ * or the caller's AbortSignal fires.
29
+ *
30
+ * IMPORTANT: for SSE transport, the response arrives asynchronously
31
+ * via startSseReader → processSseLine, so the pending entry here
32
+ * is resolved by that path. The writer callback sends the POST request
33
+ * and the SSE response is dispatched in processSseLine.
25
34
  */
26
- private sendWithTimeout;
35
+ private withTimeout;
27
36
  listTools(signal?: AbortSignal): Promise<{
28
37
  tools: MCPToolDefinition[];
29
38
  }>;
@@ -1 +1 @@
1
- {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../../src/core/mcp/client.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,iBAAiB,EAAE,WAAW,EAAE,eAAe,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAErG,YAAY,EAAE,eAAe,EAAE,CAAC;AAgBhC,MAAM,MAAM,oBAAoB,GAAG,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;AAE5D,qBAAa,SAAS;IACrB,OAAO,CAAC,OAAO,CAA6B;IAC5C,OAAO,CAAC,SAAS,CAAK;IACtB,OAAO,CAAC,OAAO,CAAqC;IACpD,OAAO,CAAC,WAAW,CAAuB;IAC1C,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,UAAU,CAAS;IAE3B,sDAAsD;IACtD,YAAY,EAAE,oBAAoB,GAAG,IAAI,CAAQ;IAEjD,YAAY,SAAS,CAAC,EAAE,MAAM,EAE7B;IAED,IAAI,WAAW,IAAI,OAAO,CAEzB;IAEK,OAAO,CAAC,MAAM,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CA2BpD;IAED,OAAO,CAAC,gBAAgB;YAcV,YAAY;YAmDZ,WAAW;YA2EX,YAAY;YA4BZ,IAAI;IAgDlB;;;;OAIG;IACH,OAAO,CAAC,eAAe;IAuDjB,SAAS,CAAC,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC;QAAE,KAAK,EAAE,iBAAiB,EAAE,CAAA;KAAE,CAAC,CAE7E;IAEK,aAAa,CAAC,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC;QAAE,SAAS,EAAE,WAAW,EAAE,CAAA;KAAE,CAAC,CAE/E;IAEK,YAAY,CACjB,GAAG,EAAE,MAAM,EACX,MAAM,CAAC,EAAE,WAAW,GAClB,OAAO,CAAC;QAAE,QAAQ,EAAE,KAAK,CAAC;YAAE,GAAG,EAAE,MAAM,CAAC;YAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;YAAC,IAAI,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC,CAAA;KAAE,CAAC,CAIjF;IAEK,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAE5G;IAEK,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC,CAehC;CACD","sourcesContent":["import { type ChildProcess, spawn } from \"child_process\";\nimport { createInterface } from \"readline\";\nimport type { MCPCallToolResult, MCPResource, MCPServerConfig, MCPToolDefinition } from \"./types.ts\";\n\nexport type { MCPServerConfig };\n\n/** Default MCP tool call timeout: 2 minutes */\nconst DEFAULT_MCP_TIMEOUT_MS = 120_000;\n/** MCP connection handshake timeout: 30 seconds */\nconst MCP_CONNECT_TIMEOUT_MS = 30_000;\n\ntype PendingRequest = { resolve: (v: unknown) => void; reject: (e: Error) => void };\n\nfunction resolveTransport(config: MCPServerConfig): \"stdio\" | \"sse\" | \"http\" {\n\tif (config.transport) return config.transport;\n\tif (config.command) return \"stdio\";\n\tif (config.url) return \"sse\";\n\treturn \"stdio\";\n}\n\nexport type MCPDisconnectHandler = (reason: string) => void;\n\nexport class MCPClient {\n\tprivate process: ChildProcess | null = null;\n\tprivate requestId = 0;\n\tprivate pending = new Map<number, PendingRequest>();\n\tprivate sseEndpoint: string | null = null;\n\tprivate timeoutMs: number;\n\tprivate _connected = false;\n\n\t/** Called when the connection is lost unexpectedly */\n\tonDisconnect: MCPDisconnectHandler | null = null;\n\n\tconstructor(timeoutMs?: number) {\n\t\tthis.timeoutMs = timeoutMs ?? DEFAULT_MCP_TIMEOUT_MS;\n\t}\n\n\tget isConnected(): boolean {\n\t\treturn this._connected;\n\t}\n\n\tasync connect(config: MCPServerConfig): Promise<void> {\n\t\tif (config.timeoutMs !== undefined) {\n\t\t\tthis.timeoutMs = config.timeoutMs;\n\t\t}\n\n\t\tconst transport = resolveTransport(config);\n\n\t\t// Use a shorter timeout for the connection handshake\n\t\t// (server startup via npx, dependency downloads, etc.)\n\t\tconst savedTimeout = this.timeoutMs;\n\t\tthis.timeoutMs = Math.min(this.timeoutMs, MCP_CONNECT_TIMEOUT_MS);\n\t\ttry {\n\t\t\tif (transport === \"stdio\") {\n\t\t\t\tawait this.connectStdio(config);\n\t\t\t} else {\n\t\t\t\tawait this.connectHttp(config, transport);\n\t\t\t}\n\t\t\tawait this.send(\"initialize\", {\n\t\t\t\tprotocolVersion: \"2024-11-05\",\n\t\t\t\tcapabilities: {},\n\t\t\t\tclientInfo: { name: \"pi\", version: \"1.0.0\" },\n\t\t\t});\n\t\t\tthis._connected = true;\n\t\t} finally {\n\t\t\t// Restore the configured timeout for subsequent tool calls\n\t\t\tthis.timeoutMs = savedTimeout;\n\t\t}\n\t}\n\n\tprivate handleDisconnect(reason: string): void {\n\t\tif (!this._connected) return;\n\t\tthis._connected = false;\n\t\tthis.pending.clear();\n\t\tthis.sseEndpoint = null;\n\t\tthis.process = null;\n\n\t\ttry {\n\t\t\tthis.onDisconnect?.(reason);\n\t\t} catch {\n\t\t\t// ignore errors from disconnect handler\n\t\t}\n\t}\n\n\tprivate async connectStdio(config: MCPServerConfig): Promise<void> {\n\t\tconst { command, args = [], env } = config;\n\t\tif (!command) throw new Error(\"MCP stdio server requires 'command'\");\n\n\t\tconst childEnv = env ? { ...process.env, ...env } : process.env;\n\t\treturn new Promise((resolve, reject) => {\n\t\t\tconst proc = spawn(command, args, { env: childEnv, stdio: [\"pipe\", \"pipe\", \"pipe\"], shell: false });\n\t\t\tthis.process = proc;\n\n\t\t\tconst onError = (error: Error) => {\n\t\t\t\treject(new Error(`Failed to spawn MCP server \"${command}\": ${error.message}`));\n\t\t\t};\n\t\t\tproc.on(\"error\", onError);\n\t\t\tproc.once(\"spawn\", () => proc.removeListener(\"error\", onError));\n\n\t\t\tif (!proc.stdout) {\n\t\t\t\treject(new Error(`MCP server \"${command}\" has no stdout`));\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst rl = createInterface({ input: proc.stdout, crlfDelay: Infinity });\n\t\t\trl.on(\"line\", (line: string) => {\n\t\t\t\ttry {\n\t\t\t\t\tconst res = JSON.parse(line);\n\t\t\t\t\tif (res.id !== undefined) {\n\t\t\t\t\t\tconst handler = this.pending.get(res.id);\n\t\t\t\t\t\tif (!handler) return;\n\t\t\t\t\t\tthis.pending.delete(res.id);\n\t\t\t\t\t\tif (res.error) {\n\t\t\t\t\t\t\thandler.reject(new Error(`MCP error: ${res.error.message}`));\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\thandler.resolve(res.result);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t} catch {\n\t\t\t\t\t/* non-JSON lines */\n\t\t\t\t}\n\t\t\t});\n\n\t\t\tproc.on(\"exit\", (code) => {\n\t\t\t\tfor (const [, h] of this.pending) {\n\t\t\t\t\th.reject(new Error(`MCP server \"${command}\" exited with code ${code}`));\n\t\t\t\t}\n\t\t\t\tthis.pending.clear();\n\t\t\t\tthis.handleDisconnect(`process exited with code ${code}`);\n\t\t\t});\n\n\t\t\tresolve();\n\t\t});\n\t}\n\n\tprivate async connectHttp(config: MCPServerConfig, transport: \"sse\" | \"http\"): Promise<void> {\n\t\tconst { url, headers = {} } = config;\n\t\tif (!url) throw new Error(\"MCP SSE/HTTP server requires 'url'\");\n\n\t\tif (transport === \"sse\") {\n\t\t\tconst res = await fetch(`${url}/sse`, { headers: { ...headers, Accept: \"text/event-stream\" } });\n\t\t\tif (!res.ok) throw new Error(`SSE connection failed: ${res.status} ${res.statusText}`);\n\n\t\t\tconst reader = res.body!.getReader();\n\t\t\tconst decoder = new TextDecoder();\n\t\t\tlet buffer = \"\";\n\n\t\t\tconst processChunk = async () => {\n\t\t\t\ttry {\n\t\t\t\t\twhile (true) {\n\t\t\t\t\t\tconst { done, value } = await reader.read();\n\t\t\t\t\t\tif (done) break;\n\t\t\t\t\t\tbuffer += decoder.decode(value, { stream: true });\n\n\t\t\t\t\t\tconst lines = buffer.split(\"\\n\");\n\t\t\t\t\t\tbuffer = lines.pop() ?? \"\";\n\n\t\t\t\t\t\tfor (const line of lines) {\n\t\t\t\t\t\t\tif (line.startsWith(\"data: \")) {\n\t\t\t\t\t\t\t\tconst data = line.slice(6);\n\t\t\t\t\t\t\t\tif (data.startsWith(\"{\") && this.sseEndpoint === null) {\n\t\t\t\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t\t\t\tconst parsed = JSON.parse(data);\n\t\t\t\t\t\t\t\t\t\tif (parsed.result) {\n\t\t\t\t\t\t\t\t\t\t\tconst handler = this.pending.get(parsed.id);\n\t\t\t\t\t\t\t\t\t\t\tif (handler) {\n\t\t\t\t\t\t\t\t\t\t\t\tthis.pending.delete(parsed.id);\n\t\t\t\t\t\t\t\t\t\t\t\tif (parsed.error) {\n\t\t\t\t\t\t\t\t\t\t\t\t\thandler.reject(new Error(`MCP error: ${parsed.error.message}`));\n\t\t\t\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\t\t\t\thandler.resolve(parsed.result);\n\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t} catch {\n\t\t\t\t\t\t\t\t\t\t/* skip */\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t} else if (data.startsWith(\"http\")) {\n\t\t\t\t\t\t\t\t\t// This is the endpoint URL\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t} else if (line.startsWith(\"event: endpoint\")) {\n\t\t\t\t\t\t\t\t// Next data line will be the endpoint URL\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t} catch (err) {\n\t\t\t\t\t// Stream error means connection lost\n\t\t\t\t\tthis.handleDisconnect(err instanceof Error ? err.message : String(err));\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\t// Stream ended normally\n\t\t\t\tthis.handleDisconnect(\"SSE stream ended\");\n\t\t\t};\n\n\t\t\t// Wait for endpoint event\n\t\t\tconst endpointLine = await this.readSSEEvent(reader, decoder);\n\t\t\tif (endpointLine) {\n\t\t\t\tthis.sseEndpoint = endpointLine.startsWith(\"/\") ? new URL(endpointLine, url).href : endpointLine;\n\t\t\t} else {\n\t\t\t\tthis.sseEndpoint = url;\n\t\t\t}\n\n\t\t\t// Start background reader\n\t\t\tprocessChunk();\n\t\t} else {\n\t\t\t// Streamable HTTP - POST to URL directly, server responds with JSON or upgrades to SSE\n\t\t\tthis.sseEndpoint = url;\n\t\t}\n\t}\n\n\tprivate async readSSEEvent(\n\t\treader: ReadableStreamDefaultReader<Uint8Array>,\n\t\tdecoder: { decode(b?: Uint8Array, o?: { stream?: boolean }): string },\n\t): Promise<string | null> {\n\t\tlet buffer = \"\";\n\t\tconst deadline = Date.now() + 10000;\n\n\t\twhile (Date.now() < deadline) {\n\t\t\tconst { value, done } = await reader.read();\n\t\t\tif (done) break;\n\t\t\tbuffer += decoder.decode(value, { stream: true });\n\n\t\t\tconst lines = buffer.split(\"\\n\");\n\t\t\tbuffer = lines.pop() ?? \"\";\n\n\t\t\tfor (let i = 0; i < lines.length; i++) {\n\t\t\t\tconst line = lines[i];\n\t\t\t\tif (line.startsWith(\"data: \") && lines[i - 1]?.startsWith(\"event: endpoint\")) {\n\t\t\t\t\treturn line.slice(6).trim();\n\t\t\t\t}\n\t\t\t\tif (line.startsWith(\"event: endpoint\") && lines[i + 1]?.startsWith(\"data: \")) {\n\t\t\t\t\treturn lines[i + 1].slice(6).trim();\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn null;\n\t}\n\n\tprivate async send(method: string, params?: Record<string, unknown>, signal?: AbortSignal): Promise<unknown> {\n\t\tconst id = ++this.requestId;\n\t\tconst body = JSON.stringify({ jsonrpc: \"2.0\", id, method, params });\n\n\t\tif (this.process?.stdin) {\n\t\t\treturn this.sendWithTimeout(id, body, signal, (_resolve, reject) => {\n\t\t\t\ttry {\n\t\t\t\t\tthis.process!.stdin!.write(`${body}\\n`);\n\t\t\t\t} catch (err) {\n\t\t\t\t\treject(err instanceof Error ? err : new Error(String(err)));\n\t\t\t\t}\n\t\t\t});\n\t\t}\n\n\t\tif (this.sseEndpoint) {\n\t\t\treturn this.sendWithTimeout(id, body, signal, async (resolve, reject) => {\n\t\t\t\ttry {\n\t\t\t\t\tconst timeoutMs = this.timeoutMs > 0 ? this.timeoutMs : undefined;\n\t\t\t\t\tconst res = await fetch(this.sseEndpoint!, {\n\t\t\t\t\t\tmethod: \"POST\",\n\t\t\t\t\t\theaders: { \"Content-Type\": \"application/json\" },\n\t\t\t\t\t\tbody,\n\t\t\t\t\t\tsignal: timeoutMs ? AbortSignal.timeout(timeoutMs) : undefined,\n\t\t\t\t\t});\n\t\t\t\t\tconst text = await res.text();\n\t\t\t\t\tif (res.headers.get(\"content-type\")?.includes(\"text/event-stream\")) {\n\t\t\t\t\t\t// SSE upgrade - response comes via event stream\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tconst parsed = JSON.parse(text);\n\t\t\t\t\tif (parsed.error) {\n\t\t\t\t\t\treject(new Error(`MCP error: ${parsed.error.message}`));\n\t\t\t\t\t} else {\n\t\t\t\t\t\tresolve(parsed.result);\n\t\t\t\t\t}\n\t\t\t\t} catch (err) {\n\t\t\t\t\tif (err instanceof Error && err.name === \"TimeoutError\") {\n\t\t\t\t\t\treject(new Error(`MCP request timed out after ${this.timeoutMs}ms`));\n\t\t\t\t\t} else {\n\t\t\t\t\t\treject(err instanceof Error ? err : new Error(String(err)));\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t});\n\t\t}\n\n\t\tthrow new Error(\"MCP client not connected\");\n\t}\n\n\t/**\n\t * Wraps a request with timeout and AbortSignal support.\n\t * Registers the pending handler so that response lines resolve it,\n\t * and rejects if the timeout fires or the caller's signal aborts.\n\t */\n\tprivate sendWithTimeout(\n\t\tid: number,\n\t\t_body: string,\n\t\tsignal: AbortSignal | undefined,\n\t\twriter: (resolve: (v: unknown) => void, reject: (e: Error) => void) => void,\n\t): Promise<unknown> {\n\t\treturn new Promise<unknown>((outerResolve, outerReject) => {\n\t\t\tlet timer: ReturnType<typeof setTimeout> | undefined;\n\t\t\tlet done = false;\n\n\t\t\tconst cleanup = () => {\n\t\t\t\tif (done) return;\n\t\t\t\tdone = true;\n\t\t\t\tif (timer) clearTimeout(timer);\n\t\t\t\tthis.pending.delete(id);\n\t\t\t};\n\n\t\t\tconst resolve = (v: unknown) => {\n\t\t\t\tcleanup();\n\t\t\t\touterResolve(v);\n\t\t\t};\n\n\t\t\tconst reject = (e: Error) => {\n\t\t\t\tcleanup();\n\t\t\t\touterReject(e);\n\t\t\t};\n\n\t\t\t// Timeout (unless disabled by setting timeoutMs = 0)\n\t\t\tif (this.timeoutMs > 0) {\n\t\t\t\ttimer = setTimeout(() => {\n\t\t\t\t\treject(new Error(`MCP request timed out after ${this.timeoutMs}ms`));\n\t\t\t\t}, this.timeoutMs);\n\t\t\t}\n\n\t\t\t// Caller abort signal\n\t\t\tif (signal) {\n\t\t\t\tif (signal.aborted) {\n\t\t\t\t\treject(new DOMException(\"MCP request cancelled\", \"AbortError\"));\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tsignal.addEventListener(\n\t\t\t\t\t\"abort\",\n\t\t\t\t\t() => {\n\t\t\t\t\t\treject(new DOMException(\"MCP request cancelled\", \"AbortError\"));\n\t\t\t\t\t},\n\t\t\t\t\t{ once: true },\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tthis.pending.set(id, { resolve, reject });\n\n\t\t\twriter(resolve, reject);\n\t\t});\n\t}\n\n\tasync listTools(signal?: AbortSignal): Promise<{ tools: MCPToolDefinition[] }> {\n\t\treturn (await this.send(\"tools/list\", undefined, signal)) as { tools: MCPToolDefinition[] };\n\t}\n\n\tasync listResources(signal?: AbortSignal): Promise<{ resources: MCPResource[] }> {\n\t\treturn (await this.send(\"resources/list\", undefined, signal)) as { resources: MCPResource[] };\n\t}\n\n\tasync readResource(\n\t\turi: string,\n\t\tsignal?: AbortSignal,\n\t): Promise<{ contents: Array<{ uri: string; mimeType?: string; text?: string }> }> {\n\t\treturn (await this.send(\"resources/read\", { uri }, signal)) as {\n\t\t\tcontents: Array<{ uri: string; mimeType?: string; text?: string }>;\n\t\t};\n\t}\n\n\tasync callTool(name: string, args: Record<string, unknown>, signal?: AbortSignal): Promise<MCPCallToolResult> {\n\t\treturn (await this.send(\"tools/call\", { name, arguments: args }, signal)) as MCPCallToolResult;\n\t}\n\n\tasync disconnect(): Promise<void> {\n\t\tthis._connected = false;\n\t\tthis.onDisconnect = null;\n\t\tconst proc = this.process;\n\t\tthis.process = null;\n\t\tthis.pending.clear();\n\t\tthis.sseEndpoint = null;\n\t\tif (proc && !proc.killed) {\n\t\t\tproc.kill();\n\t\t\tawait new Promise<void>((resolve) => {\n\t\t\t\tif (proc.exitCode !== null) return resolve();\n\t\t\t\tproc.on(\"exit\", resolve);\n\t\t\t\tsetTimeout(resolve, 3000);\n\t\t\t});\n\t\t}\n\t}\n}\n"]}
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../../src/core/mcp/client.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,iBAAiB,EAAE,WAAW,EAAE,eAAe,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAErG,YAAY,EAAE,eAAe,EAAE,CAAC;AAgBhC,MAAM,MAAM,oBAAoB,GAAG,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;AAE5D,qBAAa,SAAS;IACrB,OAAO,CAAC,OAAO,CAA6B;IAC5C,OAAO,CAAC,SAAS,CAAK;IACtB,OAAO,CAAC,OAAO,CAAmC;IAClD,OAAO,CAAC,WAAW,CAAuB;IAC1C,OAAO,CAAC,SAAS,CAAwD;IACzE,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,UAAU,CAAS;IAE3B,YAAY,EAAE,oBAAoB,GAAG,IAAI,CAAQ;IAEjD,YAAY,SAAS,CAAC,EAAE,MAAM,EAE7B;IAED,IAAI,WAAW,IAAI,OAAO,CAEzB;IAEK,OAAO,CAAC,MAAM,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CAuBpD;IAED;0EACsE;IACtE,OAAO,CAAC,UAAU;IAuBlB,OAAO,CAAC,cAAc;YAuBR,YAAY;YAiDZ,WAAW;YA+CX,cAAc;YA+Bd,IAAI;IAoDlB;;;;;;;;;;OAUG;IACH,OAAO,CAAC,WAAW;IA0Db,SAAS,CAAC,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC;QAAE,KAAK,EAAE,iBAAiB,EAAE,CAAA;KAAE,CAAC,CAE7E;IAEK,aAAa,CAAC,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC;QAAE,SAAS,EAAE,WAAW,EAAE,CAAA;KAAE,CAAC,CAE/E;IAEK,YAAY,CACjB,GAAG,EAAE,MAAM,EACX,MAAM,CAAC,EAAE,WAAW,GAClB,OAAO,CAAC;QAAE,QAAQ,EAAE,KAAK,CAAC;YAAE,GAAG,EAAE,MAAM,CAAC;YAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;YAAC,IAAI,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC,CAAA;KAAE,CAAC,CAIjF;IAEK,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAE5G;IAEK,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC,CA4BhC;CACD","sourcesContent":["import { type ChildProcess, spawn } from \"child_process\";\nimport { createInterface } from \"readline\";\nimport type { MCPCallToolResult, MCPResource, MCPServerConfig, MCPToolDefinition } from \"./types.ts\";\n\nexport type { MCPServerConfig };\n\nconst DEFAULT_MCP_TIMEOUT_MS = 120_000;\n// initialize handshake timeout matches the configured default tool timeout (2 min)\n// npx cold starts can take 30-60s just to download dependencies\nconst MCP_CONNECT_TIMEOUT_MS = 120_000;\n\ntype PendingEntry = { resolve: (v: unknown) => void; reject: (e: Error) => void };\n\nfunction resolveTransport(config: MCPServerConfig): \"stdio\" | \"sse\" | \"http\" {\n\tif (config.transport) return config.transport;\n\tif (config.command) return \"stdio\";\n\tif (config.url) return \"sse\";\n\treturn \"stdio\";\n}\n\nexport type MCPDisconnectHandler = (reason: string) => void;\n\nexport class MCPClient {\n\tprivate process: ChildProcess | null = null;\n\tprivate requestId = 0;\n\tprivate pending = new Map<number, PendingEntry>();\n\tprivate sseEndpoint: string | null = null;\n\tprivate sseReader: ReadableStreamDefaultReader<Uint8Array> | null = null;\n\tprivate timeoutMs: number;\n\tprivate _connected = false;\n\n\tonDisconnect: MCPDisconnectHandler | null = null;\n\n\tconstructor(timeoutMs?: number) {\n\t\tthis.timeoutMs = timeoutMs ?? DEFAULT_MCP_TIMEOUT_MS;\n\t}\n\n\tget isConnected(): boolean {\n\t\treturn this._connected;\n\t}\n\n\tasync connect(config: MCPServerConfig): Promise<void> {\n\t\tif (config.timeoutMs !== undefined) {\n\t\t\tthis.timeoutMs = config.timeoutMs;\n\t\t}\n\n\t\tconst transport = resolveTransport(config);\n\t\tconst savedTimeout = this.timeoutMs;\n\t\tthis.timeoutMs = Math.min(this.timeoutMs, MCP_CONNECT_TIMEOUT_MS);\n\t\ttry {\n\t\t\tif (transport === \"stdio\") {\n\t\t\t\tawait this.connectStdio(config);\n\t\t\t} else {\n\t\t\t\tawait this.connectHttp(config, transport);\n\t\t\t}\n\t\t\tawait this.send(\"initialize\", {\n\t\t\t\tprotocolVersion: \"2024-11-05\",\n\t\t\t\tcapabilities: {},\n\t\t\t\tclientInfo: { name: \"pi\", version: \"1.0.0\" },\n\t\t\t});\n\t\t\tthis._connected = true;\n\t\t} finally {\n\t\t\tthis.timeoutMs = savedTimeout;\n\t\t}\n\t}\n\n\t/** Reset all connection state (called on disconnect or intentional close).\n\t * Rejects all in-flight pending promises so tool calls don't hang. */\n\tprivate resetState(reason: string): void {\n\t\tif (!this._connected && !this.process && !this.sseReader) return;\n\t\tthis._connected = false;\n\t\tthis.process = null;\n\t\tthis.sseEndpoint = null;\n\t\tthis.sseReader = null;\n\n\t\t// MUST reject all pending promises before clearing, or tool calls freeze\n\t\t// until the 120s timeout fires. Collect entries first because rejecting\n\t\t// may trigger cleanup() which deletes from the map during iteration.\n\t\tconst entries = [...this.pending.values()];\n\t\tfor (const entry of entries) {\n\t\t\tentry.reject(new Error(`MCP disconnected: ${reason}`));\n\t\t}\n\t\tthis.pending.clear();\n\n\t\ttry {\n\t\t\tthis.onDisconnect?.(reason);\n\t\t} catch {\n\t\t\t/* ignore */\n\t\t}\n\t}\n\n\tprivate processSseLine(line: string): void {\n\t\tif (!line.startsWith(\"data: \")) return;\n\t\tconst data = line.slice(6);\n\t\tif (!data.startsWith(\"{\")) return;\n\n\t\ttry {\n\t\t\tconst parsed = JSON.parse(data);\n\t\t\tif (parsed.id === undefined) return;\n\t\t\tconst handler = this.pending.get(parsed.id);\n\t\t\tif (!handler) return;\n\t\t\tthis.pending.delete(parsed.id);\n\t\t\tif (parsed.error) {\n\t\t\t\thandler.reject(new Error(`MCP error: ${parsed.error.message}`));\n\t\t\t} else {\n\t\t\t\thandler.resolve(parsed.result);\n\t\t\t}\n\t\t} catch {\n\t\t\t/* skip */\n\t\t}\n\t}\n\n\t// ── stdio transport ──────────────────────────────────────────────\n\n\tprivate async connectStdio(config: MCPServerConfig): Promise<void> {\n\t\tconst { command, args = [], env } = config;\n\t\tif (!command) throw new Error(\"MCP stdio server requires 'command'\");\n\n\t\tconst childEnv = env ? { ...process.env, ...env } : process.env;\n\t\treturn new Promise<void>((resolve, reject) => {\n\t\t\tconst proc = spawn(command, args, { env: childEnv, stdio: [\"pipe\", \"pipe\", \"pipe\"], shell: false });\n\t\t\tthis.process = proc;\n\n\t\t\tconst onError = (error: Error) => {\n\t\t\t\treject(new Error(`Failed to spawn MCP server \"${command}\": ${error.message}`));\n\t\t\t};\n\t\t\tproc.on(\"error\", onError);\n\t\t\tproc.once(\"spawn\", () => proc.removeListener(\"error\", onError));\n\n\t\t\tif (!proc.stdout) {\n\t\t\t\treject(new Error(`MCP server \"${command}\" has no stdout`));\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst rl = createInterface({ input: proc.stdout, crlfDelay: Infinity });\n\t\t\trl.on(\"line\", (line: string) => {\n\t\t\t\ttry {\n\t\t\t\t\tconst res = JSON.parse(line);\n\t\t\t\t\tif (res.id !== undefined) {\n\t\t\t\t\t\tconst handler = this.pending.get(res.id);\n\t\t\t\t\t\tif (!handler) return;\n\t\t\t\t\t\tthis.pending.delete(res.id);\n\t\t\t\t\t\tif (res.error) {\n\t\t\t\t\t\t\thandler.reject(new Error(`MCP error: ${res.error.message}`));\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\thandler.resolve(res.result);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t} catch {\n\t\t\t\t\t/* non-JSON lines */\n\t\t\t\t}\n\t\t\t});\n\n\t\t\tproc.on(\"exit\", (code) => {\n\t\t\t\tthis.resetState(`process exited with code ${code}`);\n\t\t\t});\n\n\t\t\tresolve();\n\t\t});\n\t}\n\n\t// ── HTTP / SSE transport ─────────────────────────────────────────\n\n\tprivate async connectHttp(config: MCPServerConfig, transport: \"sse\" | \"http\"): Promise<void> {\n\t\tconst { url, headers = {} } = config;\n\t\tif (!url) throw new Error(\"MCP SSE/HTTP server requires 'url'\");\n\n\t\tif (transport === \"sse\") {\n\t\t\tconst res = await fetch(`${url}/sse`, { headers: { ...headers, Accept: \"text/event-stream\" } });\n\t\t\tif (!res.ok) throw new Error(`SSE connection failed: ${res.status} ${res.statusText}`);\n\n\t\t\tconst reader = res.body!.getReader();\n\t\t\tthis.sseReader = reader;\n\t\t\tconst decoder = new TextDecoder();\n\t\t\tlet buffer = \"\";\n\t\t\tlet endpointReceived = false;\n\n\t\t\t// Read SSE stream until we get the endpoint event or timeout\n\t\t\tconst deadline = Date.now() + MCP_CONNECT_TIMEOUT_MS;\n\t\t\twhile (Date.now() < deadline) {\n\t\t\t\tconst { done, value } = await reader.read();\n\t\t\t\tif (done) break;\n\t\t\t\tbuffer += decoder.decode(value, { stream: true });\n\n\t\t\t\tconst lines = buffer.split(\"\\n\");\n\t\t\t\tbuffer = lines.pop() ?? \"\";\n\n\t\t\t\tfor (let i = 0; i < lines.length; i++) {\n\t\t\t\t\tconst line = lines[i];\n\t\t\t\t\tif (line.startsWith(\"event: endpoint\") && i + 1 < lines.length && lines[i + 1].startsWith(\"data: \")) {\n\t\t\t\t\t\tthis.sseEndpoint = lines[i + 1].slice(6).trim();\n\t\t\t\t\t\tendpointReceived = true;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif (endpointReceived) break;\n\t\t\t}\n\n\t\t\tif (!this.sseEndpoint) {\n\t\t\t\tthis.sseEndpoint = url;\n\t\t\t}\n\n\t\t\t// Start background SSE reader for JSON-RPC responses\n\t\t\tthis.startSseReader(reader, decoder, buffer);\n\t\t} else {\n\t\t\tthis.sseEndpoint = url;\n\t\t}\n\t}\n\n\t/** Continuously read the SSE stream and dispatch JSON-RPC responses. */\n\tprivate async startSseReader(\n\t\treader: ReadableStreamDefaultReader<Uint8Array>,\n\t\tdecoder: { decode(b?: Uint8Array, o?: { stream?: boolean }): string },\n\t\tinitialBuffer: string,\n\t): Promise<void> {\n\t\tlet buffer = initialBuffer;\n\t\ttry {\n\t\t\twhile (true) {\n\t\t\t\tconst { done, value } = await reader.read();\n\t\t\t\tif (done) {\n\t\t\t\t\tthis.resetState(\"SSE stream ended\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tbuffer += decoder.decode(value, { stream: true });\n\n\t\t\t\tconst lines = buffer.split(\"\\n\");\n\t\t\t\tbuffer = lines.pop() ?? \"\";\n\n\t\t\t\tfor (const line of lines) {\n\t\t\t\t\tif (line.startsWith(\"data: \")) {\n\t\t\t\t\t\tthis.processSseLine(line);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t} catch (err) {\n\t\t\tthis.resetState(err instanceof Error ? err.message : String(err));\n\t\t}\n\t}\n\n\t// ── JSON-RPC send ────────────────────────────────────────────────\n\n\tprivate async send(method: string, params?: Record<string, unknown>, signal?: AbortSignal): Promise<unknown> {\n\t\tconst id = ++this.requestId;\n\t\tconst body = JSON.stringify({ jsonrpc: \"2.0\", id, method, params });\n\n\t\tif (this.process?.stdin) {\n\t\t\treturn this.withTimeout(id, signal, (_resolve, reject) => {\n\t\t\t\ttry {\n\t\t\t\t\tthis.process!.stdin!.write(`${body}\\n`);\n\t\t\t\t} catch (err) {\n\t\t\t\t\treject(err instanceof Error ? err : new Error(String(err)));\n\t\t\t\t}\n\t\t\t});\n\t\t}\n\n\t\tif (this.sseEndpoint) {\n\t\t\treturn this.withTimeout(id, signal, async (resolve, reject) => {\n\t\t\t\ttry {\n\t\t\t\t\tconst timeoutMs = this.timeoutMs > 0 ? this.timeoutMs : undefined;\n\t\t\t\t\tconst res = await fetch(this.sseEndpoint!, {\n\t\t\t\t\t\tmethod: \"POST\",\n\t\t\t\t\t\theaders: { \"Content-Type\": \"application/json\" },\n\t\t\t\t\t\tbody,\n\t\t\t\t\t\tsignal: timeoutMs ? AbortSignal.timeout(timeoutMs) : undefined,\n\t\t\t\t\t});\n\t\t\t\t\tconst text = await res.text();\n\n\t\t\t\t\t// SSE transport: response comes via SSE stream, not HTTP response body\n\t\t\t\t\tif (res.headers.get(\"content-type\")?.includes(\"text/event-stream\")) {\n\t\t\t\t\t\t// Response will be delivered via startSseReader → processSseLine\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\n\t\t\t\t\t// Streamable HTTP / direct response\n\t\t\t\t\tconst parsed = JSON.parse(text);\n\t\t\t\t\tif (parsed.error) {\n\t\t\t\t\t\treject(new Error(`MCP error: ${parsed.error.message}`));\n\t\t\t\t\t} else {\n\t\t\t\t\t\tresolve(parsed.result);\n\t\t\t\t\t}\n\t\t\t\t} catch (err) {\n\t\t\t\t\tif (err instanceof Error && err.name === \"TimeoutError\") {\n\t\t\t\t\t\treject(new Error(`MCP request timed out after ${this.timeoutMs}ms`));\n\t\t\t\t\t} else {\n\t\t\t\t\t\treject(err instanceof Error ? err : new Error(String(err)));\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t});\n\t\t}\n\n\t\tthrow new Error(\"MCP client not connected\");\n\t}\n\n\t/**\n\t * Register a pending request with timeout + abort signal support.\n\t * The pending entry stays alive until the response line arrives\n\t * (via stdio readline or SSE processSseLine), the timeout fires,\n\t * or the caller's AbortSignal fires.\n\t *\n\t * IMPORTANT: for SSE transport, the response arrives asynchronously\n\t * via startSseReader → processSseLine, so the pending entry here\n\t * is resolved by that path. The writer callback sends the POST request\n\t * and the SSE response is dispatched in processSseLine.\n\t */\n\tprivate withTimeout(\n\t\tid: number,\n\t\tsignal: AbortSignal | undefined,\n\t\twriter: (resolve: (v: unknown) => void, reject: (e: Error) => void) => void,\n\t): Promise<unknown> {\n\t\treturn new Promise<unknown>((outerResolve, outerReject) => {\n\t\t\tlet timer: ReturnType<typeof setTimeout> | undefined;\n\t\t\tlet resolved = false;\n\n\t\t\tconst cleanup = () => {\n\t\t\t\tif (resolved) return;\n\t\t\t\tresolved = true;\n\t\t\t\tif (timer) clearTimeout(timer);\n\t\t\t\tif (signal) {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tsignal.removeEventListener(\"abort\", onAbort);\n\t\t\t\t\t} catch {\n\t\t\t\t\t\t/* ignore */\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tthis.pending.delete(id);\n\t\t\t};\n\n\t\t\tconst resolve = (v: unknown) => {\n\t\t\t\tcleanup();\n\t\t\t\touterResolve(v);\n\t\t\t};\n\n\t\t\tconst reject = (e: Error) => {\n\t\t\t\tcleanup();\n\t\t\t\touterReject(e);\n\t\t\t};\n\n\t\t\tconst onAbort = () => {\n\t\t\t\treject(new DOMException(\"MCP request cancelled\", \"AbortError\"));\n\t\t\t};\n\n\t\t\tif (this.timeoutMs > 0) {\n\t\t\t\ttimer = setTimeout(() => {\n\t\t\t\t\treject(new Error(`MCP request timed out after ${this.timeoutMs}ms`));\n\t\t\t\t}, this.timeoutMs);\n\t\t\t}\n\n\t\t\tif (signal) {\n\t\t\t\tif (signal.aborted) {\n\t\t\t\t\treject(new DOMException(\"MCP request cancelled\", \"AbortError\"));\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tsignal.addEventListener(\"abort\", onAbort, { once: true });\n\t\t\t}\n\n\t\t\tthis.pending.set(id, { resolve, reject });\n\t\t\twriter(resolve, reject);\n\t\t});\n\t}\n\n\t// ── Public API ───────────────────────────────────────────────────\n\n\tasync listTools(signal?: AbortSignal): Promise<{ tools: MCPToolDefinition[] }> {\n\t\treturn (await this.send(\"tools/list\", undefined, signal)) as { tools: MCPToolDefinition[] };\n\t}\n\n\tasync listResources(signal?: AbortSignal): Promise<{ resources: MCPResource[] }> {\n\t\treturn (await this.send(\"resources/list\", undefined, signal)) as { resources: MCPResource[] };\n\t}\n\n\tasync readResource(\n\t\turi: string,\n\t\tsignal?: AbortSignal,\n\t): Promise<{ contents: Array<{ uri: string; mimeType?: string; text?: string }> }> {\n\t\treturn (await this.send(\"resources/read\", { uri }, signal)) as {\n\t\t\tcontents: Array<{ uri: string; mimeType?: string; text?: string }>;\n\t\t};\n\t}\n\n\tasync callTool(name: string, args: Record<string, unknown>, signal?: AbortSignal): Promise<MCPCallToolResult> {\n\t\treturn (await this.send(\"tools/call\", { name, arguments: args }, signal)) as MCPCallToolResult;\n\t}\n\n\tasync disconnect(): Promise<void> {\n\t\tthis.onDisconnect = null;\n\t\tconst proc = this.process;\n\t\tconst reader = this.sseReader;\n\t\tthis.process = null;\n\t\tthis.sseReader = null;\n\t\tthis.sseEndpoint = null;\n\t\tthis._connected = false;\n\t\tfor (const [, entry] of this.pending) {\n\t\t\tentry.reject(new Error(\"MCP client disconnected\"));\n\t\t}\n\t\tthis.pending.clear();\n\n\t\tif (reader) {\n\t\t\ttry {\n\t\t\t\tawait reader.cancel();\n\t\t\t} catch {\n\t\t\t\t/* ignore */\n\t\t\t}\n\t\t}\n\t\tif (proc && !proc.killed) {\n\t\t\tproc.kill();\n\t\t\tawait new Promise<void>((resolve) => {\n\t\t\t\tif (proc.exitCode !== null) return resolve();\n\t\t\t\tproc.on(\"exit\", resolve);\n\t\t\t\tsetTimeout(resolve, 3000);\n\t\t\t});\n\t\t}\n\t}\n}\n"]}
@@ -1,9 +1,9 @@
1
1
  import { spawn } from "child_process";
2
2
  import { createInterface } from "readline";
3
- /** Default MCP tool call timeout: 2 minutes */
4
3
  const DEFAULT_MCP_TIMEOUT_MS = 120_000;
5
- /** MCP connection handshake timeout: 30 seconds */
6
- const MCP_CONNECT_TIMEOUT_MS = 30_000;
4
+ // initialize handshake timeout matches the configured default tool timeout (2 min)
5
+ // npx cold starts can take 30-60s just to download dependencies
6
+ const MCP_CONNECT_TIMEOUT_MS = 120_000;
7
7
  function resolveTransport(config) {
8
8
  if (config.transport)
9
9
  return config.transport;
@@ -18,9 +18,9 @@ export class MCPClient {
18
18
  requestId = 0;
19
19
  pending = new Map();
20
20
  sseEndpoint = null;
21
+ sseReader = null;
21
22
  timeoutMs;
22
23
  _connected = false;
23
- /** Called when the connection is lost unexpectedly */
24
24
  onDisconnect = null;
25
25
  constructor(timeoutMs) {
26
26
  this.timeoutMs = timeoutMs ?? DEFAULT_MCP_TIMEOUT_MS;
@@ -33,8 +33,6 @@ export class MCPClient {
33
33
  this.timeoutMs = config.timeoutMs;
34
34
  }
35
35
  const transport = resolveTransport(config);
36
- // Use a shorter timeout for the connection handshake
37
- // (server startup via npx, dependency downloads, etc.)
38
36
  const savedTimeout = this.timeoutMs;
39
37
  this.timeoutMs = Math.min(this.timeoutMs, MCP_CONNECT_TIMEOUT_MS);
40
38
  try {
@@ -52,24 +50,59 @@ export class MCPClient {
52
50
  this._connected = true;
53
51
  }
54
52
  finally {
55
- // Restore the configured timeout for subsequent tool calls
56
53
  this.timeoutMs = savedTimeout;
57
54
  }
58
55
  }
59
- handleDisconnect(reason) {
60
- if (!this._connected)
56
+ /** Reset all connection state (called on disconnect or intentional close).
57
+ * Rejects all in-flight pending promises so tool calls don't hang. */
58
+ resetState(reason) {
59
+ if (!this._connected && !this.process && !this.sseReader)
61
60
  return;
62
61
  this._connected = false;
63
- this.pending.clear();
64
- this.sseEndpoint = null;
65
62
  this.process = null;
63
+ this.sseEndpoint = null;
64
+ this.sseReader = null;
65
+ // MUST reject all pending promises before clearing, or tool calls freeze
66
+ // until the 120s timeout fires. Collect entries first because rejecting
67
+ // may trigger cleanup() which deletes from the map during iteration.
68
+ const entries = [...this.pending.values()];
69
+ for (const entry of entries) {
70
+ entry.reject(new Error(`MCP disconnected: ${reason}`));
71
+ }
72
+ this.pending.clear();
66
73
  try {
67
74
  this.onDisconnect?.(reason);
68
75
  }
69
76
  catch {
70
- // ignore errors from disconnect handler
77
+ /* ignore */
78
+ }
79
+ }
80
+ processSseLine(line) {
81
+ if (!line.startsWith("data: "))
82
+ return;
83
+ const data = line.slice(6);
84
+ if (!data.startsWith("{"))
85
+ return;
86
+ try {
87
+ const parsed = JSON.parse(data);
88
+ if (parsed.id === undefined)
89
+ return;
90
+ const handler = this.pending.get(parsed.id);
91
+ if (!handler)
92
+ return;
93
+ this.pending.delete(parsed.id);
94
+ if (parsed.error) {
95
+ handler.reject(new Error(`MCP error: ${parsed.error.message}`));
96
+ }
97
+ else {
98
+ handler.resolve(parsed.result);
99
+ }
100
+ }
101
+ catch {
102
+ /* skip */
71
103
  }
72
104
  }
105
+ // ── stdio transport ──────────────────────────────────────────────
73
106
  async connectStdio(config) {
74
107
  const { command, args = [], env } = config;
75
108
  if (!command)
@@ -109,15 +142,12 @@ export class MCPClient {
109
142
  }
110
143
  });
111
144
  proc.on("exit", (code) => {
112
- for (const [, h] of this.pending) {
113
- h.reject(new Error(`MCP server "${command}" exited with code ${code}`));
114
- }
115
- this.pending.clear();
116
- this.handleDisconnect(`process exited with code ${code}`);
145
+ this.resetState(`process exited with code ${code}`);
117
146
  });
118
147
  resolve();
119
148
  });
120
149
  }
150
+ // ── HTTP / SSE transport ─────────────────────────────────────────
121
151
  async connectHttp(config, transport) {
122
152
  const { url, headers = {} } = config;
123
153
  if (!url)
@@ -127,101 +157,70 @@ export class MCPClient {
127
157
  if (!res.ok)
128
158
  throw new Error(`SSE connection failed: ${res.status} ${res.statusText}`);
129
159
  const reader = res.body.getReader();
160
+ this.sseReader = reader;
130
161
  const decoder = new TextDecoder();
131
162
  let buffer = "";
132
- const processChunk = async () => {
133
- try {
134
- while (true) {
135
- const { done, value } = await reader.read();
136
- if (done)
137
- break;
138
- buffer += decoder.decode(value, { stream: true });
139
- const lines = buffer.split("\n");
140
- buffer = lines.pop() ?? "";
141
- for (const line of lines) {
142
- if (line.startsWith("data: ")) {
143
- const data = line.slice(6);
144
- if (data.startsWith("{") && this.sseEndpoint === null) {
145
- try {
146
- const parsed = JSON.parse(data);
147
- if (parsed.result) {
148
- const handler = this.pending.get(parsed.id);
149
- if (handler) {
150
- this.pending.delete(parsed.id);
151
- if (parsed.error) {
152
- handler.reject(new Error(`MCP error: ${parsed.error.message}`));
153
- }
154
- else {
155
- handler.resolve(parsed.result);
156
- }
157
- }
158
- }
159
- }
160
- catch {
161
- /* skip */
162
- }
163
- }
164
- else if (data.startsWith("http")) {
165
- // This is the endpoint URL
166
- }
167
- }
168
- else if (line.startsWith("event: endpoint")) {
169
- // Next data line will be the endpoint URL
170
- }
171
- }
163
+ let endpointReceived = false;
164
+ // Read SSE stream until we get the endpoint event or timeout
165
+ const deadline = Date.now() + MCP_CONNECT_TIMEOUT_MS;
166
+ while (Date.now() < deadline) {
167
+ const { done, value } = await reader.read();
168
+ if (done)
169
+ break;
170
+ buffer += decoder.decode(value, { stream: true });
171
+ const lines = buffer.split("\n");
172
+ buffer = lines.pop() ?? "";
173
+ for (let i = 0; i < lines.length; i++) {
174
+ const line = lines[i];
175
+ if (line.startsWith("event: endpoint") && i + 1 < lines.length && lines[i + 1].startsWith("data: ")) {
176
+ this.sseEndpoint = lines[i + 1].slice(6).trim();
177
+ endpointReceived = true;
178
+ break;
172
179
  }
173
180
  }
174
- catch (err) {
175
- // Stream error means connection lost
176
- this.handleDisconnect(err instanceof Error ? err.message : String(err));
177
- return;
178
- }
179
- // Stream ended normally
180
- this.handleDisconnect("SSE stream ended");
181
- };
182
- // Wait for endpoint event
183
- const endpointLine = await this.readSSEEvent(reader, decoder);
184
- if (endpointLine) {
185
- this.sseEndpoint = endpointLine.startsWith("/") ? new URL(endpointLine, url).href : endpointLine;
181
+ if (endpointReceived)
182
+ break;
186
183
  }
187
- else {
184
+ if (!this.sseEndpoint) {
188
185
  this.sseEndpoint = url;
189
186
  }
190
- // Start background reader
191
- processChunk();
187
+ // Start background SSE reader for JSON-RPC responses
188
+ this.startSseReader(reader, decoder, buffer);
192
189
  }
193
190
  else {
194
- // Streamable HTTP - POST to URL directly, server responds with JSON or upgrades to SSE
195
191
  this.sseEndpoint = url;
196
192
  }
197
193
  }
198
- async readSSEEvent(reader, decoder) {
199
- let buffer = "";
200
- const deadline = Date.now() + 10000;
201
- while (Date.now() < deadline) {
202
- const { value, done } = await reader.read();
203
- if (done)
204
- break;
205
- buffer += decoder.decode(value, { stream: true });
206
- const lines = buffer.split("\n");
207
- buffer = lines.pop() ?? "";
208
- for (let i = 0; i < lines.length; i++) {
209
- const line = lines[i];
210
- if (line.startsWith("data: ") && lines[i - 1]?.startsWith("event: endpoint")) {
211
- return line.slice(6).trim();
194
+ /** Continuously read the SSE stream and dispatch JSON-RPC responses. */
195
+ async startSseReader(reader, decoder, initialBuffer) {
196
+ let buffer = initialBuffer;
197
+ try {
198
+ while (true) {
199
+ const { done, value } = await reader.read();
200
+ if (done) {
201
+ this.resetState("SSE stream ended");
202
+ return;
212
203
  }
213
- if (line.startsWith("event: endpoint") && lines[i + 1]?.startsWith("data: ")) {
214
- return lines[i + 1].slice(6).trim();
204
+ buffer += decoder.decode(value, { stream: true });
205
+ const lines = buffer.split("\n");
206
+ buffer = lines.pop() ?? "";
207
+ for (const line of lines) {
208
+ if (line.startsWith("data: ")) {
209
+ this.processSseLine(line);
210
+ }
215
211
  }
216
212
  }
217
213
  }
218
- return null;
214
+ catch (err) {
215
+ this.resetState(err instanceof Error ? err.message : String(err));
216
+ }
219
217
  }
218
+ // ── JSON-RPC send ────────────────────────────────────────────────
220
219
  async send(method, params, signal) {
221
220
  const id = ++this.requestId;
222
221
  const body = JSON.stringify({ jsonrpc: "2.0", id, method, params });
223
222
  if (this.process?.stdin) {
224
- return this.sendWithTimeout(id, body, signal, (_resolve, reject) => {
223
+ return this.withTimeout(id, signal, (_resolve, reject) => {
225
224
  try {
226
225
  this.process.stdin.write(`${body}\n`);
227
226
  }
@@ -231,7 +230,7 @@ export class MCPClient {
231
230
  });
232
231
  }
233
232
  if (this.sseEndpoint) {
234
- return this.sendWithTimeout(id, body, signal, async (resolve, reject) => {
233
+ return this.withTimeout(id, signal, async (resolve, reject) => {
235
234
  try {
236
235
  const timeoutMs = this.timeoutMs > 0 ? this.timeoutMs : undefined;
237
236
  const res = await fetch(this.sseEndpoint, {
@@ -241,10 +240,12 @@ export class MCPClient {
241
240
  signal: timeoutMs ? AbortSignal.timeout(timeoutMs) : undefined,
242
241
  });
243
242
  const text = await res.text();
243
+ // SSE transport: response comes via SSE stream, not HTTP response body
244
244
  if (res.headers.get("content-type")?.includes("text/event-stream")) {
245
- // SSE upgrade - response comes via event stream
245
+ // Response will be delivered via startSseReader → processSseLine
246
246
  return;
247
247
  }
248
+ // Streamable HTTP / direct response
248
249
  const parsed = JSON.parse(text);
249
250
  if (parsed.error) {
250
251
  reject(new Error(`MCP error: ${parsed.error.message}`));
@@ -266,20 +267,34 @@ export class MCPClient {
266
267
  throw new Error("MCP client not connected");
267
268
  }
268
269
  /**
269
- * Wraps a request with timeout and AbortSignal support.
270
- * Registers the pending handler so that response lines resolve it,
271
- * and rejects if the timeout fires or the caller's signal aborts.
270
+ * Register a pending request with timeout + abort signal support.
271
+ * The pending entry stays alive until the response line arrives
272
+ * (via stdio readline or SSE processSseLine), the timeout fires,
273
+ * or the caller's AbortSignal fires.
274
+ *
275
+ * IMPORTANT: for SSE transport, the response arrives asynchronously
276
+ * via startSseReader → processSseLine, so the pending entry here
277
+ * is resolved by that path. The writer callback sends the POST request
278
+ * and the SSE response is dispatched in processSseLine.
272
279
  */
273
- sendWithTimeout(id, _body, signal, writer) {
280
+ withTimeout(id, signal, writer) {
274
281
  return new Promise((outerResolve, outerReject) => {
275
282
  let timer;
276
- let done = false;
283
+ let resolved = false;
277
284
  const cleanup = () => {
278
- if (done)
285
+ if (resolved)
279
286
  return;
280
- done = true;
287
+ resolved = true;
281
288
  if (timer)
282
289
  clearTimeout(timer);
290
+ if (signal) {
291
+ try {
292
+ signal.removeEventListener("abort", onAbort);
293
+ }
294
+ catch {
295
+ /* ignore */
296
+ }
297
+ }
283
298
  this.pending.delete(id);
284
299
  };
285
300
  const resolve = (v) => {
@@ -290,26 +305,26 @@ export class MCPClient {
290
305
  cleanup();
291
306
  outerReject(e);
292
307
  };
293
- // Timeout (unless disabled by setting timeoutMs = 0)
308
+ const onAbort = () => {
309
+ reject(new DOMException("MCP request cancelled", "AbortError"));
310
+ };
294
311
  if (this.timeoutMs > 0) {
295
312
  timer = setTimeout(() => {
296
313
  reject(new Error(`MCP request timed out after ${this.timeoutMs}ms`));
297
314
  }, this.timeoutMs);
298
315
  }
299
- // Caller abort signal
300
316
  if (signal) {
301
317
  if (signal.aborted) {
302
318
  reject(new DOMException("MCP request cancelled", "AbortError"));
303
319
  return;
304
320
  }
305
- signal.addEventListener("abort", () => {
306
- reject(new DOMException("MCP request cancelled", "AbortError"));
307
- }, { once: true });
321
+ signal.addEventListener("abort", onAbort, { once: true });
308
322
  }
309
323
  this.pending.set(id, { resolve, reject });
310
324
  writer(resolve, reject);
311
325
  });
312
326
  }
327
+ // ── Public API ───────────────────────────────────────────────────
313
328
  async listTools(signal) {
314
329
  return (await this.send("tools/list", undefined, signal));
315
330
  }
@@ -323,12 +338,25 @@ export class MCPClient {
323
338
  return (await this.send("tools/call", { name, arguments: args }, signal));
324
339
  }
325
340
  async disconnect() {
326
- this._connected = false;
327
341
  this.onDisconnect = null;
328
342
  const proc = this.process;
343
+ const reader = this.sseReader;
329
344
  this.process = null;
330
- this.pending.clear();
345
+ this.sseReader = null;
331
346
  this.sseEndpoint = null;
347
+ this._connected = false;
348
+ for (const [, entry] of this.pending) {
349
+ entry.reject(new Error("MCP client disconnected"));
350
+ }
351
+ this.pending.clear();
352
+ if (reader) {
353
+ try {
354
+ await reader.cancel();
355
+ }
356
+ catch {
357
+ /* ignore */
358
+ }
359
+ }
332
360
  if (proc && !proc.killed) {
333
361
  proc.kill();
334
362
  await new Promise((resolve) => {
@@ -1 +1 @@
1
- {"version":3,"file":"client.js","sourceRoot":"","sources":["../../../src/core/mcp/client.ts"],"names":[],"mappings":"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","sourcesContent":["import { type ChildProcess, spawn } from \"child_process\";\nimport { createInterface } from \"readline\";\nimport type { MCPCallToolResult, MCPResource, MCPServerConfig, MCPToolDefinition } from \"./types.ts\";\n\nexport type { MCPServerConfig };\n\n/** Default MCP tool call timeout: 2 minutes */\nconst DEFAULT_MCP_TIMEOUT_MS = 120_000;\n/** MCP connection handshake timeout: 30 seconds */\nconst MCP_CONNECT_TIMEOUT_MS = 30_000;\n\ntype PendingRequest = { resolve: (v: unknown) => void; reject: (e: Error) => void };\n\nfunction resolveTransport(config: MCPServerConfig): \"stdio\" | \"sse\" | \"http\" {\n\tif (config.transport) return config.transport;\n\tif (config.command) return \"stdio\";\n\tif (config.url) return \"sse\";\n\treturn \"stdio\";\n}\n\nexport type MCPDisconnectHandler = (reason: string) => void;\n\nexport class MCPClient {\n\tprivate process: ChildProcess | null = null;\n\tprivate requestId = 0;\n\tprivate pending = new Map<number, PendingRequest>();\n\tprivate sseEndpoint: string | null = null;\n\tprivate timeoutMs: number;\n\tprivate _connected = false;\n\n\t/** Called when the connection is lost unexpectedly */\n\tonDisconnect: MCPDisconnectHandler | null = null;\n\n\tconstructor(timeoutMs?: number) {\n\t\tthis.timeoutMs = timeoutMs ?? DEFAULT_MCP_TIMEOUT_MS;\n\t}\n\n\tget isConnected(): boolean {\n\t\treturn this._connected;\n\t}\n\n\tasync connect(config: MCPServerConfig): Promise<void> {\n\t\tif (config.timeoutMs !== undefined) {\n\t\t\tthis.timeoutMs = config.timeoutMs;\n\t\t}\n\n\t\tconst transport = resolveTransport(config);\n\n\t\t// Use a shorter timeout for the connection handshake\n\t\t// (server startup via npx, dependency downloads, etc.)\n\t\tconst savedTimeout = this.timeoutMs;\n\t\tthis.timeoutMs = Math.min(this.timeoutMs, MCP_CONNECT_TIMEOUT_MS);\n\t\ttry {\n\t\t\tif (transport === \"stdio\") {\n\t\t\t\tawait this.connectStdio(config);\n\t\t\t} else {\n\t\t\t\tawait this.connectHttp(config, transport);\n\t\t\t}\n\t\t\tawait this.send(\"initialize\", {\n\t\t\t\tprotocolVersion: \"2024-11-05\",\n\t\t\t\tcapabilities: {},\n\t\t\t\tclientInfo: { name: \"pi\", version: \"1.0.0\" },\n\t\t\t});\n\t\t\tthis._connected = true;\n\t\t} finally {\n\t\t\t// Restore the configured timeout for subsequent tool calls\n\t\t\tthis.timeoutMs = savedTimeout;\n\t\t}\n\t}\n\n\tprivate handleDisconnect(reason: string): void {\n\t\tif (!this._connected) return;\n\t\tthis._connected = false;\n\t\tthis.pending.clear();\n\t\tthis.sseEndpoint = null;\n\t\tthis.process = null;\n\n\t\ttry {\n\t\t\tthis.onDisconnect?.(reason);\n\t\t} catch {\n\t\t\t// ignore errors from disconnect handler\n\t\t}\n\t}\n\n\tprivate async connectStdio(config: MCPServerConfig): Promise<void> {\n\t\tconst { command, args = [], env } = config;\n\t\tif (!command) throw new Error(\"MCP stdio server requires 'command'\");\n\n\t\tconst childEnv = env ? { ...process.env, ...env } : process.env;\n\t\treturn new Promise((resolve, reject) => {\n\t\t\tconst proc = spawn(command, args, { env: childEnv, stdio: [\"pipe\", \"pipe\", \"pipe\"], shell: false });\n\t\t\tthis.process = proc;\n\n\t\t\tconst onError = (error: Error) => {\n\t\t\t\treject(new Error(`Failed to spawn MCP server \"${command}\": ${error.message}`));\n\t\t\t};\n\t\t\tproc.on(\"error\", onError);\n\t\t\tproc.once(\"spawn\", () => proc.removeListener(\"error\", onError));\n\n\t\t\tif (!proc.stdout) {\n\t\t\t\treject(new Error(`MCP server \"${command}\" has no stdout`));\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst rl = createInterface({ input: proc.stdout, crlfDelay: Infinity });\n\t\t\trl.on(\"line\", (line: string) => {\n\t\t\t\ttry {\n\t\t\t\t\tconst res = JSON.parse(line);\n\t\t\t\t\tif (res.id !== undefined) {\n\t\t\t\t\t\tconst handler = this.pending.get(res.id);\n\t\t\t\t\t\tif (!handler) return;\n\t\t\t\t\t\tthis.pending.delete(res.id);\n\t\t\t\t\t\tif (res.error) {\n\t\t\t\t\t\t\thandler.reject(new Error(`MCP error: ${res.error.message}`));\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\thandler.resolve(res.result);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t} catch {\n\t\t\t\t\t/* non-JSON lines */\n\t\t\t\t}\n\t\t\t});\n\n\t\t\tproc.on(\"exit\", (code) => {\n\t\t\t\tfor (const [, h] of this.pending) {\n\t\t\t\t\th.reject(new Error(`MCP server \"${command}\" exited with code ${code}`));\n\t\t\t\t}\n\t\t\t\tthis.pending.clear();\n\t\t\t\tthis.handleDisconnect(`process exited with code ${code}`);\n\t\t\t});\n\n\t\t\tresolve();\n\t\t});\n\t}\n\n\tprivate async connectHttp(config: MCPServerConfig, transport: \"sse\" | \"http\"): Promise<void> {\n\t\tconst { url, headers = {} } = config;\n\t\tif (!url) throw new Error(\"MCP SSE/HTTP server requires 'url'\");\n\n\t\tif (transport === \"sse\") {\n\t\t\tconst res = await fetch(`${url}/sse`, { headers: { ...headers, Accept: \"text/event-stream\" } });\n\t\t\tif (!res.ok) throw new Error(`SSE connection failed: ${res.status} ${res.statusText}`);\n\n\t\t\tconst reader = res.body!.getReader();\n\t\t\tconst decoder = new TextDecoder();\n\t\t\tlet buffer = \"\";\n\n\t\t\tconst processChunk = async () => {\n\t\t\t\ttry {\n\t\t\t\t\twhile (true) {\n\t\t\t\t\t\tconst { done, value } = await reader.read();\n\t\t\t\t\t\tif (done) break;\n\t\t\t\t\t\tbuffer += decoder.decode(value, { stream: true });\n\n\t\t\t\t\t\tconst lines = buffer.split(\"\\n\");\n\t\t\t\t\t\tbuffer = lines.pop() ?? \"\";\n\n\t\t\t\t\t\tfor (const line of lines) {\n\t\t\t\t\t\t\tif (line.startsWith(\"data: \")) {\n\t\t\t\t\t\t\t\tconst data = line.slice(6);\n\t\t\t\t\t\t\t\tif (data.startsWith(\"{\") && this.sseEndpoint === null) {\n\t\t\t\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t\t\t\tconst parsed = JSON.parse(data);\n\t\t\t\t\t\t\t\t\t\tif (parsed.result) {\n\t\t\t\t\t\t\t\t\t\t\tconst handler = this.pending.get(parsed.id);\n\t\t\t\t\t\t\t\t\t\t\tif (handler) {\n\t\t\t\t\t\t\t\t\t\t\t\tthis.pending.delete(parsed.id);\n\t\t\t\t\t\t\t\t\t\t\t\tif (parsed.error) {\n\t\t\t\t\t\t\t\t\t\t\t\t\thandler.reject(new Error(`MCP error: ${parsed.error.message}`));\n\t\t\t\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\t\t\t\thandler.resolve(parsed.result);\n\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t} catch {\n\t\t\t\t\t\t\t\t\t\t/* skip */\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t} else if (data.startsWith(\"http\")) {\n\t\t\t\t\t\t\t\t\t// This is the endpoint URL\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t} else if (line.startsWith(\"event: endpoint\")) {\n\t\t\t\t\t\t\t\t// Next data line will be the endpoint URL\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t} catch (err) {\n\t\t\t\t\t// Stream error means connection lost\n\t\t\t\t\tthis.handleDisconnect(err instanceof Error ? err.message : String(err));\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\t// Stream ended normally\n\t\t\t\tthis.handleDisconnect(\"SSE stream ended\");\n\t\t\t};\n\n\t\t\t// Wait for endpoint event\n\t\t\tconst endpointLine = await this.readSSEEvent(reader, decoder);\n\t\t\tif (endpointLine) {\n\t\t\t\tthis.sseEndpoint = endpointLine.startsWith(\"/\") ? new URL(endpointLine, url).href : endpointLine;\n\t\t\t} else {\n\t\t\t\tthis.sseEndpoint = url;\n\t\t\t}\n\n\t\t\t// Start background reader\n\t\t\tprocessChunk();\n\t\t} else {\n\t\t\t// Streamable HTTP - POST to URL directly, server responds with JSON or upgrades to SSE\n\t\t\tthis.sseEndpoint = url;\n\t\t}\n\t}\n\n\tprivate async readSSEEvent(\n\t\treader: ReadableStreamDefaultReader<Uint8Array>,\n\t\tdecoder: { decode(b?: Uint8Array, o?: { stream?: boolean }): string },\n\t): Promise<string | null> {\n\t\tlet buffer = \"\";\n\t\tconst deadline = Date.now() + 10000;\n\n\t\twhile (Date.now() < deadline) {\n\t\t\tconst { value, done } = await reader.read();\n\t\t\tif (done) break;\n\t\t\tbuffer += decoder.decode(value, { stream: true });\n\n\t\t\tconst lines = buffer.split(\"\\n\");\n\t\t\tbuffer = lines.pop() ?? \"\";\n\n\t\t\tfor (let i = 0; i < lines.length; i++) {\n\t\t\t\tconst line = lines[i];\n\t\t\t\tif (line.startsWith(\"data: \") && lines[i - 1]?.startsWith(\"event: endpoint\")) {\n\t\t\t\t\treturn line.slice(6).trim();\n\t\t\t\t}\n\t\t\t\tif (line.startsWith(\"event: endpoint\") && lines[i + 1]?.startsWith(\"data: \")) {\n\t\t\t\t\treturn lines[i + 1].slice(6).trim();\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn null;\n\t}\n\n\tprivate async send(method: string, params?: Record<string, unknown>, signal?: AbortSignal): Promise<unknown> {\n\t\tconst id = ++this.requestId;\n\t\tconst body = JSON.stringify({ jsonrpc: \"2.0\", id, method, params });\n\n\t\tif (this.process?.stdin) {\n\t\t\treturn this.sendWithTimeout(id, body, signal, (_resolve, reject) => {\n\t\t\t\ttry {\n\t\t\t\t\tthis.process!.stdin!.write(`${body}\\n`);\n\t\t\t\t} catch (err) {\n\t\t\t\t\treject(err instanceof Error ? err : new Error(String(err)));\n\t\t\t\t}\n\t\t\t});\n\t\t}\n\n\t\tif (this.sseEndpoint) {\n\t\t\treturn this.sendWithTimeout(id, body, signal, async (resolve, reject) => {\n\t\t\t\ttry {\n\t\t\t\t\tconst timeoutMs = this.timeoutMs > 0 ? this.timeoutMs : undefined;\n\t\t\t\t\tconst res = await fetch(this.sseEndpoint!, {\n\t\t\t\t\t\tmethod: \"POST\",\n\t\t\t\t\t\theaders: { \"Content-Type\": \"application/json\" },\n\t\t\t\t\t\tbody,\n\t\t\t\t\t\tsignal: timeoutMs ? AbortSignal.timeout(timeoutMs) : undefined,\n\t\t\t\t\t});\n\t\t\t\t\tconst text = await res.text();\n\t\t\t\t\tif (res.headers.get(\"content-type\")?.includes(\"text/event-stream\")) {\n\t\t\t\t\t\t// SSE upgrade - response comes via event stream\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tconst parsed = JSON.parse(text);\n\t\t\t\t\tif (parsed.error) {\n\t\t\t\t\t\treject(new Error(`MCP error: ${parsed.error.message}`));\n\t\t\t\t\t} else {\n\t\t\t\t\t\tresolve(parsed.result);\n\t\t\t\t\t}\n\t\t\t\t} catch (err) {\n\t\t\t\t\tif (err instanceof Error && err.name === \"TimeoutError\") {\n\t\t\t\t\t\treject(new Error(`MCP request timed out after ${this.timeoutMs}ms`));\n\t\t\t\t\t} else {\n\t\t\t\t\t\treject(err instanceof Error ? err : new Error(String(err)));\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t});\n\t\t}\n\n\t\tthrow new Error(\"MCP client not connected\");\n\t}\n\n\t/**\n\t * Wraps a request with timeout and AbortSignal support.\n\t * Registers the pending handler so that response lines resolve it,\n\t * and rejects if the timeout fires or the caller's signal aborts.\n\t */\n\tprivate sendWithTimeout(\n\t\tid: number,\n\t\t_body: string,\n\t\tsignal: AbortSignal | undefined,\n\t\twriter: (resolve: (v: unknown) => void, reject: (e: Error) => void) => void,\n\t): Promise<unknown> {\n\t\treturn new Promise<unknown>((outerResolve, outerReject) => {\n\t\t\tlet timer: ReturnType<typeof setTimeout> | undefined;\n\t\t\tlet done = false;\n\n\t\t\tconst cleanup = () => {\n\t\t\t\tif (done) return;\n\t\t\t\tdone = true;\n\t\t\t\tif (timer) clearTimeout(timer);\n\t\t\t\tthis.pending.delete(id);\n\t\t\t};\n\n\t\t\tconst resolve = (v: unknown) => {\n\t\t\t\tcleanup();\n\t\t\t\touterResolve(v);\n\t\t\t};\n\n\t\t\tconst reject = (e: Error) => {\n\t\t\t\tcleanup();\n\t\t\t\touterReject(e);\n\t\t\t};\n\n\t\t\t// Timeout (unless disabled by setting timeoutMs = 0)\n\t\t\tif (this.timeoutMs > 0) {\n\t\t\t\ttimer = setTimeout(() => {\n\t\t\t\t\treject(new Error(`MCP request timed out after ${this.timeoutMs}ms`));\n\t\t\t\t}, this.timeoutMs);\n\t\t\t}\n\n\t\t\t// Caller abort signal\n\t\t\tif (signal) {\n\t\t\t\tif (signal.aborted) {\n\t\t\t\t\treject(new DOMException(\"MCP request cancelled\", \"AbortError\"));\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tsignal.addEventListener(\n\t\t\t\t\t\"abort\",\n\t\t\t\t\t() => {\n\t\t\t\t\t\treject(new DOMException(\"MCP request cancelled\", \"AbortError\"));\n\t\t\t\t\t},\n\t\t\t\t\t{ once: true },\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tthis.pending.set(id, { resolve, reject });\n\n\t\t\twriter(resolve, reject);\n\t\t});\n\t}\n\n\tasync listTools(signal?: AbortSignal): Promise<{ tools: MCPToolDefinition[] }> {\n\t\treturn (await this.send(\"tools/list\", undefined, signal)) as { tools: MCPToolDefinition[] };\n\t}\n\n\tasync listResources(signal?: AbortSignal): Promise<{ resources: MCPResource[] }> {\n\t\treturn (await this.send(\"resources/list\", undefined, signal)) as { resources: MCPResource[] };\n\t}\n\n\tasync readResource(\n\t\turi: string,\n\t\tsignal?: AbortSignal,\n\t): Promise<{ contents: Array<{ uri: string; mimeType?: string; text?: string }> }> {\n\t\treturn (await this.send(\"resources/read\", { uri }, signal)) as {\n\t\t\tcontents: Array<{ uri: string; mimeType?: string; text?: string }>;\n\t\t};\n\t}\n\n\tasync callTool(name: string, args: Record<string, unknown>, signal?: AbortSignal): Promise<MCPCallToolResult> {\n\t\treturn (await this.send(\"tools/call\", { name, arguments: args }, signal)) as MCPCallToolResult;\n\t}\n\n\tasync disconnect(): Promise<void> {\n\t\tthis._connected = false;\n\t\tthis.onDisconnect = null;\n\t\tconst proc = this.process;\n\t\tthis.process = null;\n\t\tthis.pending.clear();\n\t\tthis.sseEndpoint = null;\n\t\tif (proc && !proc.killed) {\n\t\t\tproc.kill();\n\t\t\tawait new Promise<void>((resolve) => {\n\t\t\t\tif (proc.exitCode !== null) return resolve();\n\t\t\t\tproc.on(\"exit\", resolve);\n\t\t\t\tsetTimeout(resolve, 3000);\n\t\t\t});\n\t\t}\n\t}\n}\n"]}
1
+ {"version":3,"file":"client.js","sourceRoot":"","sources":["../../../src/core/mcp/client.ts"],"names":[],"mappings":"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","sourcesContent":["import { type ChildProcess, spawn } from \"child_process\";\nimport { createInterface } from \"readline\";\nimport type { MCPCallToolResult, MCPResource, MCPServerConfig, MCPToolDefinition } from \"./types.ts\";\n\nexport type { MCPServerConfig };\n\nconst DEFAULT_MCP_TIMEOUT_MS = 120_000;\n// initialize handshake timeout matches the configured default tool timeout (2 min)\n// npx cold starts can take 30-60s just to download dependencies\nconst MCP_CONNECT_TIMEOUT_MS = 120_000;\n\ntype PendingEntry = { resolve: (v: unknown) => void; reject: (e: Error) => void };\n\nfunction resolveTransport(config: MCPServerConfig): \"stdio\" | \"sse\" | \"http\" {\n\tif (config.transport) return config.transport;\n\tif (config.command) return \"stdio\";\n\tif (config.url) return \"sse\";\n\treturn \"stdio\";\n}\n\nexport type MCPDisconnectHandler = (reason: string) => void;\n\nexport class MCPClient {\n\tprivate process: ChildProcess | null = null;\n\tprivate requestId = 0;\n\tprivate pending = new Map<number, PendingEntry>();\n\tprivate sseEndpoint: string | null = null;\n\tprivate sseReader: ReadableStreamDefaultReader<Uint8Array> | null = null;\n\tprivate timeoutMs: number;\n\tprivate _connected = false;\n\n\tonDisconnect: MCPDisconnectHandler | null = null;\n\n\tconstructor(timeoutMs?: number) {\n\t\tthis.timeoutMs = timeoutMs ?? DEFAULT_MCP_TIMEOUT_MS;\n\t}\n\n\tget isConnected(): boolean {\n\t\treturn this._connected;\n\t}\n\n\tasync connect(config: MCPServerConfig): Promise<void> {\n\t\tif (config.timeoutMs !== undefined) {\n\t\t\tthis.timeoutMs = config.timeoutMs;\n\t\t}\n\n\t\tconst transport = resolveTransport(config);\n\t\tconst savedTimeout = this.timeoutMs;\n\t\tthis.timeoutMs = Math.min(this.timeoutMs, MCP_CONNECT_TIMEOUT_MS);\n\t\ttry {\n\t\t\tif (transport === \"stdio\") {\n\t\t\t\tawait this.connectStdio(config);\n\t\t\t} else {\n\t\t\t\tawait this.connectHttp(config, transport);\n\t\t\t}\n\t\t\tawait this.send(\"initialize\", {\n\t\t\t\tprotocolVersion: \"2024-11-05\",\n\t\t\t\tcapabilities: {},\n\t\t\t\tclientInfo: { name: \"pi\", version: \"1.0.0\" },\n\t\t\t});\n\t\t\tthis._connected = true;\n\t\t} finally {\n\t\t\tthis.timeoutMs = savedTimeout;\n\t\t}\n\t}\n\n\t/** Reset all connection state (called on disconnect or intentional close).\n\t * Rejects all in-flight pending promises so tool calls don't hang. */\n\tprivate resetState(reason: string): void {\n\t\tif (!this._connected && !this.process && !this.sseReader) return;\n\t\tthis._connected = false;\n\t\tthis.process = null;\n\t\tthis.sseEndpoint = null;\n\t\tthis.sseReader = null;\n\n\t\t// MUST reject all pending promises before clearing, or tool calls freeze\n\t\t// until the 120s timeout fires. Collect entries first because rejecting\n\t\t// may trigger cleanup() which deletes from the map during iteration.\n\t\tconst entries = [...this.pending.values()];\n\t\tfor (const entry of entries) {\n\t\t\tentry.reject(new Error(`MCP disconnected: ${reason}`));\n\t\t}\n\t\tthis.pending.clear();\n\n\t\ttry {\n\t\t\tthis.onDisconnect?.(reason);\n\t\t} catch {\n\t\t\t/* ignore */\n\t\t}\n\t}\n\n\tprivate processSseLine(line: string): void {\n\t\tif (!line.startsWith(\"data: \")) return;\n\t\tconst data = line.slice(6);\n\t\tif (!data.startsWith(\"{\")) return;\n\n\t\ttry {\n\t\t\tconst parsed = JSON.parse(data);\n\t\t\tif (parsed.id === undefined) return;\n\t\t\tconst handler = this.pending.get(parsed.id);\n\t\t\tif (!handler) return;\n\t\t\tthis.pending.delete(parsed.id);\n\t\t\tif (parsed.error) {\n\t\t\t\thandler.reject(new Error(`MCP error: ${parsed.error.message}`));\n\t\t\t} else {\n\t\t\t\thandler.resolve(parsed.result);\n\t\t\t}\n\t\t} catch {\n\t\t\t/* skip */\n\t\t}\n\t}\n\n\t// ── stdio transport ──────────────────────────────────────────────\n\n\tprivate async connectStdio(config: MCPServerConfig): Promise<void> {\n\t\tconst { command, args = [], env } = config;\n\t\tif (!command) throw new Error(\"MCP stdio server requires 'command'\");\n\n\t\tconst childEnv = env ? { ...process.env, ...env } : process.env;\n\t\treturn new Promise<void>((resolve, reject) => {\n\t\t\tconst proc = spawn(command, args, { env: childEnv, stdio: [\"pipe\", \"pipe\", \"pipe\"], shell: false });\n\t\t\tthis.process = proc;\n\n\t\t\tconst onError = (error: Error) => {\n\t\t\t\treject(new Error(`Failed to spawn MCP server \"${command}\": ${error.message}`));\n\t\t\t};\n\t\t\tproc.on(\"error\", onError);\n\t\t\tproc.once(\"spawn\", () => proc.removeListener(\"error\", onError));\n\n\t\t\tif (!proc.stdout) {\n\t\t\t\treject(new Error(`MCP server \"${command}\" has no stdout`));\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst rl = createInterface({ input: proc.stdout, crlfDelay: Infinity });\n\t\t\trl.on(\"line\", (line: string) => {\n\t\t\t\ttry {\n\t\t\t\t\tconst res = JSON.parse(line);\n\t\t\t\t\tif (res.id !== undefined) {\n\t\t\t\t\t\tconst handler = this.pending.get(res.id);\n\t\t\t\t\t\tif (!handler) return;\n\t\t\t\t\t\tthis.pending.delete(res.id);\n\t\t\t\t\t\tif (res.error) {\n\t\t\t\t\t\t\thandler.reject(new Error(`MCP error: ${res.error.message}`));\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\thandler.resolve(res.result);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t} catch {\n\t\t\t\t\t/* non-JSON lines */\n\t\t\t\t}\n\t\t\t});\n\n\t\t\tproc.on(\"exit\", (code) => {\n\t\t\t\tthis.resetState(`process exited with code ${code}`);\n\t\t\t});\n\n\t\t\tresolve();\n\t\t});\n\t}\n\n\t// ── HTTP / SSE transport ─────────────────────────────────────────\n\n\tprivate async connectHttp(config: MCPServerConfig, transport: \"sse\" | \"http\"): Promise<void> {\n\t\tconst { url, headers = {} } = config;\n\t\tif (!url) throw new Error(\"MCP SSE/HTTP server requires 'url'\");\n\n\t\tif (transport === \"sse\") {\n\t\t\tconst res = await fetch(`${url}/sse`, { headers: { ...headers, Accept: \"text/event-stream\" } });\n\t\t\tif (!res.ok) throw new Error(`SSE connection failed: ${res.status} ${res.statusText}`);\n\n\t\t\tconst reader = res.body!.getReader();\n\t\t\tthis.sseReader = reader;\n\t\t\tconst decoder = new TextDecoder();\n\t\t\tlet buffer = \"\";\n\t\t\tlet endpointReceived = false;\n\n\t\t\t// Read SSE stream until we get the endpoint event or timeout\n\t\t\tconst deadline = Date.now() + MCP_CONNECT_TIMEOUT_MS;\n\t\t\twhile (Date.now() < deadline) {\n\t\t\t\tconst { done, value } = await reader.read();\n\t\t\t\tif (done) break;\n\t\t\t\tbuffer += decoder.decode(value, { stream: true });\n\n\t\t\t\tconst lines = buffer.split(\"\\n\");\n\t\t\t\tbuffer = lines.pop() ?? \"\";\n\n\t\t\t\tfor (let i = 0; i < lines.length; i++) {\n\t\t\t\t\tconst line = lines[i];\n\t\t\t\t\tif (line.startsWith(\"event: endpoint\") && i + 1 < lines.length && lines[i + 1].startsWith(\"data: \")) {\n\t\t\t\t\t\tthis.sseEndpoint = lines[i + 1].slice(6).trim();\n\t\t\t\t\t\tendpointReceived = true;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif (endpointReceived) break;\n\t\t\t}\n\n\t\t\tif (!this.sseEndpoint) {\n\t\t\t\tthis.sseEndpoint = url;\n\t\t\t}\n\n\t\t\t// Start background SSE reader for JSON-RPC responses\n\t\t\tthis.startSseReader(reader, decoder, buffer);\n\t\t} else {\n\t\t\tthis.sseEndpoint = url;\n\t\t}\n\t}\n\n\t/** Continuously read the SSE stream and dispatch JSON-RPC responses. */\n\tprivate async startSseReader(\n\t\treader: ReadableStreamDefaultReader<Uint8Array>,\n\t\tdecoder: { decode(b?: Uint8Array, o?: { stream?: boolean }): string },\n\t\tinitialBuffer: string,\n\t): Promise<void> {\n\t\tlet buffer = initialBuffer;\n\t\ttry {\n\t\t\twhile (true) {\n\t\t\t\tconst { done, value } = await reader.read();\n\t\t\t\tif (done) {\n\t\t\t\t\tthis.resetState(\"SSE stream ended\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tbuffer += decoder.decode(value, { stream: true });\n\n\t\t\t\tconst lines = buffer.split(\"\\n\");\n\t\t\t\tbuffer = lines.pop() ?? \"\";\n\n\t\t\t\tfor (const line of lines) {\n\t\t\t\t\tif (line.startsWith(\"data: \")) {\n\t\t\t\t\t\tthis.processSseLine(line);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t} catch (err) {\n\t\t\tthis.resetState(err instanceof Error ? err.message : String(err));\n\t\t}\n\t}\n\n\t// ── JSON-RPC send ────────────────────────────────────────────────\n\n\tprivate async send(method: string, params?: Record<string, unknown>, signal?: AbortSignal): Promise<unknown> {\n\t\tconst id = ++this.requestId;\n\t\tconst body = JSON.stringify({ jsonrpc: \"2.0\", id, method, params });\n\n\t\tif (this.process?.stdin) {\n\t\t\treturn this.withTimeout(id, signal, (_resolve, reject) => {\n\t\t\t\ttry {\n\t\t\t\t\tthis.process!.stdin!.write(`${body}\\n`);\n\t\t\t\t} catch (err) {\n\t\t\t\t\treject(err instanceof Error ? err : new Error(String(err)));\n\t\t\t\t}\n\t\t\t});\n\t\t}\n\n\t\tif (this.sseEndpoint) {\n\t\t\treturn this.withTimeout(id, signal, async (resolve, reject) => {\n\t\t\t\ttry {\n\t\t\t\t\tconst timeoutMs = this.timeoutMs > 0 ? this.timeoutMs : undefined;\n\t\t\t\t\tconst res = await fetch(this.sseEndpoint!, {\n\t\t\t\t\t\tmethod: \"POST\",\n\t\t\t\t\t\theaders: { \"Content-Type\": \"application/json\" },\n\t\t\t\t\t\tbody,\n\t\t\t\t\t\tsignal: timeoutMs ? AbortSignal.timeout(timeoutMs) : undefined,\n\t\t\t\t\t});\n\t\t\t\t\tconst text = await res.text();\n\n\t\t\t\t\t// SSE transport: response comes via SSE stream, not HTTP response body\n\t\t\t\t\tif (res.headers.get(\"content-type\")?.includes(\"text/event-stream\")) {\n\t\t\t\t\t\t// Response will be delivered via startSseReader → processSseLine\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\n\t\t\t\t\t// Streamable HTTP / direct response\n\t\t\t\t\tconst parsed = JSON.parse(text);\n\t\t\t\t\tif (parsed.error) {\n\t\t\t\t\t\treject(new Error(`MCP error: ${parsed.error.message}`));\n\t\t\t\t\t} else {\n\t\t\t\t\t\tresolve(parsed.result);\n\t\t\t\t\t}\n\t\t\t\t} catch (err) {\n\t\t\t\t\tif (err instanceof Error && err.name === \"TimeoutError\") {\n\t\t\t\t\t\treject(new Error(`MCP request timed out after ${this.timeoutMs}ms`));\n\t\t\t\t\t} else {\n\t\t\t\t\t\treject(err instanceof Error ? err : new Error(String(err)));\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t});\n\t\t}\n\n\t\tthrow new Error(\"MCP client not connected\");\n\t}\n\n\t/**\n\t * Register a pending request with timeout + abort signal support.\n\t * The pending entry stays alive until the response line arrives\n\t * (via stdio readline or SSE processSseLine), the timeout fires,\n\t * or the caller's AbortSignal fires.\n\t *\n\t * IMPORTANT: for SSE transport, the response arrives asynchronously\n\t * via startSseReader → processSseLine, so the pending entry here\n\t * is resolved by that path. The writer callback sends the POST request\n\t * and the SSE response is dispatched in processSseLine.\n\t */\n\tprivate withTimeout(\n\t\tid: number,\n\t\tsignal: AbortSignal | undefined,\n\t\twriter: (resolve: (v: unknown) => void, reject: (e: Error) => void) => void,\n\t): Promise<unknown> {\n\t\treturn new Promise<unknown>((outerResolve, outerReject) => {\n\t\t\tlet timer: ReturnType<typeof setTimeout> | undefined;\n\t\t\tlet resolved = false;\n\n\t\t\tconst cleanup = () => {\n\t\t\t\tif (resolved) return;\n\t\t\t\tresolved = true;\n\t\t\t\tif (timer) clearTimeout(timer);\n\t\t\t\tif (signal) {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tsignal.removeEventListener(\"abort\", onAbort);\n\t\t\t\t\t} catch {\n\t\t\t\t\t\t/* ignore */\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tthis.pending.delete(id);\n\t\t\t};\n\n\t\t\tconst resolve = (v: unknown) => {\n\t\t\t\tcleanup();\n\t\t\t\touterResolve(v);\n\t\t\t};\n\n\t\t\tconst reject = (e: Error) => {\n\t\t\t\tcleanup();\n\t\t\t\touterReject(e);\n\t\t\t};\n\n\t\t\tconst onAbort = () => {\n\t\t\t\treject(new DOMException(\"MCP request cancelled\", \"AbortError\"));\n\t\t\t};\n\n\t\t\tif (this.timeoutMs > 0) {\n\t\t\t\ttimer = setTimeout(() => {\n\t\t\t\t\treject(new Error(`MCP request timed out after ${this.timeoutMs}ms`));\n\t\t\t\t}, this.timeoutMs);\n\t\t\t}\n\n\t\t\tif (signal) {\n\t\t\t\tif (signal.aborted) {\n\t\t\t\t\treject(new DOMException(\"MCP request cancelled\", \"AbortError\"));\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tsignal.addEventListener(\"abort\", onAbort, { once: true });\n\t\t\t}\n\n\t\t\tthis.pending.set(id, { resolve, reject });\n\t\t\twriter(resolve, reject);\n\t\t});\n\t}\n\n\t// ── Public API ───────────────────────────────────────────────────\n\n\tasync listTools(signal?: AbortSignal): Promise<{ tools: MCPToolDefinition[] }> {\n\t\treturn (await this.send(\"tools/list\", undefined, signal)) as { tools: MCPToolDefinition[] };\n\t}\n\n\tasync listResources(signal?: AbortSignal): Promise<{ resources: MCPResource[] }> {\n\t\treturn (await this.send(\"resources/list\", undefined, signal)) as { resources: MCPResource[] };\n\t}\n\n\tasync readResource(\n\t\turi: string,\n\t\tsignal?: AbortSignal,\n\t): Promise<{ contents: Array<{ uri: string; mimeType?: string; text?: string }> }> {\n\t\treturn (await this.send(\"resources/read\", { uri }, signal)) as {\n\t\t\tcontents: Array<{ uri: string; mimeType?: string; text?: string }>;\n\t\t};\n\t}\n\n\tasync callTool(name: string, args: Record<string, unknown>, signal?: AbortSignal): Promise<MCPCallToolResult> {\n\t\treturn (await this.send(\"tools/call\", { name, arguments: args }, signal)) as MCPCallToolResult;\n\t}\n\n\tasync disconnect(): Promise<void> {\n\t\tthis.onDisconnect = null;\n\t\tconst proc = this.process;\n\t\tconst reader = this.sseReader;\n\t\tthis.process = null;\n\t\tthis.sseReader = null;\n\t\tthis.sseEndpoint = null;\n\t\tthis._connected = false;\n\t\tfor (const [, entry] of this.pending) {\n\t\t\tentry.reject(new Error(\"MCP client disconnected\"));\n\t\t}\n\t\tthis.pending.clear();\n\n\t\tif (reader) {\n\t\t\ttry {\n\t\t\t\tawait reader.cancel();\n\t\t\t} catch {\n\t\t\t\t/* ignore */\n\t\t\t}\n\t\t}\n\t\tif (proc && !proc.killed) {\n\t\t\tproc.kill();\n\t\t\tawait new Promise<void>((resolve) => {\n\t\t\t\tif (proc.exitCode !== null) return resolve();\n\t\t\t\tproc.on(\"exit\", resolve);\n\t\t\t\tsetTimeout(resolve, 3000);\n\t\t\t});\n\t\t}\n\t}\n}\n"]}