@openeryc/pi-coding-agent 0.75.29 → 0.75.32
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 +16 -0
- package/dist/core/agent-session.d.ts +1 -0
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +22 -2
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/hook-runner.d.ts +23 -0
- package/dist/core/hook-runner.d.ts.map +1 -0
- package/dist/core/hook-runner.js +116 -0
- package/dist/core/hook-runner.js.map +1 -0
- package/dist/core/mcp/client.d.ts +22 -12
- package/dist/core/mcp/client.d.ts.map +1 -1
- package/dist/core/mcp/client.js +206 -108
- package/dist/core/mcp/client.js.map +1 -1
- package/dist/core/mcp/index.d.ts +2 -3
- package/dist/core/mcp/index.d.ts.map +1 -1
- package/dist/core/mcp/index.js.map +1 -1
- package/dist/core/mcp/manager.d.ts +6 -9
- package/dist/core/mcp/manager.d.ts.map +1 -1
- package/dist/core/mcp/manager.js +98 -102
- package/dist/core/mcp/manager.js.map +1 -1
- package/dist/core/mcp/types.d.ts +30 -67
- package/dist/core/mcp/types.d.ts.map +1 -1
- package/dist/core/mcp/types.js +0 -6
- package/dist/core/mcp/types.js.map +1 -1
- package/dist/core/settings-manager.d.ts +8 -2
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/tools/ask-user-question.d.ts +21 -0
- package/dist/core/tools/ask-user-question.d.ts.map +1 -0
- package/dist/core/tools/ask-user-question.js +75 -0
- package/dist/core/tools/ask-user-question.js.map +1 -0
- package/dist/core/tools/index.d.ts +2 -1
- package/dist/core/tools/index.d.ts.map +1 -1
- package/dist/core/tools/index.js +9 -0
- package/dist/core/tools/index.js.map +1 -1
- package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/sandbox/package-lock.json +2 -2
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/with-deps/package-lock.json +2 -2
- package/examples/extensions/with-deps/package.json +1 -1
- package/npm-shrinkwrap.json +12 -12
- package/package.json +4 -4
- package/dist/core/index.d.ts +0 -12
- package/dist/core/index.d.ts.map +0 -1
- package/dist/core/index.js +0 -12
- package/dist/core/index.js.map +0 -1
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { SettingsManager } from "./settings-manager.ts";
|
|
2
|
+
export interface HookConfig {
|
|
3
|
+
matcher?: string;
|
|
4
|
+
command: string;
|
|
5
|
+
timeout?: number;
|
|
6
|
+
}
|
|
7
|
+
export interface PreToolResult {
|
|
8
|
+
behavior: "allow" | "deny";
|
|
9
|
+
updatedInput?: unknown;
|
|
10
|
+
message?: string;
|
|
11
|
+
}
|
|
12
|
+
export interface PostToolResult {
|
|
13
|
+
additionalContext?: string;
|
|
14
|
+
}
|
|
15
|
+
export declare class HookRunner {
|
|
16
|
+
private preToolHooks;
|
|
17
|
+
private postToolHooks;
|
|
18
|
+
private defaultTimeout;
|
|
19
|
+
constructor(settingsManager: SettingsManager);
|
|
20
|
+
runPreToolHooks(toolName: string, input: unknown): Promise<PreToolResult | null>;
|
|
21
|
+
runPostToolHooks(toolName: string, result: string): Promise<PostToolResult | null>;
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=hook-runner.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hook-runner.d.ts","sourceRoot":"","sources":["../../src/core/hook-runner.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAE7D,MAAM,WAAW,UAAU;IAC1B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,aAAa;IAC7B,QAAQ,EAAE,OAAO,GAAG,MAAM,CAAC;IAC3B,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,OAAO,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,cAAc;IAC9B,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC3B;AA6DD,qBAAa,UAAU;IACtB,OAAO,CAAC,YAAY,CAAoB;IACxC,OAAO,CAAC,aAAa,CAAoB;IACzC,OAAO,CAAC,cAAc,CAAS;IAE/B,YAAY,eAAe,EAAE,eAAe,EAO3C;IAEK,eAAe,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC,CA8BrF;IAEK,gBAAgB,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC,CAkBvF;CACD","sourcesContent":["import { spawn } from \"child_process\";\nimport type { SettingsManager } from \"./settings-manager.ts\";\n\nexport interface HookConfig {\n\tmatcher?: string;\n\tcommand: string;\n\ttimeout?: number;\n}\n\nexport interface PreToolResult {\n\tbehavior: \"allow\" | \"deny\";\n\tupdatedInput?: unknown;\n\tmessage?: string;\n}\n\nexport interface PostToolResult {\n\tadditionalContext?: string;\n}\n\nfunction matches(matcher: string | undefined, toolName: string): boolean {\n\tif (!matcher) return true;\n\tif (matcher === \"*\") return true;\n\tif (matcher === toolName) return true;\n\tif (matcher.startsWith(\"*\") && toolName.endsWith(matcher.slice(1))) return true;\n\tif (matcher.endsWith(\"*\") && toolName.startsWith(matcher.slice(0, -1))) return true;\n\ttry {\n\t\treturn new RegExp(matcher).test(toolName);\n\t} catch {\n\t\treturn false;\n\t}\n}\n\nasync function runHook(\n\tcommand: string,\n\tinput: string,\n\ttimeoutMs: number,\n): Promise<{ exitCode: number; stdout: string; stderr: string }> {\n\treturn new Promise((resolve) => {\n\t\tconst proc = spawn(\"sh\", [\"-c\", command], {\n\t\t\tstdio: [\"pipe\", \"pipe\", \"pipe\"],\n\t\t\tenv: process.env,\n\t\t});\n\n\t\tlet stdout = \"\";\n\t\tlet stderr = \"\";\n\n\t\tproc.stdout?.on(\"data\", (d: Buffer) => {\n\t\t\tstdout += d.toString();\n\t\t});\n\t\tproc.stderr?.on(\"data\", (d: Buffer) => {\n\t\t\tstderr += d.toString();\n\t\t});\n\n\t\tconst timer = setTimeout(() => {\n\t\t\tproc.kill();\n\t\t\tresolve({ exitCode: -1, stdout, stderr: `${stderr}\\nHook timed out after ${timeoutMs}ms` });\n\t\t}, timeoutMs);\n\n\t\tproc.on(\"close\", (code) => {\n\t\t\tclearTimeout(timer);\n\t\t\tresolve({ exitCode: code ?? -1, stdout, stderr });\n\t\t});\n\n\t\tproc.stdin?.write(input);\n\t\tproc.stdin?.end();\n\t});\n}\n\nfunction parseJsonResponse(output: string): unknown | undefined {\n\tconst trimmed = output.trim();\n\tif (!trimmed) return undefined;\n\ttry {\n\t\treturn JSON.parse(trimmed);\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\nexport class HookRunner {\n\tprivate preToolHooks: HookConfig[] = [];\n\tprivate postToolHooks: HookConfig[] = [];\n\tprivate defaultTimeout = 15000;\n\n\tconstructor(settingsManager: SettingsManager) {\n\t\tconst settings = settingsManager.getProjectSettings();\n\t\tconst hooks = (settings as Record<string, unknown>).hooks as\n\t\t\t| { PreToolUse?: HookConfig[]; PostToolUse?: HookConfig[] }\n\t\t\t| undefined;\n\t\tif (hooks?.PreToolUse) this.preToolHooks = hooks.PreToolUse;\n\t\tif (hooks?.PostToolUse) this.postToolHooks = hooks.PostToolUse;\n\t}\n\n\tasync runPreToolHooks(toolName: string, input: unknown): Promise<PreToolResult | null> {\n\t\tfor (const hook of this.preToolHooks) {\n\t\t\tif (!matches(hook.matcher, toolName)) continue;\n\n\t\t\tconst hookInput = JSON.stringify({ tool_name: toolName, input });\n\t\t\tconst result = await runHook(hook.command, hookInput, hook.timeout ?? this.defaultTimeout);\n\n\t\t\tif (result.exitCode === 2) {\n\t\t\t\treturn {\n\t\t\t\t\tbehavior: \"deny\",\n\t\t\t\t\tmessage: result.stderr || result.stdout || `Blocked by hook: ${hook.command}`,\n\t\t\t\t};\n\t\t\t}\n\n\t\t\tif (result.exitCode === 0) {\n\t\t\t\tconst response = parseJsonResponse(result.stdout);\n\t\t\t\tif (response && typeof response === \"object\" && !Array.isArray(response)) {\n\t\t\t\t\tconst r = response as Record<string, unknown>;\n\t\t\t\t\tif (r.continue === false || r.allow === false) {\n\t\t\t\t\t\treturn { behavior: \"deny\", message: r.message as string };\n\t\t\t\t\t}\n\t\t\t\t\tif (r.updatedInput !== undefined) {\n\t\t\t\t\t\treturn { behavior: \"allow\", updatedInput: r.updatedInput };\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Exit code != 0,2: warn only, continue\n\t\t}\n\t\treturn null;\n\t}\n\n\tasync runPostToolHooks(toolName: string, result: string): Promise<PostToolResult | null> {\n\t\tfor (const hook of this.postToolHooks) {\n\t\t\tif (!matches(hook.matcher, toolName)) continue;\n\n\t\t\tconst hookInput = JSON.stringify({ tool_name: toolName, output: result });\n\t\t\tconst res = await runHook(hook.command, hookInput, hook.timeout ?? this.defaultTimeout);\n\n\t\t\tif (res.exitCode === 0) {\n\t\t\t\tconst response = parseJsonResponse(res.stdout);\n\t\t\t\tif (response && typeof response === \"object\" && !Array.isArray(response)) {\n\t\t\t\t\tconst r = response as Record<string, unknown>;\n\t\t\t\t\tif (r.additionalContext) {\n\t\t\t\t\t\treturn { additionalContext: String(r.additionalContext) };\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn null;\n\t}\n}\n"]}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { spawn } from "child_process";
|
|
2
|
+
function matches(matcher, toolName) {
|
|
3
|
+
if (!matcher)
|
|
4
|
+
return true;
|
|
5
|
+
if (matcher === "*")
|
|
6
|
+
return true;
|
|
7
|
+
if (matcher === toolName)
|
|
8
|
+
return true;
|
|
9
|
+
if (matcher.startsWith("*") && toolName.endsWith(matcher.slice(1)))
|
|
10
|
+
return true;
|
|
11
|
+
if (matcher.endsWith("*") && toolName.startsWith(matcher.slice(0, -1)))
|
|
12
|
+
return true;
|
|
13
|
+
try {
|
|
14
|
+
return new RegExp(matcher).test(toolName);
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
async function runHook(command, input, timeoutMs) {
|
|
21
|
+
return new Promise((resolve) => {
|
|
22
|
+
const proc = spawn("sh", ["-c", command], {
|
|
23
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
24
|
+
env: process.env,
|
|
25
|
+
});
|
|
26
|
+
let stdout = "";
|
|
27
|
+
let stderr = "";
|
|
28
|
+
proc.stdout?.on("data", (d) => {
|
|
29
|
+
stdout += d.toString();
|
|
30
|
+
});
|
|
31
|
+
proc.stderr?.on("data", (d) => {
|
|
32
|
+
stderr += d.toString();
|
|
33
|
+
});
|
|
34
|
+
const timer = setTimeout(() => {
|
|
35
|
+
proc.kill();
|
|
36
|
+
resolve({ exitCode: -1, stdout, stderr: `${stderr}\nHook timed out after ${timeoutMs}ms` });
|
|
37
|
+
}, timeoutMs);
|
|
38
|
+
proc.on("close", (code) => {
|
|
39
|
+
clearTimeout(timer);
|
|
40
|
+
resolve({ exitCode: code ?? -1, stdout, stderr });
|
|
41
|
+
});
|
|
42
|
+
proc.stdin?.write(input);
|
|
43
|
+
proc.stdin?.end();
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
function parseJsonResponse(output) {
|
|
47
|
+
const trimmed = output.trim();
|
|
48
|
+
if (!trimmed)
|
|
49
|
+
return undefined;
|
|
50
|
+
try {
|
|
51
|
+
return JSON.parse(trimmed);
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
export class HookRunner {
|
|
58
|
+
preToolHooks = [];
|
|
59
|
+
postToolHooks = [];
|
|
60
|
+
defaultTimeout = 15000;
|
|
61
|
+
constructor(settingsManager) {
|
|
62
|
+
const settings = settingsManager.getProjectSettings();
|
|
63
|
+
const hooks = settings.hooks;
|
|
64
|
+
if (hooks?.PreToolUse)
|
|
65
|
+
this.preToolHooks = hooks.PreToolUse;
|
|
66
|
+
if (hooks?.PostToolUse)
|
|
67
|
+
this.postToolHooks = hooks.PostToolUse;
|
|
68
|
+
}
|
|
69
|
+
async runPreToolHooks(toolName, input) {
|
|
70
|
+
for (const hook of this.preToolHooks) {
|
|
71
|
+
if (!matches(hook.matcher, toolName))
|
|
72
|
+
continue;
|
|
73
|
+
const hookInput = JSON.stringify({ tool_name: toolName, input });
|
|
74
|
+
const result = await runHook(hook.command, hookInput, hook.timeout ?? this.defaultTimeout);
|
|
75
|
+
if (result.exitCode === 2) {
|
|
76
|
+
return {
|
|
77
|
+
behavior: "deny",
|
|
78
|
+
message: result.stderr || result.stdout || `Blocked by hook: ${hook.command}`,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
if (result.exitCode === 0) {
|
|
82
|
+
const response = parseJsonResponse(result.stdout);
|
|
83
|
+
if (response && typeof response === "object" && !Array.isArray(response)) {
|
|
84
|
+
const r = response;
|
|
85
|
+
if (r.continue === false || r.allow === false) {
|
|
86
|
+
return { behavior: "deny", message: r.message };
|
|
87
|
+
}
|
|
88
|
+
if (r.updatedInput !== undefined) {
|
|
89
|
+
return { behavior: "allow", updatedInput: r.updatedInput };
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
// Exit code != 0,2: warn only, continue
|
|
94
|
+
}
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
async runPostToolHooks(toolName, result) {
|
|
98
|
+
for (const hook of this.postToolHooks) {
|
|
99
|
+
if (!matches(hook.matcher, toolName))
|
|
100
|
+
continue;
|
|
101
|
+
const hookInput = JSON.stringify({ tool_name: toolName, output: result });
|
|
102
|
+
const res = await runHook(hook.command, hookInput, hook.timeout ?? this.defaultTimeout);
|
|
103
|
+
if (res.exitCode === 0) {
|
|
104
|
+
const response = parseJsonResponse(res.stdout);
|
|
105
|
+
if (response && typeof response === "object" && !Array.isArray(response)) {
|
|
106
|
+
const r = response;
|
|
107
|
+
if (r.additionalContext) {
|
|
108
|
+
return { additionalContext: String(r.additionalContext) };
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
//# sourceMappingURL=hook-runner.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hook-runner.js","sourceRoot":"","sources":["../../src/core/hook-runner.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawn } from \"child_process\";\nimport type { SettingsManager } from \"./settings-manager.ts\";\n\nexport interface HookConfig {\n\tmatcher?: string;\n\tcommand: string;\n\ttimeout?: number;\n}\n\nexport interface PreToolResult {\n\tbehavior: \"allow\" | \"deny\";\n\tupdatedInput?: unknown;\n\tmessage?: string;\n}\n\nexport interface PostToolResult {\n\tadditionalContext?: string;\n}\n\nfunction matches(matcher: string | undefined, toolName: string): boolean {\n\tif (!matcher) return true;\n\tif (matcher === \"*\") return true;\n\tif (matcher === toolName) return true;\n\tif (matcher.startsWith(\"*\") && toolName.endsWith(matcher.slice(1))) return true;\n\tif (matcher.endsWith(\"*\") && toolName.startsWith(matcher.slice(0, -1))) return true;\n\ttry {\n\t\treturn new RegExp(matcher).test(toolName);\n\t} catch {\n\t\treturn false;\n\t}\n}\n\nasync function runHook(\n\tcommand: string,\n\tinput: string,\n\ttimeoutMs: number,\n): Promise<{ exitCode: number; stdout: string; stderr: string }> {\n\treturn new Promise((resolve) => {\n\t\tconst proc = spawn(\"sh\", [\"-c\", command], {\n\t\t\tstdio: [\"pipe\", \"pipe\", \"pipe\"],\n\t\t\tenv: process.env,\n\t\t});\n\n\t\tlet stdout = \"\";\n\t\tlet stderr = \"\";\n\n\t\tproc.stdout?.on(\"data\", (d: Buffer) => {\n\t\t\tstdout += d.toString();\n\t\t});\n\t\tproc.stderr?.on(\"data\", (d: Buffer) => {\n\t\t\tstderr += d.toString();\n\t\t});\n\n\t\tconst timer = setTimeout(() => {\n\t\t\tproc.kill();\n\t\t\tresolve({ exitCode: -1, stdout, stderr: `${stderr}\\nHook timed out after ${timeoutMs}ms` });\n\t\t}, timeoutMs);\n\n\t\tproc.on(\"close\", (code) => {\n\t\t\tclearTimeout(timer);\n\t\t\tresolve({ exitCode: code ?? -1, stdout, stderr });\n\t\t});\n\n\t\tproc.stdin?.write(input);\n\t\tproc.stdin?.end();\n\t});\n}\n\nfunction parseJsonResponse(output: string): unknown | undefined {\n\tconst trimmed = output.trim();\n\tif (!trimmed) return undefined;\n\ttry {\n\t\treturn JSON.parse(trimmed);\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\nexport class HookRunner {\n\tprivate preToolHooks: HookConfig[] = [];\n\tprivate postToolHooks: HookConfig[] = [];\n\tprivate defaultTimeout = 15000;\n\n\tconstructor(settingsManager: SettingsManager) {\n\t\tconst settings = settingsManager.getProjectSettings();\n\t\tconst hooks = (settings as Record<string, unknown>).hooks as\n\t\t\t| { PreToolUse?: HookConfig[]; PostToolUse?: HookConfig[] }\n\t\t\t| undefined;\n\t\tif (hooks?.PreToolUse) this.preToolHooks = hooks.PreToolUse;\n\t\tif (hooks?.PostToolUse) this.postToolHooks = hooks.PostToolUse;\n\t}\n\n\tasync runPreToolHooks(toolName: string, input: unknown): Promise<PreToolResult | null> {\n\t\tfor (const hook of this.preToolHooks) {\n\t\t\tif (!matches(hook.matcher, toolName)) continue;\n\n\t\t\tconst hookInput = JSON.stringify({ tool_name: toolName, input });\n\t\t\tconst result = await runHook(hook.command, hookInput, hook.timeout ?? this.defaultTimeout);\n\n\t\t\tif (result.exitCode === 2) {\n\t\t\t\treturn {\n\t\t\t\t\tbehavior: \"deny\",\n\t\t\t\t\tmessage: result.stderr || result.stdout || `Blocked by hook: ${hook.command}`,\n\t\t\t\t};\n\t\t\t}\n\n\t\t\tif (result.exitCode === 0) {\n\t\t\t\tconst response = parseJsonResponse(result.stdout);\n\t\t\t\tif (response && typeof response === \"object\" && !Array.isArray(response)) {\n\t\t\t\t\tconst r = response as Record<string, unknown>;\n\t\t\t\t\tif (r.continue === false || r.allow === false) {\n\t\t\t\t\t\treturn { behavior: \"deny\", message: r.message as string };\n\t\t\t\t\t}\n\t\t\t\t\tif (r.updatedInput !== undefined) {\n\t\t\t\t\t\treturn { behavior: \"allow\", updatedInput: r.updatedInput };\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Exit code != 0,2: warn only, continue\n\t\t}\n\t\treturn null;\n\t}\n\n\tasync runPostToolHooks(toolName: string, result: string): Promise<PostToolResult | null> {\n\t\tfor (const hook of this.postToolHooks) {\n\t\t\tif (!matches(hook.matcher, toolName)) continue;\n\n\t\t\tconst hookInput = JSON.stringify({ tool_name: toolName, output: result });\n\t\t\tconst res = await runHook(hook.command, hookInput, hook.timeout ?? this.defaultTimeout);\n\n\t\t\tif (res.exitCode === 0) {\n\t\t\t\tconst response = parseJsonResponse(res.stdout);\n\t\t\t\tif (response && typeof response === \"object\" && !Array.isArray(response)) {\n\t\t\t\t\tconst r = response as Record<string, unknown>;\n\t\t\t\t\tif (r.additionalContext) {\n\t\t\t\t\t\treturn { additionalContext: String(r.additionalContext) };\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn null;\n\t}\n}\n"]}
|
|
@@ -1,18 +1,28 @@
|
|
|
1
|
-
import type { MCPCallToolResult,
|
|
1
|
+
import type { MCPCallToolResult, MCPResource, MCPServerConfig, MCPToolDefinition } from "./types.ts";
|
|
2
|
+
export type { MCPServerConfig };
|
|
2
3
|
export declare class MCPClient {
|
|
3
4
|
private process;
|
|
4
5
|
private requestId;
|
|
5
|
-
private
|
|
6
|
-
private
|
|
7
|
-
|
|
8
|
-
private
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
6
|
+
private pending;
|
|
7
|
+
private sseEndpoint;
|
|
8
|
+
connect(config: MCPServerConfig): Promise<void>;
|
|
9
|
+
private connectStdio;
|
|
10
|
+
private connectHttp;
|
|
11
|
+
private readSSEEvent;
|
|
12
|
+
private send;
|
|
13
|
+
listTools(): Promise<{
|
|
14
|
+
tools: MCPToolDefinition[];
|
|
15
|
+
}>;
|
|
16
|
+
listResources(): Promise<{
|
|
17
|
+
resources: MCPResource[];
|
|
18
|
+
}>;
|
|
19
|
+
readResource(uri: string): Promise<{
|
|
20
|
+
contents: Array<{
|
|
21
|
+
uri: string;
|
|
22
|
+
mimeType?: string;
|
|
23
|
+
text?: string;
|
|
24
|
+
}>;
|
|
25
|
+
}>;
|
|
16
26
|
callTool(name: string, args: Record<string, unknown>): Promise<MCPCallToolResult>;
|
|
17
27
|
disconnect(): Promise<void>;
|
|
18
28
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../../src/core/mcp/client.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACX,iBAAiB,EAIjB,kBAAkB,EAClB,eAAe,EACf,MAAM,YAAY,CAAC;AAMpB,qBAAa,SAAS;IACrB,OAAO,CAAC,OAAO,CAA6B;IAC5C,OAAO,CAAC,SAAS,CAAK;IACtB,OAAO,CAAC,eAAe,CAA4F;IACnH,OAAO,CAAC,UAAU,CAA0B;IAC5C,OAAO,CAAC,MAAM,CAAkB;IAChC,OAAO,CAAC,SAAS,CAAgB;IAEjC,IAAI,MAAM,IAAI,SAAS,MAAM,EAAE,CAE9B;IAED,YAAY,MAAM,EAAE,eAAe,EAElC;IAEK,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAkD7B;YAEa,WAAW;IAoBzB,OAAO,CAAC,UAAU;YAmBJ,UAAU;IAQlB,SAAS,IAAI,OAAO,CAAC,kBAAkB,CAAC,CAE7C;IAEK,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAKtF;IAEK,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC,CA2BhC;CACD","sourcesContent":["import { type ChildProcess, spawn } from \"child_process\";\nimport { createInterface, type Interface } from \"readline\";\nimport type {\n\tMCPCallToolResult,\n\tMCPInitializeResult,\n\tMCPJsonRpcRequest,\n\tMCPJsonRpcResponse,\n\tMCPListToolsResult,\n\tMCPServerConfig,\n} from \"./types.ts\";\n\nconst INITIALIZE_REQUEST = \"initialize\";\nconst TOOLS_LIST_REQUEST = \"tools/list\";\nconst TOOLS_CALL_REQUEST = \"tools/call\";\n\nexport class MCPClient {\n\tprivate process: ChildProcess | null = null;\n\tprivate requestId = 0;\n\tprivate pendingRequests = new Map<number, { resolve: (value: unknown) => void; reject: (error: Error) => void }>();\n\tprivate lineReader: Interface | null = null;\n\tprivate config: MCPServerConfig;\n\tprivate errorLogs: string[] = [];\n\n\tget errors(): readonly string[] {\n\t\treturn this.errorLogs;\n\t}\n\n\tconstructor(config: MCPServerConfig) {\n\t\tthis.config = config;\n\t}\n\n\tasync connect(): Promise<void> {\n\t\tconst { command, args = [], env } = this.config;\n\t\tconst childEnv = env ? { ...process.env, ...env } : process.env;\n\n\t\treturn new Promise((resolve, reject) => {\n\t\t\tthis.process = spawn(command, args, {\n\t\t\t\tenv: childEnv,\n\t\t\t\tstdio: [\"pipe\", \"pipe\", \"pipe\"],\n\t\t\t\tshell: false,\n\t\t\t});\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\n\t\t\tthis.process.on(\"error\", onError);\n\t\t\tthis.process.once(\"spawn\", () => {\n\t\t\t\tthis.process!.removeListener(\"error\", onError);\n\t\t\t});\n\n\t\t\tif (!this.process.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\tthis.lineReader = createInterface({ input: this.process.stdout, crlfDelay: Infinity });\n\n\t\t\tthis.lineReader.on(\"line\", (line: string) => {\n\t\t\t\tthis.handleLine(line);\n\t\t\t});\n\n\t\t\tif (this.process.stderr) {\n\t\t\t\tthis.process.stderr.setEncoding(\"utf-8\");\n\t\t\t\tthis.process.stderr.on(\"data\", (data: string) => {\n\t\t\t\t\tthis.errorLogs.push(data);\n\t\t\t\t});\n\t\t\t}\n\n\t\t\tthis.process.on(\"exit\", (code) => {\n\t\t\t\tconst pendingCopy = new Map(this.pendingRequests);\n\t\t\t\tthis.pendingRequests.clear();\n\t\t\t\tfor (const [, handler] of pendingCopy) {\n\t\t\t\t\thandler.reject(new Error(`MCP server \"${command}\" exited with code ${code}`));\n\t\t\t\t}\n\t\t\t});\n\n\t\t\tthis.initialize()\n\t\t\t\t.then(() => resolve())\n\t\t\t\t.catch(reject);\n\t\t});\n\t}\n\n\tprivate async sendRequest(method: string, params?: Record<string, unknown>): Promise<unknown> {\n\t\tif (!this.process || !this.process.stdin) {\n\t\t\tthrow new Error(`MCP client not connected`);\n\t\t}\n\n\t\tconst id = ++this.requestId;\n\t\tconst request: MCPJsonRpcRequest = {\n\t\t\tjsonrpc: \"2.0\",\n\t\t\tid,\n\t\t\tmethod,\n\t\t\tparams,\n\t\t};\n\n\t\treturn new Promise((resolve, reject) => {\n\t\t\tthis.pendingRequests.set(id, { resolve, reject });\n\t\t\tconst payload = JSON.stringify(request);\n\t\t\tthis.process!.stdin!.write(`${payload}\\n`);\n\t\t});\n\t}\n\n\tprivate handleLine(line: string): void {\n\t\ttry {\n\t\t\tconst response = JSON.parse(line) as MCPJsonRpcResponse;\n\t\t\tif (response.id !== undefined) {\n\t\t\t\tconst request = this.pendingRequests.get(response.id);\n\t\t\t\tif (request) {\n\t\t\t\t\tthis.pendingRequests.delete(response.id);\n\t\t\t\t\tif (response.error) {\n\t\t\t\t\t\trequest.reject(new Error(`MCP error: ${response.error.message} (code ${response.error.code})`));\n\t\t\t\t\t} else {\n\t\t\t\t\t\trequest.resolve(response.result);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t} catch {\n\t\t\t// Ignore non-JSON lines (stderr or informational messages)\n\t\t}\n\t}\n\n\tprivate async initialize(): Promise<MCPInitializeResult> {\n\t\treturn (await this.sendRequest(INITIALIZE_REQUEST, {\n\t\t\tprotocolVersion: \"2024-11-05\",\n\t\t\tcapabilities: {},\n\t\t\tclientInfo: { name: \"pi\", version: \"1.0.0\" },\n\t\t})) as MCPInitializeResult;\n\t}\n\n\tasync listTools(): Promise<MCPListToolsResult> {\n\t\treturn (await this.sendRequest(TOOLS_LIST_REQUEST)) as MCPListToolsResult;\n\t}\n\n\tasync callTool(name: string, args: Record<string, unknown>): Promise<MCPCallToolResult> {\n\t\treturn (await this.sendRequest(TOOLS_CALL_REQUEST, {\n\t\t\tname,\n\t\t\targuments: args,\n\t\t} as unknown as Record<string, unknown>)) as MCPCallToolResult;\n\t}\n\n\tasync disconnect(): Promise<void> {\n\t\tif (this.lineReader) {\n\t\t\tthis.lineReader.close();\n\t\t\tthis.lineReader = null;\n\t\t}\n\t\tif (this.process) {\n\t\t\tconst proc = this.process;\n\t\t\tthis.process = null;\n\t\t\tthis.pendingRequests.clear();\n\t\t\tif (!proc.killed) {\n\t\t\t\tproc.kill();\n\t\t\t\tawait new Promise<void>((resolve) => {\n\t\t\t\t\tconst onExit = () => {\n\t\t\t\t\t\tresolve();\n\t\t\t\t\t};\n\t\t\t\t\tproc.on(\"exit\", onExit);\n\t\t\t\t\tif (proc.exitCode !== null) {\n\t\t\t\t\t\tproc.removeListener(\"exit\", onExit);\n\t\t\t\t\t\tresolve();\n\t\t\t\t\t}\n\t\t\t\t\tsetTimeout(() => {\n\t\t\t\t\t\tproc.removeListener(\"exit\", onExit);\n\t\t\t\t\t\tresolve();\n\t\t\t\t\t}, 3000);\n\t\t\t\t});\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;AAWhC,qBAAa,SAAS;IACrB,OAAO,CAAC,OAAO,CAA6B;IAC5C,OAAO,CAAC,SAAS,CAAK;IACtB,OAAO,CAAC,OAAO,CAAqC;IACpD,OAAO,CAAC,WAAW,CAAuB;IAEpC,OAAO,CAAC,MAAM,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CAYpD;YAEa,YAAY;YAkDZ,WAAW;YAmEX,YAAY;YA4BZ,IAAI;IA+CZ,SAAS,IAAI,OAAO,CAAC;QAAE,KAAK,EAAE,iBAAiB,EAAE,CAAA;KAAE,CAAC,CAEzD;IAEK,aAAa,IAAI,OAAO,CAAC;QAAE,SAAS,EAAE,WAAW,EAAE,CAAA;KAAE,CAAC,CAE3D;IAEK,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,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,CAI/G;IAEK,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAEtF;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\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\n\tasync connect(config: MCPServerConfig): Promise<void> {\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>): 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 new Promise((resolve, reject) => {\n\t\t\t\tthis.pending.set(id, { resolve, reject });\n\t\t\t\tthis.process!.stdin!.write(`${body}\\n`);\n\t\t\t});\n\t\t}\n\n\t\tif (this.sseEndpoint) {\n\t\t\treturn new Promise((resolve, reject) => {\n\t\t\t\tthis.pending.set(id, { resolve, reject });\n\t\t\t\tfetch(this.sseEndpoint!, {\n\t\t\t\t\tmethod: \"POST\",\n\t\t\t\t\theaders: { \"Content-Type\": \"application/json\" },\n\t\t\t\t\tbody,\n\t\t\t\t})\n\t\t\t\t\t.then(async (res) => {\n\t\t\t\t\t\tconst text = await res.text();\n\t\t\t\t\t\tif (res.headers.get(\"content-type\")?.includes(\"text/event-stream\")) {\n\t\t\t\t\t\t\t// SSE upgrade - response comes via event stream\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tconst parsed = JSON.parse(text);\n\t\t\t\t\t\t\tconst handler = this.pending.get(id);\n\t\t\t\t\t\t\tif (handler) {\n\t\t\t\t\t\t\t\tthis.pending.delete(id);\n\t\t\t\t\t\t\t\tif (parsed.error) {\n\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} else {\n\t\t\t\t\t\t\t\t\thandler.resolve(parsed.result);\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t} catch {\n\t\t\t\t\t\t\treject(new Error(`Invalid JSON response from MCP server`));\n\t\t\t\t\t\t}\n\t\t\t\t\t})\n\t\t\t\t\t.catch(reject);\n\t\t\t});\n\t\t}\n\n\t\tthrow new Error(\"MCP client not connected\");\n\t}\n\n\tasync listTools(): Promise<{ tools: MCPToolDefinition[] }> {\n\t\treturn (await this.send(\"tools/list\")) as { tools: MCPToolDefinition[] };\n\t}\n\n\tasync listResources(): Promise<{ resources: MCPResource[] }> {\n\t\treturn (await this.send(\"resources/list\")) as { resources: MCPResource[] };\n\t}\n\n\tasync readResource(uri: string): Promise<{ contents: Array<{ uri: string; mimeType?: string; text?: string }> }> {\n\t\treturn (await this.send(\"resources/read\", { uri })) 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>): Promise<MCPCallToolResult> {\n\t\treturn (await this.send(\"tools/call\", { name, arguments: args })) 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"]}
|
package/dist/core/mcp/client.js
CHANGED
|
@@ -1,142 +1,240 @@
|
|
|
1
1
|
import { spawn } from "child_process";
|
|
2
2
|
import { createInterface } from "readline";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
function resolveTransport(config) {
|
|
4
|
+
if (config.transport)
|
|
5
|
+
return config.transport;
|
|
6
|
+
if (config.command)
|
|
7
|
+
return "stdio";
|
|
8
|
+
if (config.url)
|
|
9
|
+
return "sse";
|
|
10
|
+
return "stdio";
|
|
11
|
+
}
|
|
6
12
|
export class MCPClient {
|
|
7
13
|
process = null;
|
|
8
14
|
requestId = 0;
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
config
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
pending = new Map();
|
|
16
|
+
sseEndpoint = null;
|
|
17
|
+
async connect(config) {
|
|
18
|
+
const transport = resolveTransport(config);
|
|
19
|
+
if (transport === "stdio") {
|
|
20
|
+
await this.connectStdio(config);
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
await this.connectHttp(config, transport);
|
|
24
|
+
}
|
|
25
|
+
await this.send("initialize", {
|
|
26
|
+
protocolVersion: "2024-11-05",
|
|
27
|
+
capabilities: {},
|
|
28
|
+
clientInfo: { name: "pi", version: "1.0.0" },
|
|
29
|
+
});
|
|
18
30
|
}
|
|
19
|
-
async
|
|
20
|
-
const { command, args = [], env } =
|
|
31
|
+
async connectStdio(config) {
|
|
32
|
+
const { command, args = [], env } = config;
|
|
33
|
+
if (!command)
|
|
34
|
+
throw new Error("MCP stdio server requires 'command'");
|
|
21
35
|
const childEnv = env ? { ...process.env, ...env } : process.env;
|
|
22
36
|
return new Promise((resolve, reject) => {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
26
|
-
shell: false,
|
|
27
|
-
});
|
|
37
|
+
const proc = spawn(command, args, { env: childEnv, stdio: ["pipe", "pipe", "pipe"], shell: false });
|
|
38
|
+
this.process = proc;
|
|
28
39
|
const onError = (error) => {
|
|
29
40
|
reject(new Error(`Failed to spawn MCP server "${command}": ${error.message}`));
|
|
30
41
|
};
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
});
|
|
35
|
-
if (!this.process.stdout) {
|
|
42
|
+
proc.on("error", onError);
|
|
43
|
+
proc.once("spawn", () => proc.removeListener("error", onError));
|
|
44
|
+
if (!proc.stdout) {
|
|
36
45
|
reject(new Error(`MCP server "${command}" has no stdout`));
|
|
37
46
|
return;
|
|
38
47
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
48
|
+
const rl = createInterface({ input: proc.stdout, crlfDelay: Infinity });
|
|
49
|
+
rl.on("line", (line) => {
|
|
50
|
+
try {
|
|
51
|
+
const res = JSON.parse(line);
|
|
52
|
+
if (res.id !== undefined) {
|
|
53
|
+
const handler = this.pending.get(res.id);
|
|
54
|
+
if (!handler)
|
|
55
|
+
return;
|
|
56
|
+
this.pending.delete(res.id);
|
|
57
|
+
if (res.error) {
|
|
58
|
+
handler.reject(new Error(`MCP error: ${res.error.message}`));
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
handler.resolve(res.result);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
/* non-JSON lines */
|
|
67
|
+
}
|
|
42
68
|
});
|
|
43
|
-
|
|
44
|
-
this.
|
|
45
|
-
|
|
46
|
-
this.errorLogs.push(data);
|
|
47
|
-
});
|
|
48
|
-
}
|
|
49
|
-
this.process.on("exit", (code) => {
|
|
50
|
-
const pendingCopy = new Map(this.pendingRequests);
|
|
51
|
-
this.pendingRequests.clear();
|
|
52
|
-
for (const [, handler] of pendingCopy) {
|
|
53
|
-
handler.reject(new Error(`MCP server "${command}" exited with code ${code}`));
|
|
69
|
+
proc.on("exit", (code) => {
|
|
70
|
+
for (const [, h] of this.pending) {
|
|
71
|
+
h.reject(new Error(`MCP server "${command}" exited with code ${code}`));
|
|
54
72
|
}
|
|
73
|
+
this.pending.clear();
|
|
55
74
|
});
|
|
56
|
-
|
|
57
|
-
.then(() => resolve())
|
|
58
|
-
.catch(reject);
|
|
75
|
+
resolve();
|
|
59
76
|
});
|
|
60
77
|
}
|
|
61
|
-
async
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
78
|
+
async connectHttp(config, transport) {
|
|
79
|
+
const { url, headers = {} } = config;
|
|
80
|
+
if (!url)
|
|
81
|
+
throw new Error("MCP SSE/HTTP server requires 'url'");
|
|
82
|
+
if (transport === "sse") {
|
|
83
|
+
const res = await fetch(`${url}/sse`, { headers: { ...headers, Accept: "text/event-stream" } });
|
|
84
|
+
if (!res.ok)
|
|
85
|
+
throw new Error(`SSE connection failed: ${res.status} ${res.statusText}`);
|
|
86
|
+
const reader = res.body.getReader();
|
|
87
|
+
const decoder = new TextDecoder();
|
|
88
|
+
let buffer = "";
|
|
89
|
+
const processChunk = async () => {
|
|
90
|
+
while (true) {
|
|
91
|
+
const { done, value } = await reader.read();
|
|
92
|
+
if (done)
|
|
93
|
+
break;
|
|
94
|
+
buffer += decoder.decode(value, { stream: true });
|
|
95
|
+
const lines = buffer.split("\n");
|
|
96
|
+
buffer = lines.pop() ?? "";
|
|
97
|
+
for (const line of lines) {
|
|
98
|
+
if (line.startsWith("data: ")) {
|
|
99
|
+
const data = line.slice(6);
|
|
100
|
+
if (data.startsWith("{") && this.sseEndpoint === null) {
|
|
101
|
+
try {
|
|
102
|
+
const parsed = JSON.parse(data);
|
|
103
|
+
if (parsed.result) {
|
|
104
|
+
const handler = this.pending.get(parsed.id);
|
|
105
|
+
if (handler) {
|
|
106
|
+
this.pending.delete(parsed.id);
|
|
107
|
+
if (parsed.error) {
|
|
108
|
+
handler.reject(new Error(`MCP error: ${parsed.error.message}`));
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
handler.resolve(parsed.result);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
/* skip */
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
else if (data.startsWith("http")) {
|
|
121
|
+
// This is the endpoint URL
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
else if (line.startsWith("event: endpoint")) {
|
|
125
|
+
// Next data line will be the endpoint URL
|
|
126
|
+
}
|
|
90
127
|
}
|
|
91
128
|
}
|
|
129
|
+
};
|
|
130
|
+
// Wait for endpoint event
|
|
131
|
+
const endpointLine = await this.readSSEEvent(reader, decoder);
|
|
132
|
+
if (endpointLine) {
|
|
133
|
+
this.sseEndpoint = endpointLine.startsWith("/") ? new URL(endpointLine, url).href : endpointLine;
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
this.sseEndpoint = url;
|
|
92
137
|
}
|
|
138
|
+
// Start background reader
|
|
139
|
+
processChunk().catch(() => { });
|
|
93
140
|
}
|
|
94
|
-
|
|
95
|
-
//
|
|
141
|
+
else {
|
|
142
|
+
// Streamable HTTP - POST to URL directly, server responds with JSON or upgrades to SSE
|
|
143
|
+
this.sseEndpoint = url;
|
|
96
144
|
}
|
|
97
145
|
}
|
|
98
|
-
async
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
146
|
+
async readSSEEvent(reader, decoder) {
|
|
147
|
+
let buffer = "";
|
|
148
|
+
const deadline = Date.now() + 10000;
|
|
149
|
+
while (Date.now() < deadline) {
|
|
150
|
+
const { value, done } = await reader.read();
|
|
151
|
+
if (done)
|
|
152
|
+
break;
|
|
153
|
+
buffer += decoder.decode(value, { stream: true });
|
|
154
|
+
const lines = buffer.split("\n");
|
|
155
|
+
buffer = lines.pop() ?? "";
|
|
156
|
+
for (let i = 0; i < lines.length; i++) {
|
|
157
|
+
const line = lines[i];
|
|
158
|
+
if (line.startsWith("data: ") && lines[i - 1]?.startsWith("event: endpoint")) {
|
|
159
|
+
return line.slice(6).trim();
|
|
160
|
+
}
|
|
161
|
+
if (line.startsWith("event: endpoint") && lines[i + 1]?.startsWith("data: ")) {
|
|
162
|
+
return lines[i + 1].slice(6).trim();
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
async send(method, params) {
|
|
169
|
+
const id = ++this.requestId;
|
|
170
|
+
const body = JSON.stringify({ jsonrpc: "2.0", id, method, params });
|
|
171
|
+
if (this.process?.stdin) {
|
|
172
|
+
return new Promise((resolve, reject) => {
|
|
173
|
+
this.pending.set(id, { resolve, reject });
|
|
174
|
+
this.process.stdin.write(`${body}\n`);
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
if (this.sseEndpoint) {
|
|
178
|
+
return new Promise((resolve, reject) => {
|
|
179
|
+
this.pending.set(id, { resolve, reject });
|
|
180
|
+
fetch(this.sseEndpoint, {
|
|
181
|
+
method: "POST",
|
|
182
|
+
headers: { "Content-Type": "application/json" },
|
|
183
|
+
body,
|
|
184
|
+
})
|
|
185
|
+
.then(async (res) => {
|
|
186
|
+
const text = await res.text();
|
|
187
|
+
if (res.headers.get("content-type")?.includes("text/event-stream")) {
|
|
188
|
+
// SSE upgrade - response comes via event stream
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
try {
|
|
192
|
+
const parsed = JSON.parse(text);
|
|
193
|
+
const handler = this.pending.get(id);
|
|
194
|
+
if (handler) {
|
|
195
|
+
this.pending.delete(id);
|
|
196
|
+
if (parsed.error) {
|
|
197
|
+
handler.reject(new Error(`MCP error: ${parsed.error.message}`));
|
|
198
|
+
}
|
|
199
|
+
else {
|
|
200
|
+
handler.resolve(parsed.result);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
catch {
|
|
205
|
+
reject(new Error(`Invalid JSON response from MCP server`));
|
|
206
|
+
}
|
|
207
|
+
})
|
|
208
|
+
.catch(reject);
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
throw new Error("MCP client not connected");
|
|
104
212
|
}
|
|
105
213
|
async listTools() {
|
|
106
|
-
return (await this.
|
|
214
|
+
return (await this.send("tools/list"));
|
|
215
|
+
}
|
|
216
|
+
async listResources() {
|
|
217
|
+
return (await this.send("resources/list"));
|
|
218
|
+
}
|
|
219
|
+
async readResource(uri) {
|
|
220
|
+
return (await this.send("resources/read", { uri }));
|
|
107
221
|
}
|
|
108
222
|
async callTool(name, args) {
|
|
109
|
-
return (await this.
|
|
110
|
-
name,
|
|
111
|
-
arguments: args,
|
|
112
|
-
}));
|
|
223
|
+
return (await this.send("tools/call", { name, arguments: args }));
|
|
113
224
|
}
|
|
114
225
|
async disconnect() {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
if (
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
proc.
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
resolve();
|
|
128
|
-
};
|
|
129
|
-
proc.on("exit", onExit);
|
|
130
|
-
if (proc.exitCode !== null) {
|
|
131
|
-
proc.removeListener("exit", onExit);
|
|
132
|
-
resolve();
|
|
133
|
-
}
|
|
134
|
-
setTimeout(() => {
|
|
135
|
-
proc.removeListener("exit", onExit);
|
|
136
|
-
resolve();
|
|
137
|
-
}, 3000);
|
|
138
|
-
});
|
|
139
|
-
}
|
|
226
|
+
const proc = this.process;
|
|
227
|
+
this.process = null;
|
|
228
|
+
this.pending.clear();
|
|
229
|
+
this.sseEndpoint = null;
|
|
230
|
+
if (proc && !proc.killed) {
|
|
231
|
+
proc.kill();
|
|
232
|
+
await new Promise((resolve) => {
|
|
233
|
+
if (proc.exitCode !== null)
|
|
234
|
+
return resolve();
|
|
235
|
+
proc.on("exit", resolve);
|
|
236
|
+
setTimeout(resolve, 3000);
|
|
237
|
+
});
|
|
140
238
|
}
|
|
141
239
|
}
|
|
142
240
|
}
|