@openeryc/pi-coding-agent 0.75.48 → 0.75.50

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 (36) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/dist/core/agent-session.d.ts +9 -0
  3. package/dist/core/agent-session.d.ts.map +1 -1
  4. package/dist/core/agent-session.js +20 -1
  5. package/dist/core/agent-session.js.map +1 -1
  6. package/dist/core/mcp/client.d.ts +6 -0
  7. package/dist/core/mcp/client.d.ts.map +1 -1
  8. package/dist/core/mcp/client.js +84 -39
  9. package/dist/core/mcp/client.js.map +1 -1
  10. package/dist/core/mcp/manager.d.ts +21 -1
  11. package/dist/core/mcp/manager.d.ts.map +1 -1
  12. package/dist/core/mcp/manager.js +186 -57
  13. package/dist/core/mcp/manager.js.map +1 -1
  14. package/dist/core/mcp/types.d.ts +12 -0
  15. package/dist/core/mcp/types.d.ts.map +1 -1
  16. package/dist/core/mcp/types.js +3 -0
  17. package/dist/core/mcp/types.js.map +1 -1
  18. package/dist/core/settings-manager.d.ts +5 -0
  19. package/dist/core/settings-manager.d.ts.map +1 -1
  20. package/dist/core/settings-manager.js.map +1 -1
  21. package/dist/modes/interactive/components/mcp-selector.d.ts +1 -1
  22. package/dist/modes/interactive/components/mcp-selector.d.ts.map +1 -1
  23. package/dist/modes/interactive/components/mcp-selector.js +3 -1
  24. package/dist/modes/interactive/components/mcp-selector.js.map +1 -1
  25. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  26. package/dist/modes/interactive/interactive-mode.js +9 -15
  27. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  28. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  29. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  30. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  31. package/examples/extensions/sandbox/package-lock.json +2 -2
  32. package/examples/extensions/sandbox/package.json +1 -1
  33. package/examples/extensions/with-deps/package-lock.json +2 -2
  34. package/examples/extensions/with-deps/package.json +1 -1
  35. package/npm-shrinkwrap.json +12 -12
  36. package/package.json +4 -4
@@ -1,13 +1,19 @@
1
1
  import type { MCPCallToolResult, MCPResource, MCPServerConfig, MCPToolDefinition } from "./types.ts";
2
2
  export type { MCPServerConfig };
3
+ export type MCPDisconnectHandler = (reason: string) => void;
3
4
  export declare class MCPClient {
4
5
  private process;
5
6
  private requestId;
6
7
  private pending;
7
8
  private sseEndpoint;
8
9
  private timeoutMs;
10
+ private _connected;
11
+ /** Called when the connection is lost unexpectedly */
12
+ onDisconnect: MCPDisconnectHandler | null;
9
13
  constructor(timeoutMs?: number);
14
+ get isConnected(): boolean;
10
15
  connect(config: MCPServerConfig): Promise<void>;
16
+ private handleDisconnect;
11
17
  private connectStdio;
12
18
  private connectHttp;
13
19
  private readSSEEvent;
@@ -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;AAchC,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;IAE1B,YAAY,SAAS,CAAC,EAAE,MAAM,EAE7B;IAEK,OAAO,CAAC,MAAM,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CAgBpD;YAEa,YAAY;YAkDZ,WAAW;YAmEX,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,CAahC;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\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 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\n\tconstructor(timeoutMs?: number) {\n\t\tthis.timeoutMs = timeoutMs ?? DEFAULT_MCP_TIMEOUT_MS;\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\tif (transport === \"stdio\") {\n\t\t\tawait this.connectStdio(config);\n\t\t} else {\n\t\t\tawait this.connectHttp(config, transport);\n\t\t}\n\t\tawait this.send(\"initialize\", {\n\t\t\tprotocolVersion: \"2024-11-05\",\n\t\t\tcapabilities: {},\n\t\t\tclientInfo: { name: \"pi\", version: \"1.0.0\" },\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});\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\twhile (true) {\n\t\t\t\t\tconst { done, value } = await reader.read();\n\t\t\t\t\tif (done) break;\n\t\t\t\t\tbuffer += decoder.decode(value, { stream: true });\n\n\t\t\t\t\tconst lines = buffer.split(\"\\n\");\n\t\t\t\t\tbuffer = lines.pop() ?? \"\";\n\n\t\t\t\t\tfor (const line of lines) {\n\t\t\t\t\t\tif (line.startsWith(\"data: \")) {\n\t\t\t\t\t\t\tconst data = line.slice(6);\n\t\t\t\t\t\t\tif (data.startsWith(\"{\") && this.sseEndpoint === null) {\n\t\t\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t\t\tconst parsed = JSON.parse(data);\n\t\t\t\t\t\t\t\t\tif (parsed.result) {\n\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\tif (handler) {\n\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\tif (parsed.error) {\n\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} else {\n\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}\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t} catch {\n\t\t\t\t\t\t\t\t\t/* skip */\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t} else if (data.startsWith(\"http\")) {\n\t\t\t\t\t\t\t\t// This is the endpoint URL\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t} else if (line.startsWith(\"event: endpoint\")) {\n\t\t\t\t\t\t\t// Next data line will be the endpoint URL\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\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().catch(() => {});\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\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,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"]}
@@ -2,6 +2,8 @@ import { spawn } from "child_process";
2
2
  import { createInterface } from "readline";
3
3
  /** Default MCP tool call timeout: 2 minutes */
4
4
  const DEFAULT_MCP_TIMEOUT_MS = 120_000;
5
+ /** MCP connection handshake timeout: 30 seconds */
6
+ const MCP_CONNECT_TIMEOUT_MS = 30_000;
5
7
  function resolveTransport(config) {
6
8
  if (config.transport)
7
9
  return config.transport;
@@ -17,25 +19,56 @@ export class MCPClient {
17
19
  pending = new Map();
18
20
  sseEndpoint = null;
19
21
  timeoutMs;
22
+ _connected = false;
23
+ /** Called when the connection is lost unexpectedly */
24
+ onDisconnect = null;
20
25
  constructor(timeoutMs) {
21
26
  this.timeoutMs = timeoutMs ?? DEFAULT_MCP_TIMEOUT_MS;
22
27
  }
28
+ get isConnected() {
29
+ return this._connected;
30
+ }
23
31
  async connect(config) {
24
32
  if (config.timeoutMs !== undefined) {
25
33
  this.timeoutMs = config.timeoutMs;
26
34
  }
27
35
  const transport = resolveTransport(config);
28
- if (transport === "stdio") {
29
- await this.connectStdio(config);
36
+ // Use a shorter timeout for the connection handshake
37
+ // (server startup via npx, dependency downloads, etc.)
38
+ const savedTimeout = this.timeoutMs;
39
+ this.timeoutMs = Math.min(this.timeoutMs, MCP_CONNECT_TIMEOUT_MS);
40
+ try {
41
+ if (transport === "stdio") {
42
+ await this.connectStdio(config);
43
+ }
44
+ else {
45
+ await this.connectHttp(config, transport);
46
+ }
47
+ await this.send("initialize", {
48
+ protocolVersion: "2024-11-05",
49
+ capabilities: {},
50
+ clientInfo: { name: "pi", version: "1.0.0" },
51
+ });
52
+ this._connected = true;
30
53
  }
31
- else {
32
- await this.connectHttp(config, transport);
54
+ finally {
55
+ // Restore the configured timeout for subsequent tool calls
56
+ this.timeoutMs = savedTimeout;
57
+ }
58
+ }
59
+ handleDisconnect(reason) {
60
+ if (!this._connected)
61
+ return;
62
+ this._connected = false;
63
+ this.pending.clear();
64
+ this.sseEndpoint = null;
65
+ this.process = null;
66
+ try {
67
+ this.onDisconnect?.(reason);
68
+ }
69
+ catch {
70
+ // ignore errors from disconnect handler
33
71
  }
34
- await this.send("initialize", {
35
- protocolVersion: "2024-11-05",
36
- capabilities: {},
37
- clientInfo: { name: "pi", version: "1.0.0" },
38
- });
39
72
  }
40
73
  async connectStdio(config) {
41
74
  const { command, args = [], env } = config;
@@ -80,6 +113,7 @@ export class MCPClient {
80
113
  h.reject(new Error(`MCP server "${command}" exited with code ${code}`));
81
114
  }
82
115
  this.pending.clear();
116
+ this.handleDisconnect(`process exited with code ${code}`);
83
117
  });
84
118
  resolve();
85
119
  });
@@ -96,45 +130,54 @@ export class MCPClient {
96
130
  const decoder = new TextDecoder();
97
131
  let buffer = "";
98
132
  const processChunk = async () => {
99
- while (true) {
100
- const { done, value } = await reader.read();
101
- if (done)
102
- break;
103
- buffer += decoder.decode(value, { stream: true });
104
- const lines = buffer.split("\n");
105
- buffer = lines.pop() ?? "";
106
- for (const line of lines) {
107
- if (line.startsWith("data: ")) {
108
- const data = line.slice(6);
109
- if (data.startsWith("{") && this.sseEndpoint === null) {
110
- try {
111
- const parsed = JSON.parse(data);
112
- if (parsed.result) {
113
- const handler = this.pending.get(parsed.id);
114
- if (handler) {
115
- this.pending.delete(parsed.id);
116
- if (parsed.error) {
117
- handler.reject(new Error(`MCP error: ${parsed.error.message}`));
118
- }
119
- else {
120
- handler.resolve(parsed.result);
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
+ }
121
157
  }
122
158
  }
123
159
  }
160
+ catch {
161
+ /* skip */
162
+ }
124
163
  }
125
- catch {
126
- /* skip */
164
+ else if (data.startsWith("http")) {
165
+ // This is the endpoint URL
127
166
  }
128
167
  }
129
- else if (data.startsWith("http")) {
130
- // This is the endpoint URL
168
+ else if (line.startsWith("event: endpoint")) {
169
+ // Next data line will be the endpoint URL
131
170
  }
132
171
  }
133
- else if (line.startsWith("event: endpoint")) {
134
- // Next data line will be the endpoint URL
135
- }
136
172
  }
137
173
  }
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");
138
181
  };
139
182
  // Wait for endpoint event
140
183
  const endpointLine = await this.readSSEEvent(reader, decoder);
@@ -145,7 +188,7 @@ export class MCPClient {
145
188
  this.sseEndpoint = url;
146
189
  }
147
190
  // Start background reader
148
- processChunk().catch(() => { });
191
+ processChunk();
149
192
  }
150
193
  else {
151
194
  // Streamable HTTP - POST to URL directly, server responds with JSON or upgrades to SSE
@@ -280,6 +323,8 @@ export class MCPClient {
280
323
  return (await this.send("tools/call", { name, arguments: args }, signal));
281
324
  }
282
325
  async disconnect() {
326
+ this._connected = false;
327
+ this.onDisconnect = null;
283
328
  const proc = this.process;
284
329
  this.process = null;
285
330
  this.pending.clear();
@@ -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\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 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\n\tconstructor(timeoutMs?: number) {\n\t\tthis.timeoutMs = timeoutMs ?? DEFAULT_MCP_TIMEOUT_MS;\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\tif (transport === \"stdio\") {\n\t\t\tawait this.connectStdio(config);\n\t\t} else {\n\t\t\tawait this.connectHttp(config, transport);\n\t\t}\n\t\tawait this.send(\"initialize\", {\n\t\t\tprotocolVersion: \"2024-11-05\",\n\t\t\tcapabilities: {},\n\t\t\tclientInfo: { name: \"pi\", version: \"1.0.0\" },\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});\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\twhile (true) {\n\t\t\t\t\tconst { done, value } = await reader.read();\n\t\t\t\t\tif (done) break;\n\t\t\t\t\tbuffer += decoder.decode(value, { stream: true });\n\n\t\t\t\t\tconst lines = buffer.split(\"\\n\");\n\t\t\t\t\tbuffer = lines.pop() ?? \"\";\n\n\t\t\t\t\tfor (const line of lines) {\n\t\t\t\t\t\tif (line.startsWith(\"data: \")) {\n\t\t\t\t\t\t\tconst data = line.slice(6);\n\t\t\t\t\t\t\tif (data.startsWith(\"{\") && this.sseEndpoint === null) {\n\t\t\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t\t\tconst parsed = JSON.parse(data);\n\t\t\t\t\t\t\t\t\tif (parsed.result) {\n\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\tif (handler) {\n\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\tif (parsed.error) {\n\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} else {\n\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}\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t} catch {\n\t\t\t\t\t\t\t\t\t/* skip */\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t} else if (data.startsWith(\"http\")) {\n\t\t\t\t\t\t\t\t// This is the endpoint URL\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t} else if (line.startsWith(\"event: endpoint\")) {\n\t\t\t\t\t\t\t// Next data line will be the endpoint URL\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\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().catch(() => {});\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\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\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,14 +1,34 @@
1
1
  import type { ToolDefinition } from "../extensions/types.ts";
2
2
  import { type MCPServerConfig } from "./client.ts";
3
+ import type { MCPConnectionStatus } from "./types.ts";
4
+ export type MCPStatusChangeHandler = (serverName: string, status: MCPConnectionStatus) => void;
3
5
  export declare class MCPManager {
4
6
  private clients;
7
+ private serverConfigs;
5
8
  private tools;
6
9
  private resourceUris;
10
+ private reconnectTimers;
11
+ private reconnectAttempts;
12
+ private _statuses;
13
+ /** Called when any server's connection status changes */
14
+ onStatusChange: MCPStatusChangeHandler | null;
15
+ getServerStatus(serverName: string): MCPConnectionStatus;
16
+ getAllStatuses(): Array<{
17
+ serverName: string;
18
+ status: MCPConnectionStatus;
19
+ toolCount: number;
20
+ }>;
21
+ private setStatus;
7
22
  start(serverConfigs: Record<string, MCPServerConfig>): Promise<void>;
8
- readResource(uri: string): Promise<string>;
23
+ private connectServer;
24
+ private buildToolDefs;
25
+ private scheduleReconnect;
26
+ /** Manually trigger reconnection for a specific server */
27
+ reconnect(serverName: string): Promise<void>;
9
28
  getToolDefinitions(): ToolDefinition[];
10
29
  getServerNames(): string[];
11
30
  getResourceUris(): string[];
31
+ readResource(uri: string): Promise<string>;
12
32
  stop(): Promise<void>;
13
33
  }
14
34
  //# sourceMappingURL=manager.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"manager.d.ts","sourceRoot":"","sources":["../../../src/core/mcp/manager.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAC7D,OAAO,EAAa,KAAK,eAAe,EAAE,MAAM,aAAa,CAAC;AAqC9D,qBAAa,UAAU;IACtB,OAAO,CAAC,OAAO,CAAgC;IAC/C,OAAO,CAAC,KAAK,CAAuC;IACpD,OAAO,CAAC,YAAY,CAAgB;IAE9B,KAAK,CAAC,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CA0DzE;IAEK,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAQ/C;IAED,kBAAkB,IAAI,cAAc,EAAE,CAErC;IAED,cAAc,IAAI,MAAM,EAAE,CAEzB;IAED,eAAe,IAAI,MAAM,EAAE,CAE1B;IAEK,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAc1B;CACD","sourcesContent":["import type { AgentToolResult } from \"@openeryc/pi-agent-core\";\nimport { Type } from \"typebox\";\nimport type { ToolDefinition } from \"../extensions/types.ts\";\nimport { MCPClient, type MCPServerConfig } from \"./client.ts\";\nimport type { MCPToolDefinition } from \"./types.ts\";\n\nfunction toTypeBox(schema: MCPToolDefinition[\"inputSchema\"]): ReturnType<typeof Type.Object> {\n\tif (!schema.properties || Object.keys(schema.properties).length === 0) {\n\t\treturn Type.Object({});\n\t}\n\tconst required = new Set(schema.required ?? []);\n\tconst fields: Record<string, unknown> = {};\n\tfor (const [key, prop] of Object.entries(schema.properties)) {\n\t\tconst p = prop as { type?: string; description?: string; enum?: string[] };\n\t\tconst t = p.type ?? \"string\";\n\t\tconst desc = p.description ? { description: p.description } : {};\n\t\tlet f: unknown;\n\t\tswitch (t) {\n\t\t\tcase \"string\":\n\t\t\t\tf = p.enum ? Type.Unsafe<string>({ type: \"string\", enum: p.enum, ...desc }) : Type.String(desc);\n\t\t\t\tbreak;\n\t\t\tcase \"number\":\n\t\t\tcase \"integer\":\n\t\t\t\tf = Type.Number(desc);\n\t\t\t\tbreak;\n\t\t\tcase \"boolean\":\n\t\t\t\tf = Type.Boolean(desc);\n\t\t\t\tbreak;\n\t\t\tdefault:\n\t\t\t\tf = Type.Any(desc);\n\t\t}\n\t\tfields[key] = required.has(key) ? f : Type.Optional(f as never);\n\t}\n\treturn Type.Object(fields as Record<string, ReturnType<typeof Type.String>>);\n}\n\nfunction sanitizeMcpName(name: string): string {\n\treturn name.replace(/[^a-zA-Z0-9_-]/g, \"_\");\n}\n\nexport class MCPManager {\n\tprivate clients = new Map<string, MCPClient>();\n\tprivate tools = new Map<string, ToolDefinition[]>();\n\tprivate resourceUris: string[] = [];\n\n\tasync start(serverConfigs: Record<string, MCPServerConfig>): Promise<void> {\n\t\tfor (const [serverName, config] of Object.entries(serverConfigs)) {\n\t\t\tif (config.enabled === false) continue;\n\t\t\ttry {\n\t\t\t\tconst client = new MCPClient(config.timeoutMs);\n\t\t\t\tawait client.connect(config);\n\t\t\t\tconst { tools: mcpTools } = await client.listTools();\n\t\t\t\tthis.clients.set(serverName, client);\n\n\t\t\t\tconst safeServerName = sanitizeMcpName(serverName);\n\t\t\t\tconst defs: ToolDefinition[] = mcpTools.map((tool) => ({\n\t\t\t\t\tname: `mcp_${safeServerName}_${sanitizeMcpName(tool.name)}`,\n\t\t\t\t\tlabel: `mcp.${serverName}.${tool.name}`,\n\t\t\t\t\tdescription: tool.description\n\t\t\t\t\t\t? `${tool.description} (from MCP server \"${serverName}\")`\n\t\t\t\t\t\t: `Tool from MCP server \"${serverName}\"`,\n\t\t\t\t\tpromptSnippet: tool.description\n\t\t\t\t\t\t? `[${serverName}] ${tool.description.split(\"\\n\")[0]}`\n\t\t\t\t\t\t: `[${serverName}] ${tool.name}`,\n\t\t\t\t\tparameters: toTypeBox(tool.inputSchema),\n\t\t\t\t\trenderShell: \"default\" as const,\n\t\t\t\t\texecute: async (_id, params, signal) => {\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tconst result = await client.callTool(tool.name, params as Record<string, unknown>, signal);\n\t\t\t\t\t\t\tconst text = result.content\n\t\t\t\t\t\t\t\t.map((item) => (item.type === \"text\" && item.text ? item.text : `[Image: ${item.mimeType}]`))\n\t\t\t\t\t\t\t\t.join(\"\\n\");\n\t\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\t\tcontent: [{ type: \"text\" as const, text }],\n\t\t\t\t\t\t\t\tdetails: result,\n\t\t\t\t\t\t\t} satisfies AgentToolResult<unknown>;\n\t\t\t\t\t\t} catch (error) {\n\t\t\t\t\t\t\tconst msg = error instanceof Error ? error.message : String(error);\n\t\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\t\tcontent: [{ type: \"text\" as const, text: `MCP tool error: ${msg}` }],\n\t\t\t\t\t\t\t\tdetails: { error: msg },\n\t\t\t\t\t\t\t} satisfies AgentToolResult<unknown>;\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t}));\n\n\t\t\t\tthis.tools.set(serverName, defs);\n\n\t\t\t\t// Discover resources\n\t\t\t\ttry {\n\t\t\t\t\tconst { resources } = await client.listResources();\n\t\t\t\t\tfor (const r of resources) {\n\t\t\t\t\t\tthis.resourceUris.push(`mcp://${serverName}${r.uri}`);\n\t\t\t\t\t}\n\t\t\t\t} catch {\n\t\t\t\t\t// Server may not support resources\n\t\t\t\t}\n\t\t\t} catch (error) {\n\t\t\t\tconsole.warn(\n\t\t\t\t\t`Failed to connect to MCP server \"${serverName}\": ${error instanceof Error ? error.message : String(error)}`,\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\t}\n\n\tasync readResource(uri: string): Promise<string> {\n\t\tconst serverName = this.clients.keys().next().value;\n\t\tif (!serverName) throw new Error(\"No MCP servers connected\");\n\t\tconst mcpUri = uri.startsWith(`mcp://${serverName}`) ? uri.slice(`mcp://${serverName}`.length) : uri;\n\t\tconst client = this.clients.get(serverName);\n\t\tif (!client) throw new Error(`MCP server \"${serverName}\" not connected`);\n\t\tconst result = await client.readResource(mcpUri);\n\t\treturn result.contents.map((c) => c.text ?? \"\").join(\"\\n\");\n\t}\n\n\tgetToolDefinitions(): ToolDefinition[] {\n\t\treturn Array.from(this.tools.values()).flat();\n\t}\n\n\tgetServerNames(): string[] {\n\t\treturn [...this.clients.keys()];\n\t}\n\n\tgetResourceUris(): string[] {\n\t\treturn this.resourceUris;\n\t}\n\n\tasync stop(): Promise<void> {\n\t\tconst pending = [...this.clients.entries()].map(async ([name, client]) => {\n\t\t\ttry {\n\t\t\t\tawait client.disconnect();\n\t\t\t} catch (error) {\n\t\t\t\tconsole.warn(\n\t\t\t\t\t`Error disconnecting MCP server \"${name}\": ${error instanceof Error ? error.message : String(error)}`,\n\t\t\t\t);\n\t\t\t}\n\t\t});\n\t\tthis.clients.clear();\n\t\tthis.tools.clear();\n\t\tthis.resourceUris = [];\n\t\tawait Promise.all(pending);\n\t}\n}\n"]}
1
+ {"version":3,"file":"manager.d.ts","sourceRoot":"","sources":["../../../src/core/mcp/manager.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAC7D,OAAO,EAAa,KAAK,eAAe,EAAE,MAAM,aAAa,CAAC;AAC9D,OAAO,KAAK,EAAE,mBAAmB,EAAqB,MAAM,YAAY,CAAC;AAuCzE,MAAM,MAAM,sBAAsB,GAAG,CAAC,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,mBAAmB,KAAK,IAAI,CAAC;AAE/F,qBAAa,UAAU;IACtB,OAAO,CAAC,OAAO,CAAgC;IAC/C,OAAO,CAAC,aAAa,CAAsC;IAC3D,OAAO,CAAC,KAAK,CAAuC;IACpD,OAAO,CAAC,YAAY,CAAgB;IACpC,OAAO,CAAC,eAAe,CAAoD;IAC3E,OAAO,CAAC,iBAAiB,CAA6B;IACtD,OAAO,CAAC,SAAS,CAA0C;IAE3D,yDAAyD;IACzD,cAAc,EAAE,sBAAsB,GAAG,IAAI,CAAQ;IAErD,eAAe,CAAC,UAAU,EAAE,MAAM,GAAG,mBAAmB,CAEvD;IAED,cAAc,IAAI,KAAK,CAAC;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,mBAAmB,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC,CAM9F;IAED,OAAO,CAAC,SAAS;IAWX,KAAK,CAAC,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CASzE;YAEa,aAAa;IAsC3B,OAAO,CAAC,aAAa;IA+CrB,OAAO,CAAC,iBAAiB;IA6CzB,0DAA0D;IACpD,SAAS,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAsBjD;IAGD,kBAAkB,IAAI,cAAc,EAAE,CAErC;IAED,cAAc,IAAI,MAAM,EAAE,CAEzB;IAED,eAAe,IAAI,MAAM,EAAE,CAE1B;IAEK,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAQ/C;IAEK,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAsB1B;CACD","sourcesContent":["import type { AgentToolResult } from \"@openeryc/pi-agent-core\";\nimport { Type } from \"typebox\";\nimport type { ToolDefinition } from \"../extensions/types.ts\";\nimport { MCPClient, type MCPServerConfig } from \"./client.ts\";\nimport type { MCPConnectionStatus, MCPToolDefinition } from \"./types.ts\";\n\nconst DEFAULT_RECONNECT_MAX_ATTEMPTS = 5;\nconst DEFAULT_RECONNECT_INTERVAL_MS = 5000;\n\nfunction toTypeBox(schema: MCPToolDefinition[\"inputSchema\"]): ReturnType<typeof Type.Object> {\n\tif (!schema.properties || Object.keys(schema.properties).length === 0) {\n\t\treturn Type.Object({});\n\t}\n\tconst required = new Set(schema.required ?? []);\n\tconst fields: Record<string, unknown> = {};\n\tfor (const [key, prop] of Object.entries(schema.properties)) {\n\t\tconst p = prop as { type?: string; description?: string; enum?: string[] };\n\t\tconst t = p.type ?? \"string\";\n\t\tconst desc = p.description ? { description: p.description } : {};\n\t\tlet f: unknown;\n\t\tswitch (t) {\n\t\t\tcase \"string\":\n\t\t\t\tf = p.enum ? Type.Unsafe<string>({ type: \"string\", enum: p.enum, ...desc }) : Type.String(desc);\n\t\t\t\tbreak;\n\t\t\tcase \"number\":\n\t\t\tcase \"integer\":\n\t\t\t\tf = Type.Number(desc);\n\t\t\t\tbreak;\n\t\t\tcase \"boolean\":\n\t\t\t\tf = Type.Boolean(desc);\n\t\t\t\tbreak;\n\t\t\tdefault:\n\t\t\t\tf = Type.Any(desc);\n\t\t}\n\t\tfields[key] = required.has(key) ? f : Type.Optional(f as never);\n\t}\n\treturn Type.Object(fields as Record<string, ReturnType<typeof Type.String>>);\n}\n\nfunction sanitizeMcpName(name: string): string {\n\treturn name.replace(/[^a-zA-Z0-9_-]/g, \"_\");\n}\n\nexport type MCPStatusChangeHandler = (serverName: string, status: MCPConnectionStatus) => void;\n\nexport class MCPManager {\n\tprivate clients = new Map<string, MCPClient>();\n\tprivate serverConfigs = new Map<string, MCPServerConfig>();\n\tprivate tools = new Map<string, ToolDefinition[]>();\n\tprivate resourceUris: string[] = [];\n\tprivate reconnectTimers = new Map<string, ReturnType<typeof setTimeout>>();\n\tprivate reconnectAttempts = new Map<string, number>();\n\tprivate _statuses = new Map<string, MCPConnectionStatus>();\n\n\t/** Called when any server's connection status changes */\n\tonStatusChange: MCPStatusChangeHandler | null = null;\n\n\tgetServerStatus(serverName: string): MCPConnectionStatus {\n\t\treturn this._statuses.get(serverName) ?? \"disconnected\";\n\t}\n\n\tgetAllStatuses(): Array<{ serverName: string; status: MCPConnectionStatus; toolCount: number }> {\n\t\treturn [...this._statuses.entries()].map(([serverName, status]) => ({\n\t\t\tserverName,\n\t\t\tstatus,\n\t\t\ttoolCount: this.tools.get(serverName)?.length ?? 0,\n\t\t}));\n\t}\n\n\tprivate setStatus(serverName: string, status: MCPConnectionStatus): void {\n\t\tconst prev = this._statuses.get(serverName);\n\t\tif (prev === status) return;\n\t\tthis._statuses.set(serverName, status);\n\t\ttry {\n\t\t\tthis.onStatusChange?.(serverName, status);\n\t\t} catch {\n\t\t\t// ignore\n\t\t}\n\t}\n\n\tasync start(serverConfigs: Record<string, MCPServerConfig>): Promise<void> {\n\t\tfor (const [serverName, config] of Object.entries(serverConfigs)) {\n\t\t\tif (config.enabled === false) {\n\t\t\t\tthis.setStatus(serverName, \"disabled\");\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tthis.serverConfigs.set(serverName, config);\n\t\t\tawait this.connectServer(serverName, config);\n\t\t}\n\t}\n\n\tprivate async connectServer(serverName: string, config: MCPServerConfig): Promise<void> {\n\t\tthis.setStatus(serverName, \"reconnecting\");\n\t\ttry {\n\t\t\tconst client = new MCPClient(config.timeoutMs);\n\t\t\tclient.onDisconnect = (_reason) => {\n\t\t\t\tthis.setStatus(serverName, \"disconnected\");\n\t\t\t\tthis.tools.delete(serverName);\n\t\t\t\tthis.scheduleReconnect(serverName);\n\t\t\t};\n\n\t\t\tawait client.connect(config);\n\t\t\tconst { tools: mcpTools } = await client.listTools();\n\t\t\tthis.clients.set(serverName, client);\n\n\t\t\tthis.buildToolDefs(serverName, mcpTools);\n\n\t\t\t// Discover resources\n\t\t\ttry {\n\t\t\t\tconst { resources } = await client.listResources();\n\t\t\t\tfor (const r of resources) {\n\t\t\t\t\tthis.resourceUris.push(`mcp://${serverName}${r.uri}`);\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\t// Server may not support resources\n\t\t\t}\n\n\t\t\tthis.setStatus(serverName, \"connected\");\n\t\t\tthis.reconnectAttempts.delete(serverName);\n\t\t} catch (error) {\n\t\t\tthis.clients.delete(serverName);\n\t\t\tconsole.warn(\n\t\t\t\t`Failed to connect to MCP server \"${serverName}\": ${error instanceof Error ? error.message : String(error)}`,\n\t\t\t);\n\t\t\tthis.setStatus(serverName, \"disconnected\");\n\t\t\tthis.scheduleReconnect(serverName);\n\t\t}\n\t}\n\n\tprivate buildToolDefs(serverName: string, mcpTools: MCPToolDefinition[]): void {\n\t\tconst safeServerName = sanitizeMcpName(serverName);\n\t\tconst client = this.clients.get(serverName)!;\n\t\tconst defs: ToolDefinition[] = mcpTools.map((tool) => ({\n\t\t\tname: `mcp_${safeServerName}_${sanitizeMcpName(tool.name)}`,\n\t\t\tlabel: `mcp.${serverName}.${tool.name}`,\n\t\t\tdescription: tool.description\n\t\t\t\t? `${tool.description} (from MCP server \"${serverName}\")`\n\t\t\t\t: `Tool from MCP server \"${serverName}\"`,\n\t\t\tpromptSnippet: tool.description\n\t\t\t\t? `[${serverName}] ${tool.description.split(\"\\n\")[0]}`\n\t\t\t\t: `[${serverName}] ${tool.name}`,\n\t\t\tparameters: toTypeBox(tool.inputSchema),\n\t\t\trenderShell: \"default\" as const,\n\t\t\texecute: async (_id, params, signal) => {\n\t\t\t\tif (!client.isConnected) {\n\t\t\t\t\treturn {\n\t\t\t\t\t\tcontent: [\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\ttype: \"text\" as const,\n\t\t\t\t\t\t\t\ttext: `MCP server \"${serverName}\" is disconnected. Use /mcp to check status.`,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t],\n\t\t\t\t\t\tdetails: { error: \"disconnected\" },\n\t\t\t\t\t} satisfies AgentToolResult<unknown>;\n\t\t\t\t}\n\t\t\t\ttry {\n\t\t\t\t\tconst result = await client.callTool(tool.name, params as Record<string, unknown>, signal);\n\t\t\t\t\tconst text = result.content\n\t\t\t\t\t\t.map((item) => (item.type === \"text\" && item.text ? item.text : `[Image: ${item.mimeType}]`))\n\t\t\t\t\t\t.join(\"\\n\");\n\t\t\t\t\treturn {\n\t\t\t\t\t\tcontent: [{ type: \"text\" as const, text }],\n\t\t\t\t\t\tdetails: result,\n\t\t\t\t\t} satisfies AgentToolResult<unknown>;\n\t\t\t\t} catch (error) {\n\t\t\t\t\tconst msg = error instanceof Error ? error.message : String(error);\n\t\t\t\t\treturn {\n\t\t\t\t\t\tcontent: [{ type: \"text\" as const, text: `MCP tool error: ${msg}` }],\n\t\t\t\t\t\tdetails: { error: msg },\n\t\t\t\t\t} satisfies AgentToolResult<unknown>;\n\t\t\t\t}\n\t\t\t},\n\t\t}));\n\t\tthis.tools.set(serverName, defs);\n\t}\n\n\tprivate scheduleReconnect(serverName: string): void {\n\t\t// Cancel any existing reconnect timer for this server\n\t\tconst existing = this.reconnectTimers.get(serverName);\n\t\tif (existing) {\n\t\t\tclearTimeout(existing);\n\t\t}\n\n\t\tconst config = this.serverConfigs.get(serverName);\n\t\tif (!config) return;\n\n\t\tconst reconnectCfg = config.reconnect ?? {};\n\t\tif (reconnectCfg.enabled === false) return;\n\n\t\tconst maxAttempts = reconnectCfg.maxAttempts ?? DEFAULT_RECONNECT_MAX_ATTEMPTS;\n\t\tconst intervalMs = reconnectCfg.intervalMs ?? DEFAULT_RECONNECT_INTERVAL_MS;\n\t\tconst attempt = (this.reconnectAttempts.get(serverName) ?? 0) + 1;\n\n\t\tif (attempt > maxAttempts) {\n\t\t\tconsole.warn(`MCP server \"${serverName}\": max reconnection attempts (${maxAttempts}) reached, giving up.`);\n\t\t\tthis.reconnectAttempts.delete(serverName);\n\t\t\treturn;\n\t\t}\n\n\t\tthis.reconnectAttempts.set(serverName, attempt);\n\t\tthis.setStatus(serverName, \"reconnecting\");\n\n\t\tconst timer = setTimeout(async () => {\n\t\t\tthis.reconnectTimers.delete(serverName);\n\t\t\tconst cfg = this.serverConfigs.get(serverName);\n\t\t\tif (!cfg) return;\n\n\t\t\t// Remove old client if any\n\t\t\tconst oldClient = this.clients.get(serverName);\n\t\t\tif (oldClient) {\n\t\t\t\toldClient.onDisconnect = null;\n\t\t\t\tawait oldClient.disconnect().catch(() => {});\n\t\t\t\tthis.clients.delete(serverName);\n\t\t\t}\n\n\t\t\tawait this.connectServer(serverName, cfg);\n\t\t}, intervalMs * Math.min(attempt, 5)); // exponential backoff, cap at 5x\n\n\t\tthis.reconnectTimers.set(serverName, timer);\n\t}\n\n\t/** Manually trigger reconnection for a specific server */\n\tasync reconnect(serverName: string): Promise<void> {\n\t\tconst existingTimer = this.reconnectTimers.get(serverName);\n\t\tif (existingTimer) {\n\t\t\tclearTimeout(existingTimer);\n\t\t\tthis.reconnectTimers.delete(serverName);\n\t\t}\n\n\t\tconst oldClient = this.clients.get(serverName);\n\t\tif (oldClient) {\n\t\t\toldClient.onDisconnect = null;\n\t\t\tawait oldClient.disconnect().catch(() => {});\n\t\t\tthis.clients.delete(serverName);\n\t\t}\n\n\t\tconst config = this.serverConfigs.get(serverName);\n\t\tif (!config) {\n\t\t\tthis.setStatus(serverName, \"disconnected\");\n\t\t\treturn;\n\t\t}\n\n\t\tthis.reconnectAttempts.delete(serverName);\n\t\tawait this.connectServer(serverName, config);\n\t}\n\n\t// Rebuild tool definitions for a server (used after reconnect to pick up any changes)\n\tgetToolDefinitions(): ToolDefinition[] {\n\t\treturn Array.from(this.tools.values()).flat();\n\t}\n\n\tgetServerNames(): string[] {\n\t\treturn [...this.clients.keys()];\n\t}\n\n\tgetResourceUris(): string[] {\n\t\treturn this.resourceUris;\n\t}\n\n\tasync readResource(uri: string): Promise<string> {\n\t\tconst serverName = this.clients.keys().next().value;\n\t\tif (!serverName) throw new Error(\"No MCP servers connected\");\n\t\tconst mcpUri = uri.startsWith(`mcp://${serverName}`) ? uri.slice(`mcp://${serverName}`.length) : uri;\n\t\tconst client = this.clients.get(serverName);\n\t\tif (!client) throw new Error(`MCP server \"${serverName}\" not connected`);\n\t\tconst result = await client.readResource(mcpUri);\n\t\treturn result.contents.map((c) => c.text ?? \"\").join(\"\\n\");\n\t}\n\n\tasync stop(): Promise<void> {\n\t\t// Cancel all reconnect timers\n\t\tfor (const [, timer] of this.reconnectTimers) {\n\t\t\tclearTimeout(timer);\n\t\t}\n\t\tthis.reconnectTimers.clear();\n\t\tthis.reconnectAttempts.clear();\n\n\t\tconst pending = [...this.clients.entries()].map(async ([name, client]) => {\n\t\t\ttry {\n\t\t\t\tawait client.disconnect();\n\t\t\t} catch (error) {\n\t\t\t\tconsole.warn(\n\t\t\t\t\t`Error disconnecting MCP server \"${name}\": ${error instanceof Error ? error.message : String(error)}`,\n\t\t\t\t);\n\t\t\t}\n\t\t});\n\t\tthis.clients.clear();\n\t\tthis.tools.clear();\n\t\tthis.resourceUris = [];\n\t\tthis._statuses.clear();\n\t\tawait Promise.all(pending);\n\t}\n}\n"]}