@openeryc/pi-coding-agent 0.75.54 → 0.75.56
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 +30 -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 +11 -26
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/fetch-openai-models.d.ts +25 -0
- package/dist/core/fetch-openai-models.d.ts.map +1 -0
- package/dist/core/fetch-openai-models.js +107 -0
- package/dist/core/fetch-openai-models.js.map +1 -0
- 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/model-registry.d.ts +112 -0
- package/dist/core/model-registry.d.ts.map +1 -1
- package/dist/core/model-registry.js +99 -2
- package/dist/core/model-registry.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/core/settings-manager.d.ts +4 -0
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +12 -0
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/slash-commands.d.ts.map +1 -1
- package/dist/core/slash-commands.js +2 -2
- package/dist/core/slash-commands.js.map +1 -1
- package/dist/modes/interactive/components/index.d.ts +1 -1
- package/dist/modes/interactive/components/index.d.ts.map +1 -1
- package/dist/modes/interactive/components/index.js +1 -1
- package/dist/modes/interactive/components/index.js.map +1 -1
- package/dist/modes/interactive/components/model-hub.d.ts +115 -0
- package/dist/modes/interactive/components/model-hub.d.ts.map +1 -0
- package/dist/modes/interactive/components/model-hub.js +753 -0
- package/dist/modes/interactive/components/model-hub.js.map +1 -0
- package/dist/modes/interactive/components/model-selector.d.ts +2 -44
- package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/model-selector.js +2 -275
- package/dist/modes/interactive/components/model-selector.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +22 -2
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +666 -557
- 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/docs/models.md +20 -0
- package/docs/providers.md +13 -0
- 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
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Discover models from an OpenAI-compatible /models endpoint.
|
|
3
|
+
*/
|
|
4
|
+
export type DiscoveredModel = {
|
|
5
|
+
id: string;
|
|
6
|
+
name: string;
|
|
7
|
+
};
|
|
8
|
+
export type DiscoverModelsResult = {
|
|
9
|
+
ok: true;
|
|
10
|
+
models: DiscoveredModel[];
|
|
11
|
+
} | {
|
|
12
|
+
ok: false;
|
|
13
|
+
error: string;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* GET {baseUrl}/models with optional Bearer auth.
|
|
17
|
+
* Accepts OpenAI-style `{ data: [{ id, ... }] }` and plain `{ models: [...] }` / string arrays.
|
|
18
|
+
*/
|
|
19
|
+
export declare function fetchOpenAICompatibleModels(baseUrl: string, apiKey?: string, options?: {
|
|
20
|
+
signal?: AbortSignal;
|
|
21
|
+
timeoutMs?: number;
|
|
22
|
+
}): Promise<DiscoverModelsResult>;
|
|
23
|
+
/** Normalize a user-entered provider id into a safe slug. */
|
|
24
|
+
export declare function slugifyProviderId(input: string): string;
|
|
25
|
+
//# sourceMappingURL=fetch-openai-models.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fetch-openai-models.d.ts","sourceRoot":"","sources":["../../src/core/fetch-openai-models.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,MAAM,MAAM,eAAe,GAAG;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;CACb,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,MAAM,EAAE,eAAe,EAAE,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAE1G;;;GAGG;AACH,wBAAsB,2BAA2B,CAChD,OAAO,EAAE,MAAM,EACf,MAAM,CAAC,EAAE,MAAM,EACf,OAAO,CAAC,EAAE;IAAE,MAAM,CAAC,EAAE,WAAW,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,GACpD,OAAO,CAAC,oBAAoB,CAAC,CAuD/B;AAsCD,6DAA6D;AAC7D,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAOvD","sourcesContent":["/**\n * Discover models from an OpenAI-compatible /models endpoint.\n */\n\nexport type DiscoveredModel = {\n\tid: string;\n\tname: string;\n};\n\nexport type DiscoverModelsResult = { ok: true; models: DiscoveredModel[] } | { ok: false; error: string };\n\n/**\n * GET {baseUrl}/models with optional Bearer auth.\n * Accepts OpenAI-style `{ data: [{ id, ... }] }` and plain `{ models: [...] }` / string arrays.\n */\nexport async function fetchOpenAICompatibleModels(\n\tbaseUrl: string,\n\tapiKey?: string,\n\toptions?: { signal?: AbortSignal; timeoutMs?: number },\n): Promise<DiscoverModelsResult> {\n\tconst trimmedBase = baseUrl.trim().replace(/\\/+$/, \"\");\n\tif (!trimmedBase) {\n\t\treturn { ok: false, error: \"Base URL cannot be empty\" };\n\t}\n\n\tconst url = `${trimmedBase}/models`;\n\tconst timeoutMs = options?.timeoutMs ?? 10_000;\n\tconst controller = new AbortController();\n\tconst timeout = setTimeout(() => controller.abort(), timeoutMs);\n\tconst onOuterAbort = () => controller.abort();\n\toptions?.signal?.addEventListener(\"abort\", onOuterAbort);\n\n\ttry {\n\t\tconst headers: Record<string, string> = {\n\t\t\tAccept: \"application/json\",\n\t\t};\n\t\tconst key = apiKey?.trim();\n\t\tif (key && key !== \"not-needed\") {\n\t\t\theaders.Authorization = `Bearer ${key}`;\n\t\t}\n\n\t\tconst response = await fetch(url, {\n\t\t\tmethod: \"GET\",\n\t\t\theaders,\n\t\t\tsignal: controller.signal,\n\t\t});\n\n\t\tif (!response.ok) {\n\t\t\tconst body = await response.text().catch(() => \"\");\n\t\t\tconst snippet = body.slice(0, 200).trim();\n\t\t\treturn {\n\t\t\t\tok: false,\n\t\t\t\terror: `HTTP ${response.status}${snippet ? `: ${snippet}` : \"\"}`,\n\t\t\t};\n\t\t}\n\n\t\tconst json: unknown = await response.json();\n\t\tconst models = parseModelsResponse(json);\n\t\tif (models.length === 0) {\n\t\t\treturn { ok: false, error: \"No models found in response\" };\n\t\t}\n\t\treturn { ok: true, models };\n\t} catch (error) {\n\t\tif (error instanceof Error && error.name === \"AbortError\") {\n\t\t\treturn { ok: false, error: `Request timed out after ${timeoutMs}ms` };\n\t\t}\n\t\treturn {\n\t\t\tok: false,\n\t\t\terror: error instanceof Error ? error.message : String(error),\n\t\t};\n\t} finally {\n\t\tclearTimeout(timeout);\n\t\toptions?.signal?.removeEventListener(\"abort\", onOuterAbort);\n\t}\n}\n\nfunction parseModelsResponse(json: unknown): DiscoveredModel[] {\n\tif (!json || typeof json !== \"object\") return [];\n\n\tconst obj = json as Record<string, unknown>;\n\tconst candidates: unknown[] = [];\n\n\tif (Array.isArray(obj.data)) {\n\t\tcandidates.push(...obj.data);\n\t} else if (Array.isArray(obj.models)) {\n\t\tcandidates.push(...obj.models);\n\t} else if (Array.isArray(json)) {\n\t\tcandidates.push(...json);\n\t}\n\n\tconst seen = new Set<string>();\n\tconst models: DiscoveredModel[] = [];\n\tfor (const item of candidates) {\n\t\tif (typeof item === \"string\") {\n\t\t\tconst id = item.trim();\n\t\t\tif (!id || seen.has(id)) continue;\n\t\t\tseen.add(id);\n\t\t\tmodels.push({ id, name: id });\n\t\t\tcontinue;\n\t\t}\n\t\tif (item && typeof item === \"object\") {\n\t\t\tconst record = item as Record<string, unknown>;\n\t\t\tconst id = typeof record.id === \"string\" ? record.id.trim() : \"\";\n\t\t\tif (!id || seen.has(id)) continue;\n\t\t\tseen.add(id);\n\t\t\tconst name = typeof record.name === \"string\" && record.name.trim() ? record.name.trim() : id;\n\t\t\tmodels.push({ id, name });\n\t\t}\n\t}\n\treturn models;\n}\n\n/** Normalize a user-entered provider id into a safe slug. */\nexport function slugifyProviderId(input: string): string {\n\treturn input\n\t\t.trim()\n\t\t.toLowerCase()\n\t\t.replace(/[^a-z0-9._-]+/g, \"-\")\n\t\t.replace(/^-+|-+$/g, \"\")\n\t\t.slice(0, 64);\n}\n"]}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Discover models from an OpenAI-compatible /models endpoint.
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* GET {baseUrl}/models with optional Bearer auth.
|
|
6
|
+
* Accepts OpenAI-style `{ data: [{ id, ... }] }` and plain `{ models: [...] }` / string arrays.
|
|
7
|
+
*/
|
|
8
|
+
export async function fetchOpenAICompatibleModels(baseUrl, apiKey, options) {
|
|
9
|
+
const trimmedBase = baseUrl.trim().replace(/\/+$/, "");
|
|
10
|
+
if (!trimmedBase) {
|
|
11
|
+
return { ok: false, error: "Base URL cannot be empty" };
|
|
12
|
+
}
|
|
13
|
+
const url = `${trimmedBase}/models`;
|
|
14
|
+
const timeoutMs = options?.timeoutMs ?? 10_000;
|
|
15
|
+
const controller = new AbortController();
|
|
16
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
17
|
+
const onOuterAbort = () => controller.abort();
|
|
18
|
+
options?.signal?.addEventListener("abort", onOuterAbort);
|
|
19
|
+
try {
|
|
20
|
+
const headers = {
|
|
21
|
+
Accept: "application/json",
|
|
22
|
+
};
|
|
23
|
+
const key = apiKey?.trim();
|
|
24
|
+
if (key && key !== "not-needed") {
|
|
25
|
+
headers.Authorization = `Bearer ${key}`;
|
|
26
|
+
}
|
|
27
|
+
const response = await fetch(url, {
|
|
28
|
+
method: "GET",
|
|
29
|
+
headers,
|
|
30
|
+
signal: controller.signal,
|
|
31
|
+
});
|
|
32
|
+
if (!response.ok) {
|
|
33
|
+
const body = await response.text().catch(() => "");
|
|
34
|
+
const snippet = body.slice(0, 200).trim();
|
|
35
|
+
return {
|
|
36
|
+
ok: false,
|
|
37
|
+
error: `HTTP ${response.status}${snippet ? `: ${snippet}` : ""}`,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
const json = await response.json();
|
|
41
|
+
const models = parseModelsResponse(json);
|
|
42
|
+
if (models.length === 0) {
|
|
43
|
+
return { ok: false, error: "No models found in response" };
|
|
44
|
+
}
|
|
45
|
+
return { ok: true, models };
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
49
|
+
return { ok: false, error: `Request timed out after ${timeoutMs}ms` };
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
ok: false,
|
|
53
|
+
error: error instanceof Error ? error.message : String(error),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
finally {
|
|
57
|
+
clearTimeout(timeout);
|
|
58
|
+
options?.signal?.removeEventListener("abort", onOuterAbort);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function parseModelsResponse(json) {
|
|
62
|
+
if (!json || typeof json !== "object")
|
|
63
|
+
return [];
|
|
64
|
+
const obj = json;
|
|
65
|
+
const candidates = [];
|
|
66
|
+
if (Array.isArray(obj.data)) {
|
|
67
|
+
candidates.push(...obj.data);
|
|
68
|
+
}
|
|
69
|
+
else if (Array.isArray(obj.models)) {
|
|
70
|
+
candidates.push(...obj.models);
|
|
71
|
+
}
|
|
72
|
+
else if (Array.isArray(json)) {
|
|
73
|
+
candidates.push(...json);
|
|
74
|
+
}
|
|
75
|
+
const seen = new Set();
|
|
76
|
+
const models = [];
|
|
77
|
+
for (const item of candidates) {
|
|
78
|
+
if (typeof item === "string") {
|
|
79
|
+
const id = item.trim();
|
|
80
|
+
if (!id || seen.has(id))
|
|
81
|
+
continue;
|
|
82
|
+
seen.add(id);
|
|
83
|
+
models.push({ id, name: id });
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (item && typeof item === "object") {
|
|
87
|
+
const record = item;
|
|
88
|
+
const id = typeof record.id === "string" ? record.id.trim() : "";
|
|
89
|
+
if (!id || seen.has(id))
|
|
90
|
+
continue;
|
|
91
|
+
seen.add(id);
|
|
92
|
+
const name = typeof record.name === "string" && record.name.trim() ? record.name.trim() : id;
|
|
93
|
+
models.push({ id, name });
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return models;
|
|
97
|
+
}
|
|
98
|
+
/** Normalize a user-entered provider id into a safe slug. */
|
|
99
|
+
export function slugifyProviderId(input) {
|
|
100
|
+
return input
|
|
101
|
+
.trim()
|
|
102
|
+
.toLowerCase()
|
|
103
|
+
.replace(/[^a-z0-9._-]+/g, "-")
|
|
104
|
+
.replace(/^-+|-+$/g, "")
|
|
105
|
+
.slice(0, 64);
|
|
106
|
+
}
|
|
107
|
+
//# sourceMappingURL=fetch-openai-models.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fetch-openai-models.js","sourceRoot":"","sources":["../../src/core/fetch-openai-models.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Discover models from an OpenAI-compatible /models endpoint.\n */\n\nexport type DiscoveredModel = {\n\tid: string;\n\tname: string;\n};\n\nexport type DiscoverModelsResult = { ok: true; models: DiscoveredModel[] } | { ok: false; error: string };\n\n/**\n * GET {baseUrl}/models with optional Bearer auth.\n * Accepts OpenAI-style `{ data: [{ id, ... }] }` and plain `{ models: [...] }` / string arrays.\n */\nexport async function fetchOpenAICompatibleModels(\n\tbaseUrl: string,\n\tapiKey?: string,\n\toptions?: { signal?: AbortSignal; timeoutMs?: number },\n): Promise<DiscoverModelsResult> {\n\tconst trimmedBase = baseUrl.trim().replace(/\\/+$/, \"\");\n\tif (!trimmedBase) {\n\t\treturn { ok: false, error: \"Base URL cannot be empty\" };\n\t}\n\n\tconst url = `${trimmedBase}/models`;\n\tconst timeoutMs = options?.timeoutMs ?? 10_000;\n\tconst controller = new AbortController();\n\tconst timeout = setTimeout(() => controller.abort(), timeoutMs);\n\tconst onOuterAbort = () => controller.abort();\n\toptions?.signal?.addEventListener(\"abort\", onOuterAbort);\n\n\ttry {\n\t\tconst headers: Record<string, string> = {\n\t\t\tAccept: \"application/json\",\n\t\t};\n\t\tconst key = apiKey?.trim();\n\t\tif (key && key !== \"not-needed\") {\n\t\t\theaders.Authorization = `Bearer ${key}`;\n\t\t}\n\n\t\tconst response = await fetch(url, {\n\t\t\tmethod: \"GET\",\n\t\t\theaders,\n\t\t\tsignal: controller.signal,\n\t\t});\n\n\t\tif (!response.ok) {\n\t\t\tconst body = await response.text().catch(() => \"\");\n\t\t\tconst snippet = body.slice(0, 200).trim();\n\t\t\treturn {\n\t\t\t\tok: false,\n\t\t\t\terror: `HTTP ${response.status}${snippet ? `: ${snippet}` : \"\"}`,\n\t\t\t};\n\t\t}\n\n\t\tconst json: unknown = await response.json();\n\t\tconst models = parseModelsResponse(json);\n\t\tif (models.length === 0) {\n\t\t\treturn { ok: false, error: \"No models found in response\" };\n\t\t}\n\t\treturn { ok: true, models };\n\t} catch (error) {\n\t\tif (error instanceof Error && error.name === \"AbortError\") {\n\t\t\treturn { ok: false, error: `Request timed out after ${timeoutMs}ms` };\n\t\t}\n\t\treturn {\n\t\t\tok: false,\n\t\t\terror: error instanceof Error ? error.message : String(error),\n\t\t};\n\t} finally {\n\t\tclearTimeout(timeout);\n\t\toptions?.signal?.removeEventListener(\"abort\", onOuterAbort);\n\t}\n}\n\nfunction parseModelsResponse(json: unknown): DiscoveredModel[] {\n\tif (!json || typeof json !== \"object\") return [];\n\n\tconst obj = json as Record<string, unknown>;\n\tconst candidates: unknown[] = [];\n\n\tif (Array.isArray(obj.data)) {\n\t\tcandidates.push(...obj.data);\n\t} else if (Array.isArray(obj.models)) {\n\t\tcandidates.push(...obj.models);\n\t} else if (Array.isArray(json)) {\n\t\tcandidates.push(...json);\n\t}\n\n\tconst seen = new Set<string>();\n\tconst models: DiscoveredModel[] = [];\n\tfor (const item of candidates) {\n\t\tif (typeof item === \"string\") {\n\t\t\tconst id = item.trim();\n\t\t\tif (!id || seen.has(id)) continue;\n\t\t\tseen.add(id);\n\t\t\tmodels.push({ id, name: id });\n\t\t\tcontinue;\n\t\t}\n\t\tif (item && typeof item === \"object\") {\n\t\t\tconst record = item as Record<string, unknown>;\n\t\t\tconst id = typeof record.id === \"string\" ? record.id.trim() : \"\";\n\t\t\tif (!id || seen.has(id)) continue;\n\t\t\tseen.add(id);\n\t\t\tconst name = typeof record.name === \"string\" && record.name.trim() ? record.name.trim() : id;\n\t\t\tmodels.push({ id, name });\n\t\t}\n\t}\n\treturn models;\n}\n\n/** Normalize a user-entered provider id into a safe slug. */\nexport function slugifyProviderId(input: string): string {\n\treturn input\n\t\t.trim()\n\t\t.toLowerCase()\n\t\t.replace(/[^a-z0-9._-]+/g, \"-\")\n\t\t.replace(/^-+|-+$/g, \"\")\n\t\t.slice(0, 64);\n}\n"]}
|
|
@@ -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"]}
|