@kkelly-offical/kkcode 0.1.7 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (163) hide show
  1. package/LICENSE +674 -674
  2. package/README.md +452 -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/config/defaults.mjs +280 -260
  29. package/src/config/import-config.mjs +1 -1
  30. package/src/config/load-config.mjs +61 -4
  31. package/src/config/schema.mjs +591 -574
  32. package/src/context.mjs +4 -1
  33. package/src/core/constants.mjs +97 -91
  34. package/src/core/types.mjs +1 -1
  35. package/src/github/api.mjs +78 -78
  36. package/src/github/auth.mjs +294 -286
  37. package/src/github/flow.mjs +298 -298
  38. package/src/github/workspace.mjs +225 -212
  39. package/src/index.mjs +84 -82
  40. package/src/knowledge/frontend-aesthetics.txt +38 -38
  41. package/src/mcp/client-http.mjs +139 -141
  42. package/src/mcp/client-sse.mjs +297 -288
  43. package/src/mcp/client-stdio.mjs +534 -533
  44. package/src/mcp/constants.mjs +2 -2
  45. package/src/mcp/registry.mjs +498 -479
  46. package/src/mcp/stdio-framing.mjs +135 -133
  47. package/src/mcp/tool-result.mjs +24 -24
  48. package/src/observability/edit-diagnostics.mjs +449 -0
  49. package/src/observability/index.mjs +42 -42
  50. package/src/observability/metrics.mjs +165 -137
  51. package/src/observability/tracer.mjs +137 -137
  52. package/src/onboarding.mjs +209 -0
  53. package/src/orchestration/background-manager.mjs +567 -372
  54. package/src/orchestration/background-worker.mjs +419 -305
  55. package/src/orchestration/interruption-reason.mjs +21 -0
  56. package/src/orchestration/longagent-manager.mjs +197 -171
  57. package/src/orchestration/stage-scheduler.mjs +733 -728
  58. package/src/orchestration/subagent-router.mjs +7 -1
  59. package/src/orchestration/task-scheduler.mjs +219 -7
  60. package/src/permission/engine.mjs +1 -1
  61. package/src/permission/exec-policy.mjs +370 -370
  62. package/src/permission/file-edit-policy.mjs +108 -0
  63. package/src/permission/prompt.mjs +1 -1
  64. package/src/permission/rules.mjs +116 -7
  65. package/src/plugin/builtin-hooks/post-edit-format.mjs +2 -1
  66. package/src/plugin/builtin-hooks/post-edit-typecheck.mjs +104 -40
  67. package/src/plugin/hook-bus.mjs +19 -5
  68. package/src/plugin/manifest-loader.mjs +222 -0
  69. package/src/provider/anthropic.mjs +396 -390
  70. package/src/provider/ollama.mjs +7 -1
  71. package/src/provider/openai.mjs +382 -340
  72. package/src/provider/retry-policy.mjs +74 -68
  73. package/src/provider/router.mjs +242 -241
  74. package/src/provider/sse.mjs +104 -104
  75. package/src/provider/wizard.mjs +556 -0
  76. package/src/repl/capability-facade.mjs +30 -0
  77. package/src/repl/command-surface.mjs +23 -0
  78. package/src/repl/controller-entry.mjs +40 -0
  79. package/src/repl/core-shell.mjs +208 -0
  80. package/src/repl/dialog-router.mjs +87 -0
  81. package/src/repl/input-engine.mjs +76 -0
  82. package/src/repl/keymap.mjs +7 -0
  83. package/src/repl/operator-surface.mjs +15 -0
  84. package/src/repl/permission-flow.mjs +49 -0
  85. package/src/repl/runtime-facade.mjs +36 -0
  86. package/src/repl/slash-router.mjs +62 -0
  87. package/src/repl/state-store.mjs +29 -0
  88. package/src/repl/turn-controller.mjs +58 -0
  89. package/src/repl/verification.mjs +23 -0
  90. package/src/repl.mjs +3368 -2981
  91. package/src/rules/load-rules.mjs +3 -3
  92. package/src/runtime.mjs +1 -1
  93. package/src/session/agent-transaction.mjs +86 -0
  94. package/src/session/checkpoint.mjs +302 -302
  95. package/src/session/compaction.mjs +298 -298
  96. package/src/session/engine.mjs +417 -232
  97. package/src/session/longagent-4stage.mjs +467 -460
  98. package/src/session/longagent-hybrid.mjs +1344 -1097
  99. package/src/session/longagent-plan.mjs +376 -365
  100. package/src/session/longagent-project-memory.mjs +53 -53
  101. package/src/session/longagent-scaffold.mjs +291 -291
  102. package/src/session/longagent-task-bus.mjs +138 -54
  103. package/src/session/longagent-utils.mjs +828 -472
  104. package/src/session/longagent.mjs +911 -900
  105. package/src/session/loop.mjs +1005 -930
  106. package/src/session/prompt/agent.txt +25 -25
  107. package/src/session/prompt/anthropic.txt +150 -150
  108. package/src/session/prompt/beast.txt +1 -1
  109. package/src/session/prompt/plan.txt +31 -31
  110. package/src/session/prompt/qwen.txt +46 -46
  111. package/src/session/recovery.mjs +21 -0
  112. package/src/session/rollback.mjs +196 -195
  113. package/src/session/routing-observability.mjs +72 -0
  114. package/src/session/runtime-state.mjs +47 -0
  115. package/src/session/store.mjs +523 -519
  116. package/src/session/system-prompt.mjs +308 -273
  117. package/src/session/task-validator.mjs +267 -267
  118. package/src/session/usability-gates.mjs +2 -2
  119. package/src/skill/builtin/commit.mjs +64 -64
  120. package/src/skill/builtin/design.mjs +76 -76
  121. package/src/skill/generator.mjs +18 -2
  122. package/src/skill/registry.mjs +642 -390
  123. package/src/storage/audit-store.mjs +18 -11
  124. package/src/storage/event-log.mjs +7 -1
  125. package/src/storage/ghost-commit-store.mjs +243 -245
  126. package/src/storage/paths.mjs +13 -0
  127. package/src/theme/default-theme.mjs +1 -1
  128. package/src/theme/markdown.mjs +4 -0
  129. package/src/theme/schema.mjs +1 -1
  130. package/src/theme/status-bar.mjs +162 -158
  131. package/src/tool/audit-wrapper.mjs +18 -2
  132. package/src/tool/edit-transaction.mjs +23 -0
  133. package/src/tool/executor.mjs +26 -1
  134. package/src/tool/file-read-state.mjs +65 -0
  135. package/src/tool/git-auto.mjs +526 -526
  136. package/src/tool/git-full-auto.mjs +487 -478
  137. package/src/tool/mutation-guard.mjs +54 -0
  138. package/src/tool/prompt/edit.txt +3 -3
  139. package/src/tool/prompt/multiedit.txt +1 -0
  140. package/src/tool/prompt/notebookedit.txt +2 -1
  141. package/src/tool/prompt/patch.txt +25 -24
  142. package/src/tool/prompt/read.txt +3 -3
  143. package/src/tool/prompt/sysinfo.txt +29 -0
  144. package/src/tool/prompt/task.txt +66 -4
  145. package/src/tool/prompt/write.txt +2 -2
  146. package/src/tool/question-prompt.mjs +99 -93
  147. package/src/tool/registry.mjs +1701 -1343
  148. package/src/tool/task-tool.mjs +14 -6
  149. package/src/ui/activity-renderer.mjs +667 -664
  150. package/src/ui/repl-background-panel.mjs +7 -0
  151. package/src/ui/repl-capability-panel.mjs +9 -0
  152. package/src/ui/repl-dashboard.mjs +54 -4
  153. package/src/ui/repl-help.mjs +110 -0
  154. package/src/ui/repl-operator-panel.mjs +12 -0
  155. package/src/ui/repl-route-feedback.mjs +35 -0
  156. package/src/ui/repl-status-view.mjs +76 -0
  157. package/src/ui/repl-task-panel.mjs +5 -0
  158. package/src/ui/repl-transcript-panel.mjs +56 -0
  159. package/src/ui/repl-turn-summary.mjs +135 -0
  160. package/src/usage/pricing.mjs +122 -121
  161. package/src/usage/usage-meter.mjs +1 -0
  162. package/src/util/git.mjs +562 -519
  163. package/src/util/template.mjs +6 -1
@@ -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
+ }