open-multi-agent-kit 0.99.0 → 1.0.0
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 +24 -0
- package/README.md +3 -3
- package/dist/bun/cli.d.ts.map +1 -1
- package/dist/bun/cli.js +1 -0
- package/dist/bun/cli.js.map +1 -1
- package/dist/bun/register-bundled-coding-agent.d.ts +10 -0
- package/dist/bun/register-bundled-coding-agent.d.ts.map +1 -0
- package/dist/bun/register-bundled-coding-agent.js +12 -0
- package/dist/bun/register-bundled-coding-agent.js.map +1 -0
- package/dist/cli/args.d.ts +1 -1
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +1 -1
- package/dist/cli/args.js.map +1 -1
- package/dist/cli/help.d.ts.map +1 -1
- package/dist/cli/help.js +1 -1
- package/dist/cli/help.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +14 -2
- package/dist/cli.js.map +1 -1
- package/dist/commands/neo-cli.d.ts +9 -0
- package/dist/commands/neo-cli.d.ts.map +1 -0
- package/dist/commands/neo-cli.js +61 -0
- package/dist/commands/neo-cli.js.map +1 -0
- package/dist/core/agent-session.d.ts +29 -2
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +107 -30
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/bundled-skills.d.ts +4 -0
- package/dist/core/bundled-skills.d.ts.map +1 -0
- package/dist/core/bundled-skills.js +32 -0
- package/dist/core/bundled-skills.js.map +1 -0
- package/dist/core/cli-diagnostics.d.ts +6 -0
- package/dist/core/cli-diagnostics.d.ts.map +1 -0
- package/dist/core/cli-diagnostics.js +20 -0
- package/dist/core/cli-diagnostics.js.map +1 -0
- package/dist/core/context-budget-headroom.d.ts +12 -0
- package/dist/core/context-budget-headroom.d.ts.map +1 -1
- package/dist/core/context-budget-headroom.js +35 -0
- package/dist/core/context-budget-headroom.js.map +1 -1
- package/dist/core/context-budget-v2-input-validation.d.ts +4 -0
- package/dist/core/context-budget-v2-input-validation.d.ts.map +1 -0
- package/dist/core/context-budget-v2-input-validation.js +79 -0
- package/dist/core/context-budget-v2-input-validation.js.map +1 -0
- package/dist/core/context-budget-v2-observability.d.ts +15 -0
- package/dist/core/context-budget-v2-observability.d.ts.map +1 -0
- package/dist/core/context-budget-v2-observability.js +35 -0
- package/dist/core/context-budget-v2-observability.js.map +1 -0
- package/dist/core/context-budget-v2-planned-items.d.ts +5 -0
- package/dist/core/context-budget-v2-planned-items.d.ts.map +1 -0
- package/dist/core/context-budget-v2-planned-items.js +40 -0
- package/dist/core/context-budget-v2-planned-items.js.map +1 -0
- package/dist/core/context-budget-v2-planner.d.ts.map +1 -1
- package/dist/core/context-budget-v2-planner.js +57 -38
- package/dist/core/context-budget-v2-planner.js.map +1 -1
- package/dist/core/context-budget-v2-selection.d.ts +19 -3
- package/dist/core/context-budget-v2-selection.d.ts.map +1 -1
- package/dist/core/context-budget-v2-selection.js +35 -57
- package/dist/core/context-budget-v2-selection.js.map +1 -1
- package/dist/core/context-budget-v2-tiers.d.ts.map +1 -1
- package/dist/core/context-budget-v2-tiers.js +9 -42
- package/dist/core/context-budget-v2-tiers.js.map +1 -1
- package/dist/core/context-budget-v2-types.d.ts +8 -1
- package/dist/core/context-budget-v2-types.d.ts.map +1 -1
- package/dist/core/context-budget-v2-types.js.map +1 -1
- package/dist/core/extensions/bundled-virtual-modules.d.ts +15 -0
- package/dist/core/extensions/bundled-virtual-modules.d.ts.map +1 -0
- package/dist/core/extensions/bundled-virtual-modules.js +63 -0
- package/dist/core/extensions/bundled-virtual-modules.js.map +1 -0
- package/dist/core/extensions/loader.d.ts.map +1 -1
- package/dist/core/extensions/loader.js +2 -42
- package/dist/core/extensions/loader.js.map +1 -1
- package/dist/core/extensions/runner.d.ts +1 -0
- package/dist/core/extensions/runner.d.ts.map +1 -1
- package/dist/core/extensions/runner.js +6 -0
- package/dist/core/extensions/runner.js.map +1 -1
- package/dist/core/extensions/types.d.ts +10 -0
- package/dist/core/extensions/types.d.ts.map +1 -1
- package/dist/core/extensions/types.js.map +1 -1
- package/dist/core/index.d.ts +2 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +2 -1
- package/dist/core/index.js.map +1 -1
- package/dist/core/loadout-runtime.d.ts +2 -8
- package/dist/core/loadout-runtime.d.ts.map +1 -1
- package/dist/core/loadout-runtime.js.map +1 -1
- package/dist/core/mcp/client.d.ts +27 -6
- package/dist/core/mcp/client.d.ts.map +1 -1
- package/dist/core/mcp/client.js +80 -20
- package/dist/core/mcp/client.js.map +1 -1
- package/dist/core/mcp/manager.d.ts +10 -1
- package/dist/core/mcp/manager.d.ts.map +1 -1
- package/dist/core/mcp/manager.js +68 -9
- package/dist/core/mcp/manager.js.map +1 -1
- package/dist/core/mcp/protocol.d.ts +9 -3
- package/dist/core/mcp/protocol.d.ts.map +1 -1
- package/dist/core/mcp/protocol.js +61 -15
- package/dist/core/mcp/protocol.js.map +1 -1
- package/dist/core/mcp/stdio-transport.d.ts +12 -1
- package/dist/core/mcp/stdio-transport.d.ts.map +1 -1
- package/dist/core/mcp/stdio-transport.js +30 -2
- package/dist/core/mcp/stdio-transport.js.map +1 -1
- package/dist/core/mcp-descriptor-injection.d.ts +26 -0
- package/dist/core/mcp-descriptor-injection.d.ts.map +1 -0
- package/dist/core/mcp-descriptor-injection.js +25 -0
- package/dist/core/mcp-descriptor-injection.js.map +1 -0
- package/dist/core/mcp-public-presets.d.ts +1 -3
- package/dist/core/mcp-public-presets.d.ts.map +1 -1
- package/dist/core/mcp-public-presets.js +3 -15
- package/dist/core/mcp-public-presets.js.map +1 -1
- package/dist/core/model-registry.d.ts.map +1 -1
- package/dist/core/model-registry.js +24 -3
- package/dist/core/model-registry.js.map +1 -1
- package/dist/core/neo/catalog.d.ts +20 -0
- package/dist/core/neo/catalog.d.ts.map +1 -0
- package/dist/core/neo/catalog.js +50 -0
- package/dist/core/neo/catalog.js.map +1 -0
- package/dist/core/neo/setup.d.ts +3 -0
- package/dist/core/neo/setup.d.ts.map +1 -0
- package/dist/core/neo/setup.js +47 -0
- package/dist/core/neo/setup.js.map +1 -0
- package/dist/core/provider-default-models.d.ts +1 -0
- package/dist/core/provider-default-models.d.ts.map +1 -1
- package/dist/core/provider-default-models.js +1 -0
- package/dist/core/provider-default-models.js.map +1 -1
- package/dist/core/provider-display-names.d.ts.map +1 -1
- package/dist/core/provider-display-names.js +1 -0
- package/dist/core/provider-display-names.js.map +1 -1
- package/dist/core/provider-error-classification.d.ts +57 -0
- package/dist/core/provider-error-classification.d.ts.map +1 -0
- package/dist/core/provider-error-classification.js +103 -0
- package/dist/core/provider-error-classification.js.map +1 -0
- package/dist/core/provider-resilience.d.ts +10 -0
- package/dist/core/provider-resilience.d.ts.map +1 -1
- package/dist/core/provider-resilience.js +36 -3
- package/dist/core/provider-resilience.js.map +1 -1
- package/dist/core/provider-usage-commandcode.d.ts +9 -0
- package/dist/core/provider-usage-commandcode.d.ts.map +1 -0
- package/dist/core/provider-usage-commandcode.js +198 -0
- package/dist/core/provider-usage-commandcode.js.map +1 -0
- package/dist/core/provider-usage-devin.d.ts +5 -3
- package/dist/core/provider-usage-devin.d.ts.map +1 -1
- package/dist/core/provider-usage-devin.js +15 -5
- package/dist/core/provider-usage-devin.js.map +1 -1
- package/dist/core/provider-usage-types.d.ts +1 -1
- package/dist/core/provider-usage-types.d.ts.map +1 -1
- package/dist/core/provider-usage-types.js.map +1 -1
- package/dist/core/provider-usage.d.ts.map +1 -1
- package/dist/core/provider-usage.js +6 -0
- package/dist/core/provider-usage.js.map +1 -1
- package/dist/core/resource-admission.d.ts +36 -0
- package/dist/core/resource-admission.d.ts.map +1 -1
- package/dist/core/resource-admission.js +59 -0
- package/dist/core/resource-admission.js.map +1 -1
- package/dist/core/resource-loader.d.ts.map +1 -1
- package/dist/core/resource-loader.js +2 -2
- package/dist/core/resource-loader.js.map +1 -1
- package/dist/core/run-usage-ledger.d.ts +47 -0
- package/dist/core/run-usage-ledger.d.ts.map +1 -0
- package/dist/core/run-usage-ledger.js +162 -0
- package/dist/core/run-usage-ledger.js.map +1 -0
- package/dist/core/run-usage-operation.d.ts +8 -0
- package/dist/core/run-usage-operation.d.ts.map +1 -0
- package/dist/core/run-usage-operation.js +12 -0
- package/dist/core/run-usage-operation.js.map +1 -0
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +5 -6
- package/dist/core/sdk.js.map +1 -1
- package/dist/core/session-failure-cause.d.ts.map +1 -1
- package/dist/core/session-failure-cause.js +14 -5
- package/dist/core/session-failure-cause.js.map +1 -1
- package/dist/core/session-prompt-lifecycle.d.ts +6 -0
- package/dist/core/session-prompt-lifecycle.d.ts.map +1 -1
- package/dist/core/session-prompt-lifecycle.js +47 -2
- package/dist/core/session-prompt-lifecycle.js.map +1 -1
- package/dist/core/session-termination.d.ts.map +1 -1
- package/dist/core/session-termination.js +4 -2
- package/dist/core/session-termination.js.map +1 -1
- package/dist/core/subagent-lane-authority.d.ts +28 -0
- package/dist/core/subagent-lane-authority.d.ts.map +1 -0
- package/dist/core/subagent-lane-authority.js +119 -0
- package/dist/core/subagent-lane-authority.js.map +1 -0
- package/dist/core/subagent-lane-contract.d.ts +114 -0
- package/dist/core/subagent-lane-contract.d.ts.map +1 -0
- package/dist/core/subagent-lane-contract.js +18 -0
- package/dist/core/subagent-lane-contract.js.map +1 -0
- package/dist/core/subagent-lane-launcher.d.ts +3 -6
- package/dist/core/subagent-lane-launcher.d.ts.map +1 -1
- package/dist/core/subagent-lane-launcher.js +53 -12
- package/dist/core/subagent-lane-launcher.js.map +1 -1
- package/dist/core/subagent-orchestration.d.ts +4 -17
- package/dist/core/subagent-orchestration.d.ts.map +1 -1
- package/dist/core/subagent-orchestration.js.map +1 -1
- package/dist/core/verified-run/broker.d.ts.map +1 -1
- package/dist/core/verified-run/broker.js +4 -1
- package/dist/core/verified-run/broker.js.map +1 -1
- package/dist/core/workload-permit-pool.d.ts +1 -1
- package/dist/core/workload-permit-pool.d.ts.map +1 -1
- package/dist/core/workload-permit-pool.js +3 -0
- package/dist/core/workload-permit-pool.js.map +1 -1
- package/dist/guardrails/strict-evidence-approval-adapter.d.ts +34 -0
- package/dist/guardrails/strict-evidence-approval-adapter.d.ts.map +1 -0
- package/dist/guardrails/strict-evidence-approval-adapter.js +70 -0
- package/dist/guardrails/strict-evidence-approval-adapter.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +11 -18
- package/dist/main.js.map +1 -1
- package/dist/modes/acp/acp-agent.d.ts +24 -0
- package/dist/modes/acp/acp-agent.d.ts.map +1 -0
- package/dist/modes/acp/acp-agent.js +133 -0
- package/dist/modes/acp/acp-agent.js.map +1 -0
- package/dist/modes/acp/acp-mode.d.ts +4 -0
- package/dist/modes/acp/acp-mode.d.ts.map +1 -0
- package/dist/modes/acp/acp-mode.js +34 -0
- package/dist/modes/acp/acp-mode.js.map +1 -0
- package/dist/modes/acp/acp-session.d.ts +4 -0
- package/dist/modes/acp/acp-session.d.ts.map +1 -0
- package/dist/modes/acp/acp-session.js +76 -0
- package/dist/modes/acp/acp-session.js.map +1 -0
- package/dist/modes/acp/acp-transport.d.ts +5 -0
- package/dist/modes/acp/acp-transport.d.ts.map +1 -0
- package/dist/modes/acp/acp-transport.js +90 -0
- package/dist/modes/acp/acp-transport.js.map +1 -0
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +1 -0
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/docs/audit-revalidation-2026-09-17.md +77 -0
- package/docs/devin-harness.md +33 -4
- package/docs/ecraf-normalization.md +83 -0
- package/docs/model-catalog-refresh.md +51 -1
- package/docs/models.md +1 -1
- package/docs/neo.md +134 -0
- package/docs/providers.md +72 -6
- package/docs/run-usage-ledger.md +56 -0
- package/docs/runtime-algorithms.md +44 -0
- package/docs/skills.md +1 -1
- package/docs/usage.md +2 -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/gondolin/package-lock.json +2 -2
- package/examples/extensions/gondolin/package.json +1 -1
- package/examples/extensions/sandbox/package-lock.json +2 -2
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/subagent/adaptive-agent-runtime.ts +14 -1
- package/examples/extensions/subagent/graph-result.ts +48 -0
- package/examples/extensions/subagent/index.ts +246 -111
- package/examples/extensions/subagent/managed-process-tree.ts +42 -0
- package/examples/extensions/subagent/managed-process.test.ts +24 -0
- package/examples/extensions/subagent/managed-process.ts +93 -112
- package/examples/extensions/subagent/subagent-runtime-types.ts +17 -1
- package/examples/extensions/subagent/subagent-stream.ts +161 -0
- package/examples/extensions/terminal-browser/README.md +54 -0
- package/examples/extensions/terminal-browser/bridge-protocol.ts +29 -0
- package/examples/extensions/terminal-browser/bridge.ts +219 -0
- package/examples/extensions/terminal-browser/browser-surface.ts +288 -0
- package/examples/extensions/terminal-browser/index.ts +215 -0
- package/examples/extensions/terminal-browser/placeholders.ts +49 -0
- package/examples/extensions/with-deps/package-lock.json +2 -2
- package/examples/extensions/with-deps/package.json +1 -1
- package/npm-shrinkwrap.json +18 -18
- package/package.json +8 -7
- package/resources/neo/skills/omk-browser/SKILL.md +32 -0
- package/resources/neo/skills/omk-code-review/SKILL.md +24 -0
- package/resources/neo/skills/omk-computeruse/SKILL.md +34 -0
- package/resources/neo/skills/omk-mcp-setup/SKILL.md +48 -0
- package/resources/neo/skills/omk-research/SKILL.md +24 -0
- package/resources/neo/skills/omk-site/SKILL.md +28 -0
package/dist/core/mcp/manager.js
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
* - Connect work is deduplicated: concurrent `listToolDefinitions()` calls
|
|
12
12
|
* share one in-flight connect per server.
|
|
13
13
|
*/
|
|
14
|
+
import { detectMcpDescriptorPromptInjection, MCP_QUARANTINE_PATTERN_SIGNAL_THRESHOLD } from "../mcp-public-presets.js";
|
|
14
15
|
import { McpClient } from "./client.js";
|
|
15
16
|
import { createMcpToolDefinition } from "./tools.js";
|
|
16
17
|
export class McpManager {
|
|
@@ -21,7 +22,7 @@ export class McpManager {
|
|
|
21
22
|
for (const config of options.servers) {
|
|
22
23
|
if (this.runtimes.has(config.name))
|
|
23
24
|
continue; // First definition wins; inventory already applies precedence.
|
|
24
|
-
this.runtimes.set(config.name, { config, state: "idle", tools: [] });
|
|
25
|
+
this.runtimes.set(config.name, { config, state: "idle", tools: [], quarantinedTools: [], generation: 0 });
|
|
25
26
|
}
|
|
26
27
|
}
|
|
27
28
|
/** Configured server names, in configuration order. */
|
|
@@ -36,6 +37,7 @@ export class McpManager {
|
|
|
36
37
|
toolCount: runtime.tools.length,
|
|
37
38
|
error: runtime.error,
|
|
38
39
|
serverVersion: runtime.client?.serverInfo.version,
|
|
40
|
+
...(runtime.quarantinedTools.length > 0 ? { quarantinedTools: runtime.quarantinedTools } : {}),
|
|
39
41
|
}));
|
|
40
42
|
}
|
|
41
43
|
/**
|
|
@@ -70,14 +72,26 @@ export class McpManager {
|
|
|
70
72
|
toolCount: runtime.tools.length,
|
|
71
73
|
error: runtime.error,
|
|
72
74
|
serverVersion: runtime.client?.serverInfo.version,
|
|
75
|
+
...(runtime.quarantinedTools.length > 0 ? { quarantinedTools: runtime.quarantinedTools } : {}),
|
|
73
76
|
};
|
|
74
77
|
}
|
|
75
|
-
/**
|
|
78
|
+
/**
|
|
79
|
+
* Terminate every connected or connecting server. Idempotent.
|
|
80
|
+
*
|
|
81
|
+
* Bumping the generation invalidates any in-flight connect before its late
|
|
82
|
+
* completion can publish tools or flip the state back to `ready`, and the
|
|
83
|
+
* attempt's not-yet-published client is closed alongside the live one so no
|
|
84
|
+
* spawned process outlives the close.
|
|
85
|
+
*/
|
|
76
86
|
close() {
|
|
77
87
|
for (const runtime of this.runtimes.values()) {
|
|
88
|
+
runtime.generation += 1;
|
|
78
89
|
runtime.client?.close();
|
|
79
90
|
runtime.client = undefined;
|
|
91
|
+
runtime.pendingClient?.close();
|
|
92
|
+
runtime.pendingClient = undefined;
|
|
80
93
|
runtime.tools = [];
|
|
94
|
+
runtime.quarantinedTools = [];
|
|
81
95
|
if (runtime.state === "ready" || runtime.state === "connecting")
|
|
82
96
|
runtime.state = "idle";
|
|
83
97
|
}
|
|
@@ -107,14 +121,22 @@ export class McpManager {
|
|
|
107
121
|
return this.status();
|
|
108
122
|
}
|
|
109
123
|
async pingRuntime(runtime, timeoutMs) {
|
|
124
|
+
// Pin the probed client: a close or reconnect during the ping must not let
|
|
125
|
+
// this failure path dispose the replacement client.
|
|
126
|
+
const client = runtime.client;
|
|
127
|
+
if (!client)
|
|
128
|
+
return;
|
|
110
129
|
try {
|
|
111
|
-
await
|
|
130
|
+
await client.ping(timeoutMs);
|
|
112
131
|
}
|
|
113
132
|
catch (error) {
|
|
133
|
+
if (runtime.client !== client)
|
|
134
|
+
return; // A newer owner already handled it.
|
|
114
135
|
// Isolation point: only this server transitions; the rest stay ready.
|
|
115
|
-
runtime.client?.close();
|
|
116
136
|
runtime.client = undefined;
|
|
137
|
+
client.close();
|
|
117
138
|
runtime.tools = [];
|
|
139
|
+
runtime.quarantinedTools = [];
|
|
118
140
|
runtime.state = "failed";
|
|
119
141
|
runtime.error = `health check failed: ${error instanceof Error ? error.message : String(error)}`;
|
|
120
142
|
}
|
|
@@ -130,13 +152,19 @@ export class McpManager {
|
|
|
130
152
|
if (runtime.connecting)
|
|
131
153
|
return runtime.connecting;
|
|
132
154
|
runtime.state = "connecting";
|
|
133
|
-
const
|
|
134
|
-
|
|
155
|
+
const generation = runtime.generation;
|
|
156
|
+
const attempt = this.connectRuntime(runtime, generation).finally(() => {
|
|
157
|
+
// Only this attempt may clear its own in-flight markers; a newer
|
|
158
|
+
// attempt's `connecting`/`pendingClient` belong to the newer generation.
|
|
159
|
+
if (runtime.connecting === attempt)
|
|
160
|
+
runtime.connecting = undefined;
|
|
161
|
+
if (runtime.generation === generation)
|
|
162
|
+
runtime.pendingClient = undefined;
|
|
135
163
|
});
|
|
136
164
|
runtime.connecting = attempt;
|
|
137
165
|
return attempt;
|
|
138
166
|
}
|
|
139
|
-
async connectRuntime(runtime) {
|
|
167
|
+
async connectRuntime(runtime, generation) {
|
|
140
168
|
const clientOptions = {
|
|
141
169
|
name: runtime.config.name,
|
|
142
170
|
clientInfo: this.options.clientInfo,
|
|
@@ -150,19 +178,50 @@ export class McpManager {
|
|
|
150
178
|
},
|
|
151
179
|
};
|
|
152
180
|
const client = this.options.createClient?.(clientOptions) ?? new McpClient(clientOptions);
|
|
181
|
+
// Register before the first await so close() can dispose a client whose
|
|
182
|
+
// connect() promise has not resolved yet.
|
|
183
|
+
runtime.pendingClient = client;
|
|
184
|
+
const isCurrent = () => runtime.generation === generation && runtime.pendingClient === client;
|
|
153
185
|
try {
|
|
154
186
|
await client.connect();
|
|
187
|
+
if (!isCurrent()) {
|
|
188
|
+
// close() or a newer attempt superseded this one mid-handshake.
|
|
189
|
+
client.close();
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
155
192
|
const schemas = await client.listTools();
|
|
193
|
+
if (!isCurrent()) {
|
|
194
|
+
client.close();
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
156
197
|
runtime.client = client;
|
|
157
|
-
runtime.
|
|
198
|
+
runtime.pendingClient = undefined;
|
|
199
|
+
// 서술자 주입 스크리닝: MCP 도구 설명은 신뢰할 수 없는 제3자 입력이다.
|
|
200
|
+
// admitPublicMcpServer와 동일 임계값(0.7)으로 차단 — 수입 시점에서 fail-closed.
|
|
201
|
+
const definitions = schemas.map((schema) => createMcpToolDefinition(runtime.config.name, client, schema, {
|
|
158
202
|
callTimeoutMs: this.options.callTimeoutMs,
|
|
159
203
|
}));
|
|
204
|
+
const admitted = [];
|
|
205
|
+
const quarantined = [];
|
|
206
|
+
for (const definition of definitions) {
|
|
207
|
+
if (detectMcpDescriptorPromptInjection(definition.description ?? "").patternSignal >
|
|
208
|
+
MCP_QUARANTINE_PATTERN_SIGNAL_THRESHOLD) {
|
|
209
|
+
quarantined.push(definition.name);
|
|
210
|
+
}
|
|
211
|
+
else {
|
|
212
|
+
admitted.push(definition);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
runtime.tools = admitted;
|
|
216
|
+
runtime.quarantinedTools = quarantined;
|
|
160
217
|
runtime.state = "ready";
|
|
161
218
|
runtime.error = undefined;
|
|
162
219
|
}
|
|
163
220
|
catch (error) {
|
|
164
|
-
// Isolation point: this server is out, every other server is unaffected.
|
|
165
221
|
client.close();
|
|
222
|
+
if (!isCurrent())
|
|
223
|
+
return; // The current generation owns the state now.
|
|
224
|
+
// Isolation point: this server is out, every other server is unaffected.
|
|
166
225
|
runtime.client = undefined;
|
|
167
226
|
runtime.tools = [];
|
|
168
227
|
runtime.state = "failed";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"manager.js","sourceRoot":"","sources":["../../../src/core/mcp/manager.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Lazy, crash-isolated MCP server manager.\n *\n * Design constraints that shaped this:\n * - A workspace can configure 25 servers. Spawning them all at startup would\n * trade a 0.6s cold start for a multi-second one, so connection is lazy and\n * only happens when tools are actually requested.\n * - One broken server must never take a session down. Every connect and every\n * listing is isolated; a failure is recorded as server status and the other\n * servers still contribute tools.\n * - Connect work is deduplicated: concurrent `listToolDefinitions()` calls\n * share one in-flight connect per server.\n */\n\nimport type { TSchema } from \"typebox\";\nimport type { ToolDefinition } from \"../extensions/types.ts\";\nimport { McpClient, type McpClientOptions } from \"./client.ts\";\nimport { createMcpToolDefinition, type McpToolDetails } from \"./tools.ts\";\n\nexport type McpServerState = \"idle\" | \"connecting\" | \"ready\" | \"failed\";\n\nexport interface McpServerConfig {\n\treadonly name: string;\n\treadonly command: string;\n\treadonly args?: readonly string[];\n\treadonly env?: Readonly<Record<string, string>>;\n\treadonly cwd?: string;\n\t/** Skip this server without removing it from configuration. */\n\treadonly disabled?: boolean;\n\treadonly requestTimeoutMs?: number;\n\treadonly handshakeTimeoutMs?: number;\n}\n\nexport interface McpServerStatus {\n\treadonly name: string;\n\treadonly state: McpServerState;\n\treadonly toolCount: number;\n\t/** Failure reason when `state` is `failed`. Never contains configured env values. */\n\treadonly error?: string;\n\treadonly serverVersion?: string;\n}\n\nexport interface McpManagerOptions {\n\treadonly servers: readonly McpServerConfig[];\n\treadonly cwd?: string;\n\treadonly clientInfo?: { readonly name: string; readonly version: string };\n\treadonly callTimeoutMs?: number;\n\t/** Injected for tests so the manager can be exercised without spawning processes. */\n\treadonly createClient?: (options: McpClientOptions) => McpClient;\n}\n\ninterface ServerRuntime {\n\treadonly config: McpServerConfig;\n\tstate: McpServerState;\n\tclient?: McpClient;\n\ttools: ToolDefinition<TSchema, McpToolDetails>[];\n\terror?: string;\n\tconnecting?: Promise<void>;\n}\n\nexport class McpManager {\n\tprivate readonly runtimes = new Map<string, ServerRuntime>();\n\tprivate readonly options: McpManagerOptions;\n\n\tconstructor(options: McpManagerOptions) {\n\t\tthis.options = options;\n\t\tfor (const config of options.servers) {\n\t\t\tif (this.runtimes.has(config.name)) continue; // First definition wins; inventory already applies precedence.\n\t\t\tthis.runtimes.set(config.name, { config, state: \"idle\", tools: [] });\n\t\t}\n\t}\n\n\t/** Configured server names, in configuration order. */\n\tget serverNames(): string[] {\n\t\treturn [...this.runtimes.keys()];\n\t}\n\n\t/** Current status per server. Safe to call before any connection attempt. */\n\tstatus(): McpServerStatus[] {\n\t\treturn [...this.runtimes.values()].map((runtime) => ({\n\t\t\tname: runtime.config.name,\n\t\t\tstate: runtime.state,\n\t\t\ttoolCount: runtime.tools.length,\n\t\t\terror: runtime.error,\n\t\t\tserverVersion: runtime.client?.serverInfo.version,\n\t\t}));\n\t}\n\n\t/**\n\t * Connect every enabled server that has not been attempted yet, then return\n\t * the union of their tools. Servers are connected concurrently and failures\n\t * are isolated, so a partial result is the normal outcome, not an error.\n\t */\n\tasync listToolDefinitions(): Promise<ToolDefinition<TSchema, McpToolDetails>[]> {\n\t\tawait Promise.all([...this.runtimes.values()].map((runtime) => this.ensureConnected(runtime)));\n\t\tconst tools: ToolDefinition<TSchema, McpToolDetails>[] = [];\n\t\tconst seen = new Set<string>();\n\t\tfor (const runtime of this.runtimes.values()) {\n\t\t\tfor (const tool of runtime.tools) {\n\t\t\t\tif (seen.has(tool.name)) continue; // Namespacing makes this rare; keep it deterministic anyway.\n\t\t\t\tseen.add(tool.name);\n\t\t\t\ttools.push(tool);\n\t\t\t}\n\t\t}\n\t\treturn tools;\n\t}\n\n\t/** Connect a single server by name. Returns its status either way. */\n\tasync connect(name: string): Promise<McpServerStatus> {\n\t\tconst runtime = this.runtimes.get(name);\n\t\tif (!runtime) {\n\t\t\treturn { name, state: \"failed\", toolCount: 0, error: `Unknown MCP server \"${name}\"` };\n\t\t}\n\t\tawait this.ensureConnected(runtime);\n\t\treturn {\n\t\t\tname,\n\t\t\tstate: runtime.state,\n\t\t\ttoolCount: runtime.tools.length,\n\t\t\terror: runtime.error,\n\t\t\tserverVersion: runtime.client?.serverInfo.version,\n\t\t};\n\t}\n\n\t/** Terminate every connected server. Idempotent. */\n\tclose(): void {\n\t\tfor (const runtime of this.runtimes.values()) {\n\t\t\truntime.client?.close();\n\t\t\truntime.client = undefined;\n\t\t\truntime.tools = [];\n\t\t\tif (runtime.state === \"ready\" || runtime.state === \"connecting\") runtime.state = \"idle\";\n\t\t}\n\t}\n\n\t/**\n\t * Verify that `ready` servers are actually alive with a protocol ping. A\n\t * failed ping closes the client and marks the server `failed`, so a\n\t * silently killed process stops masquerading as connected. Servers already\n\t * `failed` are re-attempted only when `reconnectFailed` is set — callers\n\t * should gate that behind a slow cadence so a permanently broken server is\n\t * not respawned every probe. Idle servers stay idle: connection is lazy by\n\t * design, and a status probe must not spawn processes.\n\t */\n\tasync checkHealth(options?: { pingTimeoutMs?: number; reconnectFailed?: boolean }): Promise<McpServerStatus[]> {\n\t\tconst work: Promise<void>[] = [];\n\t\tfor (const runtime of this.runtimes.values()) {\n\t\t\tif (runtime.state === \"ready\" && runtime.client) {\n\t\t\t\twork.push(this.pingRuntime(runtime, options?.pingTimeoutMs));\n\t\t\t} else if (runtime.state === \"failed\" && options?.reconnectFailed && !runtime.config.disabled) {\n\t\t\t\truntime.state = \"idle\";\n\t\t\t\truntime.error = undefined;\n\t\t\t\twork.push(this.ensureConnected(runtime));\n\t\t\t}\n\t\t}\n\t\tawait Promise.all(work);\n\t\treturn this.status();\n\t}\n\n\tprivate async pingRuntime(runtime: ServerRuntime, timeoutMs?: number): Promise<void> {\n\t\ttry {\n\t\t\tawait runtime.client?.ping(timeoutMs);\n\t\t} catch (error) {\n\t\t\t// Isolation point: only this server transitions; the rest stay ready.\n\t\t\truntime.client?.close();\n\t\t\truntime.client = undefined;\n\t\t\truntime.tools = [];\n\t\t\truntime.state = \"failed\";\n\t\t\truntime.error = `health check failed: ${error instanceof Error ? error.message : String(error)}`;\n\t\t}\n\t}\n\n\tprivate ensureConnected(runtime: ServerRuntime): Promise<void> {\n\t\tif (runtime.config.disabled) {\n\t\t\truntime.state = \"failed\";\n\t\t\truntime.error = \"disabled by configuration\";\n\t\t\treturn Promise.resolve();\n\t\t}\n\t\tif (runtime.state === \"ready\" || runtime.state === \"failed\") return Promise.resolve();\n\t\tif (runtime.connecting) return runtime.connecting;\n\n\t\truntime.state = \"connecting\";\n\t\tconst attempt = this.connectRuntime(runtime).finally(() => {\n\t\t\truntime.connecting = undefined;\n\t\t});\n\t\truntime.connecting = attempt;\n\t\treturn attempt;\n\t}\n\n\tprivate async connectRuntime(runtime: ServerRuntime): Promise<void> {\n\t\tconst clientOptions: McpClientOptions = {\n\t\t\tname: runtime.config.name,\n\t\t\tclientInfo: this.options.clientInfo,\n\t\t\trequestTimeoutMs: runtime.config.requestTimeoutMs,\n\t\t\thandshakeTimeoutMs: runtime.config.handshakeTimeoutMs,\n\t\t\ttransport: {\n\t\t\t\tcommand: runtime.config.command,\n\t\t\t\targs: runtime.config.args,\n\t\t\t\tenv: runtime.config.env,\n\t\t\t\tcwd: runtime.config.cwd ?? this.options.cwd,\n\t\t\t},\n\t\t};\n\t\tconst client = this.options.createClient?.(clientOptions) ?? new McpClient(clientOptions);\n\t\ttry {\n\t\t\tawait client.connect();\n\t\t\tconst schemas = await client.listTools();\n\t\t\truntime.client = client;\n\t\t\truntime.tools = schemas.map((schema) =>\n\t\t\t\tcreateMcpToolDefinition(runtime.config.name, client, schema, {\n\t\t\t\t\tcallTimeoutMs: this.options.callTimeoutMs,\n\t\t\t\t}),\n\t\t\t);\n\t\t\truntime.state = \"ready\";\n\t\t\truntime.error = undefined;\n\t\t} catch (error) {\n\t\t\t// Isolation point: this server is out, every other server is unaffected.\n\t\t\tclient.close();\n\t\t\truntime.client = undefined;\n\t\t\truntime.tools = [];\n\t\t\truntime.state = \"failed\";\n\t\t\truntime.error = error instanceof Error ? error.message : String(error);\n\t\t}\n\t}\n}\n"]}
|
|
1
|
+
{"version":3,"file":"manager.js","sourceRoot":"","sources":["../../../src/core/mcp/manager.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Lazy, crash-isolated MCP server manager.\n *\n * Design constraints that shaped this:\n * - A workspace can configure 25 servers. Spawning them all at startup would\n * trade a 0.6s cold start for a multi-second one, so connection is lazy and\n * only happens when tools are actually requested.\n * - One broken server must never take a session down. Every connect and every\n * listing is isolated; a failure is recorded as server status and the other\n * servers still contribute tools.\n * - Connect work is deduplicated: concurrent `listToolDefinitions()` calls\n * share one in-flight connect per server.\n */\n\nimport type { TSchema } from \"typebox\";\nimport type { ToolDefinition } from \"../extensions/types.ts\";\nimport { detectMcpDescriptorPromptInjection, MCP_QUARANTINE_PATTERN_SIGNAL_THRESHOLD } from \"../mcp-public-presets.ts\";\nimport { McpClient, type McpClientOptions } from \"./client.ts\";\nimport { createMcpToolDefinition, type McpToolDetails } from \"./tools.ts\";\n\nexport type McpServerState = \"idle\" | \"connecting\" | \"ready\" | \"failed\";\n\nexport interface McpServerConfig {\n\treadonly name: string;\n\treadonly command: string;\n\treadonly args?: readonly string[];\n\treadonly env?: Readonly<Record<string, string>>;\n\treadonly cwd?: string;\n\t/** Skip this server without removing it from configuration. */\n\treadonly disabled?: boolean;\n\treadonly requestTimeoutMs?: number;\n\treadonly handshakeTimeoutMs?: number;\n}\n\nexport interface McpServerStatus {\n\treadonly name: string;\n\treadonly state: McpServerState;\n\treadonly toolCount: number;\n\t/** Failure reason when `state` is `failed`. Never contains configured env values. */\n\treadonly error?: string;\n\treadonly serverVersion?: string;\n\t/** Tool names excluded because their descriptions carried prompt-injection payloads. */\n\treadonly quarantinedTools?: readonly string[];\n}\n\nexport interface McpManagerOptions {\n\treadonly servers: readonly McpServerConfig[];\n\treadonly cwd?: string;\n\treadonly clientInfo?: { readonly name: string; readonly version: string };\n\treadonly callTimeoutMs?: number;\n\t/** Injected for tests so the manager can be exercised without spawning processes. */\n\treadonly createClient?: (options: McpClientOptions) => McpClient;\n}\n\ninterface ServerRuntime {\n\treadonly config: McpServerConfig;\n\tstate: McpServerState;\n\tclient?: McpClient;\n\ttools: ToolDefinition<TSchema, McpToolDetails>[];\n\t/** Descriptor prompt-injection quarantines (namespaced tool names). */\n\tquarantinedTools: string[];\n\terror?: string;\n\tconnecting?: Promise<void>;\n\t/** Client owned by the in-flight connect attempt; not yet published. */\n\tpendingClient?: McpClient;\n\t/** Monotonic generation; a completed attempt may only publish when current. */\n\tgeneration: number;\n}\n\nexport class McpManager {\n\tprivate readonly runtimes = new Map<string, ServerRuntime>();\n\tprivate readonly options: McpManagerOptions;\n\n\tconstructor(options: McpManagerOptions) {\n\t\tthis.options = options;\n\t\tfor (const config of options.servers) {\n\t\t\tif (this.runtimes.has(config.name)) continue; // First definition wins; inventory already applies precedence.\n\t\t\tthis.runtimes.set(config.name, { config, state: \"idle\", tools: [], quarantinedTools: [], generation: 0 });\n\t\t}\n\t}\n\n\t/** Configured server names, in configuration order. */\n\tget serverNames(): string[] {\n\t\treturn [...this.runtimes.keys()];\n\t}\n\n\t/** Current status per server. Safe to call before any connection attempt. */\n\tstatus(): McpServerStatus[] {\n\t\treturn [...this.runtimes.values()].map((runtime) => ({\n\t\t\tname: runtime.config.name,\n\t\t\tstate: runtime.state,\n\t\t\ttoolCount: runtime.tools.length,\n\t\t\terror: runtime.error,\n\t\t\tserverVersion: runtime.client?.serverInfo.version,\n\t\t\t...(runtime.quarantinedTools.length > 0 ? { quarantinedTools: runtime.quarantinedTools } : {}),\n\t\t}));\n\t}\n\n\t/**\n\t * Connect every enabled server that has not been attempted yet, then return\n\t * the union of their tools. Servers are connected concurrently and failures\n\t * are isolated, so a partial result is the normal outcome, not an error.\n\t */\n\tasync listToolDefinitions(): Promise<ToolDefinition<TSchema, McpToolDetails>[]> {\n\t\tawait Promise.all([...this.runtimes.values()].map((runtime) => this.ensureConnected(runtime)));\n\t\tconst tools: ToolDefinition<TSchema, McpToolDetails>[] = [];\n\t\tconst seen = new Set<string>();\n\t\tfor (const runtime of this.runtimes.values()) {\n\t\t\tfor (const tool of runtime.tools) {\n\t\t\t\tif (seen.has(tool.name)) continue; // Namespacing makes this rare; keep it deterministic anyway.\n\t\t\t\tseen.add(tool.name);\n\t\t\t\ttools.push(tool);\n\t\t\t}\n\t\t}\n\t\treturn tools;\n\t}\n\n\t/** Connect a single server by name. Returns its status either way. */\n\tasync connect(name: string): Promise<McpServerStatus> {\n\t\tconst runtime = this.runtimes.get(name);\n\t\tif (!runtime) {\n\t\t\treturn { name, state: \"failed\", toolCount: 0, error: `Unknown MCP server \"${name}\"` };\n\t\t}\n\t\tawait this.ensureConnected(runtime);\n\t\treturn {\n\t\t\tname,\n\t\t\tstate: runtime.state,\n\t\t\ttoolCount: runtime.tools.length,\n\t\t\terror: runtime.error,\n\t\t\tserverVersion: runtime.client?.serverInfo.version,\n\t\t\t...(runtime.quarantinedTools.length > 0 ? { quarantinedTools: runtime.quarantinedTools } : {}),\n\t\t};\n\t}\n\n\t/**\n\t * Terminate every connected or connecting server. Idempotent.\n\t *\n\t * Bumping the generation invalidates any in-flight connect before its late\n\t * completion can publish tools or flip the state back to `ready`, and the\n\t * attempt's not-yet-published client is closed alongside the live one so no\n\t * spawned process outlives the close.\n\t */\n\tclose(): void {\n\t\tfor (const runtime of this.runtimes.values()) {\n\t\t\truntime.generation += 1;\n\t\t\truntime.client?.close();\n\t\t\truntime.client = undefined;\n\t\t\truntime.pendingClient?.close();\n\t\t\truntime.pendingClient = undefined;\n\t\t\truntime.tools = [];\n\t\t\truntime.quarantinedTools = [];\n\t\t\tif (runtime.state === \"ready\" || runtime.state === \"connecting\") runtime.state = \"idle\";\n\t\t}\n\t}\n\n\t/**\n\t * Verify that `ready` servers are actually alive with a protocol ping. A\n\t * failed ping closes the client and marks the server `failed`, so a\n\t * silently killed process stops masquerading as connected. Servers already\n\t * `failed` are re-attempted only when `reconnectFailed` is set — callers\n\t * should gate that behind a slow cadence so a permanently broken server is\n\t * not respawned every probe. Idle servers stay idle: connection is lazy by\n\t * design, and a status probe must not spawn processes.\n\t */\n\tasync checkHealth(options?: { pingTimeoutMs?: number; reconnectFailed?: boolean }): Promise<McpServerStatus[]> {\n\t\tconst work: Promise<void>[] = [];\n\t\tfor (const runtime of this.runtimes.values()) {\n\t\t\tif (runtime.state === \"ready\" && runtime.client) {\n\t\t\t\twork.push(this.pingRuntime(runtime, options?.pingTimeoutMs));\n\t\t\t} else if (runtime.state === \"failed\" && options?.reconnectFailed && !runtime.config.disabled) {\n\t\t\t\truntime.state = \"idle\";\n\t\t\t\truntime.error = undefined;\n\t\t\t\twork.push(this.ensureConnected(runtime));\n\t\t\t}\n\t\t}\n\t\tawait Promise.all(work);\n\t\treturn this.status();\n\t}\n\n\tprivate async pingRuntime(runtime: ServerRuntime, timeoutMs?: number): Promise<void> {\n\t\t// Pin the probed client: a close or reconnect during the ping must not let\n\t\t// this failure path dispose the replacement client.\n\t\tconst client = runtime.client;\n\t\tif (!client) return;\n\t\ttry {\n\t\t\tawait client.ping(timeoutMs);\n\t\t} catch (error) {\n\t\t\tif (runtime.client !== client) return; // A newer owner already handled it.\n\t\t\t// Isolation point: only this server transitions; the rest stay ready.\n\t\t\truntime.client = undefined;\n\t\t\tclient.close();\n\t\t\truntime.tools = [];\n\t\t\truntime.quarantinedTools = [];\n\t\t\truntime.state = \"failed\";\n\t\t\truntime.error = `health check failed: ${error instanceof Error ? error.message : String(error)}`;\n\t\t}\n\t}\n\n\tprivate ensureConnected(runtime: ServerRuntime): Promise<void> {\n\t\tif (runtime.config.disabled) {\n\t\t\truntime.state = \"failed\";\n\t\t\truntime.error = \"disabled by configuration\";\n\t\t\treturn Promise.resolve();\n\t\t}\n\t\tif (runtime.state === \"ready\" || runtime.state === \"failed\") return Promise.resolve();\n\t\tif (runtime.connecting) return runtime.connecting;\n\n\t\truntime.state = \"connecting\";\n\t\tconst generation = runtime.generation;\n\t\tconst attempt = this.connectRuntime(runtime, generation).finally(() => {\n\t\t\t// Only this attempt may clear its own in-flight markers; a newer\n\t\t\t// attempt's `connecting`/`pendingClient` belong to the newer generation.\n\t\t\tif (runtime.connecting === attempt) runtime.connecting = undefined;\n\t\t\tif (runtime.generation === generation) runtime.pendingClient = undefined;\n\t\t});\n\t\truntime.connecting = attempt;\n\t\treturn attempt;\n\t}\n\n\tprivate async connectRuntime(runtime: ServerRuntime, generation: number): Promise<void> {\n\t\tconst clientOptions: McpClientOptions = {\n\t\t\tname: runtime.config.name,\n\t\t\tclientInfo: this.options.clientInfo,\n\t\t\trequestTimeoutMs: runtime.config.requestTimeoutMs,\n\t\t\thandshakeTimeoutMs: runtime.config.handshakeTimeoutMs,\n\t\t\ttransport: {\n\t\t\t\tcommand: runtime.config.command,\n\t\t\t\targs: runtime.config.args,\n\t\t\t\tenv: runtime.config.env,\n\t\t\t\tcwd: runtime.config.cwd ?? this.options.cwd,\n\t\t\t},\n\t\t};\n\t\tconst client = this.options.createClient?.(clientOptions) ?? new McpClient(clientOptions);\n\t\t// Register before the first await so close() can dispose a client whose\n\t\t// connect() promise has not resolved yet.\n\t\truntime.pendingClient = client;\n\t\tconst isCurrent = () => runtime.generation === generation && runtime.pendingClient === client;\n\t\ttry {\n\t\t\tawait client.connect();\n\t\t\tif (!isCurrent()) {\n\t\t\t\t// close() or a newer attempt superseded this one mid-handshake.\n\t\t\t\tclient.close();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst schemas = await client.listTools();\n\t\t\tif (!isCurrent()) {\n\t\t\t\tclient.close();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\truntime.client = client;\n\t\t\truntime.pendingClient = undefined;\n\t\t\t// 서술자 주입 스크리닝: MCP 도구 설명은 신뢰할 수 없는 제3자 입력이다.\n\t\t\t// admitPublicMcpServer와 동일 임계값(0.7)으로 차단 — 수입 시점에서 fail-closed.\n\t\t\tconst definitions = schemas.map((schema) =>\n\t\t\t\tcreateMcpToolDefinition(runtime.config.name, client, schema, {\n\t\t\t\t\tcallTimeoutMs: this.options.callTimeoutMs,\n\t\t\t\t}),\n\t\t\t);\n\t\t\tconst admitted: typeof definitions = [];\n\t\t\tconst quarantined: string[] = [];\n\t\t\tfor (const definition of definitions) {\n\t\t\t\tif (\n\t\t\t\t\tdetectMcpDescriptorPromptInjection(definition.description ?? \"\").patternSignal >\n\t\t\t\t\tMCP_QUARANTINE_PATTERN_SIGNAL_THRESHOLD\n\t\t\t\t) {\n\t\t\t\t\tquarantined.push(definition.name);\n\t\t\t\t} else {\n\t\t\t\t\tadmitted.push(definition);\n\t\t\t\t}\n\t\t\t}\n\t\t\truntime.tools = admitted;\n\t\t\truntime.quarantinedTools = quarantined;\n\t\t\truntime.state = \"ready\";\n\t\t\truntime.error = undefined;\n\t\t} catch (error) {\n\t\t\tclient.close();\n\t\t\tif (!isCurrent()) return; // The current generation owns the state now.\n\t\t\t// Isolation point: this server is out, every other server is unaffected.\n\t\t\truntime.client = undefined;\n\t\t\truntime.tools = [];\n\t\t\truntime.state = \"failed\";\n\t\t\truntime.error = error instanceof Error ? error.message : String(error);\n\t\t}\n\t}\n}\n"]}
|
|
@@ -46,9 +46,15 @@ export interface DecodedLine {
|
|
|
46
46
|
/**
|
|
47
47
|
* Incremental newline-delimited JSON decoder.
|
|
48
48
|
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
49
|
+
* The limit is enforced in UTF-8 bytes against the raw frame, before JSON
|
|
50
|
+
* parsing, so the same message is accepted or rejected regardless of how the
|
|
51
|
+
* transport splits it into chunks. A line that exceeds
|
|
52
|
+
* {@link MAX_MESSAGE_LINE_BYTES} is reported once, its bytes are discarded
|
|
53
|
+
* without being retained, and decoding resynchronizes at the next newline —
|
|
54
|
+
* a runaway server cannot exhaust memory by growing the retained buffer.
|
|
55
|
+
*
|
|
56
|
+
* `maxLineBytes` must be a positive finite integer; anything else is a
|
|
57
|
+
* configuration error, not a size to guess at.
|
|
52
58
|
*/
|
|
53
59
|
export declare function createLineDecoder(maxLineBytes?: number): {
|
|
54
60
|
push(chunk: string): DecodedLine[];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"protocol.d.ts","sourceRoot":"","sources":["../../../src/core/mcp/protocol.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,+FAA+F;AAC/F,eAAO,MAAM,sBAAsB,QAAmB,CAAC;AAEvD,MAAM,MAAM,SAAS,GAAG,MAAM,GAAG,MAAM,CAAC;AAExC,MAAM,WAAW,cAAc;IAC9B,QAAQ,CAAC,OAAO,EAAE,KAAK,CAAC;IACxB,QAAQ,CAAC,EAAE,EAAE,SAAS,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED,MAAM,WAAW,mBAAmB;IACnC,QAAQ,CAAC,OAAO,EAAE,KAAK,CAAC;IACxB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED,MAAM,WAAW,gBAAgB;IAChC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,CAAC;CACxB;AAED,MAAM,WAAW,eAAe;IAC/B,QAAQ,CAAC,OAAO,EAAE,KAAK,CAAC;IACxB,QAAQ,CAAC,EAAE,EAAE,SAAS,CAAC;IACvB,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,KAAK,CAAC,EAAE,gBAAgB,CAAC;CAClC;AAED,MAAM,MAAM,cAAc,GAAG,cAAc,GAAG,mBAAmB,GAAG,eAAe,CAAC;AAMpF,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,IAAI,eAAe,
|
|
1
|
+
{"version":3,"file":"protocol.d.ts","sourceRoot":"","sources":["../../../src/core/mcp/protocol.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,+FAA+F;AAC/F,eAAO,MAAM,sBAAsB,QAAmB,CAAC;AAEvD,MAAM,MAAM,SAAS,GAAG,MAAM,GAAG,MAAM,CAAC;AAExC,MAAM,WAAW,cAAc;IAC9B,QAAQ,CAAC,OAAO,EAAE,KAAK,CAAC;IACxB,QAAQ,CAAC,EAAE,EAAE,SAAS,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED,MAAM,WAAW,mBAAmB;IACnC,QAAQ,CAAC,OAAO,EAAE,KAAK,CAAC;IACxB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED,MAAM,WAAW,gBAAgB;IAChC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,CAAC;CACxB;AAED,MAAM,WAAW,eAAe;IAC/B,QAAQ,CAAC,OAAO,EAAE,KAAK,CAAC;IACxB,QAAQ,CAAC,EAAE,EAAE,SAAS,CAAC;IACvB,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,KAAK,CAAC,EAAE,gBAAgB,CAAC;CAClC;AAED,MAAM,MAAM,cAAc,GAAG,cAAc,GAAG,mBAAmB,GAAG,eAAe,CAAC;AAMpF,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,IAAI,eAAe,CAiB9E;AAED,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,IAAI,mBAAmB,CAEtF;AAED,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,IAAI,cAAc,CAK5E;AAED,sEAAsE;AACtE,wBAAgB,aAAa,CAAC,OAAO,EAAE,cAAc,GAAG,MAAM,CAQ7D;AAED,MAAM,WAAW,WAAW;IAC3B,oDAAoD;IACpD,QAAQ,CAAC,OAAO,CAAC,EAAE,cAAc,CAAC;IAClC,wDAAwD;IACxD,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;CACxB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,iBAAiB,CAAC,YAAY,GAAE,MAA+B,GAAG;IACjF,IAAI,CAAC,KAAK,EAAE,MAAM,GAAG,WAAW,EAAE,CAAC;IACnC,KAAK,IAAI,IAAI,CAAC;CACd,CA6DA;AAeD,2DAA2D;AAC3D,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,gBAAgB,GAAG,MAAM,CAGlE","sourcesContent":["/**\n * Pure JSON-RPC 2.0 framing for the MCP stdio transport.\n *\n * MCP stdio frames one JSON message per line on stdout; embedded newlines are\n * illegal, so decoding is a line split with a hard length ceiling. Nothing here\n * touches a process, a socket, or a clock — the transport owns all I/O so this\n * layer stays trivially testable.\n */\n\n/** Ceiling for a single decoded line. A server that exceeds it is malfunctioning, not slow. */\nexport const MAX_MESSAGE_LINE_BYTES = 16 * 1024 * 1024;\n\nexport type JsonRpcId = number | string;\n\nexport interface JsonRpcRequest {\n\treadonly jsonrpc: \"2.0\";\n\treadonly id: JsonRpcId;\n\treadonly method: string;\n\treadonly params?: unknown;\n}\n\nexport interface JsonRpcNotification {\n\treadonly jsonrpc: \"2.0\";\n\treadonly method: string;\n\treadonly params?: unknown;\n}\n\nexport interface JsonRpcErrorBody {\n\treadonly code: number;\n\treadonly message: string;\n\treadonly data?: unknown;\n}\n\nexport interface JsonRpcResponse {\n\treadonly jsonrpc: \"2.0\";\n\treadonly id: JsonRpcId;\n\treadonly result?: unknown;\n\treadonly error?: JsonRpcErrorBody;\n}\n\nexport type JsonRpcMessage = JsonRpcRequest | JsonRpcNotification | JsonRpcResponse;\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nexport function isJsonRpcResponse(message: unknown): message is JsonRpcResponse {\n\tif (!isRecord(message) || message.jsonrpc !== \"2.0\") return false;\n\tif (!(\"id\" in message)) return false;\n\tconst id = message.id;\n\tif (typeof id !== \"number\" && typeof id !== \"string\") return false;\n\t// JSON-RPC success and error are mutually exclusive; `error: null` is not a\n\t// valid error object, and a present error must carry its required fields.\n\tconst hasResult = \"result\" in message;\n\tconst hasError = \"error\" in message;\n\tif (hasResult === hasError) return false; // both present or both absent\n\tif (hasError) {\n\t\tconst error = message.error;\n\t\tif (!isRecord(error)) return false;\n\t\tif (typeof error.code !== \"number\" || !Number.isFinite(error.code)) return false;\n\t\tif (typeof error.message !== \"string\") return false;\n\t}\n\treturn true;\n}\n\nexport function isJsonRpcNotification(message: unknown): message is JsonRpcNotification {\n\treturn isRecord(message) && message.jsonrpc === \"2.0\" && typeof message.method === \"string\" && !(\"id\" in message);\n}\n\nexport function isJsonRpcRequest(message: unknown): message is JsonRpcRequest {\n\tif (!isRecord(message) || message.jsonrpc !== \"2.0\") return false;\n\tif (typeof message.method !== \"string\") return false;\n\tconst id = message.id;\n\treturn typeof id === \"number\" || typeof id === \"string\";\n}\n\n/** Serialize one message as a single stdio frame (JSON + newline). */\nexport function encodeMessage(message: JsonRpcMessage): string {\n\tconst line = JSON.stringify(message);\n\tif (line.includes(\"\\n\")) {\n\t\t// JSON.stringify escapes newlines, so this is unreachable for valid input;\n\t\t// keep the guard so a future custom serializer cannot corrupt framing.\n\t\tthrow new Error(\"MCP frame contains a literal newline\");\n\t}\n\treturn `${line}\\n`;\n}\n\nexport interface DecodedLine {\n\t/** Parsed message, when the line was valid JSON. */\n\treadonly message?: JsonRpcMessage;\n\t/** Why the line was dropped, when it was not usable. */\n\treadonly error?: string;\n}\n\n/**\n * Incremental newline-delimited JSON decoder.\n *\n * The limit is enforced in UTF-8 bytes against the raw frame, before JSON\n * parsing, so the same message is accepted or rejected regardless of how the\n * transport splits it into chunks. A line that exceeds\n * {@link MAX_MESSAGE_LINE_BYTES} is reported once, its bytes are discarded\n * without being retained, and decoding resynchronizes at the next newline —\n * a runaway server cannot exhaust memory by growing the retained buffer.\n *\n * `maxLineBytes` must be a positive finite integer; anything else is a\n * configuration error, not a size to guess at.\n */\nexport function createLineDecoder(maxLineBytes: number = MAX_MESSAGE_LINE_BYTES): {\n\tpush(chunk: string): DecodedLine[];\n\treset(): void;\n} {\n\tif (!Number.isSafeInteger(maxLineBytes) || maxLineBytes <= 0) {\n\t\tthrow new RangeError(`maxLineBytes must be a positive finite integer, got ${maxLineBytes}`);\n\t}\n\tlet buffer = \"\";\n\t/** UTF-8 byte length of `buffer` — the limit unit, not the JS string length. */\n\tlet bufferBytes = 0;\n\t/** True while the remainder of an oversized line is being discarded. */\n\tlet discarding = false;\n\n\treturn {\n\t\tpush(chunk: string): DecodedLine[] {\n\t\t\tconst out: DecodedLine[] = [];\n\t\t\tlet rest = chunk;\n\n\t\t\tif (discarding) {\n\t\t\t\t// Skip the tail of the rejected line without storing it.\n\t\t\t\tconst boundary = rest.indexOf(\"\\n\");\n\t\t\t\tif (boundary === -1) {\n\t\t\t\t\treturn out;\n\t\t\t\t}\n\t\t\t\tdiscarding = false;\n\t\t\t\trest = rest.slice(boundary + 1);\n\t\t\t}\n\n\t\t\tbuffer += rest;\n\t\t\tbufferBytes += Buffer.byteLength(rest, \"utf8\");\n\n\t\t\tlet newlineIndex = buffer.indexOf(\"\\n\");\n\t\t\twhile (newlineIndex !== -1) {\n\t\t\t\tconst line = buffer.slice(0, newlineIndex);\n\t\t\t\tconst lineBytes = Buffer.byteLength(line, \"utf8\");\n\t\t\t\tbuffer = buffer.slice(newlineIndex + 1);\n\t\t\t\tbufferBytes -= lineBytes + 1; // consumed line plus its newline\n\t\t\t\tconst trimmed = line.trim();\n\t\t\t\tif (trimmed.length === 0) {\n\t\t\t\t\tnewlineIndex = buffer.indexOf(\"\\n\");\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tif (lineBytes > maxLineBytes) {\n\t\t\t\t\tout.push({ error: `MCP frame exceeded ${maxLineBytes} bytes` });\n\t\t\t\t} else {\n\t\t\t\t\tout.push(decodeLine(trimmed));\n\t\t\t\t}\n\t\t\t\tnewlineIndex = buffer.indexOf(\"\\n\");\n\t\t\t}\n\n\t\t\tif (bufferBytes > maxLineBytes) {\n\t\t\t\tout.push({ error: `MCP frame exceeded ${maxLineBytes} bytes` });\n\t\t\t\tbuffer = \"\";\n\t\t\t\tbufferBytes = 0;\n\t\t\t\tdiscarding = true;\n\t\t\t}\n\t\t\treturn out;\n\t\t},\n\t\treset(): void {\n\t\t\tbuffer = \"\";\n\t\t\tbufferBytes = 0;\n\t\t\tdiscarding = false;\n\t\t},\n\t};\n}\n\nfunction decodeLine(line: string): DecodedLine {\n\tlet parsed: unknown;\n\ttry {\n\t\tparsed = JSON.parse(line);\n\t} catch {\n\t\treturn { error: \"MCP frame is not valid JSON\" };\n\t}\n\tif (isJsonRpcResponse(parsed) || isJsonRpcNotification(parsed) || isJsonRpcRequest(parsed)) {\n\t\treturn { message: parsed };\n\t}\n\treturn { error: \"MCP frame is not a JSON-RPC 2.0 message\" };\n}\n\n/** Format a JSON-RPC error body for a human or a model. */\nexport function formatJsonRpcError(error: JsonRpcErrorBody): string {\n\tconst suffix = error.data === undefined ? \"\" : ` (${JSON.stringify(error.data)})`;\n\treturn `JSON-RPC error ${error.code}: ${error.message}${suffix}`;\n}\n"]}
|
|
@@ -19,7 +19,22 @@ export function isJsonRpcResponse(message) {
|
|
|
19
19
|
const id = message.id;
|
|
20
20
|
if (typeof id !== "number" && typeof id !== "string")
|
|
21
21
|
return false;
|
|
22
|
-
|
|
22
|
+
// JSON-RPC success and error are mutually exclusive; `error: null` is not a
|
|
23
|
+
// valid error object, and a present error must carry its required fields.
|
|
24
|
+
const hasResult = "result" in message;
|
|
25
|
+
const hasError = "error" in message;
|
|
26
|
+
if (hasResult === hasError)
|
|
27
|
+
return false; // both present or both absent
|
|
28
|
+
if (hasError) {
|
|
29
|
+
const error = message.error;
|
|
30
|
+
if (!isRecord(error))
|
|
31
|
+
return false;
|
|
32
|
+
if (typeof error.code !== "number" || !Number.isFinite(error.code))
|
|
33
|
+
return false;
|
|
34
|
+
if (typeof error.message !== "string")
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
return true;
|
|
23
38
|
}
|
|
24
39
|
export function isJsonRpcNotification(message) {
|
|
25
40
|
return isRecord(message) && message.jsonrpc === "2.0" && typeof message.method === "string" && !("id" in message);
|
|
@@ -45,40 +60,71 @@ export function encodeMessage(message) {
|
|
|
45
60
|
/**
|
|
46
61
|
* Incremental newline-delimited JSON decoder.
|
|
47
62
|
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
63
|
+
* The limit is enforced in UTF-8 bytes against the raw frame, before JSON
|
|
64
|
+
* parsing, so the same message is accepted or rejected regardless of how the
|
|
65
|
+
* transport splits it into chunks. A line that exceeds
|
|
66
|
+
* {@link MAX_MESSAGE_LINE_BYTES} is reported once, its bytes are discarded
|
|
67
|
+
* without being retained, and decoding resynchronizes at the next newline —
|
|
68
|
+
* a runaway server cannot exhaust memory by growing the retained buffer.
|
|
69
|
+
*
|
|
70
|
+
* `maxLineBytes` must be a positive finite integer; anything else is a
|
|
71
|
+
* configuration error, not a size to guess at.
|
|
51
72
|
*/
|
|
52
73
|
export function createLineDecoder(maxLineBytes = MAX_MESSAGE_LINE_BYTES) {
|
|
74
|
+
if (!Number.isSafeInteger(maxLineBytes) || maxLineBytes <= 0) {
|
|
75
|
+
throw new RangeError(`maxLineBytes must be a positive finite integer, got ${maxLineBytes}`);
|
|
76
|
+
}
|
|
53
77
|
let buffer = "";
|
|
54
|
-
|
|
78
|
+
/** UTF-8 byte length of `buffer` — the limit unit, not the JS string length. */
|
|
79
|
+
let bufferBytes = 0;
|
|
80
|
+
/** True while the remainder of an oversized line is being discarded. */
|
|
81
|
+
let discarding = false;
|
|
55
82
|
return {
|
|
56
83
|
push(chunk) {
|
|
57
84
|
const out = [];
|
|
58
|
-
|
|
85
|
+
let rest = chunk;
|
|
86
|
+
if (discarding) {
|
|
87
|
+
// Skip the tail of the rejected line without storing it.
|
|
88
|
+
const boundary = rest.indexOf("\n");
|
|
89
|
+
if (boundary === -1) {
|
|
90
|
+
return out;
|
|
91
|
+
}
|
|
92
|
+
discarding = false;
|
|
93
|
+
rest = rest.slice(boundary + 1);
|
|
94
|
+
}
|
|
95
|
+
buffer += rest;
|
|
96
|
+
bufferBytes += Buffer.byteLength(rest, "utf8");
|
|
59
97
|
let newlineIndex = buffer.indexOf("\n");
|
|
60
98
|
while (newlineIndex !== -1) {
|
|
61
|
-
const line = buffer.slice(0, newlineIndex)
|
|
99
|
+
const line = buffer.slice(0, newlineIndex);
|
|
100
|
+
const lineBytes = Buffer.byteLength(line, "utf8");
|
|
62
101
|
buffer = buffer.slice(newlineIndex + 1);
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
102
|
+
bufferBytes -= lineBytes + 1; // consumed line plus its newline
|
|
103
|
+
const trimmed = line.trim();
|
|
104
|
+
if (trimmed.length === 0) {
|
|
105
|
+
newlineIndex = buffer.indexOf("\n");
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
if (lineBytes > maxLineBytes) {
|
|
109
|
+
out.push({ error: `MCP frame exceeded ${maxLineBytes} bytes` });
|
|
66
110
|
}
|
|
67
|
-
else
|
|
68
|
-
out.push(decodeLine(
|
|
111
|
+
else {
|
|
112
|
+
out.push(decodeLine(trimmed));
|
|
69
113
|
}
|
|
70
114
|
newlineIndex = buffer.indexOf("\n");
|
|
71
115
|
}
|
|
72
|
-
if (
|
|
116
|
+
if (bufferBytes > maxLineBytes) {
|
|
73
117
|
out.push({ error: `MCP frame exceeded ${maxLineBytes} bytes` });
|
|
74
|
-
overflowed = true;
|
|
75
118
|
buffer = "";
|
|
119
|
+
bufferBytes = 0;
|
|
120
|
+
discarding = true;
|
|
76
121
|
}
|
|
77
122
|
return out;
|
|
78
123
|
},
|
|
79
124
|
reset() {
|
|
80
125
|
buffer = "";
|
|
81
|
-
|
|
126
|
+
bufferBytes = 0;
|
|
127
|
+
discarding = false;
|
|
82
128
|
},
|
|
83
129
|
};
|
|
84
130
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"protocol.js","sourceRoot":"","sources":["../../../src/core/mcp/protocol.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Pure JSON-RPC 2.0 framing for the MCP stdio transport.\n *\n * MCP stdio frames one JSON message per line on stdout; embedded newlines are\n * illegal, so decoding is a line split with a hard length ceiling. Nothing here\n * touches a process, a socket, or a clock — the transport owns all I/O so this\n * layer stays trivially testable.\n */\n\n/** Ceiling for a single decoded line. A server that exceeds it is malfunctioning, not slow. */\nexport const MAX_MESSAGE_LINE_BYTES = 16 * 1024 * 1024;\n\nexport type JsonRpcId = number | string;\n\nexport interface JsonRpcRequest {\n\treadonly jsonrpc: \"2.0\";\n\treadonly id: JsonRpcId;\n\treadonly method: string;\n\treadonly params?: unknown;\n}\n\nexport interface JsonRpcNotification {\n\treadonly jsonrpc: \"2.0\";\n\treadonly method: string;\n\treadonly params?: unknown;\n}\n\nexport interface JsonRpcErrorBody {\n\treadonly code: number;\n\treadonly message: string;\n\treadonly data?: unknown;\n}\n\nexport interface JsonRpcResponse {\n\treadonly jsonrpc: \"2.0\";\n\treadonly id: JsonRpcId;\n\treadonly result?: unknown;\n\treadonly error?: JsonRpcErrorBody;\n}\n\nexport type JsonRpcMessage = JsonRpcRequest | JsonRpcNotification | JsonRpcResponse;\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nexport function isJsonRpcResponse(message: unknown): message is JsonRpcResponse {\n\tif (!isRecord(message) || message.jsonrpc !== \"2.0\") return false;\n\tif (!(\"id\" in message)) return false;\n\tconst id = message.id;\n\tif (typeof id !== \"number\" && typeof id !== \"string\") return false;\n\treturn \"result\" in message || \"error\" in message;\n}\n\nexport function isJsonRpcNotification(message: unknown): message is JsonRpcNotification {\n\treturn isRecord(message) && message.jsonrpc === \"2.0\" && typeof message.method === \"string\" && !(\"id\" in message);\n}\n\nexport function isJsonRpcRequest(message: unknown): message is JsonRpcRequest {\n\tif (!isRecord(message) || message.jsonrpc !== \"2.0\") return false;\n\tif (typeof message.method !== \"string\") return false;\n\tconst id = message.id;\n\treturn typeof id === \"number\" || typeof id === \"string\";\n}\n\n/** Serialize one message as a single stdio frame (JSON + newline). */\nexport function encodeMessage(message: JsonRpcMessage): string {\n\tconst line = JSON.stringify(message);\n\tif (line.includes(\"\\n\")) {\n\t\t// JSON.stringify escapes newlines, so this is unreachable for valid input;\n\t\t// keep the guard so a future custom serializer cannot corrupt framing.\n\t\tthrow new Error(\"MCP frame contains a literal newline\");\n\t}\n\treturn `${line}\\n`;\n}\n\nexport interface DecodedLine {\n\t/** Parsed message, when the line was valid JSON. */\n\treadonly message?: JsonRpcMessage;\n\t/** Why the line was dropped, when it was not usable. */\n\treadonly error?: string;\n}\n\n/**\n * Incremental newline-delimited JSON decoder.\n *\n * Holds at most one partial line. A line longer than {@link MAX_MESSAGE_LINE_BYTES}\n * is dropped with an error rather than buffered, so a runaway server cannot\n * exhaust memory.\n */\nexport function createLineDecoder(maxLineBytes: number = MAX_MESSAGE_LINE_BYTES): {\n\tpush(chunk: string): DecodedLine[];\n\treset(): void;\n} {\n\tlet buffer = \"\";\n\tlet overflowed = false;\n\n\treturn {\n\t\tpush(chunk: string): DecodedLine[] {\n\t\t\tconst out: DecodedLine[] = [];\n\t\t\tbuffer += chunk;\n\n\t\t\tlet newlineIndex = buffer.indexOf(\"\\n\");\n\t\t\twhile (newlineIndex !== -1) {\n\t\t\t\tconst line = buffer.slice(0, newlineIndex).trim();\n\t\t\t\tbuffer = buffer.slice(newlineIndex + 1);\n\t\t\t\tif (overflowed) {\n\t\t\t\t\t// The oversized line ends here; resume clean at the next boundary.\n\t\t\t\t\toverflowed = false;\n\t\t\t\t} else if (line.length > 0) {\n\t\t\t\t\tout.push(decodeLine(line));\n\t\t\t\t}\n\t\t\t\tnewlineIndex = buffer.indexOf(\"\\n\");\n\t\t\t}\n\n\t\t\tif (!overflowed && buffer.length > maxLineBytes) {\n\t\t\t\tout.push({ error: `MCP frame exceeded ${maxLineBytes} bytes` });\n\t\t\t\toverflowed = true;\n\t\t\t\tbuffer = \"\";\n\t\t\t}\n\t\t\treturn out;\n\t\t},\n\t\treset(): void {\n\t\t\tbuffer = \"\";\n\t\t\toverflowed = false;\n\t\t},\n\t};\n}\n\nfunction decodeLine(line: string): DecodedLine {\n\tlet parsed: unknown;\n\ttry {\n\t\tparsed = JSON.parse(line);\n\t} catch {\n\t\treturn { error: \"MCP frame is not valid JSON\" };\n\t}\n\tif (isJsonRpcResponse(parsed) || isJsonRpcNotification(parsed) || isJsonRpcRequest(parsed)) {\n\t\treturn { message: parsed };\n\t}\n\treturn { error: \"MCP frame is not a JSON-RPC 2.0 message\" };\n}\n\n/** Format a JSON-RPC error body for a human or a model. */\nexport function formatJsonRpcError(error: JsonRpcErrorBody): string {\n\tconst suffix = error.data === undefined ? \"\" : ` (${JSON.stringify(error.data)})`;\n\treturn `JSON-RPC error ${error.code}: ${error.message}${suffix}`;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"protocol.js","sourceRoot":"","sources":["../../../src/core/mcp/protocol.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Pure JSON-RPC 2.0 framing for the MCP stdio transport.\n *\n * MCP stdio frames one JSON message per line on stdout; embedded newlines are\n * illegal, so decoding is a line split with a hard length ceiling. Nothing here\n * touches a process, a socket, or a clock — the transport owns all I/O so this\n * layer stays trivially testable.\n */\n\n/** Ceiling for a single decoded line. A server that exceeds it is malfunctioning, not slow. */\nexport const MAX_MESSAGE_LINE_BYTES = 16 * 1024 * 1024;\n\nexport type JsonRpcId = number | string;\n\nexport interface JsonRpcRequest {\n\treadonly jsonrpc: \"2.0\";\n\treadonly id: JsonRpcId;\n\treadonly method: string;\n\treadonly params?: unknown;\n}\n\nexport interface JsonRpcNotification {\n\treadonly jsonrpc: \"2.0\";\n\treadonly method: string;\n\treadonly params?: unknown;\n}\n\nexport interface JsonRpcErrorBody {\n\treadonly code: number;\n\treadonly message: string;\n\treadonly data?: unknown;\n}\n\nexport interface JsonRpcResponse {\n\treadonly jsonrpc: \"2.0\";\n\treadonly id: JsonRpcId;\n\treadonly result?: unknown;\n\treadonly error?: JsonRpcErrorBody;\n}\n\nexport type JsonRpcMessage = JsonRpcRequest | JsonRpcNotification | JsonRpcResponse;\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nexport function isJsonRpcResponse(message: unknown): message is JsonRpcResponse {\n\tif (!isRecord(message) || message.jsonrpc !== \"2.0\") return false;\n\tif (!(\"id\" in message)) return false;\n\tconst id = message.id;\n\tif (typeof id !== \"number\" && typeof id !== \"string\") return false;\n\t// JSON-RPC success and error are mutually exclusive; `error: null` is not a\n\t// valid error object, and a present error must carry its required fields.\n\tconst hasResult = \"result\" in message;\n\tconst hasError = \"error\" in message;\n\tif (hasResult === hasError) return false; // both present or both absent\n\tif (hasError) {\n\t\tconst error = message.error;\n\t\tif (!isRecord(error)) return false;\n\t\tif (typeof error.code !== \"number\" || !Number.isFinite(error.code)) return false;\n\t\tif (typeof error.message !== \"string\") return false;\n\t}\n\treturn true;\n}\n\nexport function isJsonRpcNotification(message: unknown): message is JsonRpcNotification {\n\treturn isRecord(message) && message.jsonrpc === \"2.0\" && typeof message.method === \"string\" && !(\"id\" in message);\n}\n\nexport function isJsonRpcRequest(message: unknown): message is JsonRpcRequest {\n\tif (!isRecord(message) || message.jsonrpc !== \"2.0\") return false;\n\tif (typeof message.method !== \"string\") return false;\n\tconst id = message.id;\n\treturn typeof id === \"number\" || typeof id === \"string\";\n}\n\n/** Serialize one message as a single stdio frame (JSON + newline). */\nexport function encodeMessage(message: JsonRpcMessage): string {\n\tconst line = JSON.stringify(message);\n\tif (line.includes(\"\\n\")) {\n\t\t// JSON.stringify escapes newlines, so this is unreachable for valid input;\n\t\t// keep the guard so a future custom serializer cannot corrupt framing.\n\t\tthrow new Error(\"MCP frame contains a literal newline\");\n\t}\n\treturn `${line}\\n`;\n}\n\nexport interface DecodedLine {\n\t/** Parsed message, when the line was valid JSON. */\n\treadonly message?: JsonRpcMessage;\n\t/** Why the line was dropped, when it was not usable. */\n\treadonly error?: string;\n}\n\n/**\n * Incremental newline-delimited JSON decoder.\n *\n * The limit is enforced in UTF-8 bytes against the raw frame, before JSON\n * parsing, so the same message is accepted or rejected regardless of how the\n * transport splits it into chunks. A line that exceeds\n * {@link MAX_MESSAGE_LINE_BYTES} is reported once, its bytes are discarded\n * without being retained, and decoding resynchronizes at the next newline —\n * a runaway server cannot exhaust memory by growing the retained buffer.\n *\n * `maxLineBytes` must be a positive finite integer; anything else is a\n * configuration error, not a size to guess at.\n */\nexport function createLineDecoder(maxLineBytes: number = MAX_MESSAGE_LINE_BYTES): {\n\tpush(chunk: string): DecodedLine[];\n\treset(): void;\n} {\n\tif (!Number.isSafeInteger(maxLineBytes) || maxLineBytes <= 0) {\n\t\tthrow new RangeError(`maxLineBytes must be a positive finite integer, got ${maxLineBytes}`);\n\t}\n\tlet buffer = \"\";\n\t/** UTF-8 byte length of `buffer` — the limit unit, not the JS string length. */\n\tlet bufferBytes = 0;\n\t/** True while the remainder of an oversized line is being discarded. */\n\tlet discarding = false;\n\n\treturn {\n\t\tpush(chunk: string): DecodedLine[] {\n\t\t\tconst out: DecodedLine[] = [];\n\t\t\tlet rest = chunk;\n\n\t\t\tif (discarding) {\n\t\t\t\t// Skip the tail of the rejected line without storing it.\n\t\t\t\tconst boundary = rest.indexOf(\"\\n\");\n\t\t\t\tif (boundary === -1) {\n\t\t\t\t\treturn out;\n\t\t\t\t}\n\t\t\t\tdiscarding = false;\n\t\t\t\trest = rest.slice(boundary + 1);\n\t\t\t}\n\n\t\t\tbuffer += rest;\n\t\t\tbufferBytes += Buffer.byteLength(rest, \"utf8\");\n\n\t\t\tlet newlineIndex = buffer.indexOf(\"\\n\");\n\t\t\twhile (newlineIndex !== -1) {\n\t\t\t\tconst line = buffer.slice(0, newlineIndex);\n\t\t\t\tconst lineBytes = Buffer.byteLength(line, \"utf8\");\n\t\t\t\tbuffer = buffer.slice(newlineIndex + 1);\n\t\t\t\tbufferBytes -= lineBytes + 1; // consumed line plus its newline\n\t\t\t\tconst trimmed = line.trim();\n\t\t\t\tif (trimmed.length === 0) {\n\t\t\t\t\tnewlineIndex = buffer.indexOf(\"\\n\");\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tif (lineBytes > maxLineBytes) {\n\t\t\t\t\tout.push({ error: `MCP frame exceeded ${maxLineBytes} bytes` });\n\t\t\t\t} else {\n\t\t\t\t\tout.push(decodeLine(trimmed));\n\t\t\t\t}\n\t\t\t\tnewlineIndex = buffer.indexOf(\"\\n\");\n\t\t\t}\n\n\t\t\tif (bufferBytes > maxLineBytes) {\n\t\t\t\tout.push({ error: `MCP frame exceeded ${maxLineBytes} bytes` });\n\t\t\t\tbuffer = \"\";\n\t\t\t\tbufferBytes = 0;\n\t\t\t\tdiscarding = true;\n\t\t\t}\n\t\t\treturn out;\n\t\t},\n\t\treset(): void {\n\t\t\tbuffer = \"\";\n\t\t\tbufferBytes = 0;\n\t\t\tdiscarding = false;\n\t\t},\n\t};\n}\n\nfunction decodeLine(line: string): DecodedLine {\n\tlet parsed: unknown;\n\ttry {\n\t\tparsed = JSON.parse(line);\n\t} catch {\n\t\treturn { error: \"MCP frame is not valid JSON\" };\n\t}\n\tif (isJsonRpcResponse(parsed) || isJsonRpcNotification(parsed) || isJsonRpcRequest(parsed)) {\n\t\treturn { message: parsed };\n\t}\n\treturn { error: \"MCP frame is not a JSON-RPC 2.0 message\" };\n}\n\n/** Format a JSON-RPC error body for a human or a model. */\nexport function formatJsonRpcError(error: JsonRpcErrorBody): string {\n\tconst suffix = error.data === undefined ? \"\" : ` (${JSON.stringify(error.data)})`;\n\treturn `JSON-RPC error ${error.code}: ${error.message}${suffix}`;\n}\n"]}
|
|
@@ -15,6 +15,8 @@ import { type JsonRpcMessage } from "./protocol.ts";
|
|
|
15
15
|
export declare const MAX_STDERR_TAIL_CHARS = 8192;
|
|
16
16
|
/** Grace period between SIGTERM and SIGKILL on close. */
|
|
17
17
|
export declare const DEFAULT_KILL_GRACE_MS = 2000;
|
|
18
|
+
/** Framed bytes allowed to sit in the child's stdin buffer before sends refuse. */
|
|
19
|
+
export declare const DEFAULT_MAX_PENDING_WRITE_BYTES: number;
|
|
18
20
|
export interface StdioTransportOptions {
|
|
19
21
|
readonly command: string;
|
|
20
22
|
readonly args?: readonly string[];
|
|
@@ -24,6 +26,8 @@ export interface StdioTransportOptions {
|
|
|
24
26
|
readonly killGraceMs?: number;
|
|
25
27
|
/** Inherit the parent environment. Default `true`. */
|
|
26
28
|
readonly inheritEnv?: boolean;
|
|
29
|
+
/** Framed bytes allowed to sit unwritten before `send` refuses. */
|
|
30
|
+
readonly maxPendingWriteBytes?: number;
|
|
27
31
|
}
|
|
28
32
|
export interface StdioTransportHandlers {
|
|
29
33
|
readonly onMessage: (message: JsonRpcMessage) => void;
|
|
@@ -42,6 +46,9 @@ export declare class McpStdioTransport {
|
|
|
42
46
|
private stderrTail;
|
|
43
47
|
private exited;
|
|
44
48
|
private killTimer;
|
|
49
|
+
/** Framed bytes written but not yet flushed to the child's stdin. */
|
|
50
|
+
private pendingWriteBytes;
|
|
51
|
+
private waitingForDrain;
|
|
45
52
|
private readonly options;
|
|
46
53
|
private readonly handlers;
|
|
47
54
|
constructor(options: StdioTransportOptions, handlers: StdioTransportHandlers);
|
|
@@ -54,7 +61,11 @@ export declare class McpStdioTransport {
|
|
|
54
61
|
* one failure path instead of two.
|
|
55
62
|
*/
|
|
56
63
|
start(): void;
|
|
57
|
-
/**
|
|
64
|
+
/**
|
|
65
|
+
* Write one message. Returns `false` when the server is no longer writable
|
|
66
|
+
* or the outbound buffer is already saturated — the client treats that as
|
|
67
|
+
* a send failure rather than queueing unbounded frames.
|
|
68
|
+
*/
|
|
58
69
|
send(message: JsonRpcMessage): boolean;
|
|
59
70
|
/** Terminate the server: SIGTERM, then SIGKILL after the grace period. */
|
|
60
71
|
close(): void;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"stdio-transport.d.ts","sourceRoot":"","sources":["../../../src/core/mcp/stdio-transport.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAIH,OAAO,EAAoC,KAAK,cAAc,EAAE,MAAM,eAAe,CAAC;AAEtF,mEAAmE;AACnE,eAAO,MAAM,qBAAqB,OAAO,CAAC;AAC1C,yDAAyD;AACzD,eAAO,MAAM,qBAAqB,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"stdio-transport.d.ts","sourceRoot":"","sources":["../../../src/core/mcp/stdio-transport.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAIH,OAAO,EAAoC,KAAK,cAAc,EAAE,MAAM,eAAe,CAAC;AAEtF,mEAAmE;AACnE,eAAO,MAAM,qBAAqB,OAAO,CAAC;AAC1C,yDAAyD;AACzD,eAAO,MAAM,qBAAqB,OAAO,CAAC;AAC1C,mFAAmF;AACnF,eAAO,MAAM,+BAA+B,QAAmB,CAAC;AAEhE,MAAM,WAAW,qBAAqB;IACrC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,IAAI,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC,QAAQ,CAAC,GAAG,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IAChD,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,mCAAmC;IACnC,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,sDAAsD;IACtD,QAAQ,CAAC,UAAU,CAAC,EAAE,OAAO,CAAC;IAC9B,mEAAmE;IACnE,QAAQ,CAAC,oBAAoB,CAAC,EAAE,MAAM,CAAC;CACvC;AAED,MAAM,WAAW,sBAAsB;IACtC,QAAQ,CAAC,SAAS,EAAE,CAAC,OAAO,EAAE,cAAc,KAAK,IAAI,CAAC;IACtD,6DAA6D;IAC7D,QAAQ,CAAC,aAAa,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IAClD,yDAAyD;IACzD,QAAQ,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE;QAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,KAAK,IAAI,CAAC;CAClG;AAED,qBAAa,iBAAiB;IAC7B,OAAO,CAAC,KAAK,CAA6C;IAC1D,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAuB;IAC/C,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IACrC,OAAO,CAAC,UAAU,CAAM;IACxB,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,SAAS,CAA4C;IAC7D,qEAAqE;IACrE,OAAO,CAAC,iBAAiB,CAAK;IAC9B,OAAO,CAAC,eAAe,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAwB;IAChD,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAyB;IAElD,YAAY,OAAO,EAAE,qBAAqB,EAAE,QAAQ,EAAE,sBAAsB,EAI3E;IAED,iFAAiF;IACjF,IAAI,MAAM,IAAI,MAAM,CAEnB;IAED,IAAI,OAAO,IAAI,OAAO,CAErB;IAED;;;;OAIG;IACH,KAAK,IAAI,IAAI,CA+BZ;IAED;;;;OAIG;IACH,IAAI,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAyBrC;IAED,0EAA0E;IAC1E,KAAK,IAAI,IAAI,CAuBZ;IAED,OAAO,CAAC,UAAU;CAYlB","sourcesContent":["/**\n * Child-process stdio transport for MCP.\n *\n * Owns exactly one server process: writes newline-delimited JSON to stdin,\n * decodes stdout into messages, keeps a bounded tail of stderr for diagnostics,\n * and reports exit exactly once. It never interprets MCP semantics — that is\n * the client's job.\n *\n * Every configured OMK MCP server uses a `command`, so stdio is the whole\n * transport surface today. HTTP servers would get a sibling module rather than\n * a flag on this one.\n */\n\nimport type { ChildProcessWithoutNullStreams } from \"node:child_process\";\nimport spawn from \"cross-spawn\";\nimport { createLineDecoder, encodeMessage, type JsonRpcMessage } from \"./protocol.ts\";\n\n/** How much stderr tail to keep per server for error reporting. */\nexport const MAX_STDERR_TAIL_CHARS = 8192;\n/** Grace period between SIGTERM and SIGKILL on close. */\nexport const DEFAULT_KILL_GRACE_MS = 2000;\n/** Framed bytes allowed to sit in the child's stdin buffer before sends refuse. */\nexport const DEFAULT_MAX_PENDING_WRITE_BYTES = 16 * 1024 * 1024;\n\nexport interface StdioTransportOptions {\n\treadonly command: string;\n\treadonly args?: readonly string[];\n\treadonly env?: Readonly<Record<string, string>>;\n\treadonly cwd?: string;\n\t/** Grace period before SIGKILL. */\n\treadonly killGraceMs?: number;\n\t/** Inherit the parent environment. Default `true`. */\n\treadonly inheritEnv?: boolean;\n\t/** Framed bytes allowed to sit unwritten before `send` refuses. */\n\treadonly maxPendingWriteBytes?: number;\n}\n\nexport interface StdioTransportHandlers {\n\treadonly onMessage: (message: JsonRpcMessage) => void;\n\t/** Malformed frames are surfaced, never silently dropped. */\n\treadonly onDecodeError?: (reason: string) => void;\n\t/** Called at most once, with the exit code or signal. */\n\treadonly onExit: (info: { readonly code: number | null; readonly signal: string | null }) => void;\n}\n\nexport class McpStdioTransport {\n\tprivate child: ChildProcessWithoutNullStreams | undefined;\n\tprivate readonly decoder = createLineDecoder();\n\tprivate readonly killGraceMs: number;\n\tprivate stderrTail = \"\";\n\tprivate exited = false;\n\tprivate killTimer: ReturnType<typeof setTimeout> | undefined;\n\t/** Framed bytes written but not yet flushed to the child's stdin. */\n\tprivate pendingWriteBytes = 0;\n\tprivate waitingForDrain = false;\n\tprivate readonly options: StdioTransportOptions;\n\tprivate readonly handlers: StdioTransportHandlers;\n\n\tconstructor(options: StdioTransportOptions, handlers: StdioTransportHandlers) {\n\t\tthis.options = options;\n\t\tthis.handlers = handlers;\n\t\tthis.killGraceMs = options.killGraceMs ?? DEFAULT_KILL_GRACE_MS;\n\t}\n\n\t/** Bounded tail of the server's stderr. Empty when the server has been quiet. */\n\tget stderr(): string {\n\t\treturn this.stderrTail;\n\t}\n\n\tget running(): boolean {\n\t\treturn this.child !== undefined && !this.exited;\n\t}\n\n\t/**\n\t * Spawn the server process. Throws synchronously only on an invalid\n\t * configuration; a spawn failure surfaces through `onExit`, so callers have\n\t * one failure path instead of two.\n\t */\n\tstart(): void {\n\t\tif (this.child) throw new Error(\"MCP stdio transport already started\");\n\t\tconst env = this.options.inheritEnv === false ? { ...this.options.env } : { ...process.env, ...this.options.env };\n\t\tconst child = spawn(this.options.command, [...(this.options.args ?? [])], {\n\t\t\tcwd: this.options.cwd,\n\t\t\tenv,\n\t\t\tstdio: [\"pipe\", \"pipe\", \"pipe\"],\n\t\t\twindowsHide: true,\n\t\t}) as ChildProcessWithoutNullStreams;\n\t\tthis.child = child;\n\n\t\tchild.stdout.setEncoding(\"utf8\");\n\t\tchild.stdout.on(\"data\", (chunk: string) => {\n\t\t\tfor (const decoded of this.decoder.push(chunk)) {\n\t\t\t\tif (decoded.message) this.handlers.onMessage(decoded.message);\n\t\t\t\telse if (decoded.error) this.handlers.onDecodeError?.(decoded.error);\n\t\t\t}\n\t\t});\n\n\t\tchild.stderr.setEncoding(\"utf8\");\n\t\tchild.stderr.on(\"data\", (chunk: string) => {\n\t\t\tthis.stderrTail = `${this.stderrTail}${chunk}`.slice(-MAX_STDERR_TAIL_CHARS);\n\t\t});\n\n\t\t// A stdin EPIPE after the server exits is expected, not an error worth throwing.\n\t\tchild.stdin.on(\"error\", () => {});\n\t\tchild.on(\"error\", (error: Error) => {\n\t\t\tthis.stderrTail = `${this.stderrTail}${error.message}\\n`.slice(-MAX_STDERR_TAIL_CHARS);\n\t\t\tthis.settleExit(null, null);\n\t\t});\n\t\tchild.on(\"exit\", (code, signal) => this.settleExit(code, signal));\n\t}\n\n\t/**\n\t * Write one message. Returns `false` when the server is no longer writable\n\t * or the outbound buffer is already saturated — the client treats that as\n\t * a send failure rather than queueing unbounded frames.\n\t */\n\tsend(message: JsonRpcMessage): boolean {\n\t\tconst child = this.child;\n\t\tif (!child || this.exited || !child.stdin.writable) return false;\n\t\tconst frame = encodeMessage(message);\n\t\tconst frameBytes = Buffer.byteLength(frame, \"utf8\");\n\t\tconst cap = this.options.maxPendingWriteBytes ?? DEFAULT_MAX_PENDING_WRITE_BYTES;\n\t\tif (this.pendingWriteBytes + frameBytes > cap) return false;\n\t\ttry {\n\t\t\tconst flushed = child.stdin.write(frame);\n\t\t\tif (!flushed) {\n\t\t\t\t// Kernel/user buffer is saturated: account the frame and release it\n\t\t\t\t// on drain so the pending estimate tracks the real backlog.\n\t\t\t\tthis.pendingWriteBytes += frameBytes;\n\t\t\t\tif (!this.waitingForDrain) {\n\t\t\t\t\tthis.waitingForDrain = true;\n\t\t\t\t\tchild.stdin.once(\"drain\", () => {\n\t\t\t\t\t\tthis.waitingForDrain = false;\n\t\t\t\t\t\tthis.pendingWriteBytes = 0;\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn true;\n\t\t} catch {\n\t\t\treturn false;\n\t\t}\n\t}\n\n\t/** Terminate the server: SIGTERM, then SIGKILL after the grace period. */\n\tclose(): void {\n\t\tconst child = this.child;\n\t\tif (!child || this.exited) return;\n\t\ttry {\n\t\t\tchild.stdin.end();\n\t\t} catch {\n\t\t\t// Already closed.\n\t\t}\n\t\ttry {\n\t\t\tchild.kill(\"SIGTERM\");\n\t\t} catch {\n\t\t\t// Already gone.\n\t\t}\n\t\tif (this.killTimer !== undefined) return;\n\t\tthis.killTimer = setTimeout(() => {\n\t\t\tthis.killTimer = undefined;\n\t\t\ttry {\n\t\t\t\tchild.kill(\"SIGKILL\");\n\t\t\t} catch {\n\t\t\t\t// Already gone.\n\t\t\t}\n\t\t}, this.killGraceMs);\n\t\tthis.killTimer.unref?.();\n\t}\n\n\tprivate settleExit(code: number | null, signal: string | null): void {\n\t\tif (this.exited) return;\n\t\tthis.exited = true;\n\t\tthis.pendingWriteBytes = 0;\n\t\tthis.waitingForDrain = false;\n\t\tif (this.killTimer !== undefined) {\n\t\t\tclearTimeout(this.killTimer);\n\t\t\tthis.killTimer = undefined;\n\t\t}\n\t\tthis.decoder.reset();\n\t\tthis.handlers.onExit({ code, signal });\n\t}\n}\n"]}
|
|
@@ -16,6 +16,8 @@ import { createLineDecoder, encodeMessage } from "./protocol.js";
|
|
|
16
16
|
export const MAX_STDERR_TAIL_CHARS = 8192;
|
|
17
17
|
/** Grace period between SIGTERM and SIGKILL on close. */
|
|
18
18
|
export const DEFAULT_KILL_GRACE_MS = 2000;
|
|
19
|
+
/** Framed bytes allowed to sit in the child's stdin buffer before sends refuse. */
|
|
20
|
+
export const DEFAULT_MAX_PENDING_WRITE_BYTES = 16 * 1024 * 1024;
|
|
19
21
|
export class McpStdioTransport {
|
|
20
22
|
child;
|
|
21
23
|
decoder = createLineDecoder();
|
|
@@ -23,6 +25,9 @@ export class McpStdioTransport {
|
|
|
23
25
|
stderrTail = "";
|
|
24
26
|
exited = false;
|
|
25
27
|
killTimer;
|
|
28
|
+
/** Framed bytes written but not yet flushed to the child's stdin. */
|
|
29
|
+
pendingWriteBytes = 0;
|
|
30
|
+
waitingForDrain = false;
|
|
26
31
|
options;
|
|
27
32
|
handlers;
|
|
28
33
|
constructor(options, handlers) {
|
|
@@ -74,13 +79,34 @@ export class McpStdioTransport {
|
|
|
74
79
|
});
|
|
75
80
|
child.on("exit", (code, signal) => this.settleExit(code, signal));
|
|
76
81
|
}
|
|
77
|
-
/**
|
|
82
|
+
/**
|
|
83
|
+
* Write one message. Returns `false` when the server is no longer writable
|
|
84
|
+
* or the outbound buffer is already saturated — the client treats that as
|
|
85
|
+
* a send failure rather than queueing unbounded frames.
|
|
86
|
+
*/
|
|
78
87
|
send(message) {
|
|
79
88
|
const child = this.child;
|
|
80
89
|
if (!child || this.exited || !child.stdin.writable)
|
|
81
90
|
return false;
|
|
91
|
+
const frame = encodeMessage(message);
|
|
92
|
+
const frameBytes = Buffer.byteLength(frame, "utf8");
|
|
93
|
+
const cap = this.options.maxPendingWriteBytes ?? DEFAULT_MAX_PENDING_WRITE_BYTES;
|
|
94
|
+
if (this.pendingWriteBytes + frameBytes > cap)
|
|
95
|
+
return false;
|
|
82
96
|
try {
|
|
83
|
-
child.stdin.write(
|
|
97
|
+
const flushed = child.stdin.write(frame);
|
|
98
|
+
if (!flushed) {
|
|
99
|
+
// Kernel/user buffer is saturated: account the frame and release it
|
|
100
|
+
// on drain so the pending estimate tracks the real backlog.
|
|
101
|
+
this.pendingWriteBytes += frameBytes;
|
|
102
|
+
if (!this.waitingForDrain) {
|
|
103
|
+
this.waitingForDrain = true;
|
|
104
|
+
child.stdin.once("drain", () => {
|
|
105
|
+
this.waitingForDrain = false;
|
|
106
|
+
this.pendingWriteBytes = 0;
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
}
|
|
84
110
|
return true;
|
|
85
111
|
}
|
|
86
112
|
catch {
|
|
@@ -121,6 +147,8 @@ export class McpStdioTransport {
|
|
|
121
147
|
if (this.exited)
|
|
122
148
|
return;
|
|
123
149
|
this.exited = true;
|
|
150
|
+
this.pendingWriteBytes = 0;
|
|
151
|
+
this.waitingForDrain = false;
|
|
124
152
|
if (this.killTimer !== undefined) {
|
|
125
153
|
clearTimeout(this.killTimer);
|
|
126
154
|
this.killTimer = undefined;
|