@kkelly-offical/kkcode 0.1.3 → 0.1.7
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/README.md +110 -172
- package/package.json +46 -46
- package/src/agent/agent.mjs +220 -170
- package/src/agent/prompt/bug-hunter.txt +90 -0
- package/src/agent/prompt/frontend-designer.txt +58 -0
- package/src/agent/prompt/longagent-blueprint-agent.txt +83 -0
- package/src/agent/prompt/longagent-coding-agent.txt +37 -0
- package/src/agent/prompt/longagent-debugging-agent.txt +46 -0
- package/src/agent/prompt/longagent-preview-agent.txt +63 -0
- package/src/config/defaults.mjs +260 -195
- package/src/config/schema.mjs +71 -6
- package/src/core/constants.mjs +91 -46
- package/src/index.mjs +1 -1
- package/src/knowledge/frontend-aesthetics.txt +39 -0
- package/src/knowledge/loader.mjs +2 -1
- package/src/knowledge/tailwind.txt +12 -3
- package/src/mcp/client-http.mjs +141 -157
- package/src/mcp/client-sse.mjs +288 -286
- package/src/mcp/client-stdio.mjs +533 -451
- package/src/mcp/constants.mjs +2 -0
- package/src/mcp/registry.mjs +479 -394
- package/src/mcp/stdio-framing.mjs +133 -127
- package/src/mcp/tool-result.mjs +24 -0
- package/src/observability/index.mjs +42 -0
- package/src/observability/metrics.mjs +137 -0
- package/src/observability/tracer.mjs +137 -0
- package/src/orchestration/background-manager.mjs +372 -358
- package/src/orchestration/background-worker.mjs +305 -245
- package/src/orchestration/longagent-manager.mjs +171 -116
- package/src/orchestration/stage-scheduler.mjs +728 -489
- package/src/permission/exec-policy.mjs +9 -11
- package/src/provider/anthropic.mjs +1 -0
- package/src/provider/openai.mjs +340 -339
- package/src/provider/retry-policy.mjs +68 -68
- package/src/provider/router.mjs +241 -228
- package/src/provider/sse.mjs +104 -91
- package/src/repl.mjs +59 -7
- package/src/session/checkpoint.mjs +66 -3
- package/src/session/compaction.mjs +298 -276
- package/src/session/engine.mjs +232 -225
- package/src/session/longagent-4stage.mjs +460 -0
- package/src/session/longagent-hybrid.mjs +1097 -0
- package/src/session/longagent-plan.mjs +365 -329
- package/src/session/longagent-project-memory.mjs +53 -0
- package/src/session/longagent-scaffold.mjs +291 -100
- package/src/session/longagent-task-bus.mjs +54 -0
- package/src/session/longagent-utils.mjs +472 -0
- package/src/session/longagent.mjs +900 -1462
- package/src/session/loop.mjs +65 -40
- package/src/session/project-context.mjs +30 -0
- package/src/session/prompt/agent.txt +25 -0
- package/src/session/prompt/plan.txt +31 -9
- package/src/session/rollback.mjs +196 -0
- package/src/session/store.mjs +519 -503
- package/src/session/system-prompt.mjs +273 -260
- package/src/session/task-validator.mjs +4 -3
- package/src/skill/builtin/design.mjs +76 -0
- package/src/skill/builtin/frontend.mjs +8 -0
- package/src/skill/registry.mjs +390 -336
- package/src/storage/ghost-commit-store.mjs +18 -8
- package/src/tool/executor.mjs +11 -0
- package/src/tool/git-auto.mjs +0 -19
- package/src/tool/question-prompt.mjs +93 -86
- package/src/tool/registry.mjs +71 -37
- package/src/ui/activity-renderer.mjs +664 -410
- package/src/util/git.mjs +23 -0
package/src/mcp/registry.mjs
CHANGED
|
@@ -1,394 +1,479 @@
|
|
|
1
|
-
import { createHttpMcpClient } from "./client-http.mjs"
|
|
2
|
-
import { createStdioMcpClient } from "./client-stdio.mjs"
|
|
3
|
-
import { createSseMcpClient } from "./client-sse.mjs"
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
join(
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
if (
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
state.
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
1
|
+
import { createHttpMcpClient } from "./client-http.mjs"
|
|
2
|
+
import { createStdioMcpClient } from "./client-stdio.mjs"
|
|
3
|
+
import { createSseMcpClient } from "./client-sse.mjs"
|
|
4
|
+
import { McpError } from "../core/errors.mjs"
|
|
5
|
+
import { EventBus } from "../core/events.mjs"
|
|
6
|
+
import { EVENT_TYPES } from "../core/constants.mjs"
|
|
7
|
+
import { readFile } from "node:fs/promises"
|
|
8
|
+
import { execFile } from "node:child_process"
|
|
9
|
+
import { promisify } from "node:util"
|
|
10
|
+
import { join } from "node:path"
|
|
11
|
+
import { homedir } from "node:os"
|
|
12
|
+
|
|
13
|
+
const state = {
|
|
14
|
+
loaded: false,
|
|
15
|
+
servers: new Map(),
|
|
16
|
+
tools: new Map(),
|
|
17
|
+
prompts: new Map(),
|
|
18
|
+
health: new Map(),
|
|
19
|
+
configured: new Map(),
|
|
20
|
+
loadedAt: 0,
|
|
21
|
+
lastSignature: "",
|
|
22
|
+
initPromise: null,
|
|
23
|
+
shuttingDown: false
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function normalizeTool(serverName, tool) {
|
|
27
|
+
const id = `mcp_${serverName}_${tool.name}`
|
|
28
|
+
return {
|
|
29
|
+
id,
|
|
30
|
+
server: serverName,
|
|
31
|
+
name: tool.name,
|
|
32
|
+
description: tool.description || `${serverName}:${tool.name}`,
|
|
33
|
+
inputSchema: tool.inputSchema || tool.input_schema || { type: "object", properties: {}, required: [] }
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function normalizePrompt(serverName, prompt) {
|
|
38
|
+
const id = `mcp_${serverName}_${prompt.name}`
|
|
39
|
+
return {
|
|
40
|
+
id,
|
|
41
|
+
server: serverName,
|
|
42
|
+
name: prompt.name,
|
|
43
|
+
description: prompt.description || `${serverName}:${prompt.name}`,
|
|
44
|
+
arguments: prompt.arguments || []
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const execFileAsync = promisify(execFile)
|
|
49
|
+
let context7InstallLock = null
|
|
50
|
+
async function ensureGlobalPackage(pkg) {
|
|
51
|
+
const name = pkg.replace(/@[^/]*$/, "")
|
|
52
|
+
try {
|
|
53
|
+
await execFileAsync("npm", ["list", "-g", name], { timeout: 10000 })
|
|
54
|
+
} catch {
|
|
55
|
+
await execFileAsync("npm", ["install", "-g", pkg], { timeout: 120000 })
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function resolveTransport(server = {}) {
|
|
60
|
+
const transport = String(server.transport || server.type || "stdio").toLowerCase()
|
|
61
|
+
if (transport === "http") return "http"
|
|
62
|
+
if (transport === "sse" || transport === "streamable-http") return "sse"
|
|
63
|
+
return "stdio"
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function createClient(name, server) {
|
|
67
|
+
const transport = resolveTransport(server)
|
|
68
|
+
if (transport === "sse") return createSseMcpClient(name, server)
|
|
69
|
+
if (transport === "http") return createHttpMcpClient(name, server)
|
|
70
|
+
return createStdioMcpClient(name, server)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function setHealth(name, serverConfig = {}, patch = {}) {
|
|
74
|
+
const prev = state.health.get(name) || {
|
|
75
|
+
name,
|
|
76
|
+
transport: resolveTransport(serverConfig),
|
|
77
|
+
ok: false,
|
|
78
|
+
reason: "not_checked",
|
|
79
|
+
error: null,
|
|
80
|
+
lastCheckedAt: 0
|
|
81
|
+
}
|
|
82
|
+
const next = {
|
|
83
|
+
...prev,
|
|
84
|
+
...patch,
|
|
85
|
+
name,
|
|
86
|
+
transport: patch.transport || prev.transport || resolveTransport(serverConfig),
|
|
87
|
+
lastCheckedAt: Date.now()
|
|
88
|
+
}
|
|
89
|
+
state.health.set(name, next)
|
|
90
|
+
return next
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Dynamic discovery: load MCP server configs from well-known project files.
|
|
95
|
+
* Checks (in order, merged):
|
|
96
|
+
* .mcp.json — Claude Code / VS Code convention
|
|
97
|
+
* .mcp/config.json — directory-based convention
|
|
98
|
+
* .kkcode/mcp.json — kkcode-specific
|
|
99
|
+
* ~/.kkcode/mcp.json — global user-level
|
|
100
|
+
*/
|
|
101
|
+
async function discoverProjectServers(cwd) {
|
|
102
|
+
const candidates = [
|
|
103
|
+
join(cwd, ".mcp.json"),
|
|
104
|
+
join(cwd, ".mcp", "config.json"),
|
|
105
|
+
join(cwd, ".kkcode", "mcp.json"),
|
|
106
|
+
join(homedir(), ".kkcode", "mcp.json")
|
|
107
|
+
]
|
|
108
|
+
const merged = {}
|
|
109
|
+
for (const filePath of candidates) {
|
|
110
|
+
try {
|
|
111
|
+
const raw = await readFile(filePath, "utf-8")
|
|
112
|
+
const parsed = JSON.parse(raw)
|
|
113
|
+
const servers = parsed?.servers || parsed?.mcpServers || {}
|
|
114
|
+
for (const [name, cfg] of Object.entries(servers)) {
|
|
115
|
+
if (!merged[name]) merged[name] = cfg
|
|
116
|
+
}
|
|
117
|
+
} catch {
|
|
118
|
+
// ignore missing/invalid files
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return merged
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async function connectServer(name, server) {
|
|
125
|
+
// Lazy install for context7 built-in server
|
|
126
|
+
if (name === "context7" && server?.command === "context7-mcp") {
|
|
127
|
+
if (!context7InstallLock) {
|
|
128
|
+
context7InstallLock = ensureGlobalPackage("@upstash/context7-mcp@latest").catch(() => {
|
|
129
|
+
context7InstallLock = null // Reset on failure to allow retry
|
|
130
|
+
})
|
|
131
|
+
}
|
|
132
|
+
await context7InstallLock
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const transport = resolveTransport(server)
|
|
136
|
+
let client
|
|
137
|
+
try {
|
|
138
|
+
client = createClient(name, server)
|
|
139
|
+
} catch (error) {
|
|
140
|
+
const health = setHealth(name, server, {
|
|
141
|
+
ok: false,
|
|
142
|
+
reason: error.reason || "unknown",
|
|
143
|
+
error: error.message,
|
|
144
|
+
transport
|
|
145
|
+
})
|
|
146
|
+
await EventBus.emit({
|
|
147
|
+
type: EVENT_TYPES.MCP_HEALTH,
|
|
148
|
+
payload: { server: name, ...health }
|
|
149
|
+
})
|
|
150
|
+
return null
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
let health
|
|
154
|
+
try {
|
|
155
|
+
health = await client.health()
|
|
156
|
+
} catch (error) {
|
|
157
|
+
health = { ok: false, reason: error.reason || "unknown", error: error.message || String(error) }
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const normalizedHealth = setHealth(name, server, {
|
|
161
|
+
ok: Boolean(health?.ok),
|
|
162
|
+
reason: health?.reason || (health?.ok ? "ok" : "unknown"),
|
|
163
|
+
error: health?.error || null,
|
|
164
|
+
phase: health?.phase || null,
|
|
165
|
+
transport
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
await EventBus.emit({
|
|
169
|
+
type: EVENT_TYPES.MCP_HEALTH,
|
|
170
|
+
payload: { server: name, ...normalizedHealth }
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
if (!normalizedHealth.ok) return null
|
|
174
|
+
|
|
175
|
+
state.servers.set(name, client)
|
|
176
|
+
|
|
177
|
+
// Discover tools
|
|
178
|
+
try {
|
|
179
|
+
const tools = await client.listTools()
|
|
180
|
+
for (const tool of tools) {
|
|
181
|
+
const normalized = normalizeTool(name, tool)
|
|
182
|
+
state.tools.set(normalized.id, normalized)
|
|
183
|
+
}
|
|
184
|
+
} catch (error) {
|
|
185
|
+
setHealth(name, server, {
|
|
186
|
+
ok: false,
|
|
187
|
+
reason: error.reason || "unknown",
|
|
188
|
+
error: `listTools failed: ${error.message}`
|
|
189
|
+
})
|
|
190
|
+
state.servers.delete(name)
|
|
191
|
+
await EventBus.emit({
|
|
192
|
+
type: EVENT_TYPES.MCP_HEALTH,
|
|
193
|
+
payload: { server: name, ...state.health.get(name) }
|
|
194
|
+
})
|
|
195
|
+
return null
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Discover prompts (optional)
|
|
199
|
+
if (typeof client.listPrompts === "function") {
|
|
200
|
+
try {
|
|
201
|
+
const prompts = await client.listPrompts()
|
|
202
|
+
for (const prompt of prompts) {
|
|
203
|
+
const normalized = normalizePrompt(name, prompt)
|
|
204
|
+
state.prompts.set(normalized.id, normalized)
|
|
205
|
+
}
|
|
206
|
+
} catch {
|
|
207
|
+
// optional capability
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return client
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
async function reinitialize(config, { force = false, cwd = null } = {}) {
|
|
215
|
+
state.shuttingDown = false
|
|
216
|
+
const ttlMs = Math.max(0, Number(config?.runtime?.mcp_refresh_ttl_ms || 60000))
|
|
217
|
+
const effectiveCwd = cwd || process.cwd()
|
|
218
|
+
const sig = JSON.stringify({
|
|
219
|
+
mcp: config?.mcp || {},
|
|
220
|
+
runtime: config?.runtime || {},
|
|
221
|
+
cwd: effectiveCwd
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
const cacheValid = state.loaded && !force && state.lastSignature === sig && Date.now() - state.loadedAt <= ttlMs
|
|
225
|
+
if (cacheValid) return
|
|
226
|
+
|
|
227
|
+
for (const [, client] of state.servers) {
|
|
228
|
+
if (typeof client.shutdown === "function") client.shutdown()
|
|
229
|
+
}
|
|
230
|
+
state.loaded = false
|
|
231
|
+
state.servers.clear()
|
|
232
|
+
state.tools.clear()
|
|
233
|
+
state.prompts.clear()
|
|
234
|
+
state.health.clear()
|
|
235
|
+
state.configured.clear()
|
|
236
|
+
|
|
237
|
+
// Built-in MCP servers (user config can override or disable with enabled: false)
|
|
238
|
+
const builtinServers = {
|
|
239
|
+
context7: {
|
|
240
|
+
command: "context7-mcp",
|
|
241
|
+
args: [],
|
|
242
|
+
timeout_ms: 30000,
|
|
243
|
+
framing: "newline"
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
const configServers = config?.mcp?.servers || {}
|
|
247
|
+
const discoveredServers = config?.mcp?.auto_discover !== false
|
|
248
|
+
? await discoverProjectServers(effectiveCwd)
|
|
249
|
+
: {}
|
|
250
|
+
const allServers = { ...builtinServers, ...discoveredServers, ...configServers }
|
|
251
|
+
|
|
252
|
+
for (const [name, serverConfig] of Object.entries(allServers)) {
|
|
253
|
+
state.configured.set(name, serverConfig)
|
|
254
|
+
if (serverConfig?.enabled === false) {
|
|
255
|
+
setHealth(name, serverConfig, {
|
|
256
|
+
ok: false,
|
|
257
|
+
reason: "disabled",
|
|
258
|
+
error: null
|
|
259
|
+
})
|
|
260
|
+
} else {
|
|
261
|
+
setHealth(name, serverConfig, {
|
|
262
|
+
ok: false,
|
|
263
|
+
reason: "not_checked",
|
|
264
|
+
error: null
|
|
265
|
+
})
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const entries = Object.entries(allServers).filter(([, serverConfig]) => serverConfig?.enabled !== false)
|
|
270
|
+
await Promise.allSettled(entries.map(([name, serverConfig]) => connectServer(name, serverConfig)))
|
|
271
|
+
|
|
272
|
+
state.loaded = true
|
|
273
|
+
state.loadedAt = Date.now()
|
|
274
|
+
state.lastSignature = sig
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export const McpRegistry = {
|
|
278
|
+
async initialize(config, { force = false, cwd = null } = {}) {
|
|
279
|
+
if (state.initPromise) {
|
|
280
|
+
await state.initPromise
|
|
281
|
+
if (!force) return
|
|
282
|
+
}
|
|
283
|
+
state.initPromise = reinitialize(config, { force, cwd })
|
|
284
|
+
try {
|
|
285
|
+
await state.initPromise
|
|
286
|
+
} finally {
|
|
287
|
+
state.initPromise = null
|
|
288
|
+
}
|
|
289
|
+
},
|
|
290
|
+
|
|
291
|
+
isReady() {
|
|
292
|
+
return state.loaded
|
|
293
|
+
},
|
|
294
|
+
|
|
295
|
+
listServers() {
|
|
296
|
+
return [...state.servers.keys()]
|
|
297
|
+
},
|
|
298
|
+
|
|
299
|
+
serverInfo(name) {
|
|
300
|
+
const health = state.health.get(name)
|
|
301
|
+
if (!health) return null
|
|
302
|
+
return {
|
|
303
|
+
name,
|
|
304
|
+
transport: health.transport,
|
|
305
|
+
lastHealth: health.ok ? "ok" : "fail",
|
|
306
|
+
reason: health.reason || "unknown",
|
|
307
|
+
lastError: health.error || null
|
|
308
|
+
}
|
|
309
|
+
},
|
|
310
|
+
|
|
311
|
+
healthSnapshot() {
|
|
312
|
+
return [...state.health.entries()]
|
|
313
|
+
.map(([name, health]) => ({
|
|
314
|
+
name,
|
|
315
|
+
transport: health.transport || "stdio",
|
|
316
|
+
ok: Boolean(health.ok),
|
|
317
|
+
reason: health.reason || "unknown",
|
|
318
|
+
error: health.error || null,
|
|
319
|
+
phase: health.phase || null,
|
|
320
|
+
configured: state.configured.has(name),
|
|
321
|
+
enabled: state.configured.get(name)?.enabled !== false,
|
|
322
|
+
lastCheckedAt: health.lastCheckedAt || 0
|
|
323
|
+
}))
|
|
324
|
+
.sort((a, b) => a.name.localeCompare(b.name))
|
|
325
|
+
},
|
|
326
|
+
|
|
327
|
+
listTools() {
|
|
328
|
+
return [...state.tools.values()]
|
|
329
|
+
},
|
|
330
|
+
|
|
331
|
+
listPrompts() {
|
|
332
|
+
return [...state.prompts.values()]
|
|
333
|
+
},
|
|
334
|
+
|
|
335
|
+
async getPrompt(promptId, args = {}) {
|
|
336
|
+
const prompt = state.prompts.get(promptId)
|
|
337
|
+
if (!prompt) throw new Error(`mcp prompt not found: ${promptId}`)
|
|
338
|
+
const client = state.servers.get(prompt.server)
|
|
339
|
+
if (!client || typeof client.getPrompt !== "function") {
|
|
340
|
+
throw new Error(`mcp server "${prompt.server}" does not support prompts/get`)
|
|
341
|
+
}
|
|
342
|
+
return client.getPrompt(prompt.name, args)
|
|
343
|
+
},
|
|
344
|
+
|
|
345
|
+
async listResources(serverName) {
|
|
346
|
+
const client = state.servers.get(serverName)
|
|
347
|
+
if (!client) return []
|
|
348
|
+
return client.listResources()
|
|
349
|
+
},
|
|
350
|
+
|
|
351
|
+
async listTemplates(serverName) {
|
|
352
|
+
const client = state.servers.get(serverName)
|
|
353
|
+
if (!client) return []
|
|
354
|
+
return client.listTemplates()
|
|
355
|
+
},
|
|
356
|
+
|
|
357
|
+
async callTool(toolId, args = {}, signal = null) {
|
|
358
|
+
if (state.shuttingDown) {
|
|
359
|
+
throw new McpError("MCP registry is shutting down", { reason: "shutting_down" })
|
|
360
|
+
}
|
|
361
|
+
const tool = state.tools.get(toolId)
|
|
362
|
+
if (!tool) throw new Error(`mcp tool not found: ${toolId}`)
|
|
363
|
+
let client = state.servers.get(tool.server)
|
|
364
|
+
if (!client) throw new Error(`mcp server not found: ${tool.server}`)
|
|
365
|
+
const serverConfig = state.configured.get(tool.server)
|
|
366
|
+
const serverTimeout = serverConfig?.timeout_ms
|
|
367
|
+
let effectiveSignal = signal
|
|
368
|
+
if (serverTimeout && !signal) {
|
|
369
|
+
effectiveSignal = AbortSignal.timeout(serverTimeout)
|
|
370
|
+
}
|
|
371
|
+
try {
|
|
372
|
+
return await client.callTool(tool.name, args, effectiveSignal)
|
|
373
|
+
} catch (error) {
|
|
374
|
+
if (error?.reason === "spawn_failed" || error?.reason === "server_crash") {
|
|
375
|
+
setHealth(tool.server, serverConfig, {
|
|
376
|
+
ok: false, reason: error.reason, error: error.message
|
|
377
|
+
})
|
|
378
|
+
try {
|
|
379
|
+
await this.refreshServer(tool.server)
|
|
380
|
+
client = state.servers.get(tool.server)
|
|
381
|
+
if (client) return client.callTool(tool.name, args, effectiveSignal)
|
|
382
|
+
} catch {}
|
|
383
|
+
}
|
|
384
|
+
throw error
|
|
385
|
+
}
|
|
386
|
+
},
|
|
387
|
+
|
|
388
|
+
async refreshServer(name) {
|
|
389
|
+
const serverConfig = state.configured.get(name)
|
|
390
|
+
if (!serverConfig) throw new Error(`mcp server not configured: ${name}`)
|
|
391
|
+
const existing = state.servers.get(name)
|
|
392
|
+
if (existing && typeof existing.shutdown === "function") existing.shutdown()
|
|
393
|
+
state.servers.delete(name)
|
|
394
|
+
for (const [id, t] of state.tools) {
|
|
395
|
+
if (t.server === name) state.tools.delete(id)
|
|
396
|
+
}
|
|
397
|
+
for (const [id, p] of state.prompts) {
|
|
398
|
+
if (p.server === name) state.prompts.delete(id)
|
|
399
|
+
}
|
|
400
|
+
return connectServer(name, serverConfig)
|
|
401
|
+
},
|
|
402
|
+
|
|
403
|
+
async addServer(name, serverConfig) {
|
|
404
|
+
if (state.servers.has(name)) {
|
|
405
|
+
const existing = state.servers.get(name)
|
|
406
|
+
if (typeof existing.shutdown === "function") existing.shutdown()
|
|
407
|
+
state.servers.delete(name)
|
|
408
|
+
for (const [id, t] of state.tools) {
|
|
409
|
+
if (t.server === name) state.tools.delete(id)
|
|
410
|
+
}
|
|
411
|
+
for (const [id, p] of state.prompts) {
|
|
412
|
+
if (p.server === name) state.prompts.delete(id)
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
state.configured.set(name, serverConfig)
|
|
416
|
+
return connectServer(name, serverConfig)
|
|
417
|
+
},
|
|
418
|
+
|
|
419
|
+
async healthCheck(serverName) {
|
|
420
|
+
const client = state.servers.get(serverName)
|
|
421
|
+
const serverConfig = state.configured.get(serverName)
|
|
422
|
+
if (!client || !serverConfig) return { ok: false, reason: "not_found" }
|
|
423
|
+
try {
|
|
424
|
+
const result = await client.health()
|
|
425
|
+
const patch = {
|
|
426
|
+
ok: Boolean(result?.ok),
|
|
427
|
+
reason: result?.reason || (result?.ok ? "ok" : "unknown"),
|
|
428
|
+
error: result?.error || null
|
|
429
|
+
}
|
|
430
|
+
setHealth(serverName, serverConfig, patch)
|
|
431
|
+
await EventBus.emit({ type: EVENT_TYPES.MCP_HEALTH, payload: { server: serverName, ...patch } })
|
|
432
|
+
if (!result?.ok) {
|
|
433
|
+
try { await this.refreshServer(serverName) } catch {}
|
|
434
|
+
}
|
|
435
|
+
return patch
|
|
436
|
+
} catch (error) {
|
|
437
|
+
const patch = { ok: false, reason: error.reason || "unknown", error: error.message }
|
|
438
|
+
setHealth(serverName, serverConfig, patch)
|
|
439
|
+
return patch
|
|
440
|
+
}
|
|
441
|
+
},
|
|
442
|
+
|
|
443
|
+
async healthCheckAll() {
|
|
444
|
+
const results = {}
|
|
445
|
+
for (const name of state.configured.keys()) {
|
|
446
|
+
if (state.configured.get(name)?.enabled === false) continue
|
|
447
|
+
results[name] = await this.healthCheck(name)
|
|
448
|
+
}
|
|
449
|
+
return results
|
|
450
|
+
},
|
|
451
|
+
|
|
452
|
+
removeServer(name) {
|
|
453
|
+
const client = state.servers.get(name)
|
|
454
|
+
if (client && typeof client.shutdown === "function") client.shutdown()
|
|
455
|
+
state.servers.delete(name)
|
|
456
|
+
state.configured.delete(name)
|
|
457
|
+
state.health.delete(name)
|
|
458
|
+
for (const [id, t] of state.tools) {
|
|
459
|
+
if (t.server === name) state.tools.delete(id)
|
|
460
|
+
}
|
|
461
|
+
for (const [id, p] of state.prompts) {
|
|
462
|
+
if (p.server === name) state.prompts.delete(id)
|
|
463
|
+
}
|
|
464
|
+
},
|
|
465
|
+
|
|
466
|
+
shutdown() {
|
|
467
|
+
state.shuttingDown = true
|
|
468
|
+
for (const [, client] of state.servers) {
|
|
469
|
+
if (typeof client.shutdown === "function") client.shutdown()
|
|
470
|
+
}
|
|
471
|
+
state.servers.clear()
|
|
472
|
+
state.tools.clear()
|
|
473
|
+
state.prompts.clear()
|
|
474
|
+
state.health.clear()
|
|
475
|
+
state.configured.clear()
|
|
476
|
+
state.loaded = false
|
|
477
|
+
state.lastSignature = ""
|
|
478
|
+
}
|
|
479
|
+
}
|