@kkelly-offical/kkcode 0.1.7 → 0.2.3-preview.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.
Files changed (166) hide show
  1. package/LICENSE +674 -674
  2. package/README.md +474 -387
  3. package/package.json +50 -46
  4. package/src/agent/agent.mjs +228 -220
  5. package/src/agent/custom-agent-loader.mjs +6 -3
  6. package/src/agent/generator.mjs +2 -2
  7. package/src/agent/prompt/assistant.txt +12 -0
  8. package/src/agent/prompt/bug-hunter.txt +89 -89
  9. package/src/agent/prompt/frontend-designer.txt +58 -58
  10. package/src/agent/prompt/guide.txt +1 -1
  11. package/src/agent/prompt/longagent-blueprint-agent.txt +83 -83
  12. package/src/agent/prompt/longagent-coding-agent.txt +37 -37
  13. package/src/agent/prompt/longagent-debugging-agent.txt +46 -46
  14. package/src/agent/prompt/longagent-preview-agent.txt +63 -63
  15. package/src/command/custom-commands.mjs +2 -2
  16. package/src/commands/agent.mjs +1 -1
  17. package/src/commands/background.mjs +145 -4
  18. package/src/commands/chat.mjs +117 -76
  19. package/src/commands/config.mjs +148 -1
  20. package/src/commands/doctor.mjs +30 -6
  21. package/src/commands/init.mjs +32 -6
  22. package/src/commands/longagent.mjs +117 -0
  23. package/src/commands/mcp.mjs +275 -43
  24. package/src/commands/permission.mjs +1 -1
  25. package/src/commands/session.mjs +195 -140
  26. package/src/commands/skill.mjs +63 -0
  27. package/src/commands/theme.mjs +1 -1
  28. package/src/commands/update.mjs +32 -0
  29. package/src/config/defaults.mjs +289 -260
  30. package/src/config/import-config.mjs +1 -1
  31. package/src/config/load-config.mjs +61 -4
  32. package/src/config/schema.mjs +604 -574
  33. package/src/context.mjs +4 -1
  34. package/src/core/constants.mjs +97 -91
  35. package/src/core/types.mjs +1 -1
  36. package/src/github/api.mjs +78 -78
  37. package/src/github/auth.mjs +294 -286
  38. package/src/github/flow.mjs +298 -298
  39. package/src/github/workspace.mjs +225 -212
  40. package/src/index.mjs +87 -82
  41. package/src/knowledge/frontend-aesthetics.txt +38 -38
  42. package/src/mcp/client-http.mjs +139 -141
  43. package/src/mcp/client-sse.mjs +297 -288
  44. package/src/mcp/client-stdio.mjs +534 -533
  45. package/src/mcp/constants.mjs +4 -2
  46. package/src/mcp/registry.mjs +498 -479
  47. package/src/mcp/stdio-framing.mjs +135 -133
  48. package/src/mcp/tool-result.mjs +24 -24
  49. package/src/observability/edit-diagnostics.mjs +449 -0
  50. package/src/observability/index.mjs +42 -42
  51. package/src/observability/metrics.mjs +165 -137
  52. package/src/observability/tracer.mjs +137 -137
  53. package/src/onboarding.mjs +209 -0
  54. package/src/orchestration/background-manager.mjs +567 -372
  55. package/src/orchestration/background-worker.mjs +419 -305
  56. package/src/orchestration/interruption-reason.mjs +21 -0
  57. package/src/orchestration/longagent-manager.mjs +197 -171
  58. package/src/orchestration/stage-scheduler.mjs +733 -728
  59. package/src/orchestration/subagent-router.mjs +7 -1
  60. package/src/orchestration/task-scheduler.mjs +219 -7
  61. package/src/permission/engine.mjs +1 -1
  62. package/src/permission/exec-policy.mjs +370 -370
  63. package/src/permission/file-edit-policy.mjs +108 -0
  64. package/src/permission/prompt.mjs +1 -1
  65. package/src/permission/rules.mjs +116 -7
  66. package/src/plugin/builtin-hooks/post-edit-format.mjs +2 -1
  67. package/src/plugin/builtin-hooks/post-edit-typecheck.mjs +104 -40
  68. package/src/plugin/hook-bus.mjs +19 -5
  69. package/src/plugin/manifest-loader.mjs +222 -0
  70. package/src/provider/anthropic.mjs +396 -390
  71. package/src/provider/ollama.mjs +7 -1
  72. package/src/provider/openai.mjs +382 -340
  73. package/src/provider/retry-policy.mjs +74 -68
  74. package/src/provider/router.mjs +242 -241
  75. package/src/provider/sse.mjs +104 -104
  76. package/src/provider/wizard.mjs +556 -0
  77. package/src/repl/capability-facade.mjs +30 -0
  78. package/src/repl/command-surface.mjs +23 -0
  79. package/src/repl/controller-entry.mjs +40 -0
  80. package/src/repl/core-shell.mjs +208 -0
  81. package/src/repl/dialog-router.mjs +87 -0
  82. package/src/repl/input-engine.mjs +76 -0
  83. package/src/repl/keymap.mjs +7 -0
  84. package/src/repl/operator-surface.mjs +15 -0
  85. package/src/repl/permission-flow.mjs +49 -0
  86. package/src/repl/runtime-facade.mjs +36 -0
  87. package/src/repl/slash-router.mjs +62 -0
  88. package/src/repl/state-store.mjs +29 -0
  89. package/src/repl/turn-controller.mjs +58 -0
  90. package/src/repl/verification.mjs +23 -0
  91. package/src/repl.mjs +3371 -2981
  92. package/src/rules/load-rules.mjs +3 -3
  93. package/src/runtime.mjs +1 -1
  94. package/src/session/agent-transaction.mjs +86 -0
  95. package/src/session/checkpoint.mjs +302 -302
  96. package/src/session/compaction.mjs +298 -298
  97. package/src/session/engine.mjs +417 -232
  98. package/src/session/longagent-4stage.mjs +467 -460
  99. package/src/session/longagent-hybrid.mjs +1344 -1097
  100. package/src/session/longagent-plan.mjs +376 -365
  101. package/src/session/longagent-project-memory.mjs +53 -53
  102. package/src/session/longagent-scaffold.mjs +291 -291
  103. package/src/session/longagent-task-bus.mjs +138 -54
  104. package/src/session/longagent-utils.mjs +828 -472
  105. package/src/session/longagent.mjs +911 -900
  106. package/src/session/loop.mjs +1005 -930
  107. package/src/session/prompt/agent.txt +25 -25
  108. package/src/session/prompt/anthropic.txt +150 -150
  109. package/src/session/prompt/beast.txt +1 -1
  110. package/src/session/prompt/plan.txt +31 -31
  111. package/src/session/prompt/qwen.txt +46 -46
  112. package/src/session/recovery.mjs +21 -0
  113. package/src/session/rollback.mjs +196 -195
  114. package/src/session/routing-observability.mjs +72 -0
  115. package/src/session/runtime-state.mjs +47 -0
  116. package/src/session/store.mjs +523 -519
  117. package/src/session/system-prompt.mjs +308 -273
  118. package/src/session/task-validator.mjs +267 -267
  119. package/src/session/usability-gates.mjs +2 -2
  120. package/src/skill/builtin/commit.mjs +64 -64
  121. package/src/skill/builtin/design.mjs +76 -76
  122. package/src/skill/generator.mjs +18 -2
  123. package/src/skill/registry.mjs +642 -390
  124. package/src/storage/audit-store.mjs +18 -11
  125. package/src/storage/event-log.mjs +7 -1
  126. package/src/storage/ghost-commit-store.mjs +243 -245
  127. package/src/storage/paths.mjs +17 -0
  128. package/src/theme/default-theme.mjs +1 -1
  129. package/src/theme/markdown.mjs +4 -0
  130. package/src/theme/schema.mjs +1 -1
  131. package/src/theme/status-bar.mjs +162 -158
  132. package/src/tool/audit-wrapper.mjs +18 -2
  133. package/src/tool/edit-transaction.mjs +23 -0
  134. package/src/tool/executor.mjs +26 -1
  135. package/src/tool/file-read-state.mjs +65 -0
  136. package/src/tool/git-auto.mjs +526 -526
  137. package/src/tool/git-full-auto.mjs +487 -478
  138. package/src/tool/mutation-guard.mjs +54 -0
  139. package/src/tool/prompt/edit.txt +3 -3
  140. package/src/tool/prompt/multiedit.txt +1 -0
  141. package/src/tool/prompt/notebookedit.txt +2 -1
  142. package/src/tool/prompt/patch.txt +25 -24
  143. package/src/tool/prompt/read.txt +3 -3
  144. package/src/tool/prompt/sysinfo.txt +29 -0
  145. package/src/tool/prompt/task.txt +66 -4
  146. package/src/tool/prompt/write.txt +2 -2
  147. package/src/tool/question-prompt.mjs +99 -93
  148. package/src/tool/registry.mjs +1701 -1343
  149. package/src/tool/task-tool.mjs +14 -6
  150. package/src/ui/activity-renderer.mjs +667 -664
  151. package/src/ui/repl-background-panel.mjs +7 -0
  152. package/src/ui/repl-capability-panel.mjs +9 -0
  153. package/src/ui/repl-dashboard.mjs +54 -4
  154. package/src/ui/repl-help.mjs +110 -0
  155. package/src/ui/repl-operator-panel.mjs +12 -0
  156. package/src/ui/repl-route-feedback.mjs +35 -0
  157. package/src/ui/repl-status-view.mjs +76 -0
  158. package/src/ui/repl-task-panel.mjs +5 -0
  159. package/src/ui/repl-transcript-panel.mjs +56 -0
  160. package/src/ui/repl-turn-summary.mjs +135 -0
  161. package/src/update/checker.mjs +184 -0
  162. package/src/usage/pricing.mjs +122 -121
  163. package/src/usage/usage-meter.mjs +1 -0
  164. package/src/util/git.mjs +562 -519
  165. package/src/util/template.mjs +6 -1
  166. package/src/version.mjs +3 -0
@@ -1,288 +1,297 @@
1
- import { McpError } from "../core/errors.mjs"
2
- import { EventBus } from "../core/events.mjs"
3
- import { EVENT_TYPES } from "../core/constants.mjs"
4
- import { normalizeToolResult } from "./tool-result.mjs"
5
- import { MCP_PROTOCOL_VERSION, MCP_CLIENT_INFO } from "./constants.mjs"
6
-
7
- /**
8
- * MCP Streamable HTTP (SSE) client.
9
- *
10
- * Protocol: JSON-RPC 2.0 over HTTP POST with optional SSE response streaming.
11
- * - POST to endpoint: send JSON-RPC request, receive JSON or SSE stream
12
- * - GET to endpoint: open persistent SSE stream for server-initiated notifications
13
- * - Session management via Mcp-Session-Id header
14
- */
15
- export function createSseMcpClient(serverName, config) {
16
- const baseUrl = String(config.url || "").replace(/\/$/, "")
17
- const timeoutMs = Number(config.timeout_ms || 30000)
18
- const headers = config.headers || {}
19
-
20
- let sessionId = null
21
- let nextId = 1
22
- let initialized = false
23
- let notificationStream = null
24
-
25
- function buildHeaders(extra = {}) {
26
- const h = {
27
- "content-type": "application/json",
28
- accept: "application/json, text/event-stream",
29
- ...headers,
30
- ...extra
31
- }
32
- if (sessionId) h["mcp-session-id"] = sessionId
33
- return h
34
- }
35
-
36
- async function sendRequest(method, params = {}, { signal: parentSignal = null } = {}) {
37
- if (nextId > Number.MAX_SAFE_INTEGER - 1) nextId = 1
38
- const id = nextId++
39
- const body = { jsonrpc: "2.0", id, method, params }
40
- const startedAt = Date.now()
41
-
42
- const timeoutSignal = AbortSignal.timeout(timeoutMs)
43
- const combinedSignal = parentSignal
44
- ? AbortSignal.any([parentSignal, timeoutSignal])
45
- : timeoutSignal
46
-
47
- try {
48
- const res = await fetch(baseUrl, {
49
- method: "POST",
50
- headers: buildHeaders(),
51
- body: JSON.stringify(body),
52
- signal: combinedSignal
53
- })
54
-
55
- const elapsed = Date.now() - startedAt
56
-
57
- // Capture session ID from response
58
- const newSessionId = res.headers.get("mcp-session-id")
59
- if (newSessionId) sessionId = newSessionId
60
-
61
- EventBus.emit({
62
- type: EVENT_TYPES.MCP_REQUEST,
63
- payload: { server: serverName, action: method, elapsed, status: res.status }
64
- }).catch(() => {})
65
-
66
- if (!res.ok) {
67
- const text = await res.text().catch(() => "")
68
- throw new McpError(
69
- `mcp server "${serverName}" HTTP ${res.status}: ${text.slice(0, 500)}`,
70
- {
71
- reason: res.status >= 500 ? "server_crash" : "bad_response",
72
- server: serverName,
73
- action: method,
74
- phase: "request",
75
- statusCode: res.status
76
- }
77
- )
78
- }
79
-
80
- const contentType = res.headers.get("content-type") || ""
81
-
82
- // SSE response — parse events and return the final result
83
- if (contentType.includes("text/event-stream")) {
84
- return await parseSseResponse(res.body, id)
85
- }
86
-
87
- // Regular JSON response
88
- const json = await res.json().catch((parseErr) => {
89
- if (method === "initialize") {
90
- throw new McpError(
91
- `mcp server "${serverName}" malformed JSON in initialize response: ${parseErr.message}`,
92
- { reason: "bad_response", server: serverName, action: method, phase: "request" }
93
- )
94
- }
95
- EventBus.emit({
96
- type: EVENT_TYPES.MCP_REQUEST,
97
- payload: { server: serverName, action: method, warning: "malformed_json_response" }
98
- }).catch(() => {})
99
- return {}
100
- })
101
- if (json.error) {
102
- throw new McpError(
103
- `mcp server "${serverName}" error: ${json.error.message || JSON.stringify(json.error)}`,
104
- { reason: "bad_response", server: serverName, action: method, code: json.error.code, phase: "request" }
105
- )
106
- }
107
- return json.result ?? json
108
- } catch (error) {
109
- if (error instanceof McpError) throw error
110
- const reason = (error.name === "AbortError" || error.name === "TimeoutError") ? "timeout" : "connection_refused"
111
- throw new McpError(
112
- `mcp server "${serverName}" ${reason}: ${error.message}`,
113
- { reason, server: serverName, action: method, phase: "request" }
114
- )
115
- }
116
- }
117
-
118
- const maxSseBufferBytes = Number(config.max_sse_buffer_bytes || 4 * 1024 * 1024)
119
-
120
- async function parseSseResponse(body, requestId) {
121
- const reader = body.getReader()
122
- const decoder = new TextDecoder()
123
- let buffer = ""
124
- let result = null
125
-
126
- try {
127
- while (true) {
128
- const { done, value } = await reader.read()
129
- if (done) break
130
- buffer += decoder.decode(value, { stream: true })
131
- if (Buffer.byteLength(buffer, "utf8") > maxSseBufferBytes) {
132
- try { reader.releaseLock() } catch {}
133
- throw new McpError(
134
- `mcp server "${serverName}" SSE buffer exceeded ${maxSseBufferBytes} bytes`,
135
- { reason: "bad_response", server: serverName, phase: "request" }
136
- )
137
- }
138
-
139
- const parts = buffer.split("\n\n")
140
- buffer = parts.pop()
141
-
142
- for (const part of parts) {
143
- const event = parseSsePart(part)
144
- if (!event) continue
145
-
146
- try {
147
- const msg = JSON.parse(event.data)
148
- // Match our request ID
149
- if (msg.id === requestId) {
150
- if (msg.error) {
151
- throw new McpError(
152
- `mcp server "${serverName}" error: ${msg.error.message || JSON.stringify(msg.error)}`,
153
- { reason: "bad_response", server: serverName, code: msg.error.code, phase: "request" }
154
- )
155
- }
156
- result = msg.result ?? msg
157
- }
158
- // Server notifications emit as events
159
- if (!msg.id && msg.method) {
160
- EventBus.emit({
161
- type: EVENT_TYPES.MCP_REQUEST,
162
- payload: { server: serverName, action: `notification:${msg.method}`, notification: true }
163
- }).catch(() => {})
164
- }
165
- } catch (e) {
166
- if (e instanceof McpError) throw e
167
- // Non-JSON SSE data — skip
168
- }
169
- }
170
- }
171
- } finally {
172
- try { reader.releaseLock() } catch { /* reader may have pending read if stream was force-closed */ }
173
- }
174
-
175
- return result ?? {}
176
- }
177
-
178
- function parseSsePart(part) {
179
- const trimmed = part.trim()
180
- if (!trimmed) return null
181
- let event = null
182
- let data = ""
183
- for (const line of trimmed.split("\n")) {
184
- if (line.startsWith("event:")) event = line.slice(6).trim()
185
- else if (line.startsWith("data:")) data += line.slice(5).trim()
186
- }
187
- if (!data) return null
188
- return { event, data }
189
- }
190
-
191
- async function ensureInitialized() {
192
- if (initialized) return
193
- const result = await sendRequest("initialize", {
194
- protocolVersion: MCP_PROTOCOL_VERSION,
195
- capabilities: {},
196
- clientInfo: MCP_CLIENT_INFO
197
- })
198
- // Send initialized notification
199
- try {
200
- await fetch(baseUrl, {
201
- method: "POST",
202
- headers: buildHeaders(),
203
- body: JSON.stringify({ jsonrpc: "2.0", method: "notifications/initialized" }),
204
- signal: AbortSignal.timeout(timeoutMs)
205
- })
206
- } catch { /* best-effort */ }
207
- initialized = true
208
- return result
209
- }
210
-
211
- return {
212
- serverName,
213
- transport: "sse",
214
-
215
- async health() {
216
- try {
217
- await ensureInitialized()
218
- await sendRequest("ping")
219
- return { ok: true }
220
- } catch (error) {
221
- return { ok: false, error: error.message, reason: error.reason || "unknown" }
222
- }
223
- },
224
-
225
- async listTools() {
226
- await ensureInitialized()
227
- const out = await sendRequest("tools/list")
228
- return Array.isArray(out?.tools) ? out.tools : []
229
- },
230
-
231
- async listPrompts() {
232
- await ensureInitialized()
233
- try {
234
- const out = await sendRequest("prompts/list")
235
- return Array.isArray(out?.prompts) ? out.prompts : []
236
- } catch {
237
- return []
238
- }
239
- },
240
-
241
- async getPrompt(name, args = {}) {
242
- await ensureInitialized()
243
- return sendRequest("prompts/get", { name, arguments: args })
244
- },
245
-
246
- async listResources() {
247
- await ensureInitialized()
248
- try {
249
- const out = await sendRequest("resources/list")
250
- return Array.isArray(out?.resources) ? out.resources : []
251
- } catch {
252
- return []
253
- }
254
- },
255
-
256
- async listTemplates() {
257
- await ensureInitialized()
258
- try {
259
- const out = await sendRequest("resources/templates/list")
260
- return Array.isArray(out?.templates) ? out.templates : []
261
- } catch {
262
- return []
263
- }
264
- },
265
-
266
- async callTool(name, args = {}, signal = null) {
267
- await ensureInitialized()
268
- const result = await sendRequest("tools/call", { name, arguments: args }, { signal })
269
- return normalizeToolResult(result, serverName, name)
270
- },
271
-
272
- shutdown() {
273
- if (notificationStream) {
274
- try { notificationStream.cancel() } catch { /* ignore */ }
275
- notificationStream = null
276
- }
277
- // Send session termination if we have a session
278
- if (sessionId) {
279
- fetch(baseUrl, {
280
- method: "DELETE",
281
- headers: buildHeaders()
282
- }).catch(() => {})
283
- }
284
- sessionId = null
285
- initialized = false
286
- }
287
- }
288
- }
1
+ import { McpError } from "../core/errors.mjs"
2
+ import { EventBus } from "../core/events.mjs"
3
+ import { EVENT_TYPES } from "../core/constants.mjs"
4
+ import { normalizeToolResult } from "./tool-result.mjs"
5
+ import { MCP_PROTOCOL_VERSION, MCP_CLIENT_INFO } from "./constants.mjs"
6
+
7
+ /**
8
+ * MCP Streamable HTTP (SSE) client.
9
+ *
10
+ * Protocol: JSON-RPC 2.0 over HTTP POST with optional SSE response streaming.
11
+ * - POST to endpoint: send JSON-RPC request, receive JSON or SSE stream
12
+ * - GET to endpoint: open persistent SSE stream for server-initiated notifications
13
+ * - Session management via Mcp-Session-Id header
14
+ */
15
+ export function createSseMcpClient(serverName, config) {
16
+ const baseUrl = String(config.url || "").replace(/\/$/, "")
17
+ const timeoutMs = Number(config.timeout_ms || 30000)
18
+ const headers = config.headers || {}
19
+
20
+ let sessionId = null
21
+ let nextId = 1
22
+ let initialized = false
23
+
24
+ function buildHeaders(extra = {}) {
25
+ const h = {
26
+ "content-type": "application/json",
27
+ accept: "application/json, text/event-stream",
28
+ ...headers,
29
+ ...extra
30
+ }
31
+ if (sessionId) h["mcp-session-id"] = sessionId
32
+ return h
33
+ }
34
+
35
+ async function sendRequest(method, params = {}, { signal: parentSignal = null } = {}) {
36
+ if (nextId > Number.MAX_SAFE_INTEGER - 1) nextId = 1
37
+ const id = nextId++
38
+ const body = { jsonrpc: "2.0", id, method, params }
39
+ const startedAt = Date.now()
40
+
41
+ const timeoutSignal = AbortSignal.timeout(timeoutMs)
42
+ const combinedSignal = parentSignal
43
+ ? AbortSignal.any([parentSignal, timeoutSignal])
44
+ : timeoutSignal
45
+
46
+ try {
47
+ const res = await fetch(baseUrl, {
48
+ method: "POST",
49
+ headers: buildHeaders(),
50
+ body: JSON.stringify(body),
51
+ signal: combinedSignal
52
+ })
53
+
54
+ const elapsed = Date.now() - startedAt
55
+
56
+ // Capture session ID from response
57
+ const newSessionId = res.headers.get("mcp-session-id")
58
+ if (newSessionId) sessionId = newSessionId
59
+
60
+ EventBus.emit({
61
+ type: EVENT_TYPES.MCP_REQUEST,
62
+ payload: { server: serverName, action: method, elapsed, status: res.status }
63
+ }).catch(() => {})
64
+
65
+ if (!res.ok) {
66
+ const text = await res.text().catch(() => "")
67
+ throw new McpError(
68
+ `mcp server "${serverName}" HTTP ${res.status}: ${text.slice(0, 500)}`,
69
+ {
70
+ reason: res.status >= 500 ? "server_crash" : "bad_response",
71
+ server: serverName,
72
+ action: method,
73
+ phase: "request",
74
+ statusCode: res.status
75
+ }
76
+ )
77
+ }
78
+
79
+ const contentType = res.headers.get("content-type") || ""
80
+
81
+ // SSE response — parse events and return the final result
82
+ if (contentType.includes("text/event-stream")) {
83
+ return await parseSseResponse(res.body, id)
84
+ }
85
+
86
+ // Regular JSON response
87
+ const json = await res.json().catch((parseErr) => {
88
+ throw new McpError(
89
+ `mcp server "${serverName}" malformed JSON in ${method} response: ${parseErr.message}`,
90
+ { reason: "bad_response", server: serverName, action: method, phase: "request" }
91
+ )
92
+ })
93
+ if (json.error) {
94
+ throw new McpError(
95
+ `mcp server "${serverName}" error: ${json.error.message || JSON.stringify(json.error)}`,
96
+ { reason: "bad_response", server: serverName, action: method, code: json.error.code, phase: "request" }
97
+ )
98
+ }
99
+ return json.result ?? json
100
+ } catch (error) {
101
+ if (error instanceof McpError) throw error
102
+ const reason = (error.name === "AbortError" || error.name === "TimeoutError") ? "timeout" : "connection_refused"
103
+ throw new McpError(
104
+ `mcp server "${serverName}" ${reason}: ${error.message}`,
105
+ { reason, server: serverName, action: method, phase: "request" }
106
+ )
107
+ }
108
+ }
109
+
110
+ const maxSseBufferBytes = Number(config.max_sse_buffer_bytes || 4 * 1024 * 1024)
111
+
112
+ async function parseSseResponse(body, requestId) {
113
+ const reader = body.getReader()
114
+ const decoder = new TextDecoder()
115
+ let buffer = ""
116
+ let result = null
117
+
118
+ try {
119
+ while (true) {
120
+ const { done, value } = await reader.read()
121
+ if (done) break
122
+ buffer += decoder.decode(value, { stream: true })
123
+ if (Buffer.byteLength(buffer, "utf8") > maxSseBufferBytes) {
124
+ try { reader.releaseLock() } catch {}
125
+ throw new McpError(
126
+ `mcp server "${serverName}" SSE buffer exceeded ${maxSseBufferBytes} bytes`,
127
+ { reason: "bad_response", server: serverName, phase: "request" }
128
+ )
129
+ }
130
+
131
+ const parts = buffer.split("\n\n")
132
+ buffer = parts.pop()
133
+
134
+ for (const part of parts) {
135
+ const event = parseSsePart(part)
136
+ if (!event) continue
137
+
138
+ try {
139
+ const msg = JSON.parse(event.data)
140
+ // Match our request ID
141
+ if (msg.id === requestId) {
142
+ if (msg.error) {
143
+ throw new McpError(
144
+ `mcp server "${serverName}" error: ${msg.error.message || JSON.stringify(msg.error)}`,
145
+ { reason: "bad_response", server: serverName, code: msg.error.code, phase: "request" }
146
+ )
147
+ }
148
+ result = msg.result ?? msg
149
+ }
150
+ // Server notifications — emit as events
151
+ if (!msg.id && msg.method) {
152
+ EventBus.emit({
153
+ type: EVENT_TYPES.MCP_REQUEST,
154
+ payload: { server: serverName, action: `notification:${msg.method}`, notification: true }
155
+ }).catch(() => {})
156
+ }
157
+ } catch (e) {
158
+ if (e instanceof McpError) throw e
159
+ // Non-JSON SSE data — skip
160
+ }
161
+ }
162
+ }
163
+ } finally {
164
+ try { await reader.cancel() } catch { /* stream may already be closed */ }
165
+ try { reader.releaseLock() } catch { /* reader may have pending read if stream was force-closed */ }
166
+ }
167
+
168
+ if (result !== null) return result
169
+ throw new McpError(
170
+ `mcp server "${serverName}" SSE stream ended without matching response for request ${requestId}`,
171
+ { reason: "bad_response", server: serverName, phase: "request" }
172
+ )
173
+ }
174
+
175
+ function parseSsePart(part) {
176
+ const trimmed = part.trim()
177
+ if (!trimmed) return null
178
+ let event = null
179
+ let data = ""
180
+ for (const line of trimmed.split("\n")) {
181
+ if (line.startsWith("event:")) event = line.slice(6).trim()
182
+ else if (line.startsWith("data:")) {
183
+ const raw = line.slice(5)
184
+ data += (data ? "\n" : "") + (raw.startsWith(" ") ? raw.slice(1) : raw)
185
+ }
186
+ }
187
+ if (!data) return null
188
+ return { event, data }
189
+ }
190
+
191
+ let initPromise = null
192
+
193
+ async function ensureInitialized() {
194
+ if (initialized) return
195
+ if (initPromise) return initPromise
196
+ initPromise = (async () => {
197
+ try {
198
+ const result = await sendRequest("initialize", {
199
+ protocolVersion: MCP_PROTOCOL_VERSION,
200
+ capabilities: {},
201
+ clientInfo: MCP_CLIENT_INFO
202
+ })
203
+ // Send initialized notification
204
+ try {
205
+ await fetch(baseUrl, {
206
+ method: "POST",
207
+ headers: buildHeaders(),
208
+ body: JSON.stringify({ jsonrpc: "2.0", method: "notifications/initialized" }),
209
+ signal: AbortSignal.timeout(timeoutMs)
210
+ })
211
+ } catch { /* best-effort */ }
212
+ initialized = true
213
+ return result
214
+ } catch (err) {
215
+ initialized = false
216
+ throw err
217
+ } finally {
218
+ initPromise = null
219
+ }
220
+ })()
221
+ return initPromise
222
+ }
223
+
224
+ return {
225
+ serverName,
226
+ transport: "sse",
227
+
228
+ async health() {
229
+ try {
230
+ await ensureInitialized()
231
+ await sendRequest("ping")
232
+ return { ok: true }
233
+ } catch (error) {
234
+ return { ok: false, error: error.message, reason: error.reason || "unknown" }
235
+ }
236
+ },
237
+
238
+ async listTools() {
239
+ await ensureInitialized()
240
+ const out = await sendRequest("tools/list")
241
+ return Array.isArray(out?.tools) ? out.tools : []
242
+ },
243
+
244
+ async listPrompts() {
245
+ await ensureInitialized()
246
+ try {
247
+ const out = await sendRequest("prompts/list")
248
+ return Array.isArray(out?.prompts) ? out.prompts : []
249
+ } catch {
250
+ return []
251
+ }
252
+ },
253
+
254
+ async getPrompt(name, args = {}) {
255
+ await ensureInitialized()
256
+ return sendRequest("prompts/get", { name, arguments: args })
257
+ },
258
+
259
+ async listResources() {
260
+ await ensureInitialized()
261
+ try {
262
+ const out = await sendRequest("resources/list")
263
+ return Array.isArray(out?.resources) ? out.resources : []
264
+ } catch {
265
+ return []
266
+ }
267
+ },
268
+
269
+ async listTemplates() {
270
+ await ensureInitialized()
271
+ try {
272
+ const out = await sendRequest("resources/templates/list")
273
+ return Array.isArray(out?.templates) ? out.templates : []
274
+ } catch {
275
+ return []
276
+ }
277
+ },
278
+
279
+ async callTool(name, args = {}, signal = null) {
280
+ await ensureInitialized()
281
+ const result = await sendRequest("tools/call", { name, arguments: args }, { signal })
282
+ return normalizeToolResult(result, serverName, name)
283
+ },
284
+
285
+ shutdown() {
286
+ // Send session termination if we have a session
287
+ if (sessionId) {
288
+ fetch(baseUrl, {
289
+ method: "DELETE",
290
+ headers: buildHeaders()
291
+ }).catch(() => {})
292
+ }
293
+ sessionId = null
294
+ initialized = false
295
+ }
296
+ }
297
+ }