@kkelly-offical/kkcode 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +674 -0
- package/README.md +445 -0
- package/package.json +46 -0
- package/src/agent/agent.mjs +170 -0
- package/src/agent/custom-agent-loader.mjs +158 -0
- package/src/agent/generator.mjs +115 -0
- package/src/agent/prompt/architect.txt +36 -0
- package/src/agent/prompt/build-fixer.txt +71 -0
- package/src/agent/prompt/build.txt +101 -0
- package/src/agent/prompt/compaction.txt +12 -0
- package/src/agent/prompt/explore.txt +29 -0
- package/src/agent/prompt/guide.txt +40 -0
- package/src/agent/prompt/longagent.txt +178 -0
- package/src/agent/prompt/plan.txt +50 -0
- package/src/agent/prompt/researcher.txt +23 -0
- package/src/agent/prompt/reviewer.txt +44 -0
- package/src/agent/prompt/security-reviewer.txt +62 -0
- package/src/agent/prompt/tdd-guide.txt +84 -0
- package/src/agent/prompt/title.txt +8 -0
- package/src/command/custom-commands.mjs +57 -0
- package/src/commands/agent.mjs +71 -0
- package/src/commands/audit.mjs +77 -0
- package/src/commands/background.mjs +86 -0
- package/src/commands/chat.mjs +114 -0
- package/src/commands/command.mjs +41 -0
- package/src/commands/config.mjs +44 -0
- package/src/commands/doctor.mjs +148 -0
- package/src/commands/hook.mjs +29 -0
- package/src/commands/init.mjs +141 -0
- package/src/commands/longagent.mjs +100 -0
- package/src/commands/mcp.mjs +89 -0
- package/src/commands/permission.mjs +36 -0
- package/src/commands/prompt.mjs +42 -0
- package/src/commands/review.mjs +266 -0
- package/src/commands/rule.mjs +34 -0
- package/src/commands/session.mjs +235 -0
- package/src/commands/theme.mjs +98 -0
- package/src/commands/usage.mjs +91 -0
- package/src/config/defaults.mjs +195 -0
- package/src/config/import-config.mjs +76 -0
- package/src/config/load-config.mjs +76 -0
- package/src/config/schema.mjs +509 -0
- package/src/context.mjs +40 -0
- package/src/core/constants.mjs +46 -0
- package/src/core/errors.mjs +57 -0
- package/src/core/events.mjs +29 -0
- package/src/core/types.mjs +57 -0
- package/src/github/api.mjs +78 -0
- package/src/github/auth.mjs +286 -0
- package/src/github/flow.mjs +298 -0
- package/src/github/workspace.mjs +212 -0
- package/src/index.mjs +82 -0
- package/src/knowledge/api-design.txt +9 -0
- package/src/knowledge/cpp.txt +10 -0
- package/src/knowledge/docker.txt +10 -0
- package/src/knowledge/dotnet.txt +9 -0
- package/src/knowledge/electron.txt +10 -0
- package/src/knowledge/flutter.txt +10 -0
- package/src/knowledge/go.txt +9 -0
- package/src/knowledge/graphql.txt +10 -0
- package/src/knowledge/java.txt +9 -0
- package/src/knowledge/kotlin.txt +10 -0
- package/src/knowledge/loader.mjs +125 -0
- package/src/knowledge/next.txt +8 -0
- package/src/knowledge/node.txt +8 -0
- package/src/knowledge/nuxt.txt +9 -0
- package/src/knowledge/php.txt +10 -0
- package/src/knowledge/python.txt +10 -0
- package/src/knowledge/react-native.txt +10 -0
- package/src/knowledge/react.txt +9 -0
- package/src/knowledge/ruby.txt +11 -0
- package/src/knowledge/rust.txt +9 -0
- package/src/knowledge/svelte.txt +9 -0
- package/src/knowledge/swift.txt +10 -0
- package/src/knowledge/tailwind.txt +10 -0
- package/src/knowledge/testing.txt +8 -0
- package/src/knowledge/typescript.txt +8 -0
- package/src/knowledge/vue.txt +9 -0
- package/src/mcp/client-http.mjs +157 -0
- package/src/mcp/client-sse.mjs +286 -0
- package/src/mcp/client-stdio.mjs +451 -0
- package/src/mcp/registry.mjs +394 -0
- package/src/mcp/stdio-framing.mjs +127 -0
- package/src/orchestration/background-manager.mjs +358 -0
- package/src/orchestration/background-worker.mjs +245 -0
- package/src/orchestration/longagent-manager.mjs +116 -0
- package/src/orchestration/stage-scheduler.mjs +489 -0
- package/src/orchestration/subagent-router.mjs +62 -0
- package/src/orchestration/task-scheduler.mjs +74 -0
- package/src/permission/engine.mjs +92 -0
- package/src/permission/exec-policy.mjs +372 -0
- package/src/permission/prompt.mjs +39 -0
- package/src/permission/rules.mjs +120 -0
- package/src/permission/workspace-trust.mjs +44 -0
- package/src/plugin/builtin-hooks/console-warn.mjs +41 -0
- package/src/plugin/builtin-hooks/extract-patterns.mjs +75 -0
- package/src/plugin/builtin-hooks/post-edit-format.mjs +57 -0
- package/src/plugin/builtin-hooks/post-edit-typecheck.mjs +61 -0
- package/src/plugin/builtin-hooks/strategic-compaction.mjs +38 -0
- package/src/plugin/hook-bus.mjs +154 -0
- package/src/provider/anthropic.mjs +389 -0
- package/src/provider/ollama.mjs +236 -0
- package/src/provider/openai-compatible.mjs +1 -0
- package/src/provider/openai.mjs +339 -0
- package/src/provider/retry-policy.mjs +68 -0
- package/src/provider/router.mjs +228 -0
- package/src/provider/sse.mjs +91 -0
- package/src/repl.mjs +2929 -0
- package/src/review/diff-parser.mjs +36 -0
- package/src/review/rejection-queue.mjs +62 -0
- package/src/review/review-store.mjs +21 -0
- package/src/review/risk-score.mjs +61 -0
- package/src/rules/load-rules.mjs +64 -0
- package/src/runtime.mjs +1 -0
- package/src/session/checkpoint.mjs +239 -0
- package/src/session/compaction.mjs +276 -0
- package/src/session/engine.mjs +225 -0
- package/src/session/instinct-manager.mjs +172 -0
- package/src/session/instruction-loader.mjs +25 -0
- package/src/session/longagent-plan.mjs +329 -0
- package/src/session/longagent-scaffold.mjs +100 -0
- package/src/session/longagent.mjs +1462 -0
- package/src/session/loop.mjs +905 -0
- package/src/session/memory-loader.mjs +75 -0
- package/src/session/project-context.mjs +367 -0
- package/src/session/prompt/anthropic.txt +151 -0
- package/src/session/prompt/beast.txt +37 -0
- package/src/session/prompt/max-steps.txt +6 -0
- package/src/session/prompt/plan.txt +9 -0
- package/src/session/prompt/qwen.txt +46 -0
- package/src/session/prompt-loader.mjs +18 -0
- package/src/session/recovery.mjs +52 -0
- package/src/session/store.mjs +503 -0
- package/src/session/system-prompt.mjs +260 -0
- package/src/session/task-validator.mjs +266 -0
- package/src/session/usability-gates.mjs +379 -0
- package/src/skill/builtin/backend-patterns.mjs +123 -0
- package/src/skill/builtin/commit.mjs +64 -0
- package/src/skill/builtin/debug.mjs +45 -0
- package/src/skill/builtin/frontend-patterns.mjs +120 -0
- package/src/skill/builtin/frontend.mjs +188 -0
- package/src/skill/builtin/init.mjs +220 -0
- package/src/skill/builtin/review.mjs +49 -0
- package/src/skill/builtin/security-checklist.mjs +80 -0
- package/src/skill/builtin/tdd.mjs +54 -0
- package/src/skill/generator.mjs +113 -0
- package/src/skill/registry.mjs +336 -0
- package/src/storage/audit-store.mjs +83 -0
- package/src/storage/event-log.mjs +82 -0
- package/src/storage/ghost-commit-store.mjs +235 -0
- package/src/storage/json-store.mjs +53 -0
- package/src/storage/paths.mjs +148 -0
- package/src/theme/color.mjs +64 -0
- package/src/theme/default-theme.mjs +29 -0
- package/src/theme/load-theme.mjs +71 -0
- package/src/theme/markdown.mjs +135 -0
- package/src/theme/schema.mjs +45 -0
- package/src/theme/status-bar.mjs +158 -0
- package/src/tool/audit-wrapper.mjs +38 -0
- package/src/tool/edit-transaction.mjs +126 -0
- package/src/tool/executor.mjs +109 -0
- package/src/tool/file-lock-manager.mjs +85 -0
- package/src/tool/git-auto.mjs +545 -0
- package/src/tool/git-full-auto.mjs +478 -0
- package/src/tool/image-util.mjs +276 -0
- package/src/tool/prompt/background_cancel.txt +1 -0
- package/src/tool/prompt/background_output.txt +1 -0
- package/src/tool/prompt/bash.txt +71 -0
- package/src/tool/prompt/codesearch.txt +18 -0
- package/src/tool/prompt/edit.txt +27 -0
- package/src/tool/prompt/enter_plan.txt +74 -0
- package/src/tool/prompt/exit_plan.txt +62 -0
- package/src/tool/prompt/glob.txt +33 -0
- package/src/tool/prompt/grep.txt +43 -0
- package/src/tool/prompt/list.txt +8 -0
- package/src/tool/prompt/multiedit.txt +20 -0
- package/src/tool/prompt/notebookedit.txt +21 -0
- package/src/tool/prompt/patch.txt +24 -0
- package/src/tool/prompt/question.txt +44 -0
- package/src/tool/prompt/read.txt +40 -0
- package/src/tool/prompt/task.txt +83 -0
- package/src/tool/prompt/todowrite.txt +117 -0
- package/src/tool/prompt/webfetch.txt +38 -0
- package/src/tool/prompt/websearch.txt +43 -0
- package/src/tool/prompt/write.txt +38 -0
- package/src/tool/prompt-loader.mjs +18 -0
- package/src/tool/question-prompt.mjs +86 -0
- package/src/tool/registry.mjs +1309 -0
- package/src/tool/task-tool.mjs +28 -0
- package/src/ui/activity-renderer.mjs +410 -0
- package/src/ui/repl-dashboard.mjs +357 -0
- package/src/usage/pricing.mjs +121 -0
- package/src/usage/usage-meter.mjs +113 -0
- package/src/util/git.mjs +496 -0
- package/src/util/template.mjs +10 -0
- package/src/util/yaml.mjs +100 -0
|
@@ -0,0 +1,394 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
const CRLFCRLF = Buffer.from("\r\n\r\n", "utf8")
|
|
2
|
+
const NEWLINE = 0x0a
|
|
3
|
+
|
|
4
|
+
function toBuffer(chunk) {
|
|
5
|
+
if (Buffer.isBuffer(chunk)) return chunk
|
|
6
|
+
if (typeof chunk === "string") return Buffer.from(chunk, "utf8")
|
|
7
|
+
if (chunk instanceof Uint8Array) return Buffer.from(chunk)
|
|
8
|
+
return Buffer.from(String(chunk || ""), "utf8")
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function parseContentLengthHeader(headerText) {
|
|
12
|
+
const match = /(?:^|\r?\n)content-length:\s*(\d+)\s*(?:\r?\n|$)/i.exec(headerText)
|
|
13
|
+
if (!match) return null
|
|
14
|
+
const len = Number(match[1])
|
|
15
|
+
if (!Number.isFinite(len) || len < 0) return null
|
|
16
|
+
return len
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function consumeContentLengthFrame(buffer, maxFrameBytes) {
|
|
20
|
+
const headerEnd = buffer.indexOf(CRLFCRLF)
|
|
21
|
+
if (headerEnd === -1) return { type: "need_more" }
|
|
22
|
+
const headerText = buffer.subarray(0, headerEnd).toString("utf8")
|
|
23
|
+
const length = parseContentLengthHeader(headerText)
|
|
24
|
+
if (length === null) return { type: "invalid_header" }
|
|
25
|
+
if (length > maxFrameBytes) return { type: "invalid_size", size: length }
|
|
26
|
+
const total = headerEnd + CRLFCRLF.length + length
|
|
27
|
+
if (buffer.length < total) return { type: "need_more" }
|
|
28
|
+
const payload = buffer
|
|
29
|
+
.subarray(headerEnd + CRLFCRLF.length, total)
|
|
30
|
+
.toString("utf8")
|
|
31
|
+
const rest = buffer.subarray(total)
|
|
32
|
+
return { type: "ok", payload, rest }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function consumeNewlineFrame(buffer) {
|
|
36
|
+
const newlineIdx = buffer.indexOf(NEWLINE)
|
|
37
|
+
if (newlineIdx === -1) return { type: "need_more" }
|
|
38
|
+
const rawLine = buffer.subarray(0, newlineIdx).toString("utf8")
|
|
39
|
+
const rest = buffer.subarray(newlineIdx + 1)
|
|
40
|
+
const payload = rawLine.trim()
|
|
41
|
+
if (!payload) return { type: "empty", rest }
|
|
42
|
+
return { type: "ok", payload, rest }
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function dropLeadingCrlf(buffer) {
|
|
46
|
+
let cursor = 0
|
|
47
|
+
while (cursor < buffer.length) {
|
|
48
|
+
const c = buffer[cursor]
|
|
49
|
+
if (c !== 0x0d && c !== 0x0a) break
|
|
50
|
+
cursor += 1
|
|
51
|
+
}
|
|
52
|
+
return cursor > 0 ? buffer.subarray(cursor) : buffer
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function seemsContentLength(buffer) {
|
|
56
|
+
if (!buffer.length) return false
|
|
57
|
+
const probe = buffer.subarray(0, Math.min(buffer.length, 32)).toString("ascii").toLowerCase()
|
|
58
|
+
return probe.startsWith("content-length:")
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function encodeRpcMessage(message, framing = "content-length") {
|
|
62
|
+
const payload = JSON.stringify(message)
|
|
63
|
+
if (framing === "newline") return `${payload}\n`
|
|
64
|
+
const size = Buffer.byteLength(payload, "utf8")
|
|
65
|
+
return `Content-Length: ${size}\r\n\r\n${payload}`
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function createStdioFramingDecoder({ framing = "auto", maxFrameBytes = 8 * 1024 * 1024 } = {}) {
|
|
69
|
+
let buffer = Buffer.alloc(0)
|
|
70
|
+
|
|
71
|
+
function push(chunk) {
|
|
72
|
+
buffer = Buffer.concat([buffer, toBuffer(chunk)])
|
|
73
|
+
const messages = []
|
|
74
|
+
|
|
75
|
+
while (true) {
|
|
76
|
+
if (!buffer.length) break
|
|
77
|
+
|
|
78
|
+
if (framing === "content-length") {
|
|
79
|
+
const parsed = consumeContentLengthFrame(buffer, maxFrameBytes)
|
|
80
|
+
if (parsed.type === "need_more") break
|
|
81
|
+
if (parsed.type === "invalid_header") throw new Error("invalid content-length header")
|
|
82
|
+
if (parsed.type === "invalid_size") throw new Error(`content-length exceeds limit: ${parsed.size}`)
|
|
83
|
+
messages.push(parsed.payload)
|
|
84
|
+
buffer = parsed.rest
|
|
85
|
+
continue
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (framing === "newline") {
|
|
89
|
+
const parsed = consumeNewlineFrame(buffer)
|
|
90
|
+
if (parsed.type === "need_more") break
|
|
91
|
+
buffer = parsed.rest
|
|
92
|
+
if (parsed.type === "ok") messages.push(parsed.payload)
|
|
93
|
+
continue
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// auto mode: prefer standard content-length frames, then fallback to newline JSON
|
|
97
|
+
buffer = dropLeadingCrlf(buffer)
|
|
98
|
+
if (!buffer.length) break
|
|
99
|
+
|
|
100
|
+
if (seemsContentLength(buffer)) {
|
|
101
|
+
const parsed = consumeContentLengthFrame(buffer, maxFrameBytes)
|
|
102
|
+
if (parsed.type === "need_more") break
|
|
103
|
+
if (parsed.type === "invalid_header") throw new Error("invalid content-length header")
|
|
104
|
+
if (parsed.type === "invalid_size") throw new Error(`content-length exceeds limit: ${parsed.size}`)
|
|
105
|
+
messages.push(parsed.payload)
|
|
106
|
+
buffer = parsed.rest
|
|
107
|
+
continue
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const parsed = consumeNewlineFrame(buffer)
|
|
111
|
+
if (parsed.type === "need_more") break
|
|
112
|
+
buffer = parsed.rest
|
|
113
|
+
if (parsed.type === "ok") messages.push(parsed.payload)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return messages
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function reset() {
|
|
120
|
+
buffer = Buffer.alloc(0)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
push,
|
|
125
|
+
reset
|
|
126
|
+
}
|
|
127
|
+
}
|