@namzu/sdk 39.0.0 → 41.0.0

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 (289) hide show
  1. package/CHANGELOG.md +328 -0
  2. package/dist/bridge/a2a/mapper.d.ts.map +1 -1
  3. package/dist/bridge/a2a/mapper.js +8 -0
  4. package/dist/bridge/a2a/mapper.js.map +1 -1
  5. package/dist/bridge/sse/mapper.d.ts.map +1 -1
  6. package/dist/bridge/sse/mapper.js +11 -0
  7. package/dist/bridge/sse/mapper.js.map +1 -1
  8. package/dist/connector/index.d.ts +2 -2
  9. package/dist/connector/index.d.ts.map +1 -1
  10. package/dist/connector/index.js +1 -1
  11. package/dist/connector/index.js.map +1 -1
  12. package/dist/connector/mcp/adapter.d.ts.map +1 -1
  13. package/dist/connector/mcp/adapter.js +112 -10
  14. package/dist/connector/mcp/adapter.js.map +1 -1
  15. package/dist/connector/mcp/audio-admission.d.ts +17 -0
  16. package/dist/connector/mcp/audio-admission.d.ts.map +1 -0
  17. package/dist/connector/mcp/audio-admission.js +171 -0
  18. package/dist/connector/mcp/audio-admission.js.map +1 -0
  19. package/dist/connector/mcp/client.d.ts +252 -1
  20. package/dist/connector/mcp/client.d.ts.map +1 -1
  21. package/dist/connector/mcp/client.js +611 -39
  22. package/dist/connector/mcp/client.js.map +1 -1
  23. package/dist/connector/mcp/envelope.d.ts +91 -0
  24. package/dist/connector/mcp/envelope.d.ts.map +1 -0
  25. package/dist/connector/mcp/envelope.js +173 -0
  26. package/dist/connector/mcp/envelope.js.map +1 -0
  27. package/dist/connector/mcp/era.d.ts +130 -0
  28. package/dist/connector/mcp/era.d.ts.map +1 -0
  29. package/dist/connector/mcp/era.js +304 -0
  30. package/dist/connector/mcp/era.js.map +1 -0
  31. package/dist/connector/mcp/errors.d.ts +106 -0
  32. package/dist/connector/mcp/errors.d.ts.map +1 -0
  33. package/dist/connector/mcp/errors.js +154 -0
  34. package/dist/connector/mcp/errors.js.map +1 -0
  35. package/dist/connector/mcp/http-sse.d.ts +11 -0
  36. package/dist/connector/mcp/http-sse.d.ts.map +1 -1
  37. package/dist/connector/mcp/http-sse.js +21 -6
  38. package/dist/connector/mcp/http-sse.js.map +1 -1
  39. package/dist/connector/mcp/index.d.ts +7 -0
  40. package/dist/connector/mcp/index.d.ts.map +1 -1
  41. package/dist/connector/mcp/index.js +10 -0
  42. package/dist/connector/mcp/index.js.map +1 -1
  43. package/dist/connector/mcp/streamable-http.d.ts +83 -0
  44. package/dist/connector/mcp/streamable-http.d.ts.map +1 -1
  45. package/dist/connector/mcp/streamable-http.js +177 -11
  46. package/dist/connector/mcp/streamable-http.js.map +1 -1
  47. package/dist/connector/mcp/x-mcp-header.d.ts +56 -0
  48. package/dist/connector/mcp/x-mcp-header.d.ts.map +1 -0
  49. package/dist/connector/mcp/x-mcp-header.js +254 -0
  50. package/dist/connector/mcp/x-mcp-header.js.map +1 -0
  51. package/dist/constants/mcp/index.d.ts +123 -15
  52. package/dist/constants/mcp/index.d.ts.map +1 -1
  53. package/dist/constants/mcp/index.js +135 -16
  54. package/dist/constants/mcp/index.js.map +1 -1
  55. package/dist/manager/agent/lifecycle.d.ts.map +1 -1
  56. package/dist/manager/agent/lifecycle.js +23 -0
  57. package/dist/manager/agent/lifecycle.js.map +1 -1
  58. package/dist/manager/run/persistence.d.ts +8 -0
  59. package/dist/manager/run/persistence.d.ts.map +1 -1
  60. package/dist/manager/run/persistence.js +12 -0
  61. package/dist/manager/run/persistence.js.map +1 -1
  62. package/dist/prompt/coding-agent-doctrine.d.ts +20 -0
  63. package/dist/prompt/coding-agent-doctrine.d.ts.map +1 -1
  64. package/dist/prompt/coding-agent-doctrine.js +19 -3
  65. package/dist/prompt/coding-agent-doctrine.js.map +1 -1
  66. package/dist/prompt/index.d.ts +1 -1
  67. package/dist/prompt/index.d.ts.map +1 -1
  68. package/dist/prompt/index.js +1 -1
  69. package/dist/prompt/index.js.map +1 -1
  70. package/dist/public-runtime.d.ts +6 -4
  71. package/dist/public-runtime.d.ts.map +1 -1
  72. package/dist/public-runtime.js +8 -4
  73. package/dist/public-runtime.js.map +1 -1
  74. package/dist/public-tools.d.ts +11 -0
  75. package/dist/public-tools.d.ts.map +1 -1
  76. package/dist/public-tools.js +14 -0
  77. package/dist/public-tools.js.map +1 -1
  78. package/dist/registry/tool/execute.d.ts.map +1 -1
  79. package/dist/registry/tool/execute.js +2 -3
  80. package/dist/registry/tool/execute.js.map +1 -1
  81. package/dist/registry/tool/portable.d.ts +65 -0
  82. package/dist/registry/tool/portable.d.ts.map +1 -0
  83. package/dist/registry/tool/portable.js +244 -0
  84. package/dist/registry/tool/portable.js.map +1 -0
  85. package/dist/registry/tool/schema.d.ts +32 -5
  86. package/dist/registry/tool/schema.d.ts.map +1 -1
  87. package/dist/registry/tool/schema.js +35 -9
  88. package/dist/registry/tool/schema.js.map +1 -1
  89. package/dist/registry/toolset/catalog.js +8 -8
  90. package/dist/registry/toolset/catalog.js.map +1 -1
  91. package/dist/runtime/jobs/awaited-jobs.d.ts +215 -0
  92. package/dist/runtime/jobs/awaited-jobs.d.ts.map +1 -0
  93. package/dist/runtime/jobs/awaited-jobs.js +259 -0
  94. package/dist/runtime/jobs/awaited-jobs.js.map +1 -0
  95. package/dist/runtime/jobs/registry.d.ts +33 -2
  96. package/dist/runtime/jobs/registry.d.ts.map +1 -1
  97. package/dist/runtime/jobs/registry.js +37 -0
  98. package/dist/runtime/jobs/registry.js.map +1 -1
  99. package/dist/runtime/query/executor.d.ts +28 -0
  100. package/dist/runtime/query/executor.d.ts.map +1 -1
  101. package/dist/runtime/query/executor.js +39 -1
  102. package/dist/runtime/query/executor.js.map +1 -1
  103. package/dist/runtime/query/file-evidence-context.d.ts.map +1 -1
  104. package/dist/runtime/query/file-evidence-context.js +159 -43
  105. package/dist/runtime/query/file-evidence-context.js.map +1 -1
  106. package/dist/runtime/query/file-evidence-replay.d.ts +260 -0
  107. package/dist/runtime/query/file-evidence-replay.d.ts.map +1 -0
  108. package/dist/runtime/query/file-evidence-replay.js +647 -0
  109. package/dist/runtime/query/file-evidence-replay.js.map +1 -0
  110. package/dist/runtime/query/file-evidence-seed.d.ts +50 -0
  111. package/dist/runtime/query/file-evidence-seed.d.ts.map +1 -0
  112. package/dist/runtime/query/file-evidence-seed.js +100 -0
  113. package/dist/runtime/query/file-evidence-seed.js.map +1 -0
  114. package/dist/runtime/query/index.d.ts.map +1 -1
  115. package/dist/runtime/query/index.js +94 -2
  116. package/dist/runtime/query/index.js.map +1 -1
  117. package/dist/runtime/query/iteration/index.d.ts +87 -9
  118. package/dist/runtime/query/iteration/index.d.ts.map +1 -1
  119. package/dist/runtime/query/iteration/index.js +193 -28
  120. package/dist/runtime/query/iteration/index.js.map +1 -1
  121. package/dist/runtime/query/iteration/phases/context.d.ts +10 -0
  122. package/dist/runtime/query/iteration/phases/context.d.ts.map +1 -1
  123. package/dist/runtime/query/iteration/phases/context.js.map +1 -1
  124. package/dist/runtime/query/iteration/phases/tool-review.d.ts.map +1 -1
  125. package/dist/runtime/query/iteration/phases/tool-review.js +5 -1
  126. package/dist/runtime/query/iteration/phases/tool-review.js.map +1 -1
  127. package/dist/runtime/query/plugin-hooks.d.ts +14 -0
  128. package/dist/runtime/query/plugin-hooks.d.ts.map +1 -1
  129. package/dist/runtime/query/plugin-hooks.js +18 -0
  130. package/dist/runtime/query/plugin-hooks.js.map +1 -1
  131. package/dist/runtime/query/repeat-call.d.ts +17 -4
  132. package/dist/runtime/query/repeat-call.d.ts.map +1 -1
  133. package/dist/runtime/query/repeat-call.js +26 -19
  134. package/dist/runtime/query/repeat-call.js.map +1 -1
  135. package/dist/runtime/query/steering.d.ts +11 -1
  136. package/dist/runtime/query/steering.d.ts.map +1 -1
  137. package/dist/runtime/query/steering.js +12 -1
  138. package/dist/runtime/query/steering.js.map +1 -1
  139. package/dist/runtime/query/tooling.d.ts +2 -0
  140. package/dist/runtime/query/tooling.d.ts.map +1 -1
  141. package/dist/runtime/query/tooling.js +1 -0
  142. package/dist/runtime/query/tooling.js.map +1 -1
  143. package/dist/sandbox/provider/local.d.ts.map +1 -1
  144. package/dist/sandbox/provider/local.js +46 -3
  145. package/dist/sandbox/provider/local.js.map +1 -1
  146. package/dist/scheduler/completion-inbox.d.ts +48 -2
  147. package/dist/scheduler/completion-inbox.d.ts.map +1 -1
  148. package/dist/scheduler/completion-inbox.js +102 -10
  149. package/dist/scheduler/completion-inbox.js.map +1 -1
  150. package/dist/scheduler/local.d.ts.map +1 -1
  151. package/dist/scheduler/local.js +8 -0
  152. package/dist/scheduler/local.js.map +1 -1
  153. package/dist/store/run/disk.d.ts +35 -1
  154. package/dist/store/run/disk.d.ts.map +1 -1
  155. package/dist/store/run/disk.js +100 -0
  156. package/dist/store/run/disk.js.map +1 -1
  157. package/dist/tools/builtins/bash.d.ts.map +1 -1
  158. package/dist/tools/builtins/bash.js +4 -10
  159. package/dist/tools/builtins/bash.js.map +1 -1
  160. package/dist/tools/builtins/edit-apply.d.ts +126 -0
  161. package/dist/tools/builtins/edit-apply.d.ts.map +1 -0
  162. package/dist/tools/builtins/edit-apply.js +360 -0
  163. package/dist/tools/builtins/edit-apply.js.map +1 -0
  164. package/dist/tools/builtins/edit.d.ts +143 -1
  165. package/dist/tools/builtins/edit.d.ts.map +1 -1
  166. package/dist/tools/builtins/edit.js +37 -219
  167. package/dist/tools/builtins/edit.js.map +1 -1
  168. package/dist/tools/builtins/index.d.ts +1 -0
  169. package/dist/tools/builtins/index.d.ts.map +1 -1
  170. package/dist/tools/builtins/index.js +9 -3
  171. package/dist/tools/builtins/index.js.map +1 -1
  172. package/dist/tools/builtins/job.js +1 -1
  173. package/dist/tools/builtins/job.js.map +1 -1
  174. package/dist/tools/builtins/read-file.d.ts +2 -2
  175. package/dist/tools/builtins/read-file.d.ts.map +1 -1
  176. package/dist/tools/builtins/read-file.js +50 -65
  177. package/dist/tools/builtins/read-file.js.map +1 -1
  178. package/dist/tools/builtins/read-render.d.ts +56 -0
  179. package/dist/tools/builtins/read-render.d.ts.map +1 -0
  180. package/dist/tools/builtins/read-render.js +73 -0
  181. package/dist/tools/builtins/read-render.js.map +1 -0
  182. package/dist/tools/builtins/wait-for-job-bounds.d.ts +67 -0
  183. package/dist/tools/builtins/wait-for-job-bounds.d.ts.map +1 -0
  184. package/dist/tools/builtins/wait-for-job-bounds.js +108 -0
  185. package/dist/tools/builtins/wait-for-job-bounds.js.map +1 -0
  186. package/dist/tools/builtins/wait-for-job.d.ts +6 -0
  187. package/dist/tools/builtins/wait-for-job.d.ts.map +1 -0
  188. package/dist/tools/builtins/wait-for-job.js +162 -0
  189. package/dist/tools/builtins/wait-for-job.js.map +1 -0
  190. package/dist/tools/builtins/write-file.js +5 -0
  191. package/dist/tools/builtins/write-file.js.map +1 -1
  192. package/dist/tools/coordinator/index.d.ts.map +1 -1
  193. package/dist/tools/coordinator/index.js +1 -7
  194. package/dist/tools/coordinator/index.js.map +1 -1
  195. package/dist/tools/file-read-tracker.d.ts.map +1 -1
  196. package/dist/tools/file-read-tracker.js +88 -10
  197. package/dist/tools/file-read-tracker.js.map +1 -1
  198. package/dist/types/agent/scheduler.d.ts +20 -0
  199. package/dist/types/agent/scheduler.d.ts.map +1 -1
  200. package/dist/types/agent/task.d.ts +20 -0
  201. package/dist/types/agent/task.d.ts.map +1 -1
  202. package/dist/types/connector/mcp.d.ts +205 -0
  203. package/dist/types/connector/mcp.d.ts.map +1 -1
  204. package/dist/types/message/index.d.ts +1 -1
  205. package/dist/types/message/index.d.ts.map +1 -1
  206. package/dist/types/message/index.js +2 -0
  207. package/dist/types/message/index.js.map +1 -1
  208. package/dist/types/run/entity.d.ts +13 -0
  209. package/dist/types/run/entity.d.ts.map +1 -1
  210. package/dist/types/run/events.d.ts +56 -0
  211. package/dist/types/run/events.d.ts.map +1 -1
  212. package/dist/types/run/events.js.map +1 -1
  213. package/dist/types/run/store.d.ts +41 -0
  214. package/dist/types/run/store.d.ts.map +1 -1
  215. package/dist/types/sandbox/index.d.ts +83 -15
  216. package/dist/types/sandbox/index.d.ts.map +1 -1
  217. package/dist/types/sandbox/index.js.map +1 -1
  218. package/dist/types/tool/index.d.ts +109 -0
  219. package/dist/types/tool/index.d.ts.map +1 -1
  220. package/dist/types/tool/index.js.map +1 -1
  221. package/dist/utils/env.d.ts +19 -0
  222. package/dist/utils/env.d.ts.map +1 -0
  223. package/dist/utils/env.js +25 -0
  224. package/dist/utils/env.js.map +1 -0
  225. package/package.json +1 -1
  226. package/src/bridge/a2a/mapper.ts +8 -0
  227. package/src/bridge/sse/mapper.ts +11 -0
  228. package/src/connector/index.ts +27 -0
  229. package/src/connector/mcp/adapter.ts +123 -10
  230. package/src/connector/mcp/audio-admission.ts +173 -0
  231. package/src/connector/mcp/client.ts +694 -45
  232. package/src/connector/mcp/envelope.ts +235 -0
  233. package/src/connector/mcp/era.ts +400 -0
  234. package/src/connector/mcp/errors.ts +171 -0
  235. package/src/connector/mcp/http-sse.ts +23 -6
  236. package/src/connector/mcp/index.ts +37 -0
  237. package/src/connector/mcp/streamable-http.ts +199 -11
  238. package/src/connector/mcp/x-mcp-header.ts +322 -0
  239. package/src/constants/mcp/index.ts +145 -16
  240. package/src/manager/agent/lifecycle.ts +29 -0
  241. package/src/manager/run/persistence.ts +12 -0
  242. package/src/prompt/coding-agent-doctrine.ts +31 -4
  243. package/src/prompt/index.ts +1 -0
  244. package/src/public-runtime.ts +37 -1
  245. package/src/public-tools.ts +18 -0
  246. package/src/registry/tool/execute.ts +2 -4
  247. package/src/registry/tool/portable.ts +264 -0
  248. package/src/registry/tool/schema.ts +38 -8
  249. package/src/registry/toolset/catalog.ts +8 -9
  250. package/src/runtime/jobs/awaited-jobs.ts +271 -0
  251. package/src/runtime/jobs/registry.ts +50 -0
  252. package/src/runtime/query/executor.ts +49 -1
  253. package/src/runtime/query/file-evidence-context.ts +190 -46
  254. package/src/runtime/query/file-evidence-replay.ts +776 -0
  255. package/src/runtime/query/file-evidence-seed.ts +126 -0
  256. package/src/runtime/query/index.ts +104 -2
  257. package/src/runtime/query/iteration/index.ts +202 -28
  258. package/src/runtime/query/iteration/phases/context.ts +10 -0
  259. package/src/runtime/query/iteration/phases/tool-review.ts +4 -0
  260. package/src/runtime/query/plugin-hooks.ts +20 -0
  261. package/src/runtime/query/repeat-call.ts +28 -18
  262. package/src/runtime/query/steering.ts +11 -0
  263. package/src/runtime/query/tooling.ts +3 -0
  264. package/src/sandbox/provider/local.ts +45 -2
  265. package/src/scheduler/completion-inbox.ts +105 -9
  266. package/src/scheduler/local.ts +8 -0
  267. package/src/store/run/disk.ts +108 -0
  268. package/src/tools/builtins/bash.ts +4 -10
  269. package/src/tools/builtins/edit-apply.ts +456 -0
  270. package/src/tools/builtins/edit.ts +39 -270
  271. package/src/tools/builtins/index.ts +9 -3
  272. package/src/tools/builtins/job.ts +1 -1
  273. package/src/tools/builtins/read-file.ts +56 -77
  274. package/src/tools/builtins/read-render.ts +104 -0
  275. package/src/tools/builtins/wait-for-job-bounds.ts +179 -0
  276. package/src/tools/builtins/wait-for-job.ts +184 -0
  277. package/src/tools/builtins/write-file.ts +5 -0
  278. package/src/tools/coordinator/index.ts +1 -7
  279. package/src/tools/file-read-tracker.ts +85 -7
  280. package/src/types/agent/scheduler.ts +21 -0
  281. package/src/types/agent/task.ts +21 -0
  282. package/src/types/connector/mcp.ts +205 -1
  283. package/src/types/message/index.ts +2 -0
  284. package/src/types/run/entity.ts +14 -0
  285. package/src/types/run/events.ts +56 -0
  286. package/src/types/run/store.ts +42 -0
  287. package/src/types/sandbox/index.ts +84 -15
  288. package/src/types/tool/index.ts +104 -0
  289. package/src/utils/env.ts +23 -0
@@ -0,0 +1,171 @@
1
+ import { RESOURCE_NOT_FOUND_CODES } from '../../constants/mcp/index.js'
2
+ import type { MCPInputRequest, MCPJsonRpcError } from '../../types/connector/index.js'
3
+
4
+ /**
5
+ * The 2026-07-28 error codes a modern MCP server answers with. Kept private
6
+ * to this module: callers narrow on the predicates below rather than
7
+ * comparing magic numbers, so era negotiation, header recovery and the
8
+ * capability path never need to know the numbers themselves.
9
+ */
10
+ const UNSUPPORTED_PROTOCOL_VERSION_CODE = -32022
11
+ const MISSING_REQUIRED_CLIENT_CAPABILITY_CODE = -32021
12
+ const HEADER_MISMATCH_CODE = -32020
13
+
14
+ /**
15
+ * A JSON-RPC error reply from an MCP peer, with its `code` and `data`
16
+ * preserved.
17
+ *
18
+ * `MCPClient.handleMessage` used to flatten every error reply into
19
+ * `new Error('MCP error {code}: {message}')`, which threw away the one
20
+ * thing a caller needs to react to it programmatically. Era negotiation
21
+ * reads `code === -32022` and `data.supported`; header recovery reads
22
+ * `-32020`; the capability path reads `-32021` and `data.requiredCapabilities`.
23
+ * None of that is recoverable from a formatted string.
24
+ *
25
+ * A subclass of `Error`, never a replacement: every existing catch site that
26
+ * treats the rejection as a plain `Error` keeps working unchanged, and
27
+ * `.message` keeps its original `MCP error {code}: {message}` text so no
28
+ * existing log line or string-matching test is disturbed.
29
+ */
30
+ export class MCPProtocolError extends Error {
31
+ readonly code: number
32
+ readonly data?: unknown
33
+
34
+ constructor(code: number, message: string, data?: unknown) {
35
+ super(`MCP error ${code}: ${message}`)
36
+ this.name = 'MCPProtocolError'
37
+ this.code = code
38
+ this.data = data
39
+ }
40
+ }
41
+
42
+ /**
43
+ * A JSON-RPC error reply whose `code` was missing or not an integer.
44
+ *
45
+ * A well-formed peer never sends this; a malformed one must not be mistaken
46
+ * for a recognized protocol error, because era negotiation and the recovery
47
+ * paths key their fallback behaviour on exactly which modern error code (or
48
+ * none) came back. Named distinctly from `MCPProtocolError` so that no
49
+ * `is*Error` predicate below can ever match it.
50
+ */
51
+ export class MCPMalformedErrorReplyError extends Error {
52
+ constructor(receivedCode: unknown, message: unknown) {
53
+ super(`MCP error reply had a malformed code (${JSON.stringify(receivedCode)}): ${message}`)
54
+ this.name = 'MCPMalformedErrorReplyError'
55
+ }
56
+ }
57
+
58
+ /**
59
+ * An HTTP response that was not a success, with the body it carried.
60
+ *
61
+ * The body is the reason this type exists. A status alone cannot tell a
62
+ * legacy origin apart from a modern one: a modern server answers an unknown
63
+ * method with `404` and a JSON-RPC `-32601` body specifically so a client
64
+ * can distinguish it from the `404` of a server that has never heard of the
65
+ * modern protocol. Discarding the body — which this transport used to do —
66
+ * makes that distinction unreachable and turns every `404` into a fallback.
67
+ *
68
+ * `.message` is unchanged from the plain `Error` this replaces, so existing
69
+ * logs and assertions that match on the text are undisturbed.
70
+ */
71
+ export class MCPHttpStatusError extends Error {
72
+ readonly status: number
73
+ readonly statusText: string
74
+ readonly bodyText: string
75
+
76
+ constructor(where: string, status: number, statusText: string, bodyText: string) {
77
+ super(`${where}: HTTP ${status}: ${statusText}`)
78
+ this.name = 'MCPHttpStatusError'
79
+ this.status = status
80
+ this.statusText = statusText
81
+ this.bodyText = bodyText
82
+ }
83
+ }
84
+
85
+ /**
86
+ * Build the rejection reason for a JSON-RPC error reply.
87
+ *
88
+ * The wire message is cast to `MCPJsonRpcMessage` at the transport boundary
89
+ * without validation (see `JSON.parse(...) as MCPJsonRpcMessage` in
90
+ * `stdio.ts`, `http-sse.ts`, `streamable-http.ts`), so `error.code` is only
91
+ * a `number` by declared type — a misbehaving peer can still send anything.
92
+ * Guarding here, once, keeps that distrust out of `handleMessage`.
93
+ */
94
+ export function protocolErrorFromReply(error: MCPJsonRpcError): Error {
95
+ const code: unknown = error.code
96
+ if (typeof code !== 'number' || !Number.isInteger(code)) {
97
+ return new MCPMalformedErrorReplyError(code, error.message)
98
+ }
99
+ return new MCPProtocolError(code, error.message, error.data)
100
+ }
101
+
102
+ /** A modern server answered that it does not speak the requested protocol version. */
103
+ export function isUnsupportedProtocolVersionError(error: unknown): error is MCPProtocolError {
104
+ return error instanceof MCPProtocolError && error.code === UNSUPPORTED_PROTOCOL_VERSION_CODE
105
+ }
106
+
107
+ /** A modern server answered that this client lacks a capability the request required. */
108
+ export function isMissingRequiredClientCapabilityError(error: unknown): error is MCPProtocolError {
109
+ return error instanceof MCPProtocolError && error.code === MISSING_REQUIRED_CLIENT_CAPABILITY_CODE
110
+ }
111
+
112
+ /** A modern server rejected a request's `Mcp-Param-*` headers as stale against its current schema. */
113
+ export function isHeaderMismatchError(error: unknown): error is MCPProtocolError {
114
+ return error instanceof MCPProtocolError && error.code === HEADER_MISMATCH_CODE
115
+ }
116
+
117
+ /**
118
+ * A server answered that the resource, prompt or tool a request named does
119
+ * not exist — the current spec's `-32002`, or the older `-32602` a
120
+ * pre-2025-06-18 server may still use for the same condition (see
121
+ * {@link RESOURCE_NOT_FOUND_CODES}).
122
+ */
123
+ export function isResourceNotFoundError(error: unknown): error is MCPProtocolError {
124
+ return error instanceof MCPProtocolError && RESOURCE_NOT_FOUND_CODES.includes(error.code)
125
+ }
126
+
127
+ /**
128
+ * A result named a `resultType` this client does not recognize.
129
+ *
130
+ * The spec's own words: "A resultType of any value unrecognized by the
131
+ * client MUST be considered invalid." Accepting it silently would mean
132
+ * reading fields from a shape a future revision defines as if they meant
133
+ * what they mean today, or finding none and returning an empty result with
134
+ * no diagnostic. Refusing it is the only reading that MUST leaves open.
135
+ */
136
+ export class MCPInvalidResultTypeError extends Error {
137
+ readonly resultType: unknown
138
+
139
+ constructor(resultType: unknown) {
140
+ super(`MCP result carried an unrecognized resultType: ${JSON.stringify(resultType)}`)
141
+ this.name = 'MCPInvalidResultTypeError'
142
+ this.resultType = resultType
143
+ }
144
+ }
145
+
146
+ /**
147
+ * A tool call asked this client for input it has no way to supply.
148
+ *
149
+ * namzu declares `clientCapabilities: {}`, so MRTR rule 7 means a
150
+ * CONFORMING server never sends an `inputRequests` this client did not
151
+ * declare support for. This is the defensive path: a non-conforming
152
+ * server's demand, or a second `input_required` after the one automatic
153
+ * retry the spec allows for the `requestState`-only case. Carries the raw
154
+ * `inputRequests` so a caller — `mcpToolToToolDefinition`'s `execute`, or a
155
+ * host calling `MCPClient.callTool` directly — can name what was asked for
156
+ * without this class knowing what a `ToolResult` is.
157
+ */
158
+ export class MCPInputRequiredError extends Error {
159
+ readonly inputRequests: readonly MCPInputRequest[]
160
+
161
+ constructor(inputRequests: readonly MCPInputRequest[]) {
162
+ const methods = inputRequests.map((request) => request.method)
163
+ super(
164
+ methods.length > 0
165
+ ? `MCP tool call requires input this client cannot supply: ${methods.join(', ')}`
166
+ : 'MCP tool call requires input this client cannot supply',
167
+ )
168
+ this.name = 'MCPInputRequiredError'
169
+ this.inputRequests = inputRequests
170
+ }
171
+ }
@@ -1,4 +1,5 @@
1
1
  import type {
2
+ MCPFetchLike,
2
3
  MCPHttpSseTransportConfig,
3
4
  MCPJsonRpcMessage,
4
5
  MCPTransport,
@@ -38,6 +39,8 @@ export class HttpSseTransport implements MCPTransport {
38
39
  private postUrl: string
39
40
  private log: Logger
40
41
  private readonly timeoutMs: number
42
+ /** Defaults to the ambient global `fetch`; never read again once captured. */
43
+ private readonly fetchImpl: MCPFetchLike
41
44
 
42
45
  constructor(
43
46
  private readonly config: MCPHttpSseTransportConfig,
@@ -50,6 +53,7 @@ export class HttpSseTransport implements MCPTransport {
50
53
  config.timeoutMs ?? DEFAULT_TIMEOUT_MS,
51
54
  'HttpSseTransport timeoutMs',
52
55
  )
56
+ this.fetchImpl = config.fetch ?? fetch
53
57
  this.log = resolveLogger(log).child({ [SCOPE_ATTRIBUTE]: 'connector/mcp/http-sse' })
54
58
  }
55
59
 
@@ -105,12 +109,9 @@ export class HttpSseTransport implements MCPTransport {
105
109
 
106
110
  try {
107
111
  const response = await operation.run(() =>
108
- fetch(this.postUrl, {
112
+ this.fetchImpl(this.postUrl, {
109
113
  method: 'POST',
110
- headers: {
111
- 'Content-Type': 'application/json',
112
- ...this.config.headers,
113
- },
114
+ headers: this.buildHeaders(options?.headers),
114
115
  body: JSON.stringify(message),
115
116
  redirect: 'manual',
116
117
  signal: operation.signal,
@@ -153,6 +154,22 @@ export class HttpSseTransport implements MCPTransport {
153
154
  return this.connected
154
155
  }
155
156
 
157
+ /**
158
+ * `extra` comes from `MCPTransportSendOptions.headers` — the client's
159
+ * per-send authority — and is merged over this transport's own static
160
+ * config headers so a caller's value wins on a collision. See
161
+ * {@link StreamableHttpTransport}'s method of the same name, which this
162
+ * mirrors; the HTTP-SSE POST had no equivalent merge until now, so a
163
+ * per-request header or bearer token silently never reached the wire.
164
+ */
165
+ private buildHeaders(extra?: Readonly<Record<string, string>>): Record<string, string> {
166
+ return {
167
+ 'Content-Type': 'application/json',
168
+ ...this.config.headers,
169
+ ...extra,
170
+ }
171
+ }
172
+
156
173
  private beginSend(signal: AbortSignal | undefined): {
157
174
  readonly controller: AbortController
158
175
  readonly generation: number
@@ -200,7 +217,7 @@ export class HttpSseTransport implements MCPTransport {
200
217
  }
201
218
 
202
219
  private async listenSSE(generation: number, signal: AbortSignal): Promise<void> {
203
- const response = await fetch(this.sseUrl, {
220
+ const response = await this.fetchImpl(this.sseUrl, {
204
221
  headers: {
205
222
  Accept: 'text/event-stream',
206
223
  ...this.config.headers,
@@ -4,6 +4,43 @@ export { StreamableHttpTransport } from './streamable-http.js'
4
4
 
5
5
  export { MCPClient } from './client.js'
6
6
 
7
+ export {
8
+ isHeaderMismatchError,
9
+ isMissingRequiredClientCapabilityError,
10
+ isResourceNotFoundError,
11
+ isUnsupportedProtocolVersionError,
12
+ MCPHttpStatusError,
13
+ MCPInputRequiredError,
14
+ MCPInvalidResultTypeError,
15
+ MCPProtocolError,
16
+ } from './errors.js'
17
+
18
+ // Era resolution lives beside the client, never inside a transport: stdio
19
+ // and HTTP share the whole state machine and differ only in the probe, so
20
+ // a transport that owned a copy would strand the other one.
21
+ export {
22
+ classifyModernHttpFailure,
23
+ createMcpEraCache,
24
+ defaultMcpEraCache,
25
+ isRecognizedModernError,
26
+ mcpEraCacheKey,
27
+ resolveMcpEra,
28
+ } from './era.js'
29
+ export type {
30
+ McpEraProbe,
31
+ McpEraProbeAnswer,
32
+ McpEraResolution,
33
+ McpEraResolutionInput,
34
+ } from './era.js'
35
+ export { buildEnvelope, decodeResult, encodeMcpHeaderValue } from './envelope.js'
36
+ export type { McpEnvelope, McpEnvelopeInput, MCPDecodedResult } from './envelope.js'
37
+
38
+ // The repo's first refusal path: a tool definition namzu declines to expose.
39
+ // Exported so a host can ask the same question of a schema it holds — and
40
+ // because `McpEnvelopeInput.paramHeaders` names the binding type.
41
+ export { validateMcpHeaderAnnotations } from './x-mcp-header.js'
42
+ export type { McpHeaderAnnotationVerdict, McpParamHeaderBinding } from './x-mcp-header.js'
43
+
7
44
  export {
8
45
  mcpToolToToolDefinition,
9
46
  toolDefinitionToMCPTool,
@@ -1,4 +1,5 @@
1
1
  import type {
2
+ MCPFetchLike,
2
3
  MCPJsonRpcMessage,
3
4
  MCPStreamableHttpTransportConfig,
4
5
  MCPTransport,
@@ -7,20 +8,44 @@ import type {
7
8
  import { SCOPE_ATTRIBUTE } from '../../utils/log/types.js'
8
9
  import { type Logger, resolveLogger } from '../../utils/logger.js'
9
10
  import { ConnectorHttpOperation, validateConnectorTimeoutMs } from '../http-operation.js'
11
+ import { MCPHttpStatusError } from './errors.js'
10
12
  import { refuseMcpHttpRedirect } from './http-redirect.js'
11
13
 
12
14
  const DEFAULT_TIMEOUT_MS = 30_000
13
15
 
16
+ /**
17
+ * How long a best-effort session teardown DELETE is given before this
18
+ * transport stops tracking it.
19
+ *
20
+ * `close()` never awaits this at all — the bound exists only so a DELETE to
21
+ * an unresponsive peer does not accumulate as a dangling request forever.
22
+ */
23
+ const SESSION_DELETE_TIMEOUT_MS = 5_000
24
+
14
25
  export class StreamableHttpTransport implements MCPTransport {
15
26
  private messageHandlers: Array<(message: MCPJsonRpcMessage) => void> = []
16
27
  private closeHandlers: Array<() => void> = []
17
28
  private errorHandlers: Array<(error: Error) => void> = []
18
29
  private connected = false
19
30
  private sessionId: string | null = null
31
+ /**
32
+ * The most recent SSE event `id` this transport has seen, if any.
33
+ *
34
+ * Legacy-only in effect, never in name: this transport only ever holds a
35
+ * `sessionId` on a legacy connection (a modern connection has no
36
+ * `initialize` reply to capture one from — `MCPClient` probes with
37
+ * `server/discover` instead), and {@link buildHeaders} sends
38
+ * `Last-Event-ID` only alongside a session id. Deliberately NOT cleared
39
+ * by `close()`: the whole point is arming the header for the request
40
+ * that follows a reconnect, once a fresh session exists to carry it.
41
+ */
42
+ private lastEventId: string | null = null
20
43
  private generation = 0
21
44
  private activeSends = new Set<AbortController>()
22
45
  private log: Logger
23
46
  private readonly timeoutMs: number
47
+ /** Defaults to the ambient global `fetch`; never read again once captured. */
48
+ private readonly fetchImpl: MCPFetchLike
24
49
 
25
50
  constructor(
26
51
  private readonly config: MCPStreamableHttpTransportConfig,
@@ -30,6 +55,7 @@ export class StreamableHttpTransport implements MCPTransport {
30
55
  config.timeoutMs ?? DEFAULT_TIMEOUT_MS,
31
56
  'StreamableHttpTransport timeoutMs',
32
57
  )
58
+ this.fetchImpl = config.fetch ?? fetch
33
59
  this.log = resolveLogger(log).child({ [SCOPE_ATTRIBUTE]: 'connector/mcp/streamable-http' })
34
60
  }
35
61
 
@@ -41,22 +67,94 @@ export class StreamableHttpTransport implements MCPTransport {
41
67
  }
42
68
 
43
69
  async close(): Promise<void> {
70
+ // Read and clear before anything else can observe or re-enter: whatever
71
+ // happens below, this transport no longer believes it holds this session.
72
+ const sessionId = this.sessionId
73
+ this.sessionId = null
74
+
44
75
  if (!this.connected) {
45
76
  // Never connected, or already closed: nothing will notify, so this is
46
77
  // the only chance to drop what `connect()` registered before it failed.
47
78
  this.clearHandlers()
48
- this.sessionId = null
79
+ if (sessionId) this.sendSessionDelete(sessionId)
49
80
  return
50
81
  }
51
82
  this.connected = false
52
83
  this.generation++
53
- this.sessionId = null
54
84
  const reason = new Error('StreamableHttpTransport closed')
55
85
  for (const controller of this.activeSends) controller.abort(reason)
56
86
  this.activeSends.clear()
57
87
  for (const handler of this.closeHandlers) handler()
58
88
  // After the notification, never before it.
59
89
  this.clearHandlers()
90
+ if (sessionId) this.sendSessionDelete(sessionId)
91
+ }
92
+
93
+ /**
94
+ * Forget the session this transport has been attaching to requests,
95
+ * without otherwise disturbing the connection.
96
+ *
97
+ * Used by `MCPClient`'s legacy session-recovery path: a `404` on a
98
+ * request means the server has forgotten this session, and the spec's
99
+ * remedy is a fresh `initialize` sent with no session id attached — which
100
+ * only happens if this transport stops sending the stale one first.
101
+ */
102
+ resetSession(): void {
103
+ this.sessionId = null
104
+ }
105
+
106
+ /** Whether this transport is currently attaching a session id to its requests. */
107
+ hasSession(): boolean {
108
+ return this.sessionId !== null
109
+ }
110
+
111
+ /**
112
+ * Tell the peer this session is done, without making `close()` wait on
113
+ * the answer.
114
+ *
115
+ * A SHOULD, not a MUST: it lets a cooperative server free resources
116
+ * promptly instead of waiting out its own idle timeout, but a server that
117
+ * never hears it is no worse off than before this existed. Fire-and-forget
118
+ * on purpose — `close()`'s existing bounded-teardown guarantee must not
119
+ * grow a dependency on a round trip to a peer that may already be gone —
120
+ * and bounded by its own short timeout so a peer that never answers does
121
+ * not leave a request open indefinitely. A modern origin never reaches
122
+ * this: it never had a session id to send in the first place.
123
+ */
124
+ private sendSessionDelete(sessionId: string): void {
125
+ const controller = new AbortController()
126
+ const timer = setTimeout(() => {
127
+ controller.abort(new Error('MCP session DELETE timed out'))
128
+ }, SESSION_DELETE_TIMEOUT_MS)
129
+ timer.unref?.()
130
+ const headers: Record<string, string> = {
131
+ ...this.config.headers,
132
+ 'Mcp-Session-Id': sessionId,
133
+ }
134
+ let sending: Promise<Response>
135
+ try {
136
+ sending = this.fetchImpl(this.config.url, {
137
+ method: 'DELETE',
138
+ headers,
139
+ redirect: 'manual',
140
+ signal: controller.signal,
141
+ })
142
+ } catch (err) {
143
+ clearTimeout(timer)
144
+ this.log.debug('Failed to send MCP legacy session DELETE', {
145
+ 'namzu.mcp.url': this.config.url,
146
+ 'exception.message': err instanceof Error ? err.message : String(err),
147
+ })
148
+ return
149
+ }
150
+ void sending
151
+ .catch((err: unknown) => {
152
+ this.log.debug('Failed to send MCP legacy session DELETE', {
153
+ 'namzu.mcp.url': this.config.url,
154
+ 'exception.message': err instanceof Error ? err.message : String(err),
155
+ })
156
+ })
157
+ .finally(() => clearTimeout(timer))
60
158
  }
61
159
 
62
160
  /** See {@link StdioTransport} — the same append-only handler leak. */
@@ -80,9 +178,9 @@ export class StreamableHttpTransport implements MCPTransport {
80
178
 
81
179
  try {
82
180
  const response = await operation.run(() =>
83
- fetch(this.config.url, {
181
+ this.fetchImpl(this.config.url, {
84
182
  method: 'POST',
85
- headers: this.buildHeaders(),
183
+ headers: this.buildHeaders(options?.headers),
86
184
  body: JSON.stringify(message),
87
185
  redirect: 'manual',
88
186
  signal: operation.signal,
@@ -91,7 +189,12 @@ export class StreamableHttpTransport implements MCPTransport {
91
189
 
92
190
  refuseMcpHttpRedirect(response, message.method)
93
191
  if (!response.ok) {
94
- throw new Error(`StreamableHttpTransport: HTTP ${response.status}: ${response.statusText}`)
192
+ throw new MCPHttpStatusError(
193
+ 'StreamableHttpTransport',
194
+ response.status,
195
+ response.statusText,
196
+ await readErrorBody(response, operation),
197
+ )
95
198
  }
96
199
  this.assertCurrent(owned.generation, operation)
97
200
  // MCP assigns the session during initialize. Letting an ordinary or
@@ -126,15 +229,28 @@ export class StreamableHttpTransport implements MCPTransport {
126
229
  return this.connected
127
230
  }
128
231
 
129
- private buildHeaders(): Record<string, string> {
232
+ /**
233
+ * `extra` comes from `MCPTransportSendOptions.headers` — the client's
234
+ * per-send authority, `MCP-Protocol-Version` today — and is merged over
235
+ * this transport's own static config headers so a caller's value wins
236
+ * on a collision. `Mcp-Session-Id` is applied after both: it is
237
+ * transport-managed state a caller cannot see to conflict with.
238
+ */
239
+ private buildHeaders(extra?: Readonly<Record<string, string>>): Record<string, string> {
130
240
  const headers: Record<string, string> = {
131
241
  'Content-Type': 'application/json',
132
242
  Accept: 'application/json, text/event-stream',
133
243
  ...this.config.headers,
244
+ ...extra,
134
245
  }
135
246
 
136
247
  if (this.sessionId) {
137
248
  headers['Mcp-Session-Id'] = this.sessionId
249
+ // Resumption is a legacy-only concept, and gated the same way the
250
+ // session id itself is: a modern connection never captures a
251
+ // `sessionId` (see the field's own doc comment), so this branch is
252
+ // unreachable there without a second, redundant era flag.
253
+ if (this.lastEventId) headers['Last-Event-ID'] = this.lastEventId
138
254
  }
139
255
 
140
256
  return headers
@@ -191,7 +307,7 @@ export class StreamableHttpTransport implements MCPTransport {
191
307
 
192
308
  const contentType = response.headers.get('content-type') ?? ''
193
309
  const messages = contentType.includes('text/event-stream')
194
- ? parseSseMessages(text)
310
+ ? this.parseSseAndCaptureEventId(text)
195
311
  : parseJsonMessages(text)
196
312
 
197
313
  for (const message of messages) {
@@ -202,6 +318,41 @@ export class StreamableHttpTransport implements MCPTransport {
202
318
  }
203
319
  }
204
320
  }
321
+
322
+ /**
323
+ * Parse an SSE body and remember the newest event `id` it carried, if
324
+ * any.
325
+ *
326
+ * The id survives past this one call — see the `lastEventId` field's own
327
+ * doc comment — so it is available to arm `Last-Event-ID` on whatever
328
+ * request follows a later reconnect.
329
+ */
330
+ private parseSseAndCaptureEventId(raw: string): MCPJsonRpcMessage[] {
331
+ const { messages, lastEventId } = parseSseMessages(raw)
332
+ if (lastEventId !== undefined) this.lastEventId = lastEventId
333
+ return messages
334
+ }
335
+ }
336
+
337
+ /**
338
+ * The body of a failed response, or an empty string.
339
+ *
340
+ * Carried on the error rather than discarded, because a status alone cannot
341
+ * tell a legacy origin from a modern one: a modern server answers an
342
+ * unknown method with `404` and a JSON-RPC error body precisely so that a
343
+ * client can tell the two apart. Reading it must never turn a clean HTTP
344
+ * failure into a different one, so a body that cannot be read is simply
345
+ * absent — the status error is the real answer either way.
346
+ */
347
+ async function readErrorBody(
348
+ response: Response,
349
+ operation: ConnectorHttpOperation,
350
+ ): Promise<string> {
351
+ try {
352
+ return await operation.run(() => response.text())
353
+ } catch {
354
+ return ''
355
+ }
205
356
  }
206
357
 
207
358
  function parseJsonMessages(raw: string): MCPJsonRpcMessage[] {
@@ -209,14 +360,51 @@ function parseJsonMessages(raw: string): MCPJsonRpcMessage[] {
209
360
  return Array.isArray(parsed) ? parsed : [parsed]
210
361
  }
211
362
 
212
- function parseSseMessages(raw: string): MCPJsonRpcMessage[] {
363
+ /** What one SSE-formatted Streamable HTTP response body parsed into. */
364
+ export interface MCPSseParseResult {
365
+ readonly messages: MCPJsonRpcMessage[]
366
+ /**
367
+ * The value of the last `id:` field seen across every event in the body
368
+ * — including one whose `data:` was empty, SSE's own priming event.
369
+ * `undefined` when no event in the body carried an id at all.
370
+ */
371
+ readonly lastEventId?: string
372
+ }
373
+
374
+ /**
375
+ * Parse a response body served as `text/event-stream`.
376
+ *
377
+ * Exported and pure so it is a direct unit-test target, with no transport,
378
+ * socket or server needed to see what it decides.
379
+ *
380
+ * Already correct on three counts before this: `data:` with or without a
381
+ * leading space, multi-line data joined with `\n`, and the empty-data
382
+ * priming event skipped as a message. This adds the two genuinely new
383
+ * pieces — capturing `id:` (armed by the caller for a legacy
384
+ * `Last-Event-ID` reconnect) and treating a `:`-prefixed comment or another
385
+ * unrecognized line as exactly what the spec says it is: not a field, never
386
+ * malformed input. Neither needed a special case: both filters below already
387
+ * select a line by its OWN prefix and so already ignore anything else — a
388
+ * comment, an `event:` line, a `retry:` line — without one.
389
+ */
390
+ export function parseSseMessages(raw: string): MCPSseParseResult {
213
391
  const normalized = raw.replace(/\r\n/g, '\n')
214
392
  const events = normalized.split(/\n\n+/)
215
393
  const messages: MCPJsonRpcMessage[] = []
394
+ let lastEventId: string | undefined
216
395
 
217
396
  for (const event of events) {
218
- const dataLines = event
219
- .split('\n')
397
+ const lines = event.split('\n')
398
+
399
+ const idLines = lines
400
+ .filter((line) => line.startsWith('id:'))
401
+ .map((line) => line.slice('id:'.length).trim())
402
+ // The LAST `id:` field within one event wins, per SSE's own
403
+ // field-processing rules — relevant only for a malformed event that
404
+ // repeats the field, but cheap to get right.
405
+ if (idLines.length > 0) lastEventId = idLines.at(-1)
406
+
407
+ const dataLines = lines
220
408
  .filter((line) => line.startsWith('data:'))
221
409
  .map((line) => line.slice('data:'.length).trimStart())
222
410
 
@@ -233,5 +421,5 @@ function parseSseMessages(raw: string): MCPJsonRpcMessage[] {
233
421
  }
234
422
  }
235
423
 
236
- return messages
424
+ return lastEventId !== undefined ? { messages, lastEventId } : { messages }
237
425
  }