@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.
- package/CHANGELOG.md +18 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +4 -0
- package/dist/cli.js.map +1 -1
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +8 -26
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/mcp/client.d.ts +16 -7
- package/dist/core/mcp/client.d.ts.map +1 -1
- package/dist/core/mcp/client.js +135 -107
- package/dist/core/mcp/client.js.map +1 -1
- package/dist/core/mcp/manager.d.ts +8 -1
- package/dist/core/mcp/manager.d.ts.map +1 -1
- package/dist/core/mcp/manager.js +56 -33
- package/dist/core/mcp/manager.js.map +1 -1
- package/dist/core/mcp/types.d.ts +0 -13
- package/dist/core/mcp/types.d.ts.map +1 -1
- package/dist/core/mcp/types.js.map +1 -1
- package/dist/core/package-manager.d.ts.map +1 -1
- package/dist/core/package-manager.js +3 -1
- package/dist/core/package-manager.js.map +1 -1
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +1 -1
- package/dist/core/session-manager.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +18 -0
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +359 -426
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-client.js +5 -1
- package/dist/modes/rpc/rpc-client.js.map +1 -1
- package/dist/utils/sleep.d.ts.map +1 -1
- package/dist/utils/sleep.js +7 -3
- package/dist/utils/sleep.js.map +1 -1
- package/dist/utils/tools-manager.d.ts.map +1 -1
- package/dist/utils/tools-manager.js.map +1 -1
- package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/sandbox/package-lock.json +2 -2
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/with-deps/package-lock.json +2 -2
- package/examples/extensions/with-deps/package.json +1 -1
- package/npm-shrinkwrap.json +12 -12
- 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
|
-
|
|
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
|
|
22
|
+
private startSseReader;
|
|
20
23
|
private send;
|
|
21
24
|
/**
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
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
|
|
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"]}
|
package/dist/core/mcp/client.js
CHANGED
|
@@ -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
|
-
|
|
6
|
-
|
|
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
|
-
|
|
60
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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
|
-
|
|
175
|
-
|
|
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
|
-
|
|
184
|
+
if (!this.sseEndpoint) {
|
|
188
185
|
this.sseEndpoint = url;
|
|
189
186
|
}
|
|
190
|
-
// Start background reader
|
|
191
|
-
|
|
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
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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
|
-
|
|
214
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
-
//
|
|
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
|
-
*
|
|
270
|
-
*
|
|
271
|
-
*
|
|
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
|
-
|
|
280
|
+
withTimeout(id, signal, writer) {
|
|
274
281
|
return new Promise((outerResolve, outerReject) => {
|
|
275
282
|
let timer;
|
|
276
|
-
let
|
|
283
|
+
let resolved = false;
|
|
277
284
|
const cleanup = () => {
|
|
278
|
-
if (
|
|
285
|
+
if (resolved)
|
|
279
286
|
return;
|
|
280
|
-
|
|
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
|
-
|
|
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.
|
|
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"]}
|