@animalabs/agent-framework 0.32.0 → 0.34.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 (368) hide show
  1. package/README.md +340 -22
  2. package/dist/src/admission/admit.d.ts +18 -0
  3. package/dist/src/admission/admit.d.ts.map +1 -1
  4. package/dist/src/admission/admit.js +16 -3
  5. package/dist/src/admission/admit.js.map +1 -1
  6. package/dist/src/agent.d.ts +208 -80
  7. package/dist/src/agent.d.ts.map +1 -1
  8. package/dist/src/agent.js +345 -113
  9. package/dist/src/agent.js.map +1 -1
  10. package/dist/src/api/mcp-server-core.d.ts.map +1 -1
  11. package/dist/src/api/mcp-server-core.js +50 -12
  12. package/dist/src/api/mcp-server-core.js.map +1 -1
  13. package/dist/src/api/server.d.ts.map +1 -1
  14. package/dist/src/api/server.js +4 -20
  15. package/dist/src/api/server.js.map +1 -1
  16. package/dist/src/append-all.d.ts +5 -0
  17. package/dist/src/append-all.d.ts.map +1 -0
  18. package/dist/src/append-all.js +8 -0
  19. package/dist/src/append-all.js.map +1 -0
  20. package/dist/src/code-execution/isolate/scripts.d.ts +12 -8
  21. package/dist/src/code-execution/isolate/scripts.d.ts.map +1 -1
  22. package/dist/src/code-execution/isolate/scripts.js +24 -0
  23. package/dist/src/code-execution/isolate/scripts.js.map +1 -1
  24. package/dist/src/config-checks.d.ts +7 -0
  25. package/dist/src/config-checks.d.ts.map +1 -1
  26. package/dist/src/config-checks.js +14 -0
  27. package/dist/src/config-checks.js.map +1 -1
  28. package/dist/src/context-manager-errors.d.ts +25 -0
  29. package/dist/src/context-manager-errors.d.ts.map +1 -0
  30. package/dist/src/context-manager-errors.js +31 -0
  31. package/dist/src/context-manager-errors.js.map +1 -0
  32. package/dist/src/context-options.d.ts +70 -0
  33. package/dist/src/context-options.d.ts.map +1 -0
  34. package/dist/src/context-options.js +90 -0
  35. package/dist/src/context-options.js.map +1 -0
  36. package/dist/src/cut-off-round.js +4 -4
  37. package/dist/src/deferred-import.d.ts +93 -0
  38. package/dist/src/deferred-import.d.ts.map +1 -0
  39. package/dist/src/deferred-import.js +177 -0
  40. package/dist/src/deferred-import.js.map +1 -0
  41. package/dist/src/dendrite/audience.d.ts +223 -0
  42. package/dist/src/dendrite/audience.d.ts.map +1 -0
  43. package/dist/src/dendrite/audience.js +202 -0
  44. package/dist/src/dendrite/audience.js.map +1 -0
  45. package/dist/src/dendrite/index.d.ts +5 -3
  46. package/dist/src/dendrite/index.d.ts.map +1 -1
  47. package/dist/src/dendrite/index.js +2 -1
  48. package/dist/src/dendrite/index.js.map +1 -1
  49. package/dist/src/dendrite/presets.d.ts +10 -5
  50. package/dist/src/dendrite/presets.d.ts.map +1 -1
  51. package/dist/src/dendrite/presets.js +17 -5
  52. package/dist/src/dendrite/presets.js.map +1 -1
  53. package/dist/src/dendrite/registry.d.ts +62 -7
  54. package/dist/src/dendrite/registry.d.ts.map +1 -1
  55. package/dist/src/dendrite/registry.js +183 -12
  56. package/dist/src/dendrite/registry.js.map +1 -1
  57. package/dist/src/dendrite/types.d.ts +91 -8
  58. package/dist/src/dendrite/types.d.ts.map +1 -1
  59. package/dist/src/failure-text.d.ts +36 -0
  60. package/dist/src/failure-text.d.ts.map +1 -0
  61. package/dist/src/failure-text.js +89 -0
  62. package/dist/src/failure-text.js.map +1 -0
  63. package/dist/src/foreign-rounds.d.ts +69 -0
  64. package/dist/src/foreign-rounds.d.ts.map +1 -0
  65. package/dist/src/foreign-rounds.js +152 -0
  66. package/dist/src/foreign-rounds.js.map +1 -0
  67. package/dist/src/framework.d.ts +2064 -842
  68. package/dist/src/framework.d.ts.map +1 -1
  69. package/dist/src/framework.js +8586 -3739
  70. package/dist/src/framework.js.map +1 -1
  71. package/dist/src/gate/event-gate.d.ts +125 -21
  72. package/dist/src/gate/event-gate.d.ts.map +1 -1
  73. package/dist/src/gate/event-gate.js +487 -72
  74. package/dist/src/gate/event-gate.js.map +1 -1
  75. package/dist/src/gate/index.d.ts +1 -1
  76. package/dist/src/gate/index.d.ts.map +1 -1
  77. package/dist/src/gate/index.js.map +1 -1
  78. package/dist/src/gate/reply-judge.d.ts +4 -0
  79. package/dist/src/gate/reply-judge.d.ts.map +1 -1
  80. package/dist/src/gate/reply-judge.js +12 -3
  81. package/dist/src/gate/reply-judge.js.map +1 -1
  82. package/dist/src/gate/rule-scope.d.ts +30 -0
  83. package/dist/src/gate/rule-scope.d.ts.map +1 -0
  84. package/dist/src/gate/rule-scope.js +41 -0
  85. package/dist/src/gate/rule-scope.js.map +1 -0
  86. package/dist/src/gate/suppress-script-worker.d.ts +2 -0
  87. package/dist/src/gate/suppress-script-worker.d.ts.map +1 -0
  88. package/dist/src/gate/suppress-script-worker.js +103 -0
  89. package/dist/src/gate/suppress-script-worker.js.map +1 -0
  90. package/dist/src/gate/suppress-scripts.d.ts +105 -0
  91. package/dist/src/gate/suppress-scripts.d.ts.map +1 -0
  92. package/dist/src/gate/suppress-scripts.js +403 -0
  93. package/dist/src/gate/suppress-scripts.js.map +1 -0
  94. package/dist/src/gate/suppression-review.d.ts +143 -0
  95. package/dist/src/gate/suppression-review.d.ts.map +1 -0
  96. package/dist/src/gate/suppression-review.js +463 -0
  97. package/dist/src/gate/suppression-review.js.map +1 -0
  98. package/dist/src/gate/suppression-stats.d.ts +125 -0
  99. package/dist/src/gate/suppression-stats.d.ts.map +1 -0
  100. package/dist/src/gate/suppression-stats.js +214 -0
  101. package/dist/src/gate/suppression-stats.js.map +1 -0
  102. package/dist/src/gate/types.d.ts +142 -7
  103. package/dist/src/gate/types.d.ts.map +1 -1
  104. package/dist/src/gate/wake-onboarding.d.ts +119 -0
  105. package/dist/src/gate/wake-onboarding.d.ts.map +1 -0
  106. package/dist/src/gate/wake-onboarding.js +277 -0
  107. package/dist/src/gate/wake-onboarding.js.map +1 -0
  108. package/dist/src/heartbeat/heartbeat.js +1 -1
  109. package/dist/src/heartbeat/heartbeat.js.map +1 -1
  110. package/dist/src/held-writes.d.ts +31 -23
  111. package/dist/src/held-writes.d.ts.map +1 -1
  112. package/dist/src/held-writes.js +44 -59
  113. package/dist/src/held-writes.js.map +1 -1
  114. package/dist/src/index.d.ts +6 -2
  115. package/dist/src/index.d.ts.map +1 -1
  116. package/dist/src/index.js +4 -0
  117. package/dist/src/index.js.map +1 -1
  118. package/dist/src/inference-bounds.d.ts +2 -1
  119. package/dist/src/inference-bounds.d.ts.map +1 -1
  120. package/dist/src/inference-bounds.js +9 -6
  121. package/dist/src/inference-bounds.js.map +1 -1
  122. package/dist/src/jsonrpc-envelope.d.ts +69 -0
  123. package/dist/src/jsonrpc-envelope.d.ts.map +1 -0
  124. package/dist/src/jsonrpc-envelope.js +66 -0
  125. package/dist/src/jsonrpc-envelope.js.map +1 -0
  126. package/dist/src/mcpl/channel-registry.d.ts +188 -28
  127. package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
  128. package/dist/src/mcpl/channel-registry.js +426 -70
  129. package/dist/src/mcpl/channel-registry.js.map +1 -1
  130. package/dist/src/mcpl/checkpoint-manager.d.ts +2 -4
  131. package/dist/src/mcpl/checkpoint-manager.d.ts.map +1 -1
  132. package/dist/src/mcpl/checkpoint-manager.js.map +1 -1
  133. package/dist/src/mcpl/feature-set-manager.d.ts +2 -4
  134. package/dist/src/mcpl/feature-set-manager.d.ts.map +1 -1
  135. package/dist/src/mcpl/feature-set-manager.js.map +1 -1
  136. package/dist/src/mcpl/inbound-source.d.ts +2 -1
  137. package/dist/src/mcpl/inbound-source.d.ts.map +1 -1
  138. package/dist/src/mcpl/inbound-source.js +2 -1
  139. package/dist/src/mcpl/inbound-source.js.map +1 -1
  140. package/dist/src/mcpl/inference-router.d.ts +6 -6
  141. package/dist/src/mcpl/inference-router.d.ts.map +1 -1
  142. package/dist/src/mcpl/inference-router.js.map +1 -1
  143. package/dist/src/mcpl/modern-connection.d.ts +109 -15
  144. package/dist/src/mcpl/modern-connection.d.ts.map +1 -1
  145. package/dist/src/mcpl/modern-connection.js +593 -86
  146. package/dist/src/mcpl/modern-connection.js.map +1 -1
  147. package/dist/src/mcpl/protocol-family.d.ts +3 -1
  148. package/dist/src/mcpl/protocol-family.d.ts.map +1 -1
  149. package/dist/src/mcpl/protocol-family.js +15 -9
  150. package/dist/src/mcpl/protocol-family.js.map +1 -1
  151. package/dist/src/mcpl/push-coalescer.d.ts +132 -22
  152. package/dist/src/mcpl/push-coalescer.d.ts.map +1 -1
  153. package/dist/src/mcpl/push-coalescer.js +105 -15
  154. package/dist/src/mcpl/push-coalescer.js.map +1 -1
  155. package/dist/src/mcpl/push-handler.d.ts +9 -7
  156. package/dist/src/mcpl/push-handler.d.ts.map +1 -1
  157. package/dist/src/mcpl/push-handler.js.map +1 -1
  158. package/dist/src/mcpl/server-connection.d.ts +50 -28
  159. package/dist/src/mcpl/server-connection.d.ts.map +1 -1
  160. package/dist/src/mcpl/server-connection.js +165 -72
  161. package/dist/src/mcpl/server-connection.js.map +1 -1
  162. package/dist/src/mcpl/tool-classes.d.ts.map +1 -1
  163. package/dist/src/mcpl/tool-classes.js +3 -0
  164. package/dist/src/mcpl/tool-classes.js.map +1 -1
  165. package/dist/src/mcpl/tool-presence.d.ts.map +1 -1
  166. package/dist/src/mcpl/tool-presence.js +10 -6
  167. package/dist/src/mcpl/tool-presence.js.map +1 -1
  168. package/dist/src/mcpl/toolset-host.d.ts.map +1 -1
  169. package/dist/src/mcpl/toolset-host.js +7 -6
  170. package/dist/src/mcpl/toolset-host.js.map +1 -1
  171. package/dist/src/mcpl/toolsets-module.d.ts.map +1 -1
  172. package/dist/src/mcpl/toolsets-module.js +2 -1
  173. package/dist/src/mcpl/toolsets-module.js.map +1 -1
  174. package/dist/src/mcpl/types.d.ts +33 -6
  175. package/dist/src/mcpl/types.d.ts.map +1 -1
  176. package/dist/src/mcpl/types.js.map +1 -1
  177. package/dist/src/module-registry.d.ts +24 -1
  178. package/dist/src/module-registry.d.ts.map +1 -1
  179. package/dist/src/module-registry.js +68 -18
  180. package/dist/src/module-registry.js.map +1 -1
  181. package/dist/src/modules/discord/index.d.ts +45 -8
  182. package/dist/src/modules/discord/index.d.ts.map +1 -1
  183. package/dist/src/modules/discord/index.js +238 -87
  184. package/dist/src/modules/discord/index.js.map +1 -1
  185. package/dist/src/modules/health/index.js +2 -2
  186. package/dist/src/modules/health/index.js.map +1 -1
  187. package/dist/src/modules/history/index.d.ts +59 -0
  188. package/dist/src/modules/history/index.d.ts.map +1 -1
  189. package/dist/src/modules/history/index.js +432 -67
  190. package/dist/src/modules/history/index.js.map +1 -1
  191. package/dist/src/modules/history/semantic.d.ts +6 -5
  192. package/dist/src/modules/history/semantic.d.ts.map +1 -1
  193. package/dist/src/modules/history/semantic.js +18 -25
  194. package/dist/src/modules/history/semantic.js.map +1 -1
  195. package/dist/src/modules/workspace/disk-agreement.d.ts +1 -1
  196. package/dist/src/modules/workspace/disk-agreement.js +1 -1
  197. package/dist/src/modules/workspace/index.d.ts +28 -7
  198. package/dist/src/modules/workspace/index.d.ts.map +1 -1
  199. package/dist/src/modules/workspace/index.js +80 -25
  200. package/dist/src/modules/workspace/index.js.map +1 -1
  201. package/dist/src/modules/workspace/observe.d.ts.map +1 -1
  202. package/dist/src/modules/workspace/observe.js +2 -1
  203. package/dist/src/modules/workspace/observe.js.map +1 -1
  204. package/dist/src/modules/workspace/reconcile.d.ts.map +1 -1
  205. package/dist/src/modules/workspace/reconcile.js +3 -2
  206. package/dist/src/modules/workspace/reconcile.js.map +1 -1
  207. package/dist/src/notice-tags.d.ts +20 -9
  208. package/dist/src/notice-tags.d.ts.map +1 -1
  209. package/dist/src/notice-tags.js +20 -9
  210. package/dist/src/notice-tags.js.map +1 -1
  211. package/dist/src/operator-log.d.ts +36 -3
  212. package/dist/src/operator-log.d.ts.map +1 -1
  213. package/dist/src/operator-log.js +2 -2
  214. package/dist/src/operator-log.js.map +1 -1
  215. package/dist/src/presentation.d.ts +747 -0
  216. package/dist/src/presentation.d.ts.map +1 -0
  217. package/dist/src/presentation.js +1437 -0
  218. package/dist/src/presentation.js.map +1 -0
  219. package/dist/src/prose-drafts.d.ts +15 -11
  220. package/dist/src/prose-drafts.d.ts.map +1 -1
  221. package/dist/src/prose-drafts.js +16 -12
  222. package/dist/src/prose-drafts.js.map +1 -1
  223. package/dist/src/read-marks.d.ts +192 -0
  224. package/dist/src/read-marks.d.ts.map +1 -0
  225. package/dist/src/read-marks.js +242 -0
  226. package/dist/src/read-marks.js.map +1 -0
  227. package/dist/src/reasoning-loss.d.ts +5 -0
  228. package/dist/src/reasoning-loss.d.ts.map +1 -0
  229. package/dist/src/reasoning-loss.js +8 -0
  230. package/dist/src/reasoning-loss.js.map +1 -0
  231. package/dist/src/recovery/offline-branch.d.ts +91 -6
  232. package/dist/src/recovery/offline-branch.d.ts.map +1 -1
  233. package/dist/src/recovery/offline-branch.js +277 -32
  234. package/dist/src/recovery/offline-branch.js.map +1 -1
  235. package/dist/src/recovery/recover-cli.js +24 -4
  236. package/dist/src/recovery/recover-cli.js.map +1 -1
  237. package/dist/src/send-tools.d.ts +5 -9
  238. package/dist/src/send-tools.d.ts.map +1 -1
  239. package/dist/src/send-tools.js +40 -7
  240. package/dist/src/send-tools.js.map +1 -1
  241. package/dist/src/sent-rows.js +1 -1
  242. package/dist/src/stored-injections.d.ts +2 -2
  243. package/dist/src/stored-injections.js +2 -2
  244. package/dist/src/stored-removal.d.ts +17 -4
  245. package/dist/src/stored-removal.d.ts.map +1 -1
  246. package/dist/src/stored-removal.js +29 -5
  247. package/dist/src/stored-removal.js.map +1 -1
  248. package/dist/src/supplied-text.d.ts +1 -1
  249. package/dist/src/supplied-text.js +3 -3
  250. package/dist/src/supplied-text.js.map +1 -1
  251. package/dist/src/thinking-drops.d.ts +2 -0
  252. package/dist/src/thinking-drops.d.ts.map +1 -1
  253. package/dist/src/thinking-drops.js.map +1 -1
  254. package/dist/src/tool-presentation.d.ts.map +1 -1
  255. package/dist/src/tool-presentation.js +38 -9
  256. package/dist/src/tool-presentation.js.map +1 -1
  257. package/dist/src/tool-result-guard.d.ts +12 -1
  258. package/dist/src/tool-result-guard.d.ts.map +1 -1
  259. package/dist/src/tool-result-guard.js +42 -8
  260. package/dist/src/tool-result-guard.js.map +1 -1
  261. package/dist/src/tune-out/coordinator.d.ts +1170 -81
  262. package/dist/src/tune-out/coordinator.d.ts.map +1 -1
  263. package/dist/src/tune-out/coordinator.js +3732 -369
  264. package/dist/src/tune-out/coordinator.js.map +1 -1
  265. package/dist/src/tune-out/tools.d.ts +114 -17
  266. package/dist/src/tune-out/tools.d.ts.map +1 -1
  267. package/dist/src/tune-out/tools.js +89 -28
  268. package/dist/src/tune-out/tools.js.map +1 -1
  269. package/dist/src/tune-out/window-anchor.d.ts +143 -0
  270. package/dist/src/tune-out/window-anchor.d.ts.map +1 -0
  271. package/dist/src/tune-out/window-anchor.js +202 -0
  272. package/dist/src/tune-out/window-anchor.js.map +1 -0
  273. package/dist/src/types/agent.d.ts +121 -25
  274. package/dist/src/types/agent.d.ts.map +1 -1
  275. package/dist/src/types/events.d.ts +2 -0
  276. package/dist/src/types/events.d.ts.map +1 -1
  277. package/dist/src/types/framework.d.ts +72 -17
  278. package/dist/src/types/framework.d.ts.map +1 -1
  279. package/dist/src/types/inference-log.d.ts +1 -0
  280. package/dist/src/types/inference-log.d.ts.map +1 -1
  281. package/dist/src/types/module.d.ts +180 -69
  282. package/dist/src/types/module.d.ts.map +1 -1
  283. package/dist/src/types/trace.d.ts +573 -39
  284. package/dist/src/types/trace.d.ts.map +1 -1
  285. package/package.json +4 -4
  286. package/src/admission/admit.ts +32 -3
  287. package/src/agent.ts +392 -132
  288. package/src/api/mcp-server-core.ts +54 -17
  289. package/src/api/server.ts +4 -23
  290. package/src/append-all.ts +6 -0
  291. package/src/code-execution/isolate/scripts.ts +28 -3
  292. package/src/config-checks.ts +14 -0
  293. package/src/context-manager-errors.ts +37 -0
  294. package/src/context-options.ts +106 -0
  295. package/src/cut-off-round.ts +4 -4
  296. package/src/deferred-import.ts +239 -0
  297. package/src/dendrite/audience.ts +315 -0
  298. package/src/dendrite/index.ts +14 -2
  299. package/src/dendrite/presets.ts +18 -6
  300. package/src/dendrite/registry.ts +213 -14
  301. package/src/dendrite/types.ts +92 -8
  302. package/src/failure-text.ts +88 -0
  303. package/src/foreign-rounds.ts +199 -0
  304. package/src/framework.ts +8796 -3813
  305. package/src/gate/event-gate.ts +518 -91
  306. package/src/gate/index.ts +3 -0
  307. package/src/gate/reply-judge.ts +12 -3
  308. package/src/gate/rule-scope.ts +45 -0
  309. package/src/gate/suppress-script-worker.ts +89 -0
  310. package/src/gate/suppress-scripts.ts +414 -0
  311. package/src/gate/suppression-review.ts +517 -0
  312. package/src/gate/suppression-stats.ts +314 -0
  313. package/src/gate/types.ts +148 -7
  314. package/src/gate/wake-onboarding.ts +309 -0
  315. package/src/heartbeat/heartbeat.ts +1 -1
  316. package/src/held-writes.ts +49 -60
  317. package/src/index.ts +10 -2
  318. package/src/inference-bounds.ts +11 -6
  319. package/src/jsonrpc-envelope.ts +81 -0
  320. package/src/mcpl/channel-registry.ts +513 -116
  321. package/src/mcpl/checkpoint-manager.ts +3 -2
  322. package/src/mcpl/feature-set-manager.ts +2 -1
  323. package/src/mcpl/inbound-source.ts +2 -1
  324. package/src/mcpl/inference-router.ts +7 -4
  325. package/src/mcpl/modern-connection.ts +629 -91
  326. package/src/mcpl/protocol-family.ts +15 -9
  327. package/src/mcpl/push-coalescer.ts +193 -34
  328. package/src/mcpl/push-handler.ts +10 -5
  329. package/src/mcpl/server-connection.ts +169 -75
  330. package/src/mcpl/tool-classes.ts +3 -0
  331. package/src/mcpl/tool-presence.ts +10 -5
  332. package/src/mcpl/toolset-host.ts +7 -6
  333. package/src/mcpl/toolsets-module.ts +2 -1
  334. package/src/mcpl/types.ts +33 -6
  335. package/src/module-registry.ts +73 -17
  336. package/src/modules/discord/index.ts +230 -82
  337. package/src/modules/health/index.ts +2 -2
  338. package/src/modules/history/index.ts +439 -70
  339. package/src/modules/history/semantic.ts +18 -24
  340. package/src/modules/workspace/disk-agreement.ts +1 -1
  341. package/src/modules/workspace/index.ts +89 -26
  342. package/src/modules/workspace/observe.ts +2 -1
  343. package/src/modules/workspace/reconcile.ts +3 -2
  344. package/src/notice-tags.ts +21 -9
  345. package/src/operator-log.ts +36 -3
  346. package/src/presentation.ts +1678 -0
  347. package/src/prose-drafts.ts +16 -12
  348. package/src/read-marks.ts +328 -0
  349. package/src/reasoning-loss.ts +7 -0
  350. package/src/recovery/offline-branch.ts +350 -35
  351. package/src/recovery/recover-cli.ts +23 -4
  352. package/src/send-tools.ts +40 -7
  353. package/src/sent-rows.ts +1 -1
  354. package/src/stored-injections.ts +2 -2
  355. package/src/stored-removal.ts +31 -7
  356. package/src/supplied-text.ts +3 -3
  357. package/src/thinking-drops.ts +2 -0
  358. package/src/tool-presentation.ts +34 -9
  359. package/src/tool-result-guard.ts +41 -7
  360. package/src/tune-out/coordinator.ts +4121 -472
  361. package/src/tune-out/tools.ts +162 -44
  362. package/src/tune-out/window-anchor.ts +277 -0
  363. package/src/types/agent.ts +122 -25
  364. package/src/types/events.ts +2 -0
  365. package/src/types/framework.ts +73 -17
  366. package/src/types/inference-log.ts +1 -0
  367. package/src/types/module.ts +181 -68
  368. package/src/types/trace.ts +607 -43
@@ -1,11 +1,14 @@
1
1
  /**
2
- * ModernMcpConnection: a server that speaks modern MCP (2026-07-28), reached
3
- * over stdio or Streamable HTTP through the official SDK client.
2
+ * ModernMcpConnection: a server reached through the official SDK client, over
3
+ * stdio at MCP 2026-07-28, or over Streamable HTTP at the revision negotiated
4
+ * at each connect: 2026-07-28 when the server offers it, and otherwise
5
+ * 2025-11-25, 2025-06-18 or 2025-03-26.
4
6
  *
5
- * Responsibilities divide like this. The SDK owns the modern wire:
6
- * `server/discover`, the per-request `_meta` envelope, `resultType`,
7
- * continuation of `input_required` results, cancellation, list-change
8
- * subscriptions, and the HTTP binding's headers, auth retry and
7
+ * Responsibilities divide like this. The SDK owns the wire: negotiation
8
+ * (`server/discover`, falling back to `initialize` over HTTP), the per-request
9
+ * `_meta` envelope, `resultType`, continuation of `input_required` results,
10
+ * cancellation, list-change subscriptions (2026-07-28) and notifications
11
+ * (2025), and the HTTP binding's headers, session id, auth retry and
9
12
  * header-mismatch handling. This class owns what Connectome decides:
10
13
  * - **stdio** runs through Connectome's own spawner, so the child gets the
11
14
  * same env allowlist, `inheritEnv` and stderr surfacing as a legacy server,
@@ -27,7 +30,7 @@
27
30
  * dispatch, output validation after a successful result) as it does for
28
31
  * server answers. A call's answer is the server's final one: an
29
32
  * `input_required` answer asks for another round, so a call that ends
30
- * after one has no answer.
33
+ * after one has no answer. On a 2025 revision every result is final.
31
34
  * - **Lifetime.** On a lost transport the connection restarts with the same
32
35
  * backoff settings as legacy, when `reconnect` is set, once the lost
33
36
  * launch is reaped, so a second child never runs beside the first. A
@@ -35,11 +38,26 @@
35
38
  * no loss the pipes show: each call's deadline is the only bound on it.
36
39
  * A server that shares no revision with this client is a configuration
37
40
  * verdict, as for legacy (`McplProtocolVersionError`): it is never
38
- * retried. List-change
39
- * awareness is kept while the connection lives: a subscription the server
40
- * advertises but that didn't open, or that ended, is reopened. Each connect
41
- * is a generation, so a superseded client never touches a newer one.
42
- * `close()` owns a connect in flight too.
41
+ * retried. List-change awareness is kept while the connection lives: on
42
+ * 2026-07-28, a subscription the server advertises but that didn't open,
43
+ * or that ended, is reopened; on a 2025 revision, the session's standalone
44
+ * stream, which carries its list changes, is opened and reopened while the
45
+ * session lives, with a session id or without, unless the server answers
46
+ * its first open with a status that won't pass (`mayPass`), such as a 405,
47
+ * which offers no stream. Each connect is a generation, so a superseded
48
+ * client never touches a newer one. `close()` owns a connect in flight
49
+ * too.
50
+ * - **A 2025 session over HTTP** ends when the server answers 404 to a
51
+ * request that carried its id, but for the standalone stream's first open,
52
+ * or stops serving its revision (a 400 carrying -32022) or its streams (a
53
+ * 405 to a GET for one it had served), as a server upgraded to the SDK's
54
+ * 2026 handler does (`sessionEnded`). A new connect starts at once if the
55
+ * old session had served a request ('close', then 'reconnect'), and
56
+ * negotiates afresh, unless it lived less than a base interval, as did the
57
+ * last one restarted that way; otherwise the end is a failed connect. A
58
+ * request's stream the server cut is resumed for as long as the request
59
+ * lives. Closing a generation DELETEs a session the server holds by its
60
+ * id, a failed connect's included.
43
61
  *
44
62
  * Nothing here is MCPL: a modern server has no grant, no planes and no
45
63
  * server→host requests. The framework uses this connection only through its
@@ -50,13 +68,36 @@ import { EventEmitter } from 'node:events';
50
68
  import { Client, InsufficientScopeError, SdkError, SdkErrorCode, SdkHttpError, StreamableHTTPClientTransport, UnsupportedProtocolVersionError, parseJSONRPCMessage, } from '@modelcontextprotocol/client';
51
69
  import { HeldStderr, STDERR_END_WAIT_MS, StdioTransport } from './transport.js';
52
70
  import { McplProtocolVersionError, McplRequestError, McplUnreapedLaunchError, stderrTailOf, withStderr } from './server-connection.js';
53
- import { LEGACY_MCP_PROTOCOL_VERSION, MAX_TIMER_MS, MODERN_MCP_PROTOCOL_VERSION, checkServerConfig, describeCredentialError, logDroppedLine, maskedError, noteDialSent, redactServerText, redactedData, serverTextRedactor } from './protocol-family.js';
71
+ import { LEGACY_MCP_PROTOCOL_VERSION, MAX_TIMER_MS, MODERN_HTTP_PROTOCOL_VERSIONS, MODERN_MCP_PROTOCOL_VERSION, checkServerConfig, describeCredentialError, logDroppedLine, maskedError, noteDialSent, redactServerText, redactedData, serverTextRedactor } from './protocol-family.js';
54
72
  import { boundedForLog } from './errors.js';
55
73
  import { logDetail, logValue, logValueRedacted, loggedCode } from './log-value.js';
56
74
  const DEFAULT_REQUEST_TIMEOUT_MS = 60_000;
57
- /** Bound on establishing the connection (`server/discover`), like the legacy
58
- * engine's initialize timeout. */
75
+ /** Bound on each request of establishing the connection (`server/discover`,
76
+ * and over HTTP `initialize`), like the legacy engine's initialize timeout. */
59
77
  const CONNECT_TIMEOUT_MS = 30_000;
78
+ const DELETE_TIMEOUT_MS = 3_000;
79
+ const HTTP_2025_REVISIONS = new Set(MODERN_HTTP_PROTOCOL_VERSIONS.filter((version) => version !== MODERN_MCP_PROTOCOL_VERSION));
80
+ /** StreamableHTTPClientTransport's default `maxRetries`. */
81
+ const SDK_STREAM_RETRIES = 2;
82
+ const SESSION_END = {
83
+ ended: 'ended its session (HTTP 404)',
84
+ revision: "refused its session's revision (HTTP 400, -32022)",
85
+ stream: "stopped serving its session's streams (HTTP 405)",
86
+ };
87
+ /**
88
+ * An answer to the standalone stream's first open that may pass, so it is
89
+ * tried again (see `firstOpen`): a timeout (408), a stream the server still
90
+ * holds for this session from a try whose answer never reached us (409), a
91
+ * rate limit (429), or a server error, except 501 and 505, which say this
92
+ * server never will.
93
+ */
94
+ const mayPass = (status) => status === 408 || status === 409 || status === 429 || (status >= 500 && status !== 501 && status !== 505);
95
+ /**
96
+ * The reason a call whose stream was lost fails with (see WireRecord's
97
+ * `cancel`). An SdkError, so the SDK passes it on as it is, where it reads
98
+ * any other abort reason as a timeout.
99
+ */
100
+ const lostStream = () => new SdkError(SdkErrorCode.ConnectionClosed, 'the stream that would carry its answer ended, and could not be resumed');
60
101
  /** Inventory listing has its own bound: it is host work, not a resident's call. */
61
102
  const LIST_TIMEOUT_MS = 30_000;
62
103
  class NotWrittenError extends Error {
@@ -67,9 +108,17 @@ class NotWrittenError extends Error {
67
108
  this.stdioStopped = stdioStopped;
68
109
  }
69
110
  }
70
- /** The logical request a send belongs to. The SDK hands each request to the
71
- * transport inside the caller's async chain, so the scope reaches `send`. */
111
+ /**
112
+ * The logical request a send belongs to. The SDK hands each request to the
113
+ * transport inside the caller's async chain, so the scope reaches `send` and
114
+ * the request's POST (see observedFetch). Anything else started on that chain
115
+ * carries the scope too: a session's end queued from a call's fetch restarts
116
+ * the session inside that call's scope, the new session's requests included.
117
+ * So a POST's scope alone doesn't make it that call's: its body's id must be
118
+ * the call's current leg.
119
+ */
72
120
  const wireScope = new AsyncLocalStorage();
121
+ const stringList = (value) => Array.isArray(value) && value.every((v) => typeof v === 'string') ? value : undefined;
73
122
  /** A send in flight, its transport, and what that transport reported
74
123
  * meanwhile, held until the send settles (ObservedTransport.send). A
75
124
  * scope that names no transport, a notification's or a settled send's,
@@ -83,12 +132,36 @@ const sendScope = new AsyncLocalStorage();
83
132
  */
84
133
  class ObservedTransport {
85
134
  inner;
135
+ beforeClose;
86
136
  onclose;
87
137
  onerror;
88
138
  onmessage;
89
139
  awaiting = new Map();
90
- constructor(inner) {
140
+ negotiating = new Map();
141
+ negotiation = {};
142
+ established = false;
143
+ finalResults = false;
144
+ answered = false;
145
+ /**
146
+ * Streamable HTTP stops a request's stream when the request's signal fires,
147
+ * and stops resuming it. The SDK gives a request such a signal only on
148
+ * 2026-07-28 over HTTP, and never cancels an unanswered `initialize`. The
149
+ * stdio transport ignores it.
150
+ */
151
+ aborts = new Map();
152
+ /**
153
+ * @param beforeClose runs at each close before the inner transport closes,
154
+ * while it can still send: an HTTP session's DELETE. The SDK closes the
155
+ * transport through this boundary when a handshake fails, before the error
156
+ * reaches the connection.
157
+ */
158
+ constructor(inner, beforeClose) {
91
159
  this.inner = inner;
160
+ this.beforeClose = beforeClose;
161
+ }
162
+ establish(era) {
163
+ this.established = true;
164
+ this.finalResults = era === 'legacy';
92
165
  }
93
166
  get sessionId() {
94
167
  return this.inner.sessionId;
@@ -104,6 +177,13 @@ class ObservedTransport {
104
177
  }
105
178
  async start() {
106
179
  this.inner.onmessage = (message, extra) => {
180
+ if (typeof message === 'object' && message !== null && 'id' in message && ('result' in message || 'error' in message)) {
181
+ this.noteNegotiation(message);
182
+ if (message.id !== undefined)
183
+ this.aborts.delete(message.id);
184
+ if (this.established)
185
+ this.answered = true;
186
+ }
107
187
  // An answer to a request in scope is handed to the SDK inside that
108
188
  // request's scope. The SDK drives an input_required continuation from
109
189
  // this very callback, outside the caller's async chain, and the next
@@ -130,6 +210,18 @@ class ObservedTransport {
130
210
  await this.inner.start();
131
211
  }
132
212
  async send(message, options) {
213
+ if ('method' in message && 'id' in message && (message.method === 'server/discover' || message.method === 'initialize')) {
214
+ this.negotiating.set(message.id, message.method);
215
+ }
216
+ if ('method' in message && message.method === 'notifications/cancelled') {
217
+ // The request is over for the SDK, which rejected it as it cancelled:
218
+ // its stream stops now, whatever becomes of this notification's POST.
219
+ const id = message.params?.requestId;
220
+ if (typeof id === 'string' || typeof id === 'number') {
221
+ this.aborts.get(id)?.abort();
222
+ this.aborts.delete(id);
223
+ }
224
+ }
133
225
  if ('method' in message && !('id' in message)) {
134
226
  // A notification's sender can swallow what its send throws (the SDK's
135
227
  // listen teardown does), so what its transport reports is reported at
@@ -144,19 +236,43 @@ class ObservedTransport {
144
236
  // BigInt, a cycle) never leaves this host: it's refused here, before
145
237
  // it's marked handed off (#556).
146
238
  JSON.stringify(message);
147
- const prior = { handedOff: record.handedOff, currentId: record.currentId, answer: record.answer, error: record.error };
239
+ const prior = { handedOff: record.handedOff, currentId: record.currentId, answer: record.answer, error: record.error, sessionEnded: record.sessionEnded };
148
240
  // Handed off from here on: past this point nothing proves it unsent,
149
241
  // unless the transport says it wrote nothing.
150
242
  record.handedOff = true;
151
243
  record.currentId = message.id;
152
244
  record.answer = 'none';
153
245
  record.error = undefined;
246
+ record.sessionEnded = undefined;
154
247
  this.awaiting.set(message.id, record);
155
248
  undo = () => {
156
249
  this.awaiting.delete(message.id);
157
250
  Object.assign(record, prior);
158
251
  };
159
252
  }
253
+ let sendOptions = options;
254
+ if ('method' in message && 'id' in message && options?.requestSignal === undefined) {
255
+ const abort = new AbortController();
256
+ this.aborts.set(message.id, abort);
257
+ sendOptions = { ...options, requestSignal: abort.signal };
258
+ }
259
+ if (undo && record && 'id' in message && sendOptions?.onRequestStreamEnd === undefined) {
260
+ // The transport calls this once the stream that would carry this leg's
261
+ // answer is over: after the answer, or without it, when the stream
262
+ // can't be resumed. That is a stream that ended with no event id to
263
+ // resume from, a resume answered 405 or with no body, or the
264
+ // transport's retries spent (two on 2026-07-28; on 2025 it has no
265
+ // limit). Unanswered, the call is lost, so it fails now rather than at
266
+ // its deadline. A 2026-07-28 listen brings its own.
267
+ const leg = message.id;
268
+ sendOptions = {
269
+ ...sendOptions,
270
+ onRequestStreamEnd: () => {
271
+ if (record.currentId === leg && record.answer === 'none')
272
+ record.cancel.abort(lostStream());
273
+ },
274
+ };
275
+ }
160
276
  // A request's or a response's sender reports what its send throws: a
161
277
  // call through its outcome, the connect through its failure, the SDK
162
278
  // through its own report. So what the transport reports meanwhile is held
@@ -164,7 +280,7 @@ class ObservedTransport {
164
280
  const sending = { wire: this, held: [] };
165
281
  let thrown;
166
282
  try {
167
- await sendScope.run(sending, () => this.inner.send(message, options));
283
+ await sendScope.run(sending, () => this.inner.send(message, sendOptions));
168
284
  }
169
285
  catch (error) {
170
286
  thrown = error;
@@ -173,6 +289,9 @@ class ObservedTransport {
173
289
  // after part of the request went out) leaves it handed off.
174
290
  if (error instanceof NotWrittenError)
175
291
  undo?.();
292
+ // The SDK settles a request whose send failed itself: nothing cancels it.
293
+ if ('method' in message && 'id' in message)
294
+ this.aborts.delete(message.id);
176
295
  throw error;
177
296
  }
178
297
  finally {
@@ -188,9 +307,11 @@ class ObservedTransport {
188
307
  }
189
308
  }
190
309
  async close() {
310
+ await this.beforeClose?.();
191
311
  await this.inner.close();
192
312
  }
193
- /** Drop a finished request's entries (unanswered legs included). */
313
+ /** Drop a finished request's legs from `awaiting`, unanswered legs
314
+ * included. */
194
315
  forget(record) {
195
316
  for (const [id, entry] of this.awaiting)
196
317
  if (entry === record)
@@ -217,12 +338,40 @@ class ObservedTransport {
217
338
  else {
218
339
  const result = message.result;
219
340
  const type = typeof result === 'object' && result !== null ? result.resultType : undefined;
220
- record.answer = type === 'complete' ? 'complete' : type === 'input_required' ? 'input-required' : 'unrecognized';
341
+ record.answer = this.finalResults || type === 'complete' ? 'complete' : type === 'input_required' ? 'input-required' : 'unrecognized';
221
342
  if (record.answer !== 'complete')
222
343
  record.continued = true;
223
344
  }
224
345
  return record;
225
346
  }
347
+ noteNegotiation(message) {
348
+ const id = message.id;
349
+ const step = this.negotiating.get(id);
350
+ if (step === undefined)
351
+ return;
352
+ this.negotiating.delete(id);
353
+ if ('error' in message) {
354
+ if (step === 'initialize') {
355
+ this.negotiation.initialize = { refused: true };
356
+ return;
357
+ }
358
+ const { code, data } = message.error;
359
+ const versions = code === -32022 ? stringList(data?.supported) : undefined;
360
+ if (versions)
361
+ this.negotiation.listed = { versions, code, data };
362
+ return;
363
+ }
364
+ const result = message.result;
365
+ if (step === 'initialize') {
366
+ const version = result?.protocolVersion;
367
+ this.negotiation.initialize = { version: typeof version === 'string' ? version : null };
368
+ }
369
+ else {
370
+ const versions = stringList(result?.supportedVersions);
371
+ if (versions)
372
+ this.negotiation.listed = { versions };
373
+ }
374
+ }
226
375
  }
227
376
  /**
228
377
  * The diagnostic for a line that is JSON but not a JSON-RPC message: the
@@ -368,12 +517,38 @@ function bearerAuth(config) {
368
517
  },
369
518
  };
370
519
  }
520
+ function bodyId(body) {
521
+ if (typeof body !== 'string')
522
+ return undefined;
523
+ try {
524
+ const id = JSON.parse(body)?.id;
525
+ return typeof id === 'string' || typeof id === 'number' ? id : undefined;
526
+ }
527
+ catch {
528
+ return undefined;
529
+ }
530
+ }
531
+ /** The JSON-RPC error -32022 (unsupported protocol version) that a 400
532
+ * carries, read from a copy so the SDK still reads the body itself. */
533
+ async function revisionRefusal(response) {
534
+ try {
535
+ const error = (await response.clone().json())?.error;
536
+ if (error?.code !== -32022)
537
+ return undefined;
538
+ return { code: -32022, message: typeof error.message === 'string' ? error.message : '', ...(error.data !== undefined ? { data: error.data } : {}) };
539
+ }
540
+ catch {
541
+ return undefined;
542
+ }
543
+ }
544
+ const joinVersions = (versions, conjunction) => versions.length < 2 ? versions.join('') : `${versions.slice(0, -1).join(', ')} ${conjunction} ${versions[versions.length - 1]}`;
371
545
  export class ModernMcpConnection extends EventEmitter {
372
546
  config;
373
547
  id;
374
548
  family = 'modern';
375
549
  transportKind;
376
- /** The pinned revision, once a connect has established it; null before. */
550
+ /** The revision the last connect established (over HTTP, the one it
551
+ * negotiated); null before any has. */
377
552
  protocolVersion = null;
378
553
  session = null;
379
554
  /** A connect in flight, owned so `close()` can end it. */
@@ -385,6 +560,10 @@ export class ModernMcpConnection extends EventEmitter {
385
560
  reconnectMaxIntervalMs;
386
561
  reconnectTimer = null;
387
562
  reconnectAttempts = 0;
563
+ restarting = false;
564
+ adoptedAt = 0;
565
+ youngRestartFree = true;
566
+ stopped = false;
388
567
  /** At most one pending reopen, and only ever for the current generation. */
389
568
  relisten = null;
390
569
  relistenAttempts = 0;
@@ -445,16 +624,23 @@ export class ModernMcpConnection extends EventEmitter {
445
624
  get isConnected() {
446
625
  return this.session !== null;
447
626
  }
448
- /** Whether a lost connection will be re-established in the background. */
627
+ /** Whether a lost connection will be re-established in the background:
628
+ * with `reconnect` set, or while a new session starts in place of one the
629
+ * server ended. */
449
630
  get willReconnect() {
450
- return this.reconnectEnabled && !this.closedByHost && this.unreaped.length === 0;
631
+ return (this.reconnectEnabled || this.restarting) && !this.closedByHost && this.unreaped.length === 0;
451
632
  }
452
633
  /**
453
- * Stop reconnecting, closing nothing, as the legacy engine's does.
634
+ * Stop reconnecting, as the legacy engine's does, leaving the live session
635
+ * open. A session the server ends from then on isn't restarted either (see
636
+ * {@link sessionEnded}), and a reconnect or restart still connecting
637
+ * doesn't come up: its new session is closed.
454
638
  * @internal For the framework's stop(); not a host API.
455
639
  */
456
640
  stopReconnecting() {
457
641
  this.reconnectEnabled = false;
642
+ this.restarting = false;
643
+ this.stopped = true;
458
644
  }
459
645
  /** Whether close() has been called: closed for good, or closing. A
460
646
  * connection lost on its own is not closed in this sense. */
@@ -586,15 +772,37 @@ export class ModernMcpConnection extends EventEmitter {
586
772
  // reopen backs off from the base interval, not from the previous
587
773
  // generation's failures.
588
774
  this.relistenAttempts = 0;
589
- const stdio = this.transportKind === 'stdio'
590
- ? new SpawnerStdioTransport(this.config, (line) => { if (this.isCurrent(generation))
775
+ let session;
776
+ let http;
777
+ let stdio = null;
778
+ let inner;
779
+ if (this.transportKind === 'stdio') {
780
+ stdio = new SpawnerStdioTransport(this.config, (line) => { if (this.isCurrent(generation))
591
781
  this.deliverStderr(line); }, (reason, failure) => { if (this.isCurrent(generation))
592
- this.handleLost(generation, reason, failure); })
593
- : null;
594
- const inner = stdio ?? new StreamableHTTPClientTransport(new URL(this.config.url), { authProvider: bearerAuth(this.config) });
595
- const wire = new ObservedTransport(inner);
782
+ this.handleLost(generation, reason, failure); });
783
+ inner = stdio;
784
+ }
785
+ else {
786
+ // The transport's own backoff is zeroed, so the delay it hands
787
+ // `streamRetry` is the server's retry hint alone, and `streamRetry`
788
+ // applies the connection's backoff. The retry limit is set once the
789
+ // era settles (see establish): the transport keeps this object and
790
+ // reads `maxRetries` from it at each retry.
791
+ const reconnection = { initialReconnectionDelay: 0, maxReconnectionDelay: 0, reconnectionDelayGrowFactor: 1, maxRetries: Infinity };
792
+ const transport = new StreamableHTTPClientTransport(new URL(this.config.url), {
793
+ authProvider: bearerAuth(this.config),
794
+ fetch: this.observedFetch(() => session),
795
+ reconnectionOptions: reconnection,
796
+ reconnectionScheduler: this.streamRetry,
797
+ });
798
+ http = { transport, reconnection, streamOpened: false, ended: false };
799
+ inner = transport;
800
+ }
801
+ const wire = new ObservedTransport(inner, http ? () => ModernMcpConnection.endSession(session) : undefined);
596
802
  const client = new Client({ name: 'agent-framework', version: '1.0.0' }, {
597
- versionNegotiation: { mode: { pin: MODERN_MCP_PROTOCOL_VERSION } },
803
+ ...(this.transportKind === 'http'
804
+ ? { versionNegotiation: { mode: 'auto' }, supportedProtocolVersions: [...MODERN_HTTP_PROTOCOL_VERSIONS] }
805
+ : { versionNegotiation: { mode: { pin: MODERN_MCP_PROTOCOL_VERSION } } }),
598
806
  // Nothing advertised: this client supplies no sampling, elicitation
599
807
  // or roots, so a conforming server cannot require them.
600
808
  capabilities: {},
@@ -607,7 +815,7 @@ export class ModernMcpConnection extends EventEmitter {
607
815
  },
608
816
  },
609
817
  });
610
- const session = { generation, client, wire, stdio, lost: false };
818
+ session = { generation, client, wire, stdio, http, lost: false };
611
819
  client.onerror = (error) => {
612
820
  // The SDK reports late responses to deadline-abandoned calls here
613
821
  // ("unknown message ID"), and a list-change subscription it could not
@@ -656,10 +864,12 @@ export class ModernMcpConnection extends EventEmitter {
656
864
  const failure = await ModernMcpConnection.closeSession(session);
657
865
  if (failure)
658
866
  this.holdUnreaped(session, failure);
659
- throw await ModernMcpConnection.withStderrTail(session, this.versionVerdict(error) ?? error);
867
+ throw await ModernMcpConnection.withStderrTail(session, this.versionVerdict(error, session.wire.negotiation) ?? error);
660
868
  }
661
- if (!this.isCurrent(session.generation)) {
662
- // Superseded or closed while connecting: this generation never serves.
869
+ if (!this.isCurrent(session.generation) || this.stopped) {
870
+ // Superseded or closed while connecting, or a reconnect or restart
871
+ // still connecting when stopReconnecting() came: this generation never
872
+ // serves.
663
873
  const failure = await ModernMcpConnection.closeSession(session);
664
874
  if (failure)
665
875
  this.holdUnreaped(session, failure);
@@ -684,39 +894,77 @@ export class ModernMcpConnection extends EventEmitter {
684
894
  ? `MCP server "${this.id}": this host stopped using the server's stdio while connecting (${stopped.message})`
685
895
  : `MCP server "${this.id}" closed its connection while connecting`));
686
896
  }
897
+ const era = session.client.getProtocolEra() ?? 'modern';
898
+ session.wire.establish(era);
899
+ if (session.http && era === 'modern')
900
+ session.http.reconnection.maxRetries = SDK_STREAM_RETRIES;
901
+ if (this.opening?.session === session)
902
+ this.opening = null;
687
903
  this.session = session;
688
- this.protocolVersion = MODERN_MCP_PROTOCOL_VERSION;
689
- this.reconnectAttempts = 0;
690
- this.keepListening(session, session.client.autoOpenedSubscription);
904
+ this.adoptedAt = performance.now();
905
+ this.protocolVersion = session.client.getNegotiatedProtocolVersion() ?? null;
906
+ if (!session.http || era === 'modern')
907
+ this.reconnectAttempts = 0;
908
+ // On a 2025 revision list changes arrive unsolicited, on the session's
909
+ // standalone stream: there's no subscription to keep.
910
+ if (era === 'modern')
911
+ this.keepListening(session, session.client.autoOpenedSubscription);
691
912
  }
692
913
  /**
693
- * The server refused the pinned revision (no common one in its
694
- * `server/discover` answer, or an unsupported-version error): a
695
- * configuration verdict, as for the legacy engine, in the same type, so
696
- * every reader treats both engines alike. Retrying can't change it.
914
+ * This client and the server couldn't negotiate a revision: a configuration
915
+ * verdict, as for the legacy engine, in the same type, so every reader
916
+ * treats both engines alike. Retrying can't change it.
697
917
  */
698
- versionVerdict(error) {
699
- if (!(error instanceof UnsupportedProtocolVersionError))
700
- return null;
918
+ versionVerdict(error, evidence = {}) {
919
+ const offered = this.transportKind === 'http' ? MODERN_HTTP_PROTOCOL_VERSIONS : [MODERN_MCP_PROTOCOL_VERSION];
701
920
  // The server's words and data, redacted before they're bounded or kept:
702
921
  // the error goes to the host. One build for all of them, so a
703
922
  // configuration that can't be read is read once.
704
923
  const redact = this.recorded.now();
705
- const data = redactedData(error.data, redact);
706
- const supported = Array.isArray(data?.supported) && data.supported.every((v) => typeof v === 'string')
707
- ? data.supported
708
- : null;
709
924
  // Legacy MCP is reached over stdio or WebSocket. A stdio server's
710
925
  // family is its configuration's choice; an http(s) url is modern by its
711
926
  // scheme, so there is nothing to reconfigure on it.
712
- const legacyHint = !supported?.includes(LEGACY_MCP_PROTOCOL_VERSION)
927
+ const legacyHint = (supported) => !supported?.includes(LEGACY_MCP_PROTOCOL_VERSION)
713
928
  ? ''
714
929
  : this.transportKind === 'stdio'
715
930
  ? ` It speaks legacy MCP: configure it without protocol: 'modern'.`
716
- : ` It speaks only legacy MCP, which this client reaches over stdio or WebSocket (ws://, wss://), not http(s).`;
717
- return new McplProtocolVersionError(`MCP server "${this.id}" does not support MCP ${MODERN_MCP_PROTOCOL_VERSION}` +
718
- (supported ? ` (it supports: ${boundedForLog(supported.join(', ')) || 'none listed'}).` : ` (${boundedForLog(redact(error.message))}).`) +
719
- legacyHint, 'rejected', null, supported, error.code === undefined ? undefined : Number(error.code), data, MODERN_MCP_PROTOCOL_VERSION);
931
+ : ` It speaks legacy MCP (${LEGACY_MCP_PROTOCOL_VERSION}), which this client reaches over stdio or WebSocket (ws://, wss://), not http(s).`;
932
+ const rejected = (refused, supported, why, code, data, note = '') => new McplProtocolVersionError(`MCP server "${this.id}" does not support MCP ${boundedForLog(redact(joinVersions(refused, 'or')))}` +
933
+ (supported ? ` (it supports: ${boundedForLog(redact(supported.join(', '))) || 'none listed'}).` : ` (${boundedForLog(why)}).`) +
934
+ note + legacyHint(supported), 'rejected', null, supported, code, data, offered[0], offered);
935
+ if (error instanceof UnsupportedProtocolVersionError) {
936
+ // The SDK's own refusal ends negotiation, whatever else the server's
937
+ // list names. Over HTTP it raises one for a -32022 whose list names a
938
+ // revision from 2026-07-28 on that this client doesn't speak, or for a
939
+ // second refusal of the one it asked for, and tries no initialize
940
+ // after either, even when the list also names a 2025 revision this
941
+ // client offers.
942
+ const code = error.code;
943
+ const data = redactedData(error.data, redact);
944
+ const supported = stringList(data?.supported) ?? null;
945
+ const requested = data?.requested;
946
+ const asked = typeof requested === 'string' ? requested : MODERN_MCP_PROTOCOL_VERSION;
947
+ const shared = supported ? offered.filter((version) => version !== asked && supported.includes(version)) : [];
948
+ const note = shared.length > 0
949
+ ? ` It lists ${joinVersions(shared, 'and')}, which this client also speaks, but the SDK negotiates no further after this refusal.`
950
+ : '';
951
+ return rejected([asked], supported, redact(error.message), code === undefined ? undefined : Number(code), data, note);
952
+ }
953
+ const { initialize, listed } = evidence;
954
+ const accepted = offered.filter((version) => version !== MODERN_MCP_PROTOCOL_VERSION);
955
+ if (initialize && 'version' in initialize && (initialize.version === null || !accepted.includes(initialize.version))) {
956
+ const version = initialize.version === null ? null : redact(initialize.version);
957
+ const named = version === null ? 'without naming a protocol revision' : `with MCP ${boundedForLog(version)}`;
958
+ const where = initialize.version === LEGACY_MCP_PROTOCOL_VERSION
959
+ ? ', which this client reaches over stdio or WebSocket (ws://, wss://), not http(s)'
960
+ : initialize.version === MODERN_MCP_PROTOCOL_VERSION ? ', which this client negotiates through server/discover, not initialize' : '';
961
+ return new McplProtocolVersionError(`MCP server "${this.id}" answered initialize ${named}${where}. Over http(s) this client's initialize accepts ${joinVersions(accepted, 'or')}.`, 'mismatch', version, null, undefined, undefined, offered[0], offered);
962
+ }
963
+ const nothingElse = this.transportKind === 'stdio' || (initialize !== undefined && 'refused' in initialize);
964
+ if (listed && nothingElse && !listed.versions.some((version) => offered.includes(version))) {
965
+ return rejected(offered, listed.versions.map((version) => redact(version)), '', listed.code, redactedData(listed.data, redact));
966
+ }
967
+ return null;
720
968
  }
721
969
  /**
722
970
  * Keep list-change awareness while this generation lives. The SDK opens the
@@ -781,6 +1029,211 @@ export class ModernMcpConnection extends EventEmitter {
781
1029
  this.scheduleRelisten(generation);
782
1030
  }
783
1031
  }
1032
+ /**
1033
+ * The HTTP transport's fetch, observed for one session. It observes a
1034
+ * 2025 connection's requests: those carrying a 2025 revision in
1035
+ * `MCP-Protocol-Version`, which the SDK sends on each request after
1036
+ * `initialize`, with a session id or without one (the id is optional). The
1037
+ * handshake's own requests and a 2026-07-28 connection's pass through. The
1038
+ * transport reports these HTTP outcomes only as errors, or not at all:
1039
+ * - A 404 to a request that carried the session's id: the server has ended
1040
+ * the session. A POST's 404 is also that request's answer: the server
1041
+ * refused it without running it. That's marked on the leg's record (its
1042
+ * body's id names the leg) before anything else runs, since ending the
1043
+ * session closes its client, which can fail the call before the
1044
+ * transport's own 404 error reaches it.
1045
+ * - The standalone stream's first open, the one GET without
1046
+ * `Last-Event-ID` before that stream has opened, is the exception. Its
1047
+ * 404 can also mean the server has no GET route at all, which a server
1048
+ * that routes only POST answers, and reading it as the session's end
1049
+ * would make such a server unreachable. It ends nothing: the SDK reports
1050
+ * it as an error and opens no stream, as it does for any refusal of that
1051
+ * open but a 405, which it takes quietly, and a 401, which its credential
1052
+ * refresh tries once more. A session that really ended
1053
+ * meets the 404 on its next POST, such as the framework's relist after
1054
+ * the connect. The SDK never retries this open, so {@link firstOpen}
1055
+ * does, on a failure that may pass.
1056
+ * - A 400 carrying the JSON-RPC error -32022 (unsupported protocol
1057
+ * version), to any request: the server no longer serves this revision,
1058
+ * as the SDK's 2026 handler answers a 2025 request when it refuses
1059
+ * legacy traffic (`legacy: 'reject'`). The connection negotiates again,
1060
+ * and settles on 2026-07-28 when the server offers it. A POST's refusal
1061
+ * is that request's answer, a JSON-RPC error, as the SDK reads such a 400
1062
+ * on 2026-07-28: marked on its record as a 404 is.
1063
+ * - A 405 to a GET for one of the session's streams that the server had
1064
+ * served: the standalone stream opening again after it had opened, or a
1065
+ * request's stream resumed, which the server gave an event id. The server
1066
+ * no longer serves this session a stream, as the SDK's 2026 handler
1067
+ * answers a 2025 session it now serves statelessly, or refuses. The
1068
+ * connection negotiates again. A first open's 405 says only that the
1069
+ * server offers no stream, so it changes nothing. Each of these three,
1070
+ * the 404, the -32022 and the 405, marks the session ended at once
1071
+ * (`http.ended`), so it gets no DELETE: the server has let it go.
1072
+ * - The standalone stream opening again, after it had opened, without
1073
+ * `Last-Event-ID`: it couldn't ask for what it missed, whether because
1074
+ * the server's events carry no ids or because the stream that dropped
1075
+ * had carried none (the SDK keeps a stream's last id only for that
1076
+ * stream). See {@link streamReopened}. A GET that carries one resumes a
1077
+ * stream, the standalone one or a request's, and the server replays
1078
+ * what that stream missed.
1079
+ * - A standalone resume the server refuses with 400, as one does for an
1080
+ * event id it no longer holds: retrying the same id can't succeed, and
1081
+ * the SDK would retry it for the session's life. The stream opens again
1082
+ * without the id instead, which is a reopen that missed what came between.
1083
+ * A request stream's resume refused that way is left to the SDK: it is
1084
+ * retried until its request's answer arrives on it, or its deadline
1085
+ * cancels it (see ObservedTransport's `aborts`).
1086
+ */
1087
+ observedFetch(sessionOf) {
1088
+ return async (url, init) => {
1089
+ const headers = new Headers(init?.headers);
1090
+ const revision = headers.get('mcp-protocol-version');
1091
+ if (revision === null || !HTTP_2025_REVISIONS.has(revision))
1092
+ return fetch(url, init);
1093
+ const session = sessionOf();
1094
+ const http = session.http;
1095
+ const method = (init?.method ?? 'GET').toUpperCase();
1096
+ const resuming = headers.has('last-event-id');
1097
+ if (method === 'GET' && !resuming && http.standaloneSignal === undefined)
1098
+ http.standaloneSignal = init?.signal ?? null;
1099
+ const standalone = method === 'GET' && (init?.signal ?? null) === http.standaloneSignal;
1100
+ const firstOpen = standalone && !http.streamOpened;
1101
+ let response = firstOpen ? await this.firstOpen(session, url, init) : await fetch(url, init);
1102
+ let refusal = response.status === 400 ? await revisionRefusal(response) : undefined;
1103
+ let replayed = resuming;
1104
+ if (!refusal && resuming && standalone && response.status === 400) {
1105
+ await response.body?.cancel().catch(() => { });
1106
+ headers.delete('last-event-id');
1107
+ response = await fetch(url, { ...init, headers });
1108
+ replayed = false;
1109
+ refusal = response.status === 400 ? await revisionRefusal(response) : undefined;
1110
+ }
1111
+ if (refusal) {
1112
+ const record = method === 'POST' ? wireScope.getStore() : undefined;
1113
+ if (record?.currentId !== undefined && bodyId(init?.body) === record.currentId) {
1114
+ record.answer = 'error';
1115
+ record.error = refusal;
1116
+ record.sessionEnded = 'revision';
1117
+ }
1118
+ http.ended = true;
1119
+ queueMicrotask(() => this.sessionEnded(session, 'revision'));
1120
+ return response;
1121
+ }
1122
+ if (response.status === 404 && !firstOpen && headers.has('mcp-session-id')) {
1123
+ const record = method === 'POST' ? wireScope.getStore() : undefined;
1124
+ if (record?.currentId !== undefined && bodyId(init?.body) === record.currentId)
1125
+ record.sessionEnded = 'ended';
1126
+ http.ended = true;
1127
+ queueMicrotask(() => this.sessionEnded(session, 'ended'));
1128
+ }
1129
+ else if (method === 'GET' && response.status === 405 && (standalone ? http.streamOpened : resuming)) {
1130
+ http.ended = true;
1131
+ queueMicrotask(() => this.sessionEnded(session, 'stream'));
1132
+ }
1133
+ else if (standalone && response.ok) {
1134
+ if (!http.streamOpened || !replayed)
1135
+ queueMicrotask(() => this.streamReopened(session));
1136
+ http.streamOpened = true;
1137
+ }
1138
+ return response;
1139
+ };
1140
+ }
1141
+ /** The SDK makes the standalone stream's first open once, and never
1142
+ * retries it. */
1143
+ async firstOpen(session, url, init) {
1144
+ for (let failures = 1;; failures++) {
1145
+ if (failures > 1 && !this.isCurrent(session.generation)) {
1146
+ throw new Error(`MCP server "${this.id}": the list-change stream's first open was not tried again, since the connection no longer uses this session`);
1147
+ }
1148
+ let failure;
1149
+ try {
1150
+ const response = await fetch(url, init);
1151
+ if (!mayPass(response.status))
1152
+ return response;
1153
+ await response.body?.cancel().catch(() => { });
1154
+ failure = `HTTP ${response.status}`;
1155
+ }
1156
+ catch (error) {
1157
+ if (init?.signal?.aborted)
1158
+ throw error;
1159
+ failure = error.message;
1160
+ }
1161
+ if (this.isCurrent(session.generation) && !session.http.ended) {
1162
+ this.emit('error', new Error(`MCP server "${this.id}" did not open its list-change stream (${redactServerText(failure, this.config)}); trying again`));
1163
+ }
1164
+ await new Promise((resolve, reject) => {
1165
+ const signal = init?.signal;
1166
+ if (signal?.aborted)
1167
+ return reject(signal.reason);
1168
+ const timer = setTimeout(() => {
1169
+ signal?.removeEventListener('abort', aborted);
1170
+ resolve();
1171
+ }, this.backoffDelay(failures));
1172
+ timer.unref?.();
1173
+ const aborted = () => {
1174
+ clearTimeout(timer);
1175
+ reject(signal?.reason);
1176
+ };
1177
+ signal?.addEventListener('abort', aborted, { once: true });
1178
+ });
1179
+ }
1180
+ }
1181
+ /**
1182
+ * The HTTP transport's scheduler for trying a stream again, for both
1183
+ * streams it retries: the standalone stream and a request's stream the
1184
+ * server closed after a priming event (2025-11-25). The server's `retry`
1185
+ * hint is respected, as the spec requires. `hinted` is the server's hint
1186
+ * alone, or 0, since the transport's own backoff is zeroed.
1187
+ */
1188
+ streamRetry = (reconnect, hinted, attempt) => {
1189
+ const wait = attempt === 0 && hinted > 0 ? hinted : Math.max(hinted, this.backoffDelay(attempt + 1));
1190
+ const timer = setTimeout(reconnect, Math.min(wait, MAX_TIMER_MS));
1191
+ timer.unref?.();
1192
+ return () => clearTimeout(timer);
1193
+ };
1194
+ /** The spec has a client whose session met a 404 start a new one. */
1195
+ sessionEnded(session, cause) {
1196
+ if (this.session !== session || !this.isCurrent(session.generation))
1197
+ return;
1198
+ this.session = null;
1199
+ void ModernMcpConnection.closeSession(session);
1200
+ if (this.stopped) {
1201
+ this.emit('close', { reason: `the server ${SESSION_END[cause]}` });
1202
+ return;
1203
+ }
1204
+ const lived = performance.now() - this.adoptedAt >= this.reconnectIntervalMs;
1205
+ if (session.wire.answered && (lived || this.youngRestartFree)) {
1206
+ this.youngRestartFree = lived;
1207
+ this.reconnectAttempts = 0;
1208
+ this.restarting = true;
1209
+ try {
1210
+ this.emit('close', { reason: `the server ${SESSION_END[cause]}; a new one is starting` });
1211
+ }
1212
+ finally {
1213
+ void this.attemptReconnect(cause);
1214
+ }
1215
+ return;
1216
+ }
1217
+ const attempt = Math.max(1, this.reconnectAttempts);
1218
+ this.reconnectAttempts = attempt + 1;
1219
+ const when = session.wire.answered
1220
+ ? `less than ${this.reconnectIntervalMs} ms after the session started`
1221
+ : 'before the session answered a request';
1222
+ try {
1223
+ this.emit('reconnect-failed', { error: `MCP server "${this.id}" ${SESSION_END[cause]} ${when}`, attempt });
1224
+ this.emit('close', { reason: `the server ${SESSION_END[cause]} ${when}` });
1225
+ }
1226
+ finally {
1227
+ this.scheduleReconnect();
1228
+ }
1229
+ }
1230
+ streamReopened(session) {
1231
+ if (this.session !== session || !this.isCurrent(session.generation))
1232
+ return;
1233
+ if (!session.client.getServerCapabilities()?.tools?.listChanged)
1234
+ return;
1235
+ this.emit('tools-list-changed');
1236
+ }
784
1237
  /** The transport went away (child exit, or a closed HTTP client). */
785
1238
  handleLost(generation, reason, unreaped, ended) {
786
1239
  const session = this.session;
@@ -848,28 +1301,37 @@ export class ModernMcpConnection extends EventEmitter {
848
1301
  void this.attemptReconnect();
849
1302
  }, this.backoffDelay(this.reconnectAttempts));
850
1303
  }
851
- async attemptReconnect() {
1304
+ async attemptReconnect(restart) {
852
1305
  if (!this.willReconnect || this.session)
853
1306
  return;
854
1307
  const attempt = Math.max(1, this.reconnectAttempts);
855
1308
  try {
856
1309
  await this.open();
1310
+ if (restart)
1311
+ this.restarting = false;
857
1312
  if (!this.session)
858
1313
  return;
859
- console.error(`MCP server "${this.id}" reconnected`);
1314
+ console.error(restart ? `MCP server "${this.id}" ${SESSION_END[restart]}; a new one started` : `MCP server "${this.id}" reconnected`);
860
1315
  this.emit('reconnect', { attempts: attempt });
861
1316
  }
862
1317
  catch (error) {
1318
+ if (restart)
1319
+ this.restarting = false;
863
1320
  if (this.closedByHost)
864
1321
  return;
1322
+ const reconnecting = this.reconnectEnabled;
865
1323
  this.reconnectAttempts = attempt + 1;
866
1324
  const stderrTail = stderrTailOf(error);
867
- const failed = (message, permanent) => this.emit('reconnect-failed', {
868
- error: message,
869
- ...(stderrTail === undefined ? {} : { stderrTail }),
870
- attempt,
871
- ...(permanent ? { permanent } : {}),
872
- });
1325
+ const failed = (message, permanent) => {
1326
+ this.emit('reconnect-failed', {
1327
+ error: message,
1328
+ ...(stderrTail === undefined ? {} : { stderrTail }),
1329
+ attempt,
1330
+ ...(permanent ? { permanent } : {}),
1331
+ });
1332
+ if (restart && !reconnecting && !this.session)
1333
+ this.emit('close', { reason: `the server ${SESSION_END[restart]}, and a new one could not start` });
1334
+ };
873
1335
  const halted = this.unreapedFailure();
874
1336
  if (halted) {
875
1337
  failed(`${error.message}; ${halted}`, true);
@@ -894,10 +1356,10 @@ export class ModernMcpConnection extends EventEmitter {
894
1356
  * to that call, and leaves the call's evidence alone.
895
1357
  */
896
1358
  async listTools() {
897
- const { tools } = await this.request('tools/list', 'tools/list', (client) => client.listTools(undefined, {
1359
+ const { tools } = await this.request('tools/list', 'tools/list', (client, signal) => client.listTools(undefined, {
898
1360
  cacheMode: 'refresh',
899
1361
  timeout: LIST_TIMEOUT_MS,
900
- signal: AbortSignal.timeout(LIST_TIMEOUT_MS),
1362
+ signal,
901
1363
  }), LIST_TIMEOUT_MS);
902
1364
  return tools.map((tool) => ({
903
1365
  name: tool.name,
@@ -910,11 +1372,12 @@ export class ModernMcpConnection extends EventEmitter {
910
1372
  * Call a tool within the configured deadline. One budget covers the whole
911
1373
  * logical call: the SDK's per-leg timer and its `maxTotalTimeout` across
912
1374
  * continuation rounds, and an abort signal around any retry the SDK makes
913
- * before dispatch (an auth refresh, or a header-mismatch relist).
1375
+ * before dispatch (an auth refresh, or a header-mismatch relist). That
1376
+ * signal also fires when the call's stream is lost (WireRecord's `cancel`).
914
1377
  */
915
1378
  async callTool(name, args) {
916
1379
  const timeoutMs = this.requestTimeoutMs;
917
- const result = await this.request('tools/call', `tools/call ${logValue(name)}`, (client) => client.callTool({ name, arguments: args }, { timeout: timeoutMs, maxTotalTimeout: timeoutMs, signal: AbortSignal.timeout(timeoutMs) }), timeoutMs);
1380
+ const result = await this.request('tools/call', `tools/call ${logValue(name)}`, (client, signal) => client.callTool({ name, arguments: args }, { timeout: timeoutMs, maxTotalTimeout: timeoutMs, signal }), timeoutMs);
918
1381
  return {
919
1382
  content: result.content ?? [],
920
1383
  ...(result.isError !== undefined ? { isError: result.isError } : {}),
@@ -938,14 +1401,20 @@ export class ModernMcpConnection extends EventEmitter {
938
1401
  'If calling it again fails, ask whoever runs this host.'
939
1402
  : `The request wasn't sent: the connection to the "${this.id}" server is closed, so nothing ran. ${this.retryText()}`, 'not-sent');
940
1403
  }
941
- const record = { method, handedOff: false, answer: 'none', continued: false };
1404
+ const record = { method, handedOff: false, answer: 'none', continued: false, cancel: new AbortController() };
1405
+ // The deadline aborts with its own reason, a timeout, which the SDK reads
1406
+ // as one.
1407
+ const deadline = AbortSignal.timeout(timeoutMs);
1408
+ const atDeadline = () => record.cancel.abort(deadline.reason);
1409
+ deadline.addEventListener('abort', atDeadline, { once: true });
942
1410
  try {
943
- return await wireScope.run(record, () => run(session.client));
1411
+ return await wireScope.run(record, () => run(session.client, record.cancel.signal));
944
1412
  }
945
1413
  catch (error) {
946
1414
  throw this.requestFailure(what, record, error, timeoutMs, session.lost);
947
1415
  }
948
1416
  finally {
1417
+ deadline.removeEventListener('abort', atDeadline);
949
1418
  session.wire.forget(record);
950
1419
  }
951
1420
  }
@@ -967,6 +1436,29 @@ export class ModernMcpConnection extends EventEmitter {
967
1436
  await session.client.close().catch(() => { });
968
1437
  return failure;
969
1438
  }
1439
+ /**
1440
+ * End an HTTP session on the server (a DELETE), as the spec asks of a
1441
+ * client that no longer needs it. It runs before the inner transport
1442
+ * closes, since that close would abort it. When a connect fails after its
1443
+ * `initialize` was answered, the SDK closes the transport itself before
1444
+ * the error reaches `establish`. The transport sends nothing for a session
1445
+ * without an id, and the SDK reports a DELETE's failure through the
1446
+ * transport's onerror.
1447
+ */
1448
+ static endSession(session) {
1449
+ const http = session.http;
1450
+ if (!http || http.ended)
1451
+ return Promise.resolve();
1452
+ http.deleting ??= (async () => {
1453
+ let timer;
1454
+ await Promise.race([
1455
+ http.transport.terminateSession().catch(() => { }),
1456
+ new Promise((resolve) => { timer = setTimeout(resolve, DELETE_TIMEOUT_MS); }),
1457
+ ]);
1458
+ clearTimeout(timer);
1459
+ })();
1460
+ return http.deleting;
1461
+ }
970
1462
  /** The close in flight, which later calls wait behind. */
971
1463
  closing = null;
972
1464
  /** Set when the host closes a live session; cleared, with the one
@@ -1079,13 +1571,17 @@ export class ModernMcpConnection extends EventEmitter {
1079
1571
  * rounds cap). Nothing is in flight then, so nothing is cancelled. A
1080
1572
  * result of a type this client doesn't recognize ends the call where it
1081
1573
  * is;
1082
- * - an HTTP 401/403 refusal of the current leg: `error-response`, the
1083
- * status in `data.httpStatus` (the server answered at the HTTP layer,
1084
- * outside the JSON-RPC code space). After an earlier leg
1085
- * asked for another round it is `no-response` instead: the server may
1086
- * already have acted on the call;
1087
- * - handed off and unanswered: `no-response`. At the deadline, cancellation
1088
- * was requested and the outcome is unknown.
1574
+ * - an HTTP refusal of the current leg: `error-response`, the status in
1575
+ * `data.httpStatus` (the server answered at the HTTP layer, outside the
1576
+ * JSON-RPC code space). That's a 401 or 403, or a 404 to a POST from a
1577
+ * server that had ended the session (`sessionEnded`, read at the HTTP
1578
+ * layer, whatever error the call then met as its session closed). After
1579
+ * an earlier leg asked for another round it is `no-response` instead:
1580
+ * the server may already have acted on the call;
1581
+ * - handed off and unanswered: `no-response`, and the outcome is unknown.
1582
+ * At the deadline, cancellation was requested, and so it was when the
1583
+ * stream that would carry the answer ended unresumed (WireRecord's
1584
+ * `cancel`).
1089
1585
  *
1090
1586
  * The message is the agent's, in the legacy engine's words where it has
1091
1587
  * words for the same outcome (#506), and in their shape where it hasn't:
@@ -1129,6 +1625,9 @@ export class ModernMcpConnection extends EventEmitter {
1129
1625
  // and the message are redacted there, whatever JSON values they are,
1130
1626
  // as every other line this connection writes is.
1131
1627
  log(`answered ${what} with error ${loggedCode(code, redact)}: ${logValueRedacted(wireMessage, redact)}`);
1628
+ if (record.sessionEnded === 'revision') {
1629
+ return new McplRequestError(`${Server} refused this session's revision, so the request didn't run. ${this.retryText()}`, 'error-response', redactedData(data, redact));
1630
+ }
1132
1631
  // The server's own words, redacted and then bounded as errors.ts says,
1133
1632
  // since they're in an error the caller gets (redactServerText's
1134
1633
  // scope), and its data's strings redacted too. It's trimmed after it's
@@ -1155,23 +1654,31 @@ export class ModernMcpConnection extends EventEmitter {
1155
1654
  ? `${Server}'s answer wasn't a complete result`
1156
1655
  : `${Server} didn't finish the request: it asked for more from this host, and ${deadline ? 'the time ran out' : 'the call ended'} before this host sent it`), 'no-response');
1157
1656
  }
1158
- // An HTTP refusal of the credential: 401 or 403, however the SDK raised
1159
- // it. It raises a plain 403 under another code than its step-up limit's,
1160
- // and an insufficient_scope 403 as InsufficientScopeError when no OAuth
1161
- // provider is configured. A 403 can refuse a valid credential that lacks
1657
+ // An HTTP refusal: the session's end, read at the HTTP layer, or the
1658
+ // credential's, 401 or 403, however the SDK raised it. It raises a plain
1659
+ // 403 under another code than its step-up limit's, and an
1660
+ // insufficient_scope 403 as InsufficientScopeError when no OAuth provider
1661
+ // is configured. A 403 can refuse a valid credential that lacks
1162
1662
  // permission, so the message says the server refused this host access.
1163
- const refusedWith = error instanceof SdkHttpError && (error.status === 401 || error.status === 403)
1164
- ? error.status
1165
- : error instanceof InsufficientScopeError ? 403 : null;
1663
+ const refusedWith = record.sessionEnded === 'ended'
1664
+ ? 404
1665
+ : error instanceof SdkHttpError && (error.status === 401 || error.status === 403)
1666
+ ? error.status
1667
+ : error instanceof InsufficientScopeError ? 403 : null;
1166
1668
  if (refusedWith !== null) {
1669
+ const why = refusedWith === 404 ? "the server had ended this connection's session" : logDetail(message);
1167
1670
  if (record.continued) {
1168
- log(`refused a later round of ${what}: HTTP ${refusedWith} (${logDetail(message)})`);
1671
+ log(`refused a later round of ${what}: HTTP ${refusedWith} (${why})`);
1169
1672
  return new McplRequestError(listing
1170
1673
  ? unknown(`${Server} refused this host access partway through the request, after asking for more`)
1171
1674
  : `${Server} refused this host access partway through the request, after asking for more, so whether it took effect is unknown. ` +
1172
1675
  'Check its effect before trying again; if calling it again fails, ask whoever runs this host.', 'no-response');
1173
1676
  }
1174
- log(`refused ${what}: HTTP ${refusedWith} (${logDetail(message)})`);
1677
+ if (refusedWith === 404) {
1678
+ log(`refused ${what}: HTTP 404 (${why})`);
1679
+ return new McplRequestError(`${Server} had ended this connection's session, so the request didn't run. ${this.retryText()}`, 'error-response', { httpStatus: 404 });
1680
+ }
1681
+ log(`refused ${what}: HTTP ${refusedWith} (${why})`);
1175
1682
  // The response body (the SDK's `text`) is the server's text and can
1176
1683
  // echo the request's url, so it's left out of the data; the status stays.
1177
1684
  const { text: _body, ...data } = (error instanceof SdkHttpError && error.data && typeof error.data === 'object' ? error.data : {});