@kkelly-offical/kkcode 0.1.7 → 0.2.1
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/LICENSE +674 -674
- package/README.md +452 -387
- package/package.json +50 -46
- package/src/agent/agent.mjs +228 -220
- package/src/agent/custom-agent-loader.mjs +6 -3
- package/src/agent/generator.mjs +2 -2
- package/src/agent/prompt/assistant.txt +12 -0
- package/src/agent/prompt/bug-hunter.txt +89 -89
- package/src/agent/prompt/frontend-designer.txt +58 -58
- package/src/agent/prompt/guide.txt +1 -1
- package/src/agent/prompt/longagent-blueprint-agent.txt +83 -83
- package/src/agent/prompt/longagent-coding-agent.txt +37 -37
- package/src/agent/prompt/longagent-debugging-agent.txt +46 -46
- package/src/agent/prompt/longagent-preview-agent.txt +63 -63
- package/src/command/custom-commands.mjs +2 -2
- package/src/commands/agent.mjs +1 -1
- package/src/commands/background.mjs +145 -4
- package/src/commands/chat.mjs +117 -76
- package/src/commands/config.mjs +148 -1
- package/src/commands/doctor.mjs +30 -6
- package/src/commands/init.mjs +32 -6
- package/src/commands/longagent.mjs +117 -0
- package/src/commands/mcp.mjs +275 -43
- package/src/commands/permission.mjs +1 -1
- package/src/commands/session.mjs +195 -140
- package/src/commands/skill.mjs +63 -0
- package/src/commands/theme.mjs +1 -1
- package/src/config/defaults.mjs +280 -260
- package/src/config/import-config.mjs +1 -1
- package/src/config/load-config.mjs +61 -4
- package/src/config/schema.mjs +591 -574
- package/src/context.mjs +4 -1
- package/src/core/constants.mjs +97 -91
- package/src/core/types.mjs +1 -1
- package/src/github/api.mjs +78 -78
- package/src/github/auth.mjs +294 -286
- package/src/github/flow.mjs +298 -298
- package/src/github/workspace.mjs +225 -212
- package/src/index.mjs +84 -82
- package/src/knowledge/frontend-aesthetics.txt +38 -38
- package/src/mcp/client-http.mjs +139 -141
- package/src/mcp/client-sse.mjs +297 -288
- package/src/mcp/client-stdio.mjs +534 -533
- package/src/mcp/constants.mjs +2 -2
- package/src/mcp/registry.mjs +498 -479
- package/src/mcp/stdio-framing.mjs +135 -133
- package/src/mcp/tool-result.mjs +24 -24
- package/src/observability/edit-diagnostics.mjs +449 -0
- package/src/observability/index.mjs +42 -42
- package/src/observability/metrics.mjs +165 -137
- package/src/observability/tracer.mjs +137 -137
- package/src/onboarding.mjs +209 -0
- package/src/orchestration/background-manager.mjs +567 -372
- package/src/orchestration/background-worker.mjs +419 -305
- package/src/orchestration/interruption-reason.mjs +21 -0
- package/src/orchestration/longagent-manager.mjs +197 -171
- package/src/orchestration/stage-scheduler.mjs +733 -728
- package/src/orchestration/subagent-router.mjs +7 -1
- package/src/orchestration/task-scheduler.mjs +219 -7
- package/src/permission/engine.mjs +1 -1
- package/src/permission/exec-policy.mjs +370 -370
- package/src/permission/file-edit-policy.mjs +108 -0
- package/src/permission/prompt.mjs +1 -1
- package/src/permission/rules.mjs +116 -7
- package/src/plugin/builtin-hooks/post-edit-format.mjs +2 -1
- package/src/plugin/builtin-hooks/post-edit-typecheck.mjs +104 -40
- package/src/plugin/hook-bus.mjs +19 -5
- package/src/plugin/manifest-loader.mjs +222 -0
- package/src/provider/anthropic.mjs +396 -390
- package/src/provider/ollama.mjs +7 -1
- package/src/provider/openai.mjs +382 -340
- package/src/provider/retry-policy.mjs +74 -68
- package/src/provider/router.mjs +242 -241
- package/src/provider/sse.mjs +104 -104
- package/src/provider/wizard.mjs +556 -0
- package/src/repl/capability-facade.mjs +30 -0
- package/src/repl/command-surface.mjs +23 -0
- package/src/repl/controller-entry.mjs +40 -0
- package/src/repl/core-shell.mjs +208 -0
- package/src/repl/dialog-router.mjs +87 -0
- package/src/repl/input-engine.mjs +76 -0
- package/src/repl/keymap.mjs +7 -0
- package/src/repl/operator-surface.mjs +15 -0
- package/src/repl/permission-flow.mjs +49 -0
- package/src/repl/runtime-facade.mjs +36 -0
- package/src/repl/slash-router.mjs +62 -0
- package/src/repl/state-store.mjs +29 -0
- package/src/repl/turn-controller.mjs +58 -0
- package/src/repl/verification.mjs +23 -0
- package/src/repl.mjs +3368 -2981
- package/src/rules/load-rules.mjs +3 -3
- package/src/runtime.mjs +1 -1
- package/src/session/agent-transaction.mjs +86 -0
- package/src/session/checkpoint.mjs +302 -302
- package/src/session/compaction.mjs +298 -298
- package/src/session/engine.mjs +417 -232
- package/src/session/longagent-4stage.mjs +467 -460
- package/src/session/longagent-hybrid.mjs +1344 -1097
- package/src/session/longagent-plan.mjs +376 -365
- package/src/session/longagent-project-memory.mjs +53 -53
- package/src/session/longagent-scaffold.mjs +291 -291
- package/src/session/longagent-task-bus.mjs +138 -54
- package/src/session/longagent-utils.mjs +828 -472
- package/src/session/longagent.mjs +911 -900
- package/src/session/loop.mjs +1005 -930
- package/src/session/prompt/agent.txt +25 -25
- package/src/session/prompt/anthropic.txt +150 -150
- package/src/session/prompt/beast.txt +1 -1
- package/src/session/prompt/plan.txt +31 -31
- package/src/session/prompt/qwen.txt +46 -46
- package/src/session/recovery.mjs +21 -0
- package/src/session/rollback.mjs +196 -195
- package/src/session/routing-observability.mjs +72 -0
- package/src/session/runtime-state.mjs +47 -0
- package/src/session/store.mjs +523 -519
- package/src/session/system-prompt.mjs +308 -273
- package/src/session/task-validator.mjs +267 -267
- package/src/session/usability-gates.mjs +2 -2
- package/src/skill/builtin/commit.mjs +64 -64
- package/src/skill/builtin/design.mjs +76 -76
- package/src/skill/generator.mjs +18 -2
- package/src/skill/registry.mjs +642 -390
- package/src/storage/audit-store.mjs +18 -11
- package/src/storage/event-log.mjs +7 -1
- package/src/storage/ghost-commit-store.mjs +243 -245
- package/src/storage/paths.mjs +13 -0
- package/src/theme/default-theme.mjs +1 -1
- package/src/theme/markdown.mjs +4 -0
- package/src/theme/schema.mjs +1 -1
- package/src/theme/status-bar.mjs +162 -158
- package/src/tool/audit-wrapper.mjs +18 -2
- package/src/tool/edit-transaction.mjs +23 -0
- package/src/tool/executor.mjs +26 -1
- package/src/tool/file-read-state.mjs +65 -0
- package/src/tool/git-auto.mjs +526 -526
- package/src/tool/git-full-auto.mjs +487 -478
- package/src/tool/mutation-guard.mjs +54 -0
- package/src/tool/prompt/edit.txt +3 -3
- package/src/tool/prompt/multiedit.txt +1 -0
- package/src/tool/prompt/notebookedit.txt +2 -1
- package/src/tool/prompt/patch.txt +25 -24
- package/src/tool/prompt/read.txt +3 -3
- package/src/tool/prompt/sysinfo.txt +29 -0
- package/src/tool/prompt/task.txt +66 -4
- package/src/tool/prompt/write.txt +2 -2
- package/src/tool/question-prompt.mjs +99 -93
- package/src/tool/registry.mjs +1701 -1343
- package/src/tool/task-tool.mjs +14 -6
- package/src/ui/activity-renderer.mjs +667 -664
- package/src/ui/repl-background-panel.mjs +7 -0
- package/src/ui/repl-capability-panel.mjs +9 -0
- package/src/ui/repl-dashboard.mjs +54 -4
- package/src/ui/repl-help.mjs +110 -0
- package/src/ui/repl-operator-panel.mjs +12 -0
- package/src/ui/repl-route-feedback.mjs +35 -0
- package/src/ui/repl-status-view.mjs +76 -0
- package/src/ui/repl-task-panel.mjs +5 -0
- package/src/ui/repl-transcript-panel.mjs +56 -0
- package/src/ui/repl-turn-summary.mjs +135 -0
- package/src/usage/pricing.mjs +122 -121
- package/src/usage/usage-meter.mjs +1 -0
- package/src/util/git.mjs +562 -519
- package/src/util/template.mjs +6 -1
package/src/mcp/client-stdio.mjs
CHANGED
|
@@ -1,533 +1,534 @@
|
|
|
1
|
-
import { spawn } from "node:child_process"
|
|
2
|
-
import { McpError } from "../core/errors.mjs"
|
|
3
|
-
import { EventBus } from "../core/events.mjs"
|
|
4
|
-
import { EVENT_TYPES } from "../core/constants.mjs"
|
|
5
|
-
import { createStdioFramingDecoder, encodeRpcMessage } from "./stdio-framing.mjs"
|
|
6
|
-
import { normalizeToolResult } from "./tool-result.mjs"
|
|
7
|
-
import { MCP_PROTOCOL_VERSION, MCP_CLIENT_INFO } from "./constants.mjs"
|
|
8
|
-
|
|
9
|
-
const VALID_FRAMING = new Set(["auto", "content-length", "newline"])
|
|
10
|
-
const VALID_HEALTH_METHOD = new Set(["auto", "ping", "tools_list"])
|
|
11
|
-
|
|
12
|
-
function normalizeFraming(value) {
|
|
13
|
-
const framing = String(value || "auto").toLowerCase()
|
|
14
|
-
return VALID_FRAMING.has(framing) ? framing : "auto"
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function normalizeHealthMethod(value) {
|
|
18
|
-
const method = String(value || "auto").toLowerCase()
|
|
19
|
-
return VALID_HEALTH_METHOD.has(method) ? method : "auto"
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function classifySpawnError(error) {
|
|
23
|
-
const code = String(error?.code || "").toUpperCase()
|
|
24
|
-
const msg = String(error?.message || error || "")
|
|
25
|
-
if (code === "ENOENT" || code === "EACCES") return "spawn_failed"
|
|
26
|
-
if (msg.includes("ENOENT") || msg.includes("EACCES") || msg.includes("spawn")) return "spawn_failed"
|
|
27
|
-
return "unknown"
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export function createStdioMcpClient(serverName, config = {}) {
|
|
31
|
-
const command = config.command
|
|
32
|
-
const cmdArgs = Array.isArray(config.args) ? config.args : []
|
|
33
|
-
const envOverrides = config.env || {}
|
|
34
|
-
const startupTimeoutMs = Math.max(100, Number(config.startup_timeout_ms || 5000))
|
|
35
|
-
const requestTimeoutMs = Math.max(100, Number(config.request_timeout_ms || config.timeout_ms || 30000))
|
|
36
|
-
const healthCheckMethod = normalizeHealthMethod(config.health_check_method)
|
|
37
|
-
const configuredFraming = normalizeFraming(config.framing)
|
|
38
|
-
const isWindows = process.platform === "win32"
|
|
39
|
-
const explicitShell = config.shell === true || (config.shell !== false && isWindows)
|
|
40
|
-
|
|
41
|
-
let executable
|
|
42
|
-
let spawnArgs
|
|
43
|
-
if (Array.isArray(command)) {
|
|
44
|
-
executable = command[0]
|
|
45
|
-
spawnArgs = command.slice(1)
|
|
46
|
-
} else {
|
|
47
|
-
executable = command
|
|
48
|
-
spawnArgs = cmdArgs
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
if (!executable) {
|
|
52
|
-
throw new McpError(`mcp server "${serverName}" missing command`, {
|
|
53
|
-
reason: "spawn_failed",
|
|
54
|
-
server: serverName,
|
|
55
|
-
phase: "startup"
|
|
56
|
-
})
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const maxReconnectAttempts = Number(config.max_reconnect_attempts ?? 5)
|
|
60
|
-
const circuitResetMs = Number(config.circuit_reset_ms ?? 60000)
|
|
61
|
-
|
|
62
|
-
let child = null
|
|
63
|
-
let lifecycle = "closed"
|
|
64
|
-
let nextId = 1
|
|
65
|
-
let initialized = false
|
|
66
|
-
let activeFraming = configuredFraming === "auto" ? "content-length" : configuredFraming
|
|
67
|
-
let decoder = createStdioFramingDecoder({
|
|
68
|
-
framing: configuredFraming === "auto" ? "auto" : activeFraming
|
|
69
|
-
})
|
|
70
|
-
let malformedSeen = false
|
|
71
|
-
let malformedSnippet = ""
|
|
72
|
-
let stderrLines = []
|
|
73
|
-
let stderrTotalBytes = 0
|
|
74
|
-
let ignoreClose = false
|
|
75
|
-
let reconnectAttempts = 0
|
|
76
|
-
let circuitState = "closed" // "closed" | "open" | "half_open"
|
|
77
|
-
let circuitOpenedAt = 0
|
|
78
|
-
let wasEverInitialized = false
|
|
79
|
-
|
|
80
|
-
const pending = new Map()
|
|
81
|
-
|
|
82
|
-
function resetRuntime() {
|
|
83
|
-
decoder = createStdioFramingDecoder({
|
|
84
|
-
framing: configuredFraming === "auto" ? "auto" : activeFraming
|
|
85
|
-
})
|
|
86
|
-
malformedSeen = false
|
|
87
|
-
malformedSnippet = ""
|
|
88
|
-
stderrLines = []
|
|
89
|
-
stderrTotalBytes = 0
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
function appendStderr(chunk) {
|
|
93
|
-
const text = String(chunk || "").trim()
|
|
94
|
-
if (!text) return
|
|
95
|
-
stderrTotalBytes += Buffer.byteLength(chunk)
|
|
96
|
-
stderrLines.push(text)
|
|
97
|
-
if (stderrLines.length > 32) stderrLines = stderrLines.slice(stderrLines.length - 32)
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
function rejectPending(reason, message, details = {}) {
|
|
101
|
-
for (const [, entry] of pending) {
|
|
102
|
-
clearTimeout(entry.timer)
|
|
103
|
-
entry.reject(
|
|
104
|
-
new McpError(message, {
|
|
105
|
-
reason,
|
|
106
|
-
server: serverName,
|
|
107
|
-
action: entry.method,
|
|
108
|
-
phase: entry.phase || details.phase || "request",
|
|
109
|
-
stderrSnippet: stderrLines.join(" | ") || undefined,
|
|
110
|
-
...details
|
|
111
|
-
})
|
|
112
|
-
)
|
|
113
|
-
}
|
|
114
|
-
pending.clear()
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
function cleanupChild() {
|
|
118
|
-
child = null
|
|
119
|
-
initialized = false
|
|
120
|
-
lifecycle = "closed"
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
async function startProcess() {
|
|
124
|
-
if (child && lifecycle !== "closed") return
|
|
125
|
-
|
|
126
|
-
resetRuntime()
|
|
127
|
-
lifecycle = "starting"
|
|
128
|
-
ignoreClose = false
|
|
129
|
-
|
|
130
|
-
await new Promise((resolve, reject) => {
|
|
131
|
-
let settled = false
|
|
132
|
-
const proc = spawn(executable, spawnArgs, {
|
|
133
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
134
|
-
env: { ...process.env, ...envOverrides },
|
|
135
|
-
windowsHide: true,
|
|
136
|
-
shell: explicitShell
|
|
137
|
-
})
|
|
138
|
-
child = proc
|
|
139
|
-
|
|
140
|
-
const timer = setTimeout(() => {
|
|
141
|
-
if (settled) return
|
|
142
|
-
settled = true
|
|
143
|
-
try { proc.kill() } catch {}
|
|
144
|
-
reject(
|
|
145
|
-
new McpError(`mcp server "${serverName}" startup timeout after ${startupTimeoutMs}ms`, {
|
|
146
|
-
reason: "timeout",
|
|
147
|
-
server: serverName,
|
|
148
|
-
phase: "startup"
|
|
149
|
-
})
|
|
150
|
-
)
|
|
151
|
-
}, startupTimeoutMs)
|
|
152
|
-
|
|
153
|
-
proc.once("spawn", () => {
|
|
154
|
-
if (settled) return
|
|
155
|
-
settled = true
|
|
156
|
-
clearTimeout(timer)
|
|
157
|
-
lifecycle = "running"
|
|
158
|
-
resolve()
|
|
159
|
-
})
|
|
160
|
-
|
|
161
|
-
proc.once("error", (err) => {
|
|
162
|
-
const reason = classifySpawnError(err)
|
|
163
|
-
if (!settled) {
|
|
164
|
-
settled = true
|
|
165
|
-
clearTimeout(timer)
|
|
166
|
-
reject(
|
|
167
|
-
new McpError(`mcp server "${serverName}" process error: ${err.message}`, {
|
|
168
|
-
reason,
|
|
169
|
-
server: serverName,
|
|
170
|
-
phase: "startup"
|
|
171
|
-
})
|
|
172
|
-
)
|
|
173
|
-
} else {
|
|
174
|
-
rejectPending(reason, `mcp server "${serverName}" process error: ${err.message}`, { phase: "request" })
|
|
175
|
-
}
|
|
176
|
-
})
|
|
177
|
-
|
|
178
|
-
proc.stdout.on("data", (chunk) => {
|
|
179
|
-
let payloads = []
|
|
180
|
-
try {
|
|
181
|
-
payloads = decoder.push(chunk)
|
|
182
|
-
} catch (error) {
|
|
183
|
-
malformedSeen = true
|
|
184
|
-
malformedSnippet = String(error.message || "invalid framing").slice(0, 240)
|
|
185
|
-
return
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
for (const payload of payloads) {
|
|
189
|
-
let msg
|
|
190
|
-
try {
|
|
191
|
-
msg = JSON.parse(payload)
|
|
192
|
-
} catch {
|
|
193
|
-
malformedSeen = true
|
|
194
|
-
malformedSnippet = String(payload || "").slice(0, 240)
|
|
195
|
-
continue
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
if (msg?.id != null && pending.has(msg.id)) {
|
|
199
|
-
const entry = pending.get(msg.id)
|
|
200
|
-
pending.delete(msg.id)
|
|
201
|
-
clearTimeout(entry.timer)
|
|
202
|
-
if (msg.error) {
|
|
203
|
-
entry.reject(
|
|
204
|
-
new McpError(
|
|
205
|
-
`mcp server "${serverName}" error: ${msg.error.message || JSON.stringify(msg.error)}`,
|
|
206
|
-
{
|
|
207
|
-
reason: "bad_response",
|
|
208
|
-
server: serverName,
|
|
209
|
-
action: entry.method,
|
|
210
|
-
phase: entry.phase,
|
|
211
|
-
code: msg.error.code,
|
|
212
|
-
stderrSnippet: stderrLines.join(" | ") || undefined
|
|
213
|
-
}
|
|
214
|
-
)
|
|
215
|
-
)
|
|
216
|
-
} else {
|
|
217
|
-
const elapsed = Date.now() - entry.startedAt
|
|
218
|
-
EventBus.emit({
|
|
219
|
-
type: EVENT_TYPES.MCP_REQUEST,
|
|
220
|
-
payload: { server: serverName, action: entry.method, elapsed, transport: "stdio" }
|
|
221
|
-
}).catch(() => {})
|
|
222
|
-
entry.resolve(msg.result ?? {})
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
})
|
|
227
|
-
|
|
228
|
-
proc.stderr.on("data", (chunk) => appendStderr(chunk))
|
|
229
|
-
|
|
230
|
-
proc.on("close", (code, signal) => {
|
|
231
|
-
if (ignoreClose) {
|
|
232
|
-
cleanupChild()
|
|
233
|
-
return
|
|
234
|
-
}
|
|
235
|
-
const reason = malformedSeen ? "bad_response" : "server_crash"
|
|
236
|
-
const extra = malformedSeen && malformedSnippet
|
|
237
|
-
? `; malformed stdout: ${malformedSnippet}`
|
|
238
|
-
: ""
|
|
239
|
-
rejectPending(
|
|
240
|
-
reason,
|
|
241
|
-
`mcp server "${serverName}" process exited unexpectedly (code=${code ?? "null"}, signal=${signal || "null"})${extra}`,
|
|
242
|
-
{ phase: lifecycle === "starting" ? "startup" : "request" }
|
|
243
|
-
)
|
|
244
|
-
cleanupChild()
|
|
245
|
-
})
|
|
246
|
-
})
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
const shutdownTimeoutMs = Number(config.shutdown_timeout_ms || 5000)
|
|
250
|
-
|
|
251
|
-
async function shutdownProcess() {
|
|
252
|
-
if (!child) return
|
|
253
|
-
const proc = child
|
|
254
|
-
lifecycle = "stopping"
|
|
255
|
-
ignoreClose = true
|
|
256
|
-
try { proc.kill() } catch {}
|
|
257
|
-
rejectPending("unknown", `mcp server "${serverName}" shutdown`, { phase: "shutdown" })
|
|
258
|
-
await new Promise((resolve) => {
|
|
259
|
-
const killTimer = setTimeout(() => {
|
|
260
|
-
try { proc.kill("SIGKILL") } catch {}
|
|
261
|
-
resolve()
|
|
262
|
-
}, shutdownTimeoutMs)
|
|
263
|
-
proc.once("close", () => {
|
|
264
|
-
clearTimeout(killTimer)
|
|
265
|
-
resolve()
|
|
266
|
-
})
|
|
267
|
-
})
|
|
268
|
-
cleanupChild()
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
async function ensureAlive() {
|
|
272
|
-
// Circuit breaker: open state rejects immediately
|
|
273
|
-
if (circuitState === "open") {
|
|
274
|
-
if (Date.now() - circuitOpenedAt >= circuitResetMs) {
|
|
275
|
-
circuitState = "half_open"
|
|
276
|
-
} else {
|
|
277
|
-
throw new McpError(`mcp server "${serverName}" circuit breaker open`, {
|
|
278
|
-
reason: "server_crash", server: serverName, phase: "request"
|
|
279
|
-
})
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
// Only attempt lazy reconnect if we were previously initialized
|
|
284
|
-
if ((lifecycle === "closed" || lifecycle === "stopping") && wasEverInitialized) {
|
|
285
|
-
const delay = Math.min(1000 * Math.pow(2, reconnectAttempts), 30000)
|
|
286
|
-
if (reconnectAttempts > 0) {
|
|
287
|
-
await new Promise((r) => setTimeout(r, delay))
|
|
288
|
-
}
|
|
289
|
-
try {
|
|
290
|
-
initialized = false
|
|
291
|
-
await startProcess()
|
|
292
|
-
await initializeOnce()
|
|
293
|
-
reconnectAttempts = 0
|
|
294
|
-
if (circuitState === "half_open") {
|
|
295
|
-
circuitState = "closed"
|
|
296
|
-
EventBus.emit({ type: EVENT_TYPES.MCP_CIRCUIT_CLOSE, payload: { server: serverName } }).catch(() => {})
|
|
297
|
-
}
|
|
298
|
-
EventBus.emit({ type: EVENT_TYPES.MCP_RECONNECT, payload: { server: serverName, success: true } }).catch(() => {})
|
|
299
|
-
} catch (error) {
|
|
300
|
-
reconnectAttempts++
|
|
301
|
-
if (circuitState === "half_open" || reconnectAttempts >= maxReconnectAttempts) {
|
|
302
|
-
circuitState = "open"
|
|
303
|
-
circuitOpenedAt = Date.now()
|
|
304
|
-
EventBus.emit({ type: EVENT_TYPES.MCP_CIRCUIT_OPEN, payload: { server: serverName, attempts: reconnectAttempts } }).catch(() => {})
|
|
305
|
-
}
|
|
306
|
-
EventBus.emit({ type: EVENT_TYPES.MCP_RECONNECT, payload: { server: serverName, success: false, attempt: reconnectAttempts } }).catch(() => {})
|
|
307
|
-
throw error
|
|
308
|
-
}
|
|
309
|
-
return
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
// Normal first-time startup
|
|
313
|
-
await startProcess()
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
async function sendRequest(method, params = {}, { phase = "request", timeoutMs = requestTimeoutMs, signal = null } = {}) {
|
|
317
|
-
if (signal?.aborted) {
|
|
318
|
-
throw new McpError(`mcp server "${serverName}" request cancelled`, {
|
|
319
|
-
reason: "timeout", server: serverName, action: method, phase
|
|
320
|
-
})
|
|
321
|
-
}
|
|
322
|
-
await ensureAlive()
|
|
323
|
-
if (nextId > Number.MAX_SAFE_INTEGER - 1) nextId = 1
|
|
324
|
-
const id = nextId++
|
|
325
|
-
const payload = { jsonrpc: "2.0", id, method, params }
|
|
326
|
-
|
|
327
|
-
return new Promise((resolve, reject) => {
|
|
328
|
-
const startedAt = Date.now()
|
|
329
|
-
let settled = false
|
|
330
|
-
|
|
331
|
-
function settle() {
|
|
332
|
-
if (settled) return false
|
|
333
|
-
settled = true
|
|
334
|
-
if (signal) signal.removeEventListener("abort", onAbort)
|
|
335
|
-
return true
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
function onAbort() {
|
|
339
|
-
if (!settle()) return
|
|
340
|
-
clearTimeout(timer)
|
|
341
|
-
pending.delete(id)
|
|
342
|
-
sendNotification("notifications/cancelled", { requestId: id, reason: "client_cancelled" })
|
|
343
|
-
reject(new McpError(`mcp server "${serverName}" request cancelled`, {
|
|
344
|
-
reason: "timeout", server: serverName, action: method, phase
|
|
345
|
-
}))
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
const timer = setTimeout(() => {
|
|
349
|
-
if (!settle()) return
|
|
350
|
-
pending.delete(id)
|
|
351
|
-
reject(
|
|
352
|
-
new McpError(`mcp server "${serverName}" timed out after ${timeoutMs}ms on "${method}"`, {
|
|
353
|
-
reason: "timeout",
|
|
354
|
-
server: serverName,
|
|
355
|
-
action: method,
|
|
356
|
-
phase
|
|
357
|
-
})
|
|
358
|
-
)
|
|
359
|
-
}, timeoutMs)
|
|
360
|
-
|
|
361
|
-
if (signal) signal.addEventListener("abort", onAbort, { once: true })
|
|
362
|
-
|
|
363
|
-
pending.set(id, {
|
|
364
|
-
resolve: (v) => { if (settle()) { clearTimeout(timer); resolve(v) } },
|
|
365
|
-
reject: (e) => { if (settle()) { clearTimeout(timer); reject(e) } },
|
|
366
|
-
timer, method, phase, startedAt
|
|
367
|
-
})
|
|
368
|
-
try {
|
|
369
|
-
const wireFraming = configuredFraming === "auto" ? activeFraming : configuredFraming
|
|
370
|
-
child.stdin.write(encodeRpcMessage(payload, wireFraming))
|
|
371
|
-
} catch (error) {
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
let
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
initialized
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
initialized
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
await
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
}
|
|
1
|
+
import { spawn } from "node:child_process"
|
|
2
|
+
import { McpError } from "../core/errors.mjs"
|
|
3
|
+
import { EventBus } from "../core/events.mjs"
|
|
4
|
+
import { EVENT_TYPES } from "../core/constants.mjs"
|
|
5
|
+
import { createStdioFramingDecoder, encodeRpcMessage } from "./stdio-framing.mjs"
|
|
6
|
+
import { normalizeToolResult } from "./tool-result.mjs"
|
|
7
|
+
import { MCP_PROTOCOL_VERSION, MCP_CLIENT_INFO } from "./constants.mjs"
|
|
8
|
+
|
|
9
|
+
const VALID_FRAMING = new Set(["auto", "content-length", "newline"])
|
|
10
|
+
const VALID_HEALTH_METHOD = new Set(["auto", "ping", "tools_list"])
|
|
11
|
+
|
|
12
|
+
function normalizeFraming(value) {
|
|
13
|
+
const framing = String(value || "auto").toLowerCase()
|
|
14
|
+
return VALID_FRAMING.has(framing) ? framing : "auto"
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function normalizeHealthMethod(value) {
|
|
18
|
+
const method = String(value || "auto").toLowerCase()
|
|
19
|
+
return VALID_HEALTH_METHOD.has(method) ? method : "auto"
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function classifySpawnError(error) {
|
|
23
|
+
const code = String(error?.code || "").toUpperCase()
|
|
24
|
+
const msg = String(error?.message || error || "")
|
|
25
|
+
if (code === "ENOENT" || code === "EACCES") return "spawn_failed"
|
|
26
|
+
if (msg.includes("ENOENT") || msg.includes("EACCES") || msg.includes("spawn")) return "spawn_failed"
|
|
27
|
+
return "unknown"
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function createStdioMcpClient(serverName, config = {}) {
|
|
31
|
+
const command = config.command
|
|
32
|
+
const cmdArgs = Array.isArray(config.args) ? config.args : []
|
|
33
|
+
const envOverrides = config.env || {}
|
|
34
|
+
const startupTimeoutMs = Math.max(100, Number(config.startup_timeout_ms || 5000))
|
|
35
|
+
const requestTimeoutMs = Math.max(100, Number(config.request_timeout_ms || config.timeout_ms || 30000))
|
|
36
|
+
const healthCheckMethod = normalizeHealthMethod(config.health_check_method)
|
|
37
|
+
const configuredFraming = normalizeFraming(config.framing)
|
|
38
|
+
const isWindows = process.platform === "win32"
|
|
39
|
+
const explicitShell = config.shell === true || (config.shell !== false && isWindows)
|
|
40
|
+
|
|
41
|
+
let executable
|
|
42
|
+
let spawnArgs
|
|
43
|
+
if (Array.isArray(command)) {
|
|
44
|
+
executable = command[0]
|
|
45
|
+
spawnArgs = command.slice(1)
|
|
46
|
+
} else {
|
|
47
|
+
executable = command
|
|
48
|
+
spawnArgs = cmdArgs
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (!executable) {
|
|
52
|
+
throw new McpError(`mcp server "${serverName}" missing command`, {
|
|
53
|
+
reason: "spawn_failed",
|
|
54
|
+
server: serverName,
|
|
55
|
+
phase: "startup"
|
|
56
|
+
})
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const maxReconnectAttempts = Number(config.max_reconnect_attempts ?? 5)
|
|
60
|
+
const circuitResetMs = Number(config.circuit_reset_ms ?? 60000)
|
|
61
|
+
|
|
62
|
+
let child = null
|
|
63
|
+
let lifecycle = "closed"
|
|
64
|
+
let nextId = 1
|
|
65
|
+
let initialized = false
|
|
66
|
+
let activeFraming = configuredFraming === "auto" ? "content-length" : configuredFraming
|
|
67
|
+
let decoder = createStdioFramingDecoder({
|
|
68
|
+
framing: configuredFraming === "auto" ? "auto" : activeFraming
|
|
69
|
+
})
|
|
70
|
+
let malformedSeen = false
|
|
71
|
+
let malformedSnippet = ""
|
|
72
|
+
let stderrLines = []
|
|
73
|
+
let stderrTotalBytes = 0
|
|
74
|
+
let ignoreClose = false
|
|
75
|
+
let reconnectAttempts = 0
|
|
76
|
+
let circuitState = "closed" // "closed" | "open" | "half_open"
|
|
77
|
+
let circuitOpenedAt = 0
|
|
78
|
+
let wasEverInitialized = false
|
|
79
|
+
|
|
80
|
+
const pending = new Map()
|
|
81
|
+
|
|
82
|
+
function resetRuntime() {
|
|
83
|
+
decoder = createStdioFramingDecoder({
|
|
84
|
+
framing: configuredFraming === "auto" ? "auto" : activeFraming
|
|
85
|
+
})
|
|
86
|
+
malformedSeen = false
|
|
87
|
+
malformedSnippet = ""
|
|
88
|
+
stderrLines = []
|
|
89
|
+
stderrTotalBytes = 0
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function appendStderr(chunk) {
|
|
93
|
+
const text = String(chunk || "").trim()
|
|
94
|
+
if (!text) return
|
|
95
|
+
stderrTotalBytes += Buffer.byteLength(chunk)
|
|
96
|
+
stderrLines.push(text)
|
|
97
|
+
if (stderrLines.length > 32) stderrLines = stderrLines.slice(stderrLines.length - 32)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function rejectPending(reason, message, details = {}) {
|
|
101
|
+
for (const [, entry] of pending) {
|
|
102
|
+
clearTimeout(entry.timer)
|
|
103
|
+
entry.reject(
|
|
104
|
+
new McpError(message, {
|
|
105
|
+
reason,
|
|
106
|
+
server: serverName,
|
|
107
|
+
action: entry.method,
|
|
108
|
+
phase: entry.phase || details.phase || "request",
|
|
109
|
+
stderrSnippet: stderrLines.join(" | ") || undefined,
|
|
110
|
+
...details
|
|
111
|
+
})
|
|
112
|
+
)
|
|
113
|
+
}
|
|
114
|
+
pending.clear()
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function cleanupChild() {
|
|
118
|
+
child = null
|
|
119
|
+
initialized = false
|
|
120
|
+
lifecycle = "closed"
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function startProcess() {
|
|
124
|
+
if (child && lifecycle !== "closed") return
|
|
125
|
+
|
|
126
|
+
resetRuntime()
|
|
127
|
+
lifecycle = "starting"
|
|
128
|
+
ignoreClose = false
|
|
129
|
+
|
|
130
|
+
await new Promise((resolve, reject) => {
|
|
131
|
+
let settled = false
|
|
132
|
+
const proc = spawn(executable, spawnArgs, {
|
|
133
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
134
|
+
env: { ...process.env, ...envOverrides },
|
|
135
|
+
windowsHide: true,
|
|
136
|
+
shell: explicitShell
|
|
137
|
+
})
|
|
138
|
+
child = proc
|
|
139
|
+
|
|
140
|
+
const timer = setTimeout(() => {
|
|
141
|
+
if (settled) return
|
|
142
|
+
settled = true
|
|
143
|
+
try { proc.kill() } catch {}
|
|
144
|
+
reject(
|
|
145
|
+
new McpError(`mcp server "${serverName}" startup timeout after ${startupTimeoutMs}ms`, {
|
|
146
|
+
reason: "timeout",
|
|
147
|
+
server: serverName,
|
|
148
|
+
phase: "startup"
|
|
149
|
+
})
|
|
150
|
+
)
|
|
151
|
+
}, startupTimeoutMs)
|
|
152
|
+
|
|
153
|
+
proc.once("spawn", () => {
|
|
154
|
+
if (settled) return
|
|
155
|
+
settled = true
|
|
156
|
+
clearTimeout(timer)
|
|
157
|
+
lifecycle = "running"
|
|
158
|
+
resolve()
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
proc.once("error", (err) => {
|
|
162
|
+
const reason = classifySpawnError(err)
|
|
163
|
+
if (!settled) {
|
|
164
|
+
settled = true
|
|
165
|
+
clearTimeout(timer)
|
|
166
|
+
reject(
|
|
167
|
+
new McpError(`mcp server "${serverName}" process error: ${err.message}`, {
|
|
168
|
+
reason,
|
|
169
|
+
server: serverName,
|
|
170
|
+
phase: "startup"
|
|
171
|
+
})
|
|
172
|
+
)
|
|
173
|
+
} else {
|
|
174
|
+
rejectPending(reason, `mcp server "${serverName}" process error: ${err.message}`, { phase: "request" })
|
|
175
|
+
}
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
proc.stdout.on("data", (chunk) => {
|
|
179
|
+
let payloads = []
|
|
180
|
+
try {
|
|
181
|
+
payloads = decoder.push(chunk)
|
|
182
|
+
} catch (error) {
|
|
183
|
+
malformedSeen = true
|
|
184
|
+
malformedSnippet = String(error.message || "invalid framing").slice(0, 240)
|
|
185
|
+
return
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
for (const payload of payloads) {
|
|
189
|
+
let msg
|
|
190
|
+
try {
|
|
191
|
+
msg = JSON.parse(payload)
|
|
192
|
+
} catch {
|
|
193
|
+
malformedSeen = true
|
|
194
|
+
malformedSnippet = String(payload || "").slice(0, 240)
|
|
195
|
+
continue
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (msg?.id != null && pending.has(msg.id)) {
|
|
199
|
+
const entry = pending.get(msg.id)
|
|
200
|
+
pending.delete(msg.id)
|
|
201
|
+
clearTimeout(entry.timer)
|
|
202
|
+
if (msg.error) {
|
|
203
|
+
entry.reject(
|
|
204
|
+
new McpError(
|
|
205
|
+
`mcp server "${serverName}" error: ${msg.error.message || JSON.stringify(msg.error)}`,
|
|
206
|
+
{
|
|
207
|
+
reason: "bad_response",
|
|
208
|
+
server: serverName,
|
|
209
|
+
action: entry.method,
|
|
210
|
+
phase: entry.phase,
|
|
211
|
+
code: msg.error.code,
|
|
212
|
+
stderrSnippet: stderrLines.join(" | ") || undefined
|
|
213
|
+
}
|
|
214
|
+
)
|
|
215
|
+
)
|
|
216
|
+
} else {
|
|
217
|
+
const elapsed = Date.now() - entry.startedAt
|
|
218
|
+
EventBus.emit({
|
|
219
|
+
type: EVENT_TYPES.MCP_REQUEST,
|
|
220
|
+
payload: { server: serverName, action: entry.method, elapsed, transport: "stdio" }
|
|
221
|
+
}).catch(() => {})
|
|
222
|
+
entry.resolve(msg.result ?? {})
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
proc.stderr.on("data", (chunk) => appendStderr(chunk))
|
|
229
|
+
|
|
230
|
+
proc.on("close", (code, signal) => {
|
|
231
|
+
if (ignoreClose) {
|
|
232
|
+
cleanupChild()
|
|
233
|
+
return
|
|
234
|
+
}
|
|
235
|
+
const reason = malformedSeen ? "bad_response" : "server_crash"
|
|
236
|
+
const extra = malformedSeen && malformedSnippet
|
|
237
|
+
? `; malformed stdout: ${malformedSnippet}`
|
|
238
|
+
: ""
|
|
239
|
+
rejectPending(
|
|
240
|
+
reason,
|
|
241
|
+
`mcp server "${serverName}" process exited unexpectedly (code=${code ?? "null"}, signal=${signal || "null"})${extra}`,
|
|
242
|
+
{ phase: lifecycle === "starting" ? "startup" : "request" }
|
|
243
|
+
)
|
|
244
|
+
cleanupChild()
|
|
245
|
+
})
|
|
246
|
+
})
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const shutdownTimeoutMs = Number(config.shutdown_timeout_ms || 5000)
|
|
250
|
+
|
|
251
|
+
async function shutdownProcess() {
|
|
252
|
+
if (!child) return
|
|
253
|
+
const proc = child
|
|
254
|
+
lifecycle = "stopping"
|
|
255
|
+
ignoreClose = true
|
|
256
|
+
try { proc.kill() } catch {}
|
|
257
|
+
rejectPending("unknown", `mcp server "${serverName}" shutdown`, { phase: "shutdown" })
|
|
258
|
+
await new Promise((resolve) => {
|
|
259
|
+
const killTimer = setTimeout(() => {
|
|
260
|
+
try { proc.kill("SIGKILL") } catch {}
|
|
261
|
+
resolve()
|
|
262
|
+
}, shutdownTimeoutMs)
|
|
263
|
+
proc.once("close", () => {
|
|
264
|
+
clearTimeout(killTimer)
|
|
265
|
+
resolve()
|
|
266
|
+
})
|
|
267
|
+
})
|
|
268
|
+
cleanupChild()
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
async function ensureAlive() {
|
|
272
|
+
// Circuit breaker: open state rejects immediately
|
|
273
|
+
if (circuitState === "open") {
|
|
274
|
+
if (Date.now() - circuitOpenedAt >= circuitResetMs) {
|
|
275
|
+
circuitState = "half_open"
|
|
276
|
+
} else {
|
|
277
|
+
throw new McpError(`mcp server "${serverName}" circuit breaker open`, {
|
|
278
|
+
reason: "server_crash", server: serverName, phase: "request"
|
|
279
|
+
})
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Only attempt lazy reconnect if we were previously initialized
|
|
284
|
+
if ((lifecycle === "closed" || lifecycle === "stopping") && wasEverInitialized) {
|
|
285
|
+
const delay = Math.min(1000 * Math.pow(2, reconnectAttempts), 30000)
|
|
286
|
+
if (reconnectAttempts > 0) {
|
|
287
|
+
await new Promise((r) => setTimeout(r, delay))
|
|
288
|
+
}
|
|
289
|
+
try {
|
|
290
|
+
initialized = false
|
|
291
|
+
await startProcess()
|
|
292
|
+
await initializeOnce()
|
|
293
|
+
reconnectAttempts = 0
|
|
294
|
+
if (circuitState === "half_open") {
|
|
295
|
+
circuitState = "closed"
|
|
296
|
+
EventBus.emit({ type: EVENT_TYPES.MCP_CIRCUIT_CLOSE, payload: { server: serverName } }).catch(() => {})
|
|
297
|
+
}
|
|
298
|
+
EventBus.emit({ type: EVENT_TYPES.MCP_RECONNECT, payload: { server: serverName, success: true } }).catch(() => {})
|
|
299
|
+
} catch (error) {
|
|
300
|
+
reconnectAttempts++
|
|
301
|
+
if (circuitState === "half_open" || reconnectAttempts >= maxReconnectAttempts) {
|
|
302
|
+
circuitState = "open"
|
|
303
|
+
circuitOpenedAt = Date.now()
|
|
304
|
+
EventBus.emit({ type: EVENT_TYPES.MCP_CIRCUIT_OPEN, payload: { server: serverName, attempts: reconnectAttempts } }).catch(() => {})
|
|
305
|
+
}
|
|
306
|
+
EventBus.emit({ type: EVENT_TYPES.MCP_RECONNECT, payload: { server: serverName, success: false, attempt: reconnectAttempts } }).catch(() => {})
|
|
307
|
+
throw error
|
|
308
|
+
}
|
|
309
|
+
return
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Normal first-time startup
|
|
313
|
+
await startProcess()
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
async function sendRequest(method, params = {}, { phase = "request", timeoutMs = requestTimeoutMs, signal = null } = {}) {
|
|
317
|
+
if (signal?.aborted) {
|
|
318
|
+
throw new McpError(`mcp server "${serverName}" request cancelled`, {
|
|
319
|
+
reason: "timeout", server: serverName, action: method, phase
|
|
320
|
+
})
|
|
321
|
+
}
|
|
322
|
+
await ensureAlive()
|
|
323
|
+
if (nextId > Number.MAX_SAFE_INTEGER - 1) nextId = 1
|
|
324
|
+
const id = nextId++
|
|
325
|
+
const payload = { jsonrpc: "2.0", id, method, params }
|
|
326
|
+
|
|
327
|
+
return new Promise((resolve, reject) => {
|
|
328
|
+
const startedAt = Date.now()
|
|
329
|
+
let settled = false
|
|
330
|
+
|
|
331
|
+
function settle() {
|
|
332
|
+
if (settled) return false
|
|
333
|
+
settled = true
|
|
334
|
+
if (signal) signal.removeEventListener("abort", onAbort)
|
|
335
|
+
return true
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function onAbort() {
|
|
339
|
+
if (!settle()) return
|
|
340
|
+
clearTimeout(timer)
|
|
341
|
+
pending.delete(id)
|
|
342
|
+
sendNotification("notifications/cancelled", { requestId: id, reason: "client_cancelled" })
|
|
343
|
+
reject(new McpError(`mcp server "${serverName}" request cancelled`, {
|
|
344
|
+
reason: "timeout", server: serverName, action: method, phase
|
|
345
|
+
}))
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const timer = setTimeout(() => {
|
|
349
|
+
if (!settle()) return
|
|
350
|
+
pending.delete(id)
|
|
351
|
+
reject(
|
|
352
|
+
new McpError(`mcp server "${serverName}" timed out after ${timeoutMs}ms on "${method}"`, {
|
|
353
|
+
reason: "timeout",
|
|
354
|
+
server: serverName,
|
|
355
|
+
action: method,
|
|
356
|
+
phase
|
|
357
|
+
})
|
|
358
|
+
)
|
|
359
|
+
}, timeoutMs)
|
|
360
|
+
|
|
361
|
+
if (signal) signal.addEventListener("abort", onAbort, { once: true })
|
|
362
|
+
|
|
363
|
+
pending.set(id, {
|
|
364
|
+
resolve: (v) => { if (settle()) { clearTimeout(timer); resolve(v) } },
|
|
365
|
+
reject: (e) => { if (settle()) { clearTimeout(timer); reject(e) } },
|
|
366
|
+
timer, method, phase, startedAt
|
|
367
|
+
})
|
|
368
|
+
try {
|
|
369
|
+
const wireFraming = configuredFraming === "auto" ? activeFraming : configuredFraming
|
|
370
|
+
child.stdin.write(encodeRpcMessage(payload, wireFraming))
|
|
371
|
+
} catch (error) {
|
|
372
|
+
if (!settle()) return
|
|
373
|
+
clearTimeout(timer)
|
|
374
|
+
pending.delete(id)
|
|
375
|
+
reject(
|
|
376
|
+
new McpError(`mcp server "${serverName}" stdin write failed: ${error.message}`, {
|
|
377
|
+
reason: "server_crash",
|
|
378
|
+
server: serverName,
|
|
379
|
+
action: method,
|
|
380
|
+
phase
|
|
381
|
+
})
|
|
382
|
+
)
|
|
383
|
+
}
|
|
384
|
+
})
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function sendNotification(method, params = {}) {
|
|
388
|
+
if (!child || lifecycle === "closed") return
|
|
389
|
+
const payload = { jsonrpc: "2.0", method, params }
|
|
390
|
+
try {
|
|
391
|
+
const wireFraming = configuredFraming === "auto" ? activeFraming : configuredFraming
|
|
392
|
+
child.stdin.write(encodeRpcMessage(payload, wireFraming))
|
|
393
|
+
} catch {
|
|
394
|
+
// best effort
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
async function initializeOnce() {
|
|
399
|
+
if (initialized) return
|
|
400
|
+
const initParams = {
|
|
401
|
+
protocolVersion: MCP_PROTOCOL_VERSION,
|
|
402
|
+
capabilities: {},
|
|
403
|
+
clientInfo: MCP_CLIENT_INFO
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
if (configuredFraming === "auto") {
|
|
407
|
+
let lastError = null
|
|
408
|
+
let needRestart = false
|
|
409
|
+
for (const candidate of ["content-length", "newline"]) {
|
|
410
|
+
activeFraming = candidate
|
|
411
|
+
if (needRestart) {
|
|
412
|
+
await shutdownProcess()
|
|
413
|
+
}
|
|
414
|
+
try {
|
|
415
|
+
decoder.reset()
|
|
416
|
+
await sendRequest("initialize", initParams, { phase: "initialize" })
|
|
417
|
+
sendNotification("notifications/initialized")
|
|
418
|
+
initialized = true
|
|
419
|
+
wasEverInitialized = true
|
|
420
|
+
return
|
|
421
|
+
} catch (error) {
|
|
422
|
+
lastError = error
|
|
423
|
+
needRestart = true
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
throw lastError || new McpError(`mcp server "${serverName}" failed to initialize`, {
|
|
427
|
+
reason: "unknown",
|
|
428
|
+
server: serverName,
|
|
429
|
+
phase: "initialize"
|
|
430
|
+
})
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
await sendRequest("initialize", initParams, { phase: "initialize" })
|
|
434
|
+
sendNotification("notifications/initialized")
|
|
435
|
+
initialized = true
|
|
436
|
+
wasEverInitialized = true
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
async function healthPingOrTools() {
|
|
440
|
+
if (healthCheckMethod === "ping") {
|
|
441
|
+
await sendRequest("ping", {}, { phase: "request" })
|
|
442
|
+
return
|
|
443
|
+
}
|
|
444
|
+
if (healthCheckMethod === "tools_list") {
|
|
445
|
+
await sendRequest("tools/list", {}, { phase: "request" })
|
|
446
|
+
return
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
try {
|
|
450
|
+
await sendRequest("ping", {}, { phase: "request" })
|
|
451
|
+
} catch (error) {
|
|
452
|
+
if (!["bad_response", "protocol_error", "unknown"].includes(error.reason)) throw error
|
|
453
|
+
await sendRequest("tools/list", {}, { phase: "request" })
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
return {
|
|
458
|
+
serverName,
|
|
459
|
+
transport: "stdio",
|
|
460
|
+
|
|
461
|
+
async health() {
|
|
462
|
+
try {
|
|
463
|
+
await initializeOnce()
|
|
464
|
+
await healthPingOrTools()
|
|
465
|
+
return {
|
|
466
|
+
ok: true,
|
|
467
|
+
reason: "ok",
|
|
468
|
+
framing: configuredFraming === "auto" ? activeFraming : configuredFraming
|
|
469
|
+
}
|
|
470
|
+
} catch (error) {
|
|
471
|
+
return {
|
|
472
|
+
ok: false,
|
|
473
|
+
error: error.message,
|
|
474
|
+
reason: error.reason || "unknown",
|
|
475
|
+
phase: error.details?.phase || "unknown",
|
|
476
|
+
framing: configuredFraming === "auto" ? activeFraming : configuredFraming
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
},
|
|
480
|
+
|
|
481
|
+
async listTools() {
|
|
482
|
+
await initializeOnce()
|
|
483
|
+
const out = await sendRequest("tools/list")
|
|
484
|
+
return Array.isArray(out?.tools) ? out.tools : []
|
|
485
|
+
},
|
|
486
|
+
|
|
487
|
+
async listPrompts() {
|
|
488
|
+
await initializeOnce()
|
|
489
|
+
try {
|
|
490
|
+
const out = await sendRequest("prompts/list")
|
|
491
|
+
return Array.isArray(out?.prompts) ? out.prompts : []
|
|
492
|
+
} catch {
|
|
493
|
+
return []
|
|
494
|
+
}
|
|
495
|
+
},
|
|
496
|
+
|
|
497
|
+
async getPrompt(name, args = {}) {
|
|
498
|
+
await initializeOnce()
|
|
499
|
+
return sendRequest("prompts/get", { name, arguments: args })
|
|
500
|
+
},
|
|
501
|
+
|
|
502
|
+
async listResources() {
|
|
503
|
+
await initializeOnce()
|
|
504
|
+
try {
|
|
505
|
+
const out = await sendRequest("resources/list")
|
|
506
|
+
return Array.isArray(out?.resources) ? out.resources : []
|
|
507
|
+
} catch {
|
|
508
|
+
return []
|
|
509
|
+
}
|
|
510
|
+
},
|
|
511
|
+
|
|
512
|
+
async listTemplates() {
|
|
513
|
+
await initializeOnce()
|
|
514
|
+
try {
|
|
515
|
+
const out = await sendRequest("resources/templates/list")
|
|
516
|
+
return Array.isArray(out?.templates) ? out.templates : []
|
|
517
|
+
} catch {
|
|
518
|
+
return []
|
|
519
|
+
}
|
|
520
|
+
},
|
|
521
|
+
|
|
522
|
+
async callTool(name, args = {}, signal = null) {
|
|
523
|
+
await initializeOnce()
|
|
524
|
+
const result = await sendRequest("tools/call", { name, arguments: args }, { signal })
|
|
525
|
+
return normalizeToolResult(result, serverName, name)
|
|
526
|
+
},
|
|
527
|
+
|
|
528
|
+
shutdown() {
|
|
529
|
+
sendNotification("notifications/cancelled", { reason: "shutdown" })
|
|
530
|
+
rejectPending("shutdown", `mcp server "${serverName}" shutdown`, { phase: "shutdown" })
|
|
531
|
+
shutdownProcess().catch(() => {})
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
}
|