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