open-multi-agent-kit 0.95.0 → 0.95.2
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 +33 -0
- package/README.md +1 -1
- package/dist/approvals/approval-receipt-store.d.ts +18 -0
- package/dist/approvals/approval-receipt-store.d.ts.map +1 -0
- package/dist/approvals/approval-receipt-store.js +169 -0
- package/dist/approvals/approval-receipt-store.js.map +1 -0
- package/dist/approvals/approval-receipt.d.ts +43 -0
- package/dist/approvals/approval-receipt.d.ts.map +1 -0
- package/dist/approvals/approval-receipt.js +208 -0
- package/dist/approvals/approval-receipt.js.map +1 -0
- package/dist/approvals/approval-verifier.d.ts +17 -0
- package/dist/approvals/approval-verifier.d.ts.map +1 -0
- package/dist/approvals/approval-verifier.js +23 -0
- package/dist/approvals/approval-verifier.js.map +1 -0
- package/dist/approvals/plannotator-approval-bridge.d.ts +58 -0
- package/dist/approvals/plannotator-approval-bridge.d.ts.map +1 -0
- package/dist/approvals/plannotator-approval-bridge.js +169 -0
- package/dist/approvals/plannotator-approval-bridge.js.map +1 -0
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +2 -0
- package/dist/cli/args.js.map +1 -1
- package/dist/commands/package-doctor-cli.d.ts +13 -0
- package/dist/commands/package-doctor-cli.d.ts.map +1 -0
- package/dist/commands/package-doctor-cli.js +80 -0
- package/dist/commands/package-doctor-cli.js.map +1 -0
- package/dist/commands/router-feedback-cli.d.ts +11 -0
- package/dist/commands/router-feedback-cli.d.ts.map +1 -0
- package/dist/commands/router-feedback-cli.js +99 -0
- package/dist/commands/router-feedback-cli.js.map +1 -0
- package/dist/commands/stats-cli.d.ts +20 -0
- package/dist/commands/stats-cli.d.ts.map +1 -0
- package/dist/commands/stats-cli.js +99 -0
- package/dist/commands/stats-cli.js.map +1 -0
- package/dist/core/agent-session-runtime.d.ts.map +1 -1
- package/dist/core/agent-session-runtime.js +4 -1
- package/dist/core/agent-session-runtime.js.map +1 -1
- package/dist/core/agent-session.d.ts +97 -29
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +583 -199
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/bash-executor.d.ts.map +1 -1
- package/dist/core/bash-executor.js +8 -2
- package/dist/core/bash-executor.js.map +1 -1
- package/dist/core/compaction/transaction.d.ts +9 -0
- package/dist/core/compaction/transaction.d.ts.map +1 -1
- package/dist/core/compaction/transaction.js +17 -2
- package/dist/core/compaction/transaction.js.map +1 -1
- package/dist/core/context-budget-governor-v2.d.ts +1 -1
- package/dist/core/context-budget-governor-v2.d.ts.map +1 -1
- package/dist/core/context-budget-governor-v2.js +1 -1
- package/dist/core/context-budget-governor-v2.js.map +1 -1
- package/dist/core/context-budget-system-prompt.d.ts.map +1 -1
- package/dist/core/context-budget-system-prompt.js +18 -2
- package/dist/core/context-budget-system-prompt.js.map +1 -1
- package/dist/core/context-budget-v2-cache-disk.d.ts +109 -0
- package/dist/core/context-budget-v2-cache-disk.d.ts.map +1 -0
- package/dist/core/context-budget-v2-cache-disk.js +343 -0
- package/dist/core/context-budget-v2-cache-disk.js.map +1 -0
- package/dist/core/context-budget-v2-cache-keys.d.ts +22 -0
- package/dist/core/context-budget-v2-cache-keys.d.ts.map +1 -1
- package/dist/core/context-budget-v2-cache-keys.js +4 -4
- package/dist/core/context-budget-v2-cache-keys.js.map +1 -1
- package/dist/core/context-budget-v2-cache.d.ts +1 -0
- package/dist/core/context-budget-v2-cache.d.ts.map +1 -1
- package/dist/core/context-budget-v2-cache.js +1 -0
- package/dist/core/context-budget-v2-cache.js.map +1 -1
- package/dist/core/context-budget-v2-plan-cache-keys.d.ts.map +1 -1
- package/dist/core/context-budget-v2-plan-cache-keys.js +39 -5
- package/dist/core/context-budget-v2-plan-cache-keys.js.map +1 -1
- package/dist/core/context-budget-v2-planner.d.ts.map +1 -1
- package/dist/core/context-budget-v2-planner.js +17 -14
- package/dist/core/context-budget-v2-planner.js.map +1 -1
- package/dist/core/context-budget-v2-types.d.ts +1 -0
- 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/loader.d.ts.map +1 -1
- package/dist/core/extensions/loader.js +45 -22
- package/dist/core/extensions/loader.js.bak-sharedjiti-20260812-111246 +553 -0
- package/dist/core/extensions/loader.js.map +1 -1
- package/dist/core/extensions/types.d.ts +4 -0
- package/dist/core/extensions/types.d.ts.map +1 -1
- package/dist/core/extensions/types.js.map +1 -1
- package/dist/core/headroom.d.ts.map +1 -1
- package/dist/core/headroom.js +7 -2
- package/dist/core/headroom.js.map +1 -1
- package/dist/core/mcp/client.d.ts +105 -0
- package/dist/core/mcp/client.d.ts.map +1 -0
- package/dist/core/mcp/client.js +198 -0
- package/dist/core/mcp/client.js.map +1 -0
- package/dist/core/mcp/config.d.ts +23 -0
- package/dist/core/mcp/config.d.ts.map +1 -0
- package/dist/core/mcp/config.js +89 -0
- package/dist/core/mcp/config.js.map +1 -0
- package/dist/core/mcp/index.d.ts +15 -0
- package/dist/core/mcp/index.d.ts.map +1 -0
- package/dist/core/mcp/index.js +15 -0
- package/dist/core/mcp/index.js.map +1 -0
- package/dist/core/mcp/manager.d.ts +84 -0
- package/dist/core/mcp/manager.d.ts.map +1 -0
- package/dist/core/mcp/manager.js +173 -0
- package/dist/core/mcp/manager.js.map +1 -0
- package/dist/core/mcp/protocol.d.ts +59 -0
- package/dist/core/mcp/protocol.d.ts.map +1 -0
- package/dist/core/mcp/protocol.js +103 -0
- package/dist/core/mcp/protocol.js.map +1 -0
- package/dist/core/mcp/stdio-transport.d.ts +63 -0
- package/dist/core/mcp/stdio-transport.d.ts.map +1 -0
- package/dist/core/mcp/stdio-transport.js +132 -0
- package/dist/core/mcp/stdio-transport.js.map +1 -0
- package/dist/core/mcp/tools.d.ts +47 -0
- package/dist/core/mcp/tools.d.ts.map +1 -0
- package/dist/core/mcp/tools.js +102 -0
- package/dist/core/mcp/tools.js.map +1 -0
- package/dist/core/package-archive.d.ts +3 -0
- package/dist/core/package-archive.d.ts.map +1 -0
- package/dist/core/package-archive.js +161 -0
- package/dist/core/package-archive.js.map +1 -0
- package/dist/core/package-doctor-source-scan.d.ts +13 -0
- package/dist/core/package-doctor-source-scan.d.ts.map +1 -0
- package/dist/core/package-doctor-source-scan.js +124 -0
- package/dist/core/package-doctor-source-scan.js.map +1 -0
- package/dist/core/package-doctor.d.ts +41 -0
- package/dist/core/package-doctor.d.ts.map +1 -0
- package/dist/core/package-doctor.js +145 -0
- package/dist/core/package-doctor.js.map +1 -0
- package/dist/core/package-manager.d.ts +10 -1
- package/dist/core/package-manager.d.ts.map +1 -1
- package/dist/core/package-manager.js +175 -42
- package/dist/core/package-manager.js.map +1 -1
- package/dist/core/package-manifest.d.ts +26 -0
- package/dist/core/package-manifest.d.ts.map +1 -0
- package/dist/core/package-manifest.js +85 -0
- package/dist/core/package-manifest.js.map +1 -0
- package/dist/core/pi-compat.d.ts +5 -0
- package/dist/core/pi-compat.d.ts.map +1 -0
- package/dist/core/pi-compat.js +18 -0
- package/dist/core/pi-compat.js.map +1 -0
- package/dist/core/reasoning-router-bias.d.ts +10 -14
- package/dist/core/reasoning-router-bias.d.ts.map +1 -1
- package/dist/core/reasoning-router-bias.js +22 -15
- package/dist/core/reasoning-router-bias.js.map +1 -1
- package/dist/core/reasoning-router-v4.d.ts +9 -1
- package/dist/core/reasoning-router-v4.d.ts.map +1 -1
- package/dist/core/reasoning-router-v4.js +20 -5
- package/dist/core/reasoning-router-v4.js.map +1 -1
- package/dist/core/redaction.d.ts +2 -5
- package/dist/core/redaction.d.ts.map +1 -1
- package/dist/core/redaction.js +14 -1
- package/dist/core/redaction.js.map +1 -1
- package/dist/core/repository-learning-scope.d.ts +10 -0
- package/dist/core/repository-learning-scope.d.ts.map +1 -0
- package/dist/core/repository-learning-scope.js +42 -0
- package/dist/core/repository-learning-scope.js.map +1 -0
- package/dist/core/resource-loader.d.ts.map +1 -1
- package/dist/core/resource-loader.js +16 -1
- package/dist/core/resource-loader.js.map +1 -1
- package/dist/core/router-feedback-collector.d.ts +2 -1
- package/dist/core/router-feedback-collector.d.ts.map +1 -1
- package/dist/core/router-feedback-collector.js +2 -2
- package/dist/core/router-feedback-collector.js.map +1 -1
- package/dist/core/runtime-provenance.d.ts +54 -0
- package/dist/core/runtime-provenance.d.ts.map +1 -0
- package/dist/core/runtime-provenance.js +115 -0
- package/dist/core/runtime-provenance.js.map +1 -0
- package/dist/core/sandbox/backend.d.ts.map +1 -1
- package/dist/core/sandbox/backend.js +39 -21
- package/dist/core/sandbox/backend.js.map +1 -1
- package/dist/core/sandbox/default-policy.d.ts +4 -5
- package/dist/core/sandbox/default-policy.d.ts.map +1 -1
- package/dist/core/sandbox/default-policy.js +9 -11
- package/dist/core/sandbox/default-policy.js.map +1 -1
- package/dist/core/session-bash-runtime.d.ts +4 -4
- package/dist/core/session-bash-runtime.d.ts.map +1 -1
- package/dist/core/session-bash-runtime.js +6 -5
- package/dist/core/session-bash-runtime.js.map +1 -1
- package/dist/core/session-compaction-service.d.ts.map +1 -1
- package/dist/core/session-compaction-service.js +7 -7
- package/dist/core/session-compaction-service.js.map +1 -1
- package/dist/core/session-manager.d.ts +4 -1
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +47 -7
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/settings-manager.d.ts +7 -4
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +11 -3
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/system-prompt.d.ts +9 -1
- package/dist/core/system-prompt.d.ts.map +1 -1
- package/dist/core/system-prompt.js +10 -4
- package/dist/core/system-prompt.js.map +1 -1
- package/dist/core/tools/bash.d.ts +2 -2
- package/dist/core/tools/bash.d.ts.map +1 -1
- package/dist/core/tools/bash.js +2 -1
- package/dist/core/tools/bash.js.map +1 -1
- package/dist/core/tools/tool-definition-wrapper.d.ts.map +1 -1
- package/dist/core/tools/tool-definition-wrapper.js +15 -0
- package/dist/core/tools/tool-definition-wrapper.js.map +1 -1
- package/dist/core/turn-metrics.d.ts +135 -0
- package/dist/core/turn-metrics.d.ts.map +1 -0
- package/dist/core/turn-metrics.js +261 -0
- package/dist/core/turn-metrics.js.map +1 -0
- package/dist/core/verified-bash-runtime.d.ts.map +1 -1
- package/dist/core/verified-bash-runtime.js +10 -4
- package/dist/core/verified-bash-runtime.js.map +1 -1
- package/dist/guardrails/replay-ledger-lock-owner.d.ts +7 -0
- package/dist/guardrails/replay-ledger-lock-owner.d.ts.map +1 -1
- package/dist/guardrails/replay-ledger-lock-owner.js +39 -0
- package/dist/guardrails/replay-ledger-lock-owner.js.map +1 -1
- package/dist/index.d.ts +6 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -1
- package/dist/index.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +18 -0
- package/dist/main.js.map +1 -1
- package/dist/modes/interactive/components/control-panel-layout.d.ts.map +1 -1
- package/dist/modes/interactive/components/control-panel-layout.js +2 -2
- package/dist/modes/interactive/components/control-panel-layout.js.map +1 -1
- package/dist/modes/interactive/components/footer.d.ts +3 -0
- package/dist/modes/interactive/components/footer.d.ts.map +1 -1
- package/dist/modes/interactive/components/footer.js +40 -12
- package/dist/modes/interactive/components/footer.js.map +1 -1
- package/dist/modes/interactive/components/settings-selector.d.ts +2 -0
- package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/settings-selector.js +10 -0
- package/dist/modes/interactive/components/settings-selector.js.map +1 -1
- package/dist/modes/interactive/components/status-sidebar.d.ts +14 -2
- package/dist/modes/interactive/components/status-sidebar.d.ts.map +1 -1
- package/dist/modes/interactive/components/status-sidebar.js +106 -19
- package/dist/modes/interactive/components/status-sidebar.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +10 -1
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +104 -17
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/interactive/theme/dark.json +1 -1
- package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
- package/dist/modes/interactive/theme/theme.js +1 -0
- package/dist/modes/interactive/theme/theme.js.map +1 -1
- package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-mode.js +1 -1
- package/dist/modes/rpc/rpc-mode.js.map +1 -1
- package/docs/compaction.md +25 -3
- package/docs/containerization.md +14 -2
- package/docs/docs.json +8 -0
- package/docs/environment-variables.md +1 -1
- package/docs/extensions.md +75 -16
- package/docs/mcp.md +97 -0
- package/docs/metrics.md +87 -0
- package/docs/packages.md +25 -4
- package/docs/providers.md +1 -1
- package/docs/sdk.md +25 -4
- package/docs/settings.md +8 -7
- package/docs/usage.md +13 -3
- 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/plannotator-approval-bridge.ts +83 -0
- package/examples/extensions/sandbox/package-lock.json +2 -2
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/subagent/README.md +21 -11
- package/examples/extensions/subagent/adaptive-agent-runtime.ts +9 -3
- package/examples/extensions/subagent/agents/planner.md +1 -3
- package/examples/extensions/subagent/agents/reviewer.md +1 -3
- package/examples/extensions/subagent/agents/scout.md +1 -3
- package/examples/extensions/subagent/agents/worker.md +0 -1
- package/examples/extensions/subagent/agents.test.ts +9 -0
- package/examples/extensions/subagent/agents.ts +11 -4
- package/examples/extensions/subagent/deadline-budget.test.ts +32 -0
- package/examples/extensions/subagent/deadline-budget.ts +25 -1
- package/examples/extensions/subagent/fixtures/fake-omk-json.mjs +10 -0
- package/examples/extensions/subagent/index.ts +234 -55
- package/examples/extensions/subagent/subagent-execution-policy.test.ts +27 -0
- package/examples/extensions/subagent/subagent-extension-smoke.test.ts +167 -2
- package/examples/extensions/subagent/workflow-graph.test.ts +80 -0
- package/examples/extensions/subagent/workflow-graph.ts +89 -0
- package/examples/extensions/with-deps/package-lock.json +2 -2
- package/examples/extensions/with-deps/package.json +1 -1
- package/npm-shrinkwrap.json +253 -439
- package/package.json +8 -8
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lazy, crash-isolated MCP server manager.
|
|
3
|
+
*
|
|
4
|
+
* Design constraints that shaped this:
|
|
5
|
+
* - A workspace can configure 25 servers. Spawning them all at startup would
|
|
6
|
+
* trade a 0.6s cold start for a multi-second one, so connection is lazy and
|
|
7
|
+
* only happens when tools are actually requested.
|
|
8
|
+
* - One broken server must never take a session down. Every connect and every
|
|
9
|
+
* listing is isolated; a failure is recorded as server status and the other
|
|
10
|
+
* servers still contribute tools.
|
|
11
|
+
* - Connect work is deduplicated: concurrent `listToolDefinitions()` calls
|
|
12
|
+
* share one in-flight connect per server.
|
|
13
|
+
*/
|
|
14
|
+
import { McpClient } from "./client.js";
|
|
15
|
+
import { createMcpToolDefinition } from "./tools.js";
|
|
16
|
+
export class McpManager {
|
|
17
|
+
runtimes = new Map();
|
|
18
|
+
options;
|
|
19
|
+
constructor(options) {
|
|
20
|
+
this.options = options;
|
|
21
|
+
for (const config of options.servers) {
|
|
22
|
+
if (this.runtimes.has(config.name))
|
|
23
|
+
continue; // First definition wins; inventory already applies precedence.
|
|
24
|
+
this.runtimes.set(config.name, { config, state: "idle", tools: [] });
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/** Configured server names, in configuration order. */
|
|
28
|
+
get serverNames() {
|
|
29
|
+
return [...this.runtimes.keys()];
|
|
30
|
+
}
|
|
31
|
+
/** Current status per server. Safe to call before any connection attempt. */
|
|
32
|
+
status() {
|
|
33
|
+
return [...this.runtimes.values()].map((runtime) => ({
|
|
34
|
+
name: runtime.config.name,
|
|
35
|
+
state: runtime.state,
|
|
36
|
+
toolCount: runtime.tools.length,
|
|
37
|
+
error: runtime.error,
|
|
38
|
+
serverVersion: runtime.client?.serverInfo.version,
|
|
39
|
+
}));
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Connect every enabled server that has not been attempted yet, then return
|
|
43
|
+
* the union of their tools. Servers are connected concurrently and failures
|
|
44
|
+
* are isolated, so a partial result is the normal outcome, not an error.
|
|
45
|
+
*/
|
|
46
|
+
async listToolDefinitions() {
|
|
47
|
+
await Promise.all([...this.runtimes.values()].map((runtime) => this.ensureConnected(runtime)));
|
|
48
|
+
const tools = [];
|
|
49
|
+
const seen = new Set();
|
|
50
|
+
for (const runtime of this.runtimes.values()) {
|
|
51
|
+
for (const tool of runtime.tools) {
|
|
52
|
+
if (seen.has(tool.name))
|
|
53
|
+
continue; // Namespacing makes this rare; keep it deterministic anyway.
|
|
54
|
+
seen.add(tool.name);
|
|
55
|
+
tools.push(tool);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return tools;
|
|
59
|
+
}
|
|
60
|
+
/** Connect a single server by name. Returns its status either way. */
|
|
61
|
+
async connect(name) {
|
|
62
|
+
const runtime = this.runtimes.get(name);
|
|
63
|
+
if (!runtime) {
|
|
64
|
+
return { name, state: "failed", toolCount: 0, error: `Unknown MCP server "${name}"` };
|
|
65
|
+
}
|
|
66
|
+
await this.ensureConnected(runtime);
|
|
67
|
+
return {
|
|
68
|
+
name,
|
|
69
|
+
state: runtime.state,
|
|
70
|
+
toolCount: runtime.tools.length,
|
|
71
|
+
error: runtime.error,
|
|
72
|
+
serverVersion: runtime.client?.serverInfo.version,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
/** Terminate every connected server. Idempotent. */
|
|
76
|
+
close() {
|
|
77
|
+
for (const runtime of this.runtimes.values()) {
|
|
78
|
+
runtime.client?.close();
|
|
79
|
+
runtime.client = undefined;
|
|
80
|
+
runtime.tools = [];
|
|
81
|
+
if (runtime.state === "ready" || runtime.state === "connecting")
|
|
82
|
+
runtime.state = "idle";
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Verify that `ready` servers are actually alive with a protocol ping. A
|
|
87
|
+
* failed ping closes the client and marks the server `failed`, so a
|
|
88
|
+
* silently killed process stops masquerading as connected. Servers already
|
|
89
|
+
* `failed` are re-attempted only when `reconnectFailed` is set — callers
|
|
90
|
+
* should gate that behind a slow cadence so a permanently broken server is
|
|
91
|
+
* not respawned every probe. Idle servers stay idle: connection is lazy by
|
|
92
|
+
* design, and a status probe must not spawn processes.
|
|
93
|
+
*/
|
|
94
|
+
async checkHealth(options) {
|
|
95
|
+
const work = [];
|
|
96
|
+
for (const runtime of this.runtimes.values()) {
|
|
97
|
+
if (runtime.state === "ready" && runtime.client) {
|
|
98
|
+
work.push(this.pingRuntime(runtime, options?.pingTimeoutMs));
|
|
99
|
+
}
|
|
100
|
+
else if (runtime.state === "failed" && options?.reconnectFailed && !runtime.config.disabled) {
|
|
101
|
+
runtime.state = "idle";
|
|
102
|
+
runtime.error = undefined;
|
|
103
|
+
work.push(this.ensureConnected(runtime));
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
await Promise.all(work);
|
|
107
|
+
return this.status();
|
|
108
|
+
}
|
|
109
|
+
async pingRuntime(runtime, timeoutMs) {
|
|
110
|
+
try {
|
|
111
|
+
await runtime.client?.ping(timeoutMs);
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
// Isolation point: only this server transitions; the rest stay ready.
|
|
115
|
+
runtime.client?.close();
|
|
116
|
+
runtime.client = undefined;
|
|
117
|
+
runtime.tools = [];
|
|
118
|
+
runtime.state = "failed";
|
|
119
|
+
runtime.error = `health check failed: ${error instanceof Error ? error.message : String(error)}`;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
ensureConnected(runtime) {
|
|
123
|
+
if (runtime.config.disabled) {
|
|
124
|
+
runtime.state = "failed";
|
|
125
|
+
runtime.error = "disabled by configuration";
|
|
126
|
+
return Promise.resolve();
|
|
127
|
+
}
|
|
128
|
+
if (runtime.state === "ready" || runtime.state === "failed")
|
|
129
|
+
return Promise.resolve();
|
|
130
|
+
if (runtime.connecting)
|
|
131
|
+
return runtime.connecting;
|
|
132
|
+
runtime.state = "connecting";
|
|
133
|
+
const attempt = this.connectRuntime(runtime).finally(() => {
|
|
134
|
+
runtime.connecting = undefined;
|
|
135
|
+
});
|
|
136
|
+
runtime.connecting = attempt;
|
|
137
|
+
return attempt;
|
|
138
|
+
}
|
|
139
|
+
async connectRuntime(runtime) {
|
|
140
|
+
const clientOptions = {
|
|
141
|
+
name: runtime.config.name,
|
|
142
|
+
clientInfo: this.options.clientInfo,
|
|
143
|
+
requestTimeoutMs: runtime.config.requestTimeoutMs,
|
|
144
|
+
handshakeTimeoutMs: runtime.config.handshakeTimeoutMs,
|
|
145
|
+
transport: {
|
|
146
|
+
command: runtime.config.command,
|
|
147
|
+
args: runtime.config.args,
|
|
148
|
+
env: runtime.config.env,
|
|
149
|
+
cwd: runtime.config.cwd ?? this.options.cwd,
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
const client = this.options.createClient?.(clientOptions) ?? new McpClient(clientOptions);
|
|
153
|
+
try {
|
|
154
|
+
await client.connect();
|
|
155
|
+
const schemas = await client.listTools();
|
|
156
|
+
runtime.client = client;
|
|
157
|
+
runtime.tools = schemas.map((schema) => createMcpToolDefinition(runtime.config.name, client, schema, {
|
|
158
|
+
callTimeoutMs: this.options.callTimeoutMs,
|
|
159
|
+
}));
|
|
160
|
+
runtime.state = "ready";
|
|
161
|
+
runtime.error = undefined;
|
|
162
|
+
}
|
|
163
|
+
catch (error) {
|
|
164
|
+
// Isolation point: this server is out, every other server is unaffected.
|
|
165
|
+
client.close();
|
|
166
|
+
runtime.client = undefined;
|
|
167
|
+
runtime.tools = [];
|
|
168
|
+
runtime.state = "failed";
|
|
169
|
+
runtime.error = error instanceof Error ? error.message : String(error);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
//# sourceMappingURL=manager.js.map
|
|
@@ -0,0 +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"]}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure JSON-RPC 2.0 framing for the MCP stdio transport.
|
|
3
|
+
*
|
|
4
|
+
* MCP stdio frames one JSON message per line on stdout; embedded newlines are
|
|
5
|
+
* illegal, so decoding is a line split with a hard length ceiling. Nothing here
|
|
6
|
+
* touches a process, a socket, or a clock — the transport owns all I/O so this
|
|
7
|
+
* layer stays trivially testable.
|
|
8
|
+
*/
|
|
9
|
+
/** Ceiling for a single decoded line. A server that exceeds it is malfunctioning, not slow. */
|
|
10
|
+
export declare const MAX_MESSAGE_LINE_BYTES: number;
|
|
11
|
+
export type JsonRpcId = number | string;
|
|
12
|
+
export interface JsonRpcRequest {
|
|
13
|
+
readonly jsonrpc: "2.0";
|
|
14
|
+
readonly id: JsonRpcId;
|
|
15
|
+
readonly method: string;
|
|
16
|
+
readonly params?: unknown;
|
|
17
|
+
}
|
|
18
|
+
export interface JsonRpcNotification {
|
|
19
|
+
readonly jsonrpc: "2.0";
|
|
20
|
+
readonly method: string;
|
|
21
|
+
readonly params?: unknown;
|
|
22
|
+
}
|
|
23
|
+
export interface JsonRpcErrorBody {
|
|
24
|
+
readonly code: number;
|
|
25
|
+
readonly message: string;
|
|
26
|
+
readonly data?: unknown;
|
|
27
|
+
}
|
|
28
|
+
export interface JsonRpcResponse {
|
|
29
|
+
readonly jsonrpc: "2.0";
|
|
30
|
+
readonly id: JsonRpcId;
|
|
31
|
+
readonly result?: unknown;
|
|
32
|
+
readonly error?: JsonRpcErrorBody;
|
|
33
|
+
}
|
|
34
|
+
export type JsonRpcMessage = JsonRpcRequest | JsonRpcNotification | JsonRpcResponse;
|
|
35
|
+
export declare function isJsonRpcResponse(message: unknown): message is JsonRpcResponse;
|
|
36
|
+
export declare function isJsonRpcNotification(message: unknown): message is JsonRpcNotification;
|
|
37
|
+
export declare function isJsonRpcRequest(message: unknown): message is JsonRpcRequest;
|
|
38
|
+
/** Serialize one message as a single stdio frame (JSON + newline). */
|
|
39
|
+
export declare function encodeMessage(message: JsonRpcMessage): string;
|
|
40
|
+
export interface DecodedLine {
|
|
41
|
+
/** Parsed message, when the line was valid JSON. */
|
|
42
|
+
readonly message?: JsonRpcMessage;
|
|
43
|
+
/** Why the line was dropped, when it was not usable. */
|
|
44
|
+
readonly error?: string;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Incremental newline-delimited JSON decoder.
|
|
48
|
+
*
|
|
49
|
+
* Holds at most one partial line. A line longer than {@link MAX_MESSAGE_LINE_BYTES}
|
|
50
|
+
* is dropped with an error rather than buffered, so a runaway server cannot
|
|
51
|
+
* exhaust memory.
|
|
52
|
+
*/
|
|
53
|
+
export declare function createLineDecoder(maxLineBytes?: number): {
|
|
54
|
+
push(chunk: string): DecodedLine[];
|
|
55
|
+
reset(): void;
|
|
56
|
+
};
|
|
57
|
+
/** Format a JSON-RPC error body for a human or a model. */
|
|
58
|
+
export declare function formatJsonRpcError(error: JsonRpcErrorBody): string;
|
|
59
|
+
//# sourceMappingURL=protocol.d.ts.map
|
|
@@ -0,0 +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,CAM9E;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;;;;;;GAMG;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,CAkCA;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\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"]}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure JSON-RPC 2.0 framing for the MCP stdio transport.
|
|
3
|
+
*
|
|
4
|
+
* MCP stdio frames one JSON message per line on stdout; embedded newlines are
|
|
5
|
+
* illegal, so decoding is a line split with a hard length ceiling. Nothing here
|
|
6
|
+
* touches a process, a socket, or a clock — the transport owns all I/O so this
|
|
7
|
+
* layer stays trivially testable.
|
|
8
|
+
*/
|
|
9
|
+
/** Ceiling for a single decoded line. A server that exceeds it is malfunctioning, not slow. */
|
|
10
|
+
export const MAX_MESSAGE_LINE_BYTES = 16 * 1024 * 1024;
|
|
11
|
+
function isRecord(value) {
|
|
12
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
13
|
+
}
|
|
14
|
+
export function isJsonRpcResponse(message) {
|
|
15
|
+
if (!isRecord(message) || message.jsonrpc !== "2.0")
|
|
16
|
+
return false;
|
|
17
|
+
if (!("id" in message))
|
|
18
|
+
return false;
|
|
19
|
+
const id = message.id;
|
|
20
|
+
if (typeof id !== "number" && typeof id !== "string")
|
|
21
|
+
return false;
|
|
22
|
+
return "result" in message || "error" in message;
|
|
23
|
+
}
|
|
24
|
+
export function isJsonRpcNotification(message) {
|
|
25
|
+
return isRecord(message) && message.jsonrpc === "2.0" && typeof message.method === "string" && !("id" in message);
|
|
26
|
+
}
|
|
27
|
+
export function isJsonRpcRequest(message) {
|
|
28
|
+
if (!isRecord(message) || message.jsonrpc !== "2.0")
|
|
29
|
+
return false;
|
|
30
|
+
if (typeof message.method !== "string")
|
|
31
|
+
return false;
|
|
32
|
+
const id = message.id;
|
|
33
|
+
return typeof id === "number" || typeof id === "string";
|
|
34
|
+
}
|
|
35
|
+
/** Serialize one message as a single stdio frame (JSON + newline). */
|
|
36
|
+
export function encodeMessage(message) {
|
|
37
|
+
const line = JSON.stringify(message);
|
|
38
|
+
if (line.includes("\n")) {
|
|
39
|
+
// JSON.stringify escapes newlines, so this is unreachable for valid input;
|
|
40
|
+
// keep the guard so a future custom serializer cannot corrupt framing.
|
|
41
|
+
throw new Error("MCP frame contains a literal newline");
|
|
42
|
+
}
|
|
43
|
+
return `${line}\n`;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Incremental newline-delimited JSON decoder.
|
|
47
|
+
*
|
|
48
|
+
* Holds at most one partial line. A line longer than {@link MAX_MESSAGE_LINE_BYTES}
|
|
49
|
+
* is dropped with an error rather than buffered, so a runaway server cannot
|
|
50
|
+
* exhaust memory.
|
|
51
|
+
*/
|
|
52
|
+
export function createLineDecoder(maxLineBytes = MAX_MESSAGE_LINE_BYTES) {
|
|
53
|
+
let buffer = "";
|
|
54
|
+
let overflowed = false;
|
|
55
|
+
return {
|
|
56
|
+
push(chunk) {
|
|
57
|
+
const out = [];
|
|
58
|
+
buffer += chunk;
|
|
59
|
+
let newlineIndex = buffer.indexOf("\n");
|
|
60
|
+
while (newlineIndex !== -1) {
|
|
61
|
+
const line = buffer.slice(0, newlineIndex).trim();
|
|
62
|
+
buffer = buffer.slice(newlineIndex + 1);
|
|
63
|
+
if (overflowed) {
|
|
64
|
+
// The oversized line ends here; resume clean at the next boundary.
|
|
65
|
+
overflowed = false;
|
|
66
|
+
}
|
|
67
|
+
else if (line.length > 0) {
|
|
68
|
+
out.push(decodeLine(line));
|
|
69
|
+
}
|
|
70
|
+
newlineIndex = buffer.indexOf("\n");
|
|
71
|
+
}
|
|
72
|
+
if (!overflowed && buffer.length > maxLineBytes) {
|
|
73
|
+
out.push({ error: `MCP frame exceeded ${maxLineBytes} bytes` });
|
|
74
|
+
overflowed = true;
|
|
75
|
+
buffer = "";
|
|
76
|
+
}
|
|
77
|
+
return out;
|
|
78
|
+
},
|
|
79
|
+
reset() {
|
|
80
|
+
buffer = "";
|
|
81
|
+
overflowed = false;
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
function decodeLine(line) {
|
|
86
|
+
let parsed;
|
|
87
|
+
try {
|
|
88
|
+
parsed = JSON.parse(line);
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
return { error: "MCP frame is not valid JSON" };
|
|
92
|
+
}
|
|
93
|
+
if (isJsonRpcResponse(parsed) || isJsonRpcNotification(parsed) || isJsonRpcRequest(parsed)) {
|
|
94
|
+
return { message: parsed };
|
|
95
|
+
}
|
|
96
|
+
return { error: "MCP frame is not a JSON-RPC 2.0 message" };
|
|
97
|
+
}
|
|
98
|
+
/** Format a JSON-RPC error body for a human or a model. */
|
|
99
|
+
export function formatJsonRpcError(error) {
|
|
100
|
+
const suffix = error.data === undefined ? "" : ` (${JSON.stringify(error.data)})`;
|
|
101
|
+
return `JSON-RPC error ${error.code}: ${error.message}${suffix}`;
|
|
102
|
+
}
|
|
103
|
+
//# sourceMappingURL=protocol.js.map
|
|
@@ -0,0 +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"]}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Child-process stdio transport for MCP.
|
|
3
|
+
*
|
|
4
|
+
* Owns exactly one server process: writes newline-delimited JSON to stdin,
|
|
5
|
+
* decodes stdout into messages, keeps a bounded tail of stderr for diagnostics,
|
|
6
|
+
* and reports exit exactly once. It never interprets MCP semantics — that is
|
|
7
|
+
* the client's job.
|
|
8
|
+
*
|
|
9
|
+
* Every configured OMK MCP server uses a `command`, so stdio is the whole
|
|
10
|
+
* transport surface today. HTTP servers would get a sibling module rather than
|
|
11
|
+
* a flag on this one.
|
|
12
|
+
*/
|
|
13
|
+
import { type JsonRpcMessage } from "./protocol.ts";
|
|
14
|
+
/** How much stderr tail to keep per server for error reporting. */
|
|
15
|
+
export declare const MAX_STDERR_TAIL_CHARS = 8192;
|
|
16
|
+
/** Grace period between SIGTERM and SIGKILL on close. */
|
|
17
|
+
export declare const DEFAULT_KILL_GRACE_MS = 2000;
|
|
18
|
+
export interface StdioTransportOptions {
|
|
19
|
+
readonly command: string;
|
|
20
|
+
readonly args?: readonly string[];
|
|
21
|
+
readonly env?: Readonly<Record<string, string>>;
|
|
22
|
+
readonly cwd?: string;
|
|
23
|
+
/** Grace period before SIGKILL. */
|
|
24
|
+
readonly killGraceMs?: number;
|
|
25
|
+
/** Inherit the parent environment. Default `true`. */
|
|
26
|
+
readonly inheritEnv?: boolean;
|
|
27
|
+
}
|
|
28
|
+
export interface StdioTransportHandlers {
|
|
29
|
+
readonly onMessage: (message: JsonRpcMessage) => void;
|
|
30
|
+
/** Malformed frames are surfaced, never silently dropped. */
|
|
31
|
+
readonly onDecodeError?: (reason: string) => void;
|
|
32
|
+
/** Called at most once, with the exit code or signal. */
|
|
33
|
+
readonly onExit: (info: {
|
|
34
|
+
readonly code: number | null;
|
|
35
|
+
readonly signal: string | null;
|
|
36
|
+
}) => void;
|
|
37
|
+
}
|
|
38
|
+
export declare class McpStdioTransport {
|
|
39
|
+
private child;
|
|
40
|
+
private readonly decoder;
|
|
41
|
+
private readonly killGraceMs;
|
|
42
|
+
private stderrTail;
|
|
43
|
+
private exited;
|
|
44
|
+
private killTimer;
|
|
45
|
+
private readonly options;
|
|
46
|
+
private readonly handlers;
|
|
47
|
+
constructor(options: StdioTransportOptions, handlers: StdioTransportHandlers);
|
|
48
|
+
/** Bounded tail of the server's stderr. Empty when the server has been quiet. */
|
|
49
|
+
get stderr(): string;
|
|
50
|
+
get running(): boolean;
|
|
51
|
+
/**
|
|
52
|
+
* Spawn the server process. Throws synchronously only on an invalid
|
|
53
|
+
* configuration; a spawn failure surfaces through `onExit`, so callers have
|
|
54
|
+
* one failure path instead of two.
|
|
55
|
+
*/
|
|
56
|
+
start(): void;
|
|
57
|
+
/** Write one message. Returns `false` when the server is no longer writable. */
|
|
58
|
+
send(message: JsonRpcMessage): boolean;
|
|
59
|
+
/** Terminate the server: SIGTERM, then SIGKILL after the grace period. */
|
|
60
|
+
close(): void;
|
|
61
|
+
private settleExit;
|
|
62
|
+
}
|
|
63
|
+
//# sourceMappingURL=stdio-transport.d.ts.map
|
|
@@ -0,0 +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;AAE1C,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;CAC9B;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,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,gFAAgF;IAChF,IAAI,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CASrC;IAED,0EAA0E;IAC1E,KAAK,IAAI,IAAI,CAuBZ;IAED,OAAO,CAAC,UAAU;CAUlB","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\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}\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\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/** Write one message. Returns `false` when the server is no longer writable. */\n\tsend(message: JsonRpcMessage): boolean {\n\t\tconst child = this.child;\n\t\tif (!child || this.exited || !child.stdin.writable) return false;\n\t\ttry {\n\t\t\tchild.stdin.write(encodeMessage(message));\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\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"]}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Child-process stdio transport for MCP.
|
|
3
|
+
*
|
|
4
|
+
* Owns exactly one server process: writes newline-delimited JSON to stdin,
|
|
5
|
+
* decodes stdout into messages, keeps a bounded tail of stderr for diagnostics,
|
|
6
|
+
* and reports exit exactly once. It never interprets MCP semantics — that is
|
|
7
|
+
* the client's job.
|
|
8
|
+
*
|
|
9
|
+
* Every configured OMK MCP server uses a `command`, so stdio is the whole
|
|
10
|
+
* transport surface today. HTTP servers would get a sibling module rather than
|
|
11
|
+
* a flag on this one.
|
|
12
|
+
*/
|
|
13
|
+
import spawn from "cross-spawn";
|
|
14
|
+
import { createLineDecoder, encodeMessage } from "./protocol.js";
|
|
15
|
+
/** How much stderr tail to keep per server for error reporting. */
|
|
16
|
+
export const MAX_STDERR_TAIL_CHARS = 8192;
|
|
17
|
+
/** Grace period between SIGTERM and SIGKILL on close. */
|
|
18
|
+
export const DEFAULT_KILL_GRACE_MS = 2000;
|
|
19
|
+
export class McpStdioTransport {
|
|
20
|
+
child;
|
|
21
|
+
decoder = createLineDecoder();
|
|
22
|
+
killGraceMs;
|
|
23
|
+
stderrTail = "";
|
|
24
|
+
exited = false;
|
|
25
|
+
killTimer;
|
|
26
|
+
options;
|
|
27
|
+
handlers;
|
|
28
|
+
constructor(options, handlers) {
|
|
29
|
+
this.options = options;
|
|
30
|
+
this.handlers = handlers;
|
|
31
|
+
this.killGraceMs = options.killGraceMs ?? DEFAULT_KILL_GRACE_MS;
|
|
32
|
+
}
|
|
33
|
+
/** Bounded tail of the server's stderr. Empty when the server has been quiet. */
|
|
34
|
+
get stderr() {
|
|
35
|
+
return this.stderrTail;
|
|
36
|
+
}
|
|
37
|
+
get running() {
|
|
38
|
+
return this.child !== undefined && !this.exited;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Spawn the server process. Throws synchronously only on an invalid
|
|
42
|
+
* configuration; a spawn failure surfaces through `onExit`, so callers have
|
|
43
|
+
* one failure path instead of two.
|
|
44
|
+
*/
|
|
45
|
+
start() {
|
|
46
|
+
if (this.child)
|
|
47
|
+
throw new Error("MCP stdio transport already started");
|
|
48
|
+
const env = this.options.inheritEnv === false ? { ...this.options.env } : { ...process.env, ...this.options.env };
|
|
49
|
+
const child = spawn(this.options.command, [...(this.options.args ?? [])], {
|
|
50
|
+
cwd: this.options.cwd,
|
|
51
|
+
env,
|
|
52
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
53
|
+
windowsHide: true,
|
|
54
|
+
});
|
|
55
|
+
this.child = child;
|
|
56
|
+
child.stdout.setEncoding("utf8");
|
|
57
|
+
child.stdout.on("data", (chunk) => {
|
|
58
|
+
for (const decoded of this.decoder.push(chunk)) {
|
|
59
|
+
if (decoded.message)
|
|
60
|
+
this.handlers.onMessage(decoded.message);
|
|
61
|
+
else if (decoded.error)
|
|
62
|
+
this.handlers.onDecodeError?.(decoded.error);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
child.stderr.setEncoding("utf8");
|
|
66
|
+
child.stderr.on("data", (chunk) => {
|
|
67
|
+
this.stderrTail = `${this.stderrTail}${chunk}`.slice(-MAX_STDERR_TAIL_CHARS);
|
|
68
|
+
});
|
|
69
|
+
// A stdin EPIPE after the server exits is expected, not an error worth throwing.
|
|
70
|
+
child.stdin.on("error", () => { });
|
|
71
|
+
child.on("error", (error) => {
|
|
72
|
+
this.stderrTail = `${this.stderrTail}${error.message}\n`.slice(-MAX_STDERR_TAIL_CHARS);
|
|
73
|
+
this.settleExit(null, null);
|
|
74
|
+
});
|
|
75
|
+
child.on("exit", (code, signal) => this.settleExit(code, signal));
|
|
76
|
+
}
|
|
77
|
+
/** Write one message. Returns `false` when the server is no longer writable. */
|
|
78
|
+
send(message) {
|
|
79
|
+
const child = this.child;
|
|
80
|
+
if (!child || this.exited || !child.stdin.writable)
|
|
81
|
+
return false;
|
|
82
|
+
try {
|
|
83
|
+
child.stdin.write(encodeMessage(message));
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
/** Terminate the server: SIGTERM, then SIGKILL after the grace period. */
|
|
91
|
+
close() {
|
|
92
|
+
const child = this.child;
|
|
93
|
+
if (!child || this.exited)
|
|
94
|
+
return;
|
|
95
|
+
try {
|
|
96
|
+
child.stdin.end();
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
// Already closed.
|
|
100
|
+
}
|
|
101
|
+
try {
|
|
102
|
+
child.kill("SIGTERM");
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
// Already gone.
|
|
106
|
+
}
|
|
107
|
+
if (this.killTimer !== undefined)
|
|
108
|
+
return;
|
|
109
|
+
this.killTimer = setTimeout(() => {
|
|
110
|
+
this.killTimer = undefined;
|
|
111
|
+
try {
|
|
112
|
+
child.kill("SIGKILL");
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
// Already gone.
|
|
116
|
+
}
|
|
117
|
+
}, this.killGraceMs);
|
|
118
|
+
this.killTimer.unref?.();
|
|
119
|
+
}
|
|
120
|
+
settleExit(code, signal) {
|
|
121
|
+
if (this.exited)
|
|
122
|
+
return;
|
|
123
|
+
this.exited = true;
|
|
124
|
+
if (this.killTimer !== undefined) {
|
|
125
|
+
clearTimeout(this.killTimer);
|
|
126
|
+
this.killTimer = undefined;
|
|
127
|
+
}
|
|
128
|
+
this.decoder.reset();
|
|
129
|
+
this.handlers.onExit({ code, signal });
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
//# sourceMappingURL=stdio-transport.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stdio-transport.js","sourceRoot":"","sources":["../../../src/core/mcp/stdio-transport.ts"],"names":[],"mappings":"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","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\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}\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\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/** Write one message. Returns `false` when the server is no longer writable. */\n\tsend(message: JsonRpcMessage): boolean {\n\t\tconst child = this.child;\n\t\tif (!child || this.exited || !child.stdin.writable) return false;\n\t\ttry {\n\t\t\tchild.stdin.write(encodeMessage(message));\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\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"]}
|