chainlesschain 0.165.6 → 0.165.9

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 (251) hide show
  1. package/package.json +2 -1
  2. package/src/assets/web-panel/.build-hash +1 -1
  3. package/src/assets/web-panel/assets/{AIOps-vfoDb6HZ.js → AIOps-DxwjrnEA.js} +1 -1
  4. package/src/assets/web-panel/assets/{ActionButton-Cm5f7p93.js → ActionButton-JdbYowzn.js} +1 -1
  5. package/src/assets/web-panel/assets/{Analytics-yRuvgWxa.js → Analytics-C06GCahv.js} +3 -3
  6. package/src/assets/web-panel/assets/{AppLayout-BROsC4h1.js → AppLayout-BUVEEyi1.js} +4 -4
  7. package/src/assets/web-panel/assets/{Artifacts-CimHApb1.js → Artifacts-D_ORzkJd.js} +1 -1
  8. package/src/assets/web-panel/assets/{Audit-DI8kr8zI.js → Audit-8o_Y5ejG.js} +1 -1
  9. package/src/assets/web-panel/assets/{BackgroundAgents-DSLzKy-n.js → BackgroundAgents-DrkHd5VU.js} +1 -1
  10. package/src/assets/web-panel/assets/{Backup-DkH_n2r1.js → Backup-CKBZCX11.js} +1 -1
  11. package/src/assets/web-panel/assets/{BaseInput-CKSrUoRA.js → BaseInput-C8HkI399.js} +1 -1
  12. package/src/assets/web-panel/assets/{Chat-ClwfMXUS.js → Chat-zb2H2Wux.js} +6 -6
  13. package/src/assets/web-panel/assets/{ChatBubbleRenderer-CjMaqfL7.js → ChatBubbleRenderer-BDcNpU_F.js} +1 -1
  14. package/src/assets/web-panel/assets/{Checkbox-BYTehcQG.js → Checkbox-MwPtQmNz.js} +1 -1
  15. package/src/assets/web-panel/assets/{Codegen-B8AMcp6J.js → Codegen-3p5QCkFK.js} +1 -1
  16. package/src/assets/web-panel/assets/{Col-BNSogPo3.js → Col-BPLlhn52.js} +1 -1
  17. package/src/assets/web-panel/assets/{Community-JxPvGb8O.js → Community-C-3dBNOM.js} +1 -1
  18. package/src/assets/web-panel/assets/{Compact-DF7bFAgE.js → Compact-BUcy-F_b.js} +1 -1
  19. package/src/assets/web-panel/assets/{Compliance-Bvhqnwrq.js → Compliance-CI8NxT8m.js} +1 -1
  20. package/src/assets/web-panel/assets/{Cowork-BDS0CJvM.js → Cowork-DaDEBS-Q.js} +3 -3
  21. package/src/assets/web-panel/assets/{Cron-DqvRfxoQ.js → Cron-BSv9HXu5.js} +2 -2
  22. package/src/assets/web-panel/assets/{Crosschain-CDcIvnqS.js → Crosschain-yuiKlJmv.js} +1 -1
  23. package/src/assets/web-panel/assets/{DID-D7jaUxU3.js → DID-D6LfldB0.js} +2 -2
  24. package/src/assets/web-panel/assets/{Dashboard-B5A18wEV.js → Dashboard-CBhLqG61.js} +2 -2
  25. package/src/assets/web-panel/assets/{Dropdown-_6IVkGag.js → Dropdown-BG8pIl3O.js} +1 -1
  26. package/src/assets/web-panel/assets/{EmailListRenderer-C9PeiKCG.js → EmailListRenderer-CY92VIE6.js} +1 -1
  27. package/src/assets/web-panel/assets/{FamilyGuardDashboard-DXjuEHrX.js → FamilyGuardDashboard-1Q_ehauM.js} +1 -1
  28. package/src/assets/web-panel/assets/{Federation-B4IAR9zv.js → Federation-D3XirJIU.js} +1 -1
  29. package/src/assets/web-panel/assets/{FormItemContext-CxQeSNx1.js → FormItemContext-DfI9UuGZ.js} +1 -1
  30. package/src/assets/web-panel/assets/{GenericCardRenderer-B8GS-Y3N.js → GenericCardRenderer-IeCZk2m7.js} +1 -1
  31. package/src/assets/web-panel/assets/{Git-BS8SouUX.js → Git-DzK4IJAZ.js} +2 -2
  32. package/src/assets/web-panel/assets/{Governance-Dq9aYb5Q.js → Governance-BHf5KfmQ.js} +1 -1
  33. package/src/assets/web-panel/assets/{Inference-BrCEe9AJ.js → Inference-BmaxSALS.js} +1 -1
  34. package/src/assets/web-panel/assets/{KnowledgeGraph-DybOpiY2.js → KnowledgeGraph-BgePFLA9.js} +1 -1
  35. package/src/assets/web-panel/assets/{Logs-DBpzbRfx.js → Logs-nqgxpOc4.js} +2 -2
  36. package/src/assets/web-panel/assets/MarkdownRenderer-xkX6izBt.js +1 -0
  37. package/src/assets/web-panel/assets/{Marketplace-Cc7CCSCu.js → Marketplace-Bu8to-86.js} +1 -1
  38. package/src/assets/web-panel/assets/McpTools-BFWTzEk0.js +4 -0
  39. package/src/assets/web-panel/assets/{Memory-KCqlp9ol.js → Memory-B026G2GA.js} +2 -2
  40. package/src/assets/web-panel/assets/{MobileBridge-Bys84ySN.js → MobileBridge-CM2Eeg6I.js} +1 -1
  41. package/src/assets/web-panel/assets/{MobileProjects-B-6qU-je.js → MobileProjects-B79Ob3hy.js} +1 -1
  42. package/src/assets/web-panel/assets/{Mtc-BgY-fvCq.js → Mtc-CxevSZuV.js} +4 -4
  43. package/src/assets/web-panel/assets/{MtcAudit-B8pCmqQJ.js → MtcAudit-X396cPEX.js} +4 -4
  44. package/src/assets/web-panel/assets/{Multisig-BbOmYutA.js → Multisig-HWIB_aZ1.js} +3 -3
  45. package/src/assets/web-panel/assets/{NLProgramming-BocLQz_M.js → NLProgramming-gnYD1Xwy.js} +1 -1
  46. package/src/assets/web-panel/assets/{Notes-CSwbxEop.js → Notes-Dn2Gf_J1.js} +3 -3
  47. package/src/assets/web-panel/assets/{NotificationSettings-CqFNSYG3.js → NotificationSettings-BRKBQ5rq.js} +1 -1
  48. package/src/assets/web-panel/assets/{OrderTableRenderer-C6gMfAk7.js → OrderTableRenderer-Dy___Cgo.js} +1 -1
  49. package/src/assets/web-panel/assets/{Organization-QrDXVdMJ.js → Organization-cqfccxnt.js} +4 -4
  50. package/src/assets/web-panel/assets/{Overflow-Cxntotjt.js → Overflow-CSX4FHN3.js} +1 -1
  51. package/src/assets/web-panel/assets/{P2P-CIdBOwNq.js → P2P-Bwa4aghp.js} +2 -2
  52. package/src/assets/web-panel/assets/{PdhVaultBrowser-CT5aP8rD.js → PdhVaultBrowser-Dhszyjig.js} +3 -3
  53. package/src/assets/web-panel/assets/{Permissions-DH7OM8XU.js → Permissions-CXzTyk6e.js} +4 -4
  54. package/src/assets/web-panel/assets/{PersonalDataHub-CnJqbWce.js → PersonalDataHub-CFCAAJbb.js} +4 -4
  55. package/src/assets/web-panel/assets/{Pipeline-5C3HG1kH.js → Pipeline-BPJbx6xt.js} +1 -1
  56. package/src/assets/web-panel/assets/{Privacy-B4yIp7xK.js → Privacy-BbiXvEHz.js} +1 -1
  57. package/src/assets/web-panel/assets/{ProjectInit-BMvt-2M0.js → ProjectInit-B-s7-wSD.js} +2 -2
  58. package/src/assets/web-panel/assets/{ProjectSettings-CvSTEMRG.js → ProjectSettings-Dyug206y.js} +2 -2
  59. package/src/assets/web-panel/assets/{Projects-BKBJxEek.js → Projects-BtAI5Fwo.js} +1 -1
  60. package/src/assets/web-panel/assets/{Providers-BdwUqFWN.js → Providers-JpIPmz6N.js} +1 -1
  61. package/src/assets/web-panel/assets/{QrScannerModal-DtnCKlTm.js → QrScannerModal-CZdoN64A.js} +1 -1
  62. package/src/assets/web-panel/assets/{QuickAsk-sTbgwyNr.js → QuickAsk-BwvnoRIT.js} +1 -1
  63. package/src/assets/web-panel/assets/{Recommend-Bzr14gSb.js → Recommend-ASMlMQrD.js} +1 -1
  64. package/src/assets/web-panel/assets/{RemoteSession-C_TF5IaJ.js → RemoteSession-BiJzKFKi.js} +3 -3
  65. package/src/assets/web-panel/assets/{Reputation-GCAM4H2K.js → Reputation-C80TWLIO.js} +1 -1
  66. package/src/assets/web-panel/assets/{Row-Dir0r6OQ.js → Row-C1VNmjmX.js} +1 -1
  67. package/src/assets/web-panel/assets/{RssFeed-CpGnlecX.js → RssFeed-T2MyGUlG.js} +3 -3
  68. package/src/assets/web-panel/assets/{Search-DMMamC6u.js → Search-DXM6O83L.js} +1 -1
  69. package/src/assets/web-panel/assets/{Security-yqay5yG-.js → Security-nv69kU7W.js} +3 -3
  70. package/src/assets/web-panel/assets/{Services-C5i2wh9G.js → Services-AyHWe7Qj.js} +2 -2
  71. package/src/assets/web-panel/assets/{Skeleton-vUqk35yj.js → Skeleton-D6PWLxFI.js} +1 -1
  72. package/src/assets/web-panel/assets/{Skills-DzcSkhot.js → Skills-BqVeOmNQ.js} +1 -1
  73. package/src/assets/web-panel/assets/{Sla-CB6qbYNr.js → Sla-BegIPl9M.js} +1 -1
  74. package/src/assets/web-panel/assets/{SpeechSettings-BHUI7cox.js → SpeechSettings-CKJn3xk-.js} +1 -1
  75. package/src/assets/web-panel/assets/{SyncSettings-D-RrsxTJ.js → SyncSettings-Th4cnAMl.js} +2 -2
  76. package/src/assets/web-panel/assets/{Tasks-DFxdOJiq.js → Tasks-BuKfBroh.js} +1 -1
  77. package/src/assets/web-panel/assets/{Templates-SKAob_Aq.js → Templates-CMuWN8Ac.js} +1 -1
  78. package/src/assets/web-panel/assets/{Tenant-BPfsfHQ-.js → Tenant-C4je8E8Q.js} +1 -1
  79. package/src/assets/web-panel/assets/{Terminal-8iw7FpUf.js → Terminal-agJESOcO.js} +2 -2
  80. package/src/assets/web-panel/assets/{TimelineRenderer-C4Gjj5Su.js → TimelineRenderer-D2qDuVZ_.js} +1 -1
  81. package/src/assets/web-panel/assets/{Tokens-COYhN18e.js → Tokens-Cvmn99rH.js} +1 -1
  82. package/src/assets/web-panel/assets/{Trigger-B-JzXcf6.js → Trigger-D_QYIHH8.js} +1 -1
  83. package/src/assets/web-panel/assets/{Trust-BJ5bg0nI.js → Trust-BmzvQLnX.js} +1 -1
  84. package/src/assets/web-panel/assets/{UkeySign-BG_ByRMf.js → UkeySign-D_LopHwH.js} +1 -1
  85. package/src/assets/web-panel/assets/{VideoEditing-B3kOnSiU.js → VideoEditing-DoWl3PhQ.js} +1 -1
  86. package/src/assets/web-panel/assets/{Wallet-CcoGFnK7.js → Wallet-5NYcAkxe.js} +4 -4
  87. package/src/assets/web-panel/assets/{WebAuthn-DdT8vRC_.js → WebAuthn-B-_Q3ewp.js} +4 -4
  88. package/src/assets/web-panel/assets/{WorkflowEditor-CZu3M9N8.js → WorkflowEditor-CHET-gi2.js} +1 -1
  89. package/src/assets/web-panel/assets/{chat-DxJt3Qba.js → chat-C6nvb-n6.js} +1 -1
  90. package/src/assets/web-panel/assets/{colors-DdYfwJaB.js → colors-CTDE9LTX.js} +1 -1
  91. package/src/assets/web-panel/assets/{compact-item-BG3SUzwj.js → compact-item-KnemdF8D.js} +1 -1
  92. package/src/assets/web-panel/assets/{createContext-By51z2-1.js → createContext-DUJR3Uek.js} +1 -1
  93. package/src/assets/web-panel/assets/devWarning-1DqLH1KX.js +1 -0
  94. package/src/assets/web-panel/assets/{hasIn-B0apZ3KJ.js → hasIn-CFNTaby8.js} +1 -1
  95. package/src/assets/web-panel/assets/{index-ZxtP8jG7.js → index-65HO56Bv.js} +1 -1
  96. package/src/assets/web-panel/assets/{index-Da_WBdLX.js → index-B8RtbSkV.js} +1 -1
  97. package/src/assets/web-panel/assets/{index-BBPf3TpM.js → index-BHqSqnW7.js} +1 -1
  98. package/src/assets/web-panel/assets/{index-BP94CrBV.js → index-BTetEUWd.js} +1 -1
  99. package/src/assets/web-panel/assets/{index-p1_8SbI1.js → index-BXWg1crv.js} +1 -1
  100. package/src/assets/web-panel/assets/{index-BWF8_mkx.js → index-BdiC4LOs.js} +1 -1
  101. package/src/assets/web-panel/assets/index-BhJtcPWo.js +1 -0
  102. package/src/assets/web-panel/assets/{index-SQqOlR3d.js → index-BuizJo2E.js} +1 -1
  103. package/src/assets/web-panel/assets/{index-CpjZhgNd.js → index-BwZM1rod.js} +1 -1
  104. package/src/assets/web-panel/assets/{index-CzmQElZy.js → index-C96N9xeL.js} +1 -1
  105. package/src/assets/web-panel/assets/{index-bP_VlDb4.js → index-CHfUdnJH.js} +1 -1
  106. package/src/assets/web-panel/assets/{index-ZFbps9po.js → index-CKvBF3Ov.js} +1 -1
  107. package/src/assets/web-panel/assets/{index-DaIz0NAW.js → index-CSD9_I2_.js} +1 -1
  108. package/src/assets/web-panel/assets/{index-BewKYrX9.js → index-Cc7dbl_p.js} +1 -1
  109. package/src/assets/web-panel/assets/{index-CNIxXMmN.js → index-CcF0D7UZ.js} +1 -1
  110. package/src/assets/web-panel/assets/{index-Q2SCX-MI.js → index-Ceu92a8D.js} +1 -1
  111. package/src/assets/web-panel/assets/{index-DE76D-kA.js → index-CnLpuTAU.js} +1 -1
  112. package/src/assets/web-panel/assets/index-Cq8MWPDT.js +1 -0
  113. package/src/assets/web-panel/assets/{index-BkvcEo4p.js → index-CquKnw5r.js} +1 -1
  114. package/src/assets/web-panel/assets/{index-Cp6e-hGw.js → index-CyEtHvEE.js} +1 -1
  115. package/src/assets/web-panel/assets/{index-BLIbGOLX.js → index-DF-fcTvQ.js} +1 -1
  116. package/src/assets/web-panel/assets/{index-DCn8IcxV.js → index-DF1Y0Vuw.js} +1 -1
  117. package/src/assets/web-panel/assets/{index-Bissefox.js → index-DJyPeBBk.js} +3 -3
  118. package/src/assets/web-panel/assets/{index-BZ_0qm4g.js → index-DP-PbO11.js} +1 -1
  119. package/src/assets/web-panel/assets/{index-kf57LJ93.js → index-DZgCRMxk.js} +1 -1
  120. package/src/assets/web-panel/assets/{index-BYINoRLK.js → index-DfJzS3Qa.js} +1 -1
  121. package/src/assets/web-panel/assets/{index-3jWkIu5j.js → index-DqPpJkte.js} +1 -1
  122. package/src/assets/web-panel/assets/{index-B5rLiEo0.js → index-DzReoLyh.js} +1 -1
  123. package/src/assets/web-panel/assets/{index-CW93tZDv.js → index-JGTiDeOW.js} +1 -1
  124. package/src/assets/web-panel/assets/{index-C1C9i79A.js → index-NUR0If7O.js} +1 -1
  125. package/src/assets/web-panel/assets/{index-CAhHirVH.js → index-NtAcnJYW.js} +1 -1
  126. package/src/assets/web-panel/assets/{index-RD6O-0C6.js → index-OmuhFYKE.js} +1 -1
  127. package/src/assets/web-panel/assets/{index-oxRrmFO9.js → index-Wlc7o_C8.js} +1 -1
  128. package/src/assets/web-panel/assets/{index-B3rWuPMW.js → index-bxTPfdri.js} +1 -1
  129. package/src/assets/web-panel/assets/{index-BIrJfqO4.js → index-k6uorYmf.js} +1 -1
  130. package/src/assets/web-panel/assets/{index-BfWYsB9t.js → index-ox6mIwE2.js} +1 -1
  131. package/src/assets/web-panel/assets/{index-Bos5_aMV.js → index-rVDD7AAR.js} +1 -1
  132. package/src/assets/web-panel/assets/{index-CWEuZWCK.js → index-u0h6QqVV.js} +1 -1
  133. package/src/assets/web-panel/assets/{index-Dz49VOyQ.js → index-v-mtxmWH.js} +1 -1
  134. package/src/assets/web-panel/assets/{initDefaultProps-BWJktcg8.js → initDefaultProps-qEt8dI-t.js} +1 -1
  135. package/src/assets/web-panel/assets/{motion-BEXBzUFV.js → motion-DetKPWy-.js} +1 -1
  136. package/src/assets/web-panel/assets/{move-BUKwn2sx.js → move-BeNYeDV2.js} +1 -1
  137. package/src/assets/web-panel/assets/{mtc-parser-FTI7btqQ.js → mtc-parser-C0q0MK_T.js} +1 -1
  138. package/src/assets/web-panel/assets/{omit-CsLtSPH3.js → omit-DLKkrKpE.js} +1 -1
  139. package/src/assets/web-panel/assets/{pickAttrs-DWZxsT_S.js → pickAttrs-Bw1tWsY8.js} +1 -1
  140. package/src/assets/web-panel/assets/{placementArrow-Bcfs43CX.js → placementArrow-DeesBLbd.js} +1 -1
  141. package/src/assets/web-panel/assets/{responsiveObserve-a9bcC1p-.js → responsiveObserve-bqLhxHhn.js} +1 -1
  142. package/src/assets/web-panel/assets/{slide-BDTzgOXN.js → slide-DGCXRoQm.js} +1 -1
  143. package/src/assets/web-panel/assets/{statusUtils-Iq1j_HUg.js → statusUtils-H-re3Egl.js} +1 -1
  144. package/src/assets/web-panel/assets/{styleChecker-DAZEYxeh.js → styleChecker-BXg4qGX_.js} +1 -1
  145. package/src/assets/web-panel/assets/{useFlexGapSupport-DVkjyDrh.js → useFlexGapSupport-CKLb2KA6.js} +1 -1
  146. package/src/assets/web-panel/assets/{useFs-wXTpNTHt.js → useFs-CTAhHeyw.js} +1 -1
  147. package/src/assets/web-panel/assets/{usePersonalDataHub-CtQM_aZr.js → usePersonalDataHub-BQ4pDh5g.js} +1 -1
  148. package/src/assets/web-panel/assets/{vnode-hjEoG85v.js → vnode-jrSK6j4j.js} +1 -1
  149. package/src/assets/web-panel/assets/{zoom-BHVpyKg_.js → zoom-fOQE24pw.js} +1 -1
  150. package/src/assets/web-panel/index.html +1 -1
  151. package/src/command-help-index.json +3 -3
  152. package/src/commands/agent.js +91 -60
  153. package/src/commands/git.js +39 -0
  154. package/src/commands/plugin.js +17 -2
  155. package/src/commands/session-group.js +131 -0
  156. package/src/commands/session-location.js +48 -0
  157. package/src/commands/session.js +11 -0
  158. package/src/commands/sync-providers.js +4 -1
  159. package/src/commands/update.js +1 -0
  160. package/src/data/changelog.json +40 -1
  161. package/src/gateways/ws/mcp-topic-handlers.js +167 -0
  162. package/src/gateways/ws/ws-agent-handler.js +20 -12
  163. package/src/harness/jsonl-session-store.js +279 -37
  164. package/src/harness/mcp-client.js +701 -91
  165. package/src/harness/session-index.js +15 -4
  166. package/src/lib/agent-network.js +166 -29
  167. package/src/lib/agent-router.js +62 -6
  168. package/src/lib/agent-sandbox.js +7 -1
  169. package/src/lib/agent-worktree.js +8 -1
  170. package/src/lib/auto-mode-safety-classifier.js +3 -17
  171. package/src/lib/background-agent-keeper-protocol.js +26 -6
  172. package/src/lib/background-agent-supervisor.js +17 -1
  173. package/src/lib/browser-evidence.js +1405 -0
  174. package/src/lib/chrome-connector.js +465 -79
  175. package/src/lib/claude-code-bridge.js +159 -54
  176. package/src/lib/claude-project-auto-memory.js +481 -0
  177. package/src/lib/claude-project-storage-layout.js +104 -0
  178. package/src/lib/cloud/cloud-client.js +40 -0
  179. package/src/lib/cowork-workflow.js +69 -6
  180. package/src/lib/execution-location-local-supervisor.mjs +226 -0
  181. package/src/lib/execution-location-runner-lifecycle.js +991 -0
  182. package/src/lib/execution-location-target-preflight.js +333 -0
  183. package/src/lib/execution-location-target.js +783 -13
  184. package/src/lib/external-agent-adapters.js +269 -0
  185. package/src/lib/file-lines.js +6 -1
  186. package/src/lib/gateway-client-resilience.js +571 -0
  187. package/src/lib/gitlab-mr-worktree.js +176 -0
  188. package/src/lib/hooks-v2-producers.js +119 -15
  189. package/src/lib/hooks-v2-workspace-context.js +44 -1
  190. package/src/lib/host-resource-budget.js +295 -0
  191. package/src/lib/ide-context.js +15 -2
  192. package/src/lib/ipfs-storage.js +127 -25
  193. package/src/lib/llm-config-defaults.js +43 -0
  194. package/src/lib/mcp-lifecycle-authority.js +887 -0
  195. package/src/lib/mcp-oauth.js +101 -20
  196. package/src/lib/mcp-tls.js +103 -3
  197. package/src/lib/orchestrator.js +47 -0
  198. package/src/lib/paths.js +260 -32
  199. package/src/lib/permission-request.js +48 -0
  200. package/src/lib/plugin-runtime/bin.js +3 -1
  201. package/src/lib/plugin-runtime/hooks.js +3 -1
  202. package/src/lib/plugin-runtime/install.js +117 -1
  203. package/src/lib/plugin-runtime/lsp.js +4 -4
  204. package/src/lib/plugin-runtime/marketplace-archive-source.js +40 -5
  205. package/src/lib/plugin-runtime/marketplace-catalog.js +13 -2
  206. package/src/lib/plugin-runtime/marketplace-command-descriptor.js +343 -0
  207. package/src/lib/plugin-runtime/marketplace-command-source.js +580 -0
  208. package/src/lib/plugin-runtime/marketplace-source-adapter.js +33 -8
  209. package/src/lib/plugin-runtime/mcp.js +3 -1
  210. package/src/lib/plugin-runtime/monitors.js +3 -1
  211. package/src/lib/plugin-runtime/remote-source.js +151 -6
  212. package/src/lib/plugin-runtime/settings.js +3 -1
  213. package/src/lib/plugin-runtime/trust.js +274 -13
  214. package/src/lib/process-execution-broker/credential-agent.js +210 -41
  215. package/src/lib/process-execution-broker/index.js +15 -1
  216. package/src/lib/process-execution-broker/linux-cgroup-v2.js +391 -0
  217. package/src/lib/process-execution-broker/platform-sandbox.js +50 -28
  218. package/src/lib/project-mcp-trust.js +165 -10
  219. package/src/lib/resume-session.js +9 -6
  220. package/src/lib/session-core-singletons.js +26 -11
  221. package/src/lib/session-host-lease.js +52 -16
  222. package/src/lib/session-lifecycle-scope.js +153 -0
  223. package/src/lib/session-message-fabric.js +168 -21
  224. package/src/lib/session-projection.js +244 -4
  225. package/src/lib/session-store-guard.js +3 -1
  226. package/src/lib/session-workbench-store.js +466 -0
  227. package/src/lib/shell-approval.js +7 -5
  228. package/src/lib/spellcheck.js +211 -0
  229. package/src/lib/sub-agent-context.js +9 -9
  230. package/src/lib/sync-cli-db.js +485 -129
  231. package/src/lib/sync-credentials.js +31 -20
  232. package/src/lib/web-fetch.js +112 -40
  233. package/src/lib/web-search.js +127 -61
  234. package/src/lib/workflow-engine.js +42 -11
  235. package/src/lib/workspace-trust.js +793 -0
  236. package/src/repl/agent-repl.js +124 -5
  237. package/src/repl/permission-prompt.js +10 -12
  238. package/src/runtime/agent-core.js +311 -18
  239. package/src/runtime/agent-runtime.js +12 -1
  240. package/src/runtime/bootstrap.js +37 -13
  241. package/src/runtime/coding-agent-contract-shared.cjs +11 -4
  242. package/src/runtime/headless-runner.js +234 -36
  243. package/src/runtime/headless-stream.js +181 -6
  244. package/src/runtime/mcp-config.js +298 -8
  245. package/src/runtime/policies/agent-policy.js +8 -0
  246. package/src/runtime/status-diagnostics-lite.js +9 -1
  247. package/src/assets/web-panel/assets/MarkdownRenderer-CFyt8iUd.js +0 -1
  248. package/src/assets/web-panel/assets/McpTools-DFaIhOfu.js +0 -7
  249. package/src/assets/web-panel/assets/devWarning-DorRZcI9.js +0 -1
  250. package/src/assets/web-panel/assets/index-BETiL0a3.js +0 -1
  251. package/src/assets/web-panel/assets/index-BhAVHuW8.js +0 -1
@@ -10,6 +10,7 @@
10
10
 
11
11
  import { executionBroker } from "../lib/process-execution-broker/index.js";
12
12
  import { AsyncLocalStorage } from "node:async_hooks";
13
+ import { randomUUID } from "node:crypto";
13
14
  import { EventEmitter } from "events";
14
15
  import { pathToFileURL } from "url";
15
16
  import { isProxy } from "node:util/types";
@@ -46,10 +47,16 @@ import {
46
47
  } from "../lib/mcp-stdio-executable-identity.js";
47
48
  import { resolveMcpStdioSandboxContext } from "../lib/mcp-stdio-workspace-authority.js";
48
49
  import {
50
+ assertManagedMcpTlsConfig,
49
51
  closeMcpTlsDispatcher,
50
52
  createMcpTlsDispatcher,
51
53
  loadMcpTlsMaterial,
52
54
  } from "../lib/mcp-tls.js";
55
+ import {
56
+ MCP_LIFECYCLE_ERROR_CODES,
57
+ MCP_LIFECYCLE_PHASES,
58
+ McpLifecycleAuthority,
59
+ } from "../lib/mcp-lifecycle-authority.js";
53
60
 
54
61
  /**
55
62
  * Injectable dependencies — overridable from tests.
@@ -237,6 +244,14 @@ const MCP_HTTP_RESPONSE_MAX_BYTES = 16 * 1024 * 1024;
237
244
  * hard 30s regardless of its config — an inconsistency with HTTP servers.
238
245
  */
239
246
  const STDIO_REQUEST_TIMEOUT_MS = 30000;
247
+ // Long-running MCP tools are allowed to exceed the ordinary request deadline,
248
+ // but may not retain an agent turn forever without any protocol progress. A
249
+ // server-specific value can only tighten this host ceiling; zero/invalid values
250
+ // never disable it.
251
+ const MCP_TOOL_IDLE_TIMEOUT_MS = 5 * 60 * 1000;
252
+ const MCP_DISCOVERY_CACHE_TTL_MS = 60 * 1000;
253
+ const MCP_DISCOVERY_CACHE_MAX_STALE_MS = 10 * 60 * 1000;
254
+ const MCP_DISCOVERY_CACHE_MAX_STRIKES = 2;
240
255
  const MCP_PROTOCOL_VERSION = "2025-11-25";
241
256
  export const SUPPORTED_MCP_PROTOCOL_VERSIONS = Object.freeze([
242
257
  "2024-11-05",
@@ -246,6 +261,97 @@ export const SUPPORTED_MCP_PROTOCOL_VERSIONS = Object.freeze([
246
261
  ]);
247
262
  const supportedMcpProtocolVersions = new Set(SUPPORTED_MCP_PROTOCOL_VERSIONS);
248
263
 
264
+ function mcpToolIdleTimeoutMs(config, options = {}) {
265
+ const candidate = Number.isFinite(options.idleTimeoutMs)
266
+ ? options.idleTimeoutMs
267
+ : config?.toolIdleTimeoutMs;
268
+ if (Number.isFinite(candidate) && candidate > 0) {
269
+ return Math.min(MCP_TOOL_IDLE_TIMEOUT_MS, Math.floor(candidate));
270
+ }
271
+ return MCP_TOOL_IDLE_TIMEOUT_MS;
272
+ }
273
+
274
+ function mcpToolIdleTimeoutError(serverName, toolName, timeoutMs) {
275
+ const error = new Error(
276
+ `MCP tool "${toolName}" on server "${serverName}" made no progress for ${timeoutMs}ms`,
277
+ );
278
+ error.code = "CC_MCP_TOOL_IDLE_TIMEOUT";
279
+ error.timeoutMs = timeoutMs;
280
+ return error;
281
+ }
282
+
283
+ function boundedPositiveInt(value, fallback, maximum) {
284
+ if (!Number.isFinite(value) || value <= 0) return fallback;
285
+ return Math.min(maximum, Math.floor(value));
286
+ }
287
+
288
+ function discoveryCachePolicy(config = {}) {
289
+ const configured = config?.discoveryCache;
290
+ if (configured !== true && (!configured || typeof configured !== "object")) {
291
+ return { enabled: false };
292
+ }
293
+ if (configured?.enabled === false) return { enabled: false };
294
+ const fields = configured === true ? {} : configured;
295
+ const ttlMs = boundedPositiveInt(
296
+ fields.ttlMs,
297
+ MCP_DISCOVERY_CACHE_TTL_MS,
298
+ MCP_DISCOVERY_CACHE_MAX_STALE_MS,
299
+ );
300
+ const maxStaleMs = boundedPositiveInt(
301
+ fields.maxStaleMs,
302
+ MCP_DISCOVERY_CACHE_MAX_STALE_MS,
303
+ 24 * 60 * 60 * 1000,
304
+ );
305
+ return {
306
+ enabled: true,
307
+ ttlMs,
308
+ maxStaleMs: Math.max(ttlMs, maxStaleMs),
309
+ maxStrikes: boundedPositiveInt(
310
+ fields.maxStrikes,
311
+ MCP_DISCOVERY_CACHE_MAX_STRIKES,
312
+ 10,
313
+ ),
314
+ };
315
+ }
316
+
317
+ function discoveryCapabilitiesSignature(capabilities) {
318
+ const value =
319
+ capabilities && typeof capabilities === "object" ? capabilities : {};
320
+ return JSON.stringify({
321
+ tools: value.tools !== undefined,
322
+ resources: value.resources !== undefined,
323
+ prompts: value.prompts !== undefined,
324
+ });
325
+ }
326
+
327
+ function discoveryCacheKey(name, entry) {
328
+ const config = entry?.config || {};
329
+ // This key never leaves process memory. Bind to an endpoint/launcher so a
330
+ // same-named server pointed at a different target cannot inherit tools.
331
+ return JSON.stringify([
332
+ String(name),
333
+ entry?.transportKind || inferTransport(config),
334
+ entry?.httpUrl || entry?.socketUrl || config.url || null,
335
+ config.command || null,
336
+ Array.isArray(config.args) ? config.args : [],
337
+ ]);
338
+ }
339
+
340
+ function cloneDiscoveryList(value) {
341
+ return Array.isArray(value)
342
+ ? value.map((item) =>
343
+ item && typeof item === "object" ? { ...item } : item,
344
+ )
345
+ : [];
346
+ }
347
+
348
+ function applyDiscoveryCache(entry, cached) {
349
+ entry.resources = cloneDiscoveryList(cached.resources);
350
+ entry.resourceTemplates = cloneDiscoveryList(cached.resourceTemplates);
351
+ entry.prompts = cloneDiscoveryList(cached.prompts);
352
+ entry.toolsError = null;
353
+ }
354
+
249
355
  export function assertSupportedMcpProtocolVersion(value) {
250
356
  if (typeof value !== "string" || !supportedMcpProtocolVersions.has(value)) {
251
357
  const error = new Error(
@@ -911,6 +1017,10 @@ function isMcpNonRetryableProtocolError(error) {
911
1017
  );
912
1018
  }
913
1019
 
1020
+ function isMcpFatalDiscoveryError(error) {
1021
+ return isMcpNonRetryableProtocolError(error) || isMcpJsonBudgetError(error);
1022
+ }
1023
+
914
1024
  /**
915
1025
  * Heuristic: does this error look like the server went away (vs. the tool
916
1026
  * itself failing)? Used to gate reconnect-and-retry for servers that have a
@@ -1313,12 +1423,27 @@ export class MCPClient extends EventEmitter {
1313
1423
  this._nextId = 1;
1314
1424
  this._reconnectors = new Map(); // name → async () => config|null
1315
1425
  this._reconnecting = new Map(); // name → in-flight reconnect promise
1426
+ // progressToken → reset-idle callback for in-flight tools/call requests.
1427
+ // Tokens are generated by this client and are removed on every terminal
1428
+ // path, so a peer notification cannot retain timers or request state.
1429
+ this._toolProgress = new Map();
1430
+ // Capability metadata stays process-local by default. A server must opt in
1431
+ // through `discoveryCache`; each reconnect still performs initialize and
1432
+ // all policy/transport admission before a cache entry is considered.
1433
+ this._discoveryCache =
1434
+ options?.discoveryCache instanceof Map
1435
+ ? options.discoveryCache
1436
+ : new Map();
1316
1437
  // Desired resource subscriptions outlive a single transport entry. Keeping
1317
1438
  // them on the client lets a later reconnect retry restoration even when a
1318
1439
  // previous partial reconnect was torn down fail-closed.
1319
1440
  this._resourceSubscriptions = new Map(); // name → Set<uri>
1441
+ this._lifecycleAttempts = new Map(); // name → latest durable generation token
1320
1442
  this._sessionId =
1321
1443
  options && options.sessionId != null ? String(options.sessionId) : null;
1444
+ this._lifecycleSessionId = this._sessionId || "default";
1445
+ this._lifecycleAuthority =
1446
+ options?.lifecycleAuthority || new McpLifecycleAuthority();
1322
1447
  this._workspaceBinding =
1323
1448
  options?.workspaceBinding || currentHostHooksV2WorkspaceBinding();
1324
1449
  // MCP roots capability (Claude-Code 2.1.203 parity): servers may ask the
@@ -1878,6 +2003,90 @@ export class MCPClient extends EventEmitter {
1878
2003
  */
1879
2004
  setSessionId(id) {
1880
2005
  this._sessionId = id != null && id !== "" ? String(id) : null;
2006
+ if (this.servers.size === 0) {
2007
+ this._lifecycleSessionId = this._sessionId || "default";
2008
+ }
2009
+ }
2010
+
2011
+ lifecycleSnapshot(name) {
2012
+ return this._lifecycleAuthority.snapshot({
2013
+ name,
2014
+ sessionId: this._lifecycleSessionId,
2015
+ });
2016
+ }
2017
+
2018
+ _transitionLifecycle(
2019
+ entry,
2020
+ phase,
2021
+ details = {},
2022
+ { tolerateFence = false } = {},
2023
+ ) {
2024
+ if (!entry?._lifecycleAttempt) return null;
2025
+ try {
2026
+ return this._lifecycleAuthority.transition(
2027
+ entry._lifecycleAttempt,
2028
+ phase,
2029
+ details,
2030
+ );
2031
+ } catch (error) {
2032
+ if (tolerateFence && error?.code === MCP_LIFECYCLE_ERROR_CODES.FENCED) {
2033
+ return null;
2034
+ }
2035
+ throw error;
2036
+ }
2037
+ }
2038
+
2039
+ _registerLifecycleRpc(entry, requestId, method) {
2040
+ if (!entry?._lifecycleAttempt) return null;
2041
+ return this._lifecycleAuthority.registerRpc(
2042
+ entry._lifecycleAttempt,
2043
+ requestId,
2044
+ method,
2045
+ );
2046
+ }
2047
+
2048
+ _settleLifecycleRpc(entry, requestId, outcome) {
2049
+ if (!entry?._lifecycleAttempt) return true;
2050
+ return this._lifecycleAuthority.settleRpc(
2051
+ entry._lifecycleAttempt,
2052
+ requestId,
2053
+ outcome,
2054
+ );
2055
+ }
2056
+
2057
+ _staleLifecycleCallbackError() {
2058
+ const error = new Error(
2059
+ "MCP callback belongs to a stale lifecycle generation",
2060
+ );
2061
+ error.code = MCP_LIFECYCLE_ERROR_CODES.FENCED;
2062
+ return error;
2063
+ }
2064
+
2065
+ _rejectUnexpectedLifecycleRpc(entry, requestId, reason) {
2066
+ if (!entry?._lifecycleAttempt) return false;
2067
+ return this._lifecycleAuthority.rejectUnexpectedRpc(
2068
+ entry._lifecycleAttempt,
2069
+ requestId,
2070
+ reason,
2071
+ );
2072
+ }
2073
+
2074
+ _failLifecycle(entry, error) {
2075
+ try {
2076
+ return this._transitionLifecycle(
2077
+ entry,
2078
+ MCP_LIFECYCLE_PHASES.FAILED,
2079
+ { reasonCode: error?.code || "CC_MCP_TRANSPORT_FAILED" },
2080
+ { tolerateFence: true },
2081
+ );
2082
+ } catch (transitionError) {
2083
+ if (
2084
+ transitionError?.code === MCP_LIFECYCLE_ERROR_CODES.INVALID_TRANSITION
2085
+ ) {
2086
+ return null;
2087
+ }
2088
+ return null;
2089
+ }
1881
2090
  }
1882
2091
 
1883
2092
  /**
@@ -2020,11 +2229,27 @@ export class MCPClient extends EventEmitter {
2020
2229
  * @param {string} name - Server name
2021
2230
  * @param {object} config - { command?, args?, env?, url?, transport? }
2022
2231
  */
2023
- async connect(name, config, _authRetryUsed = false) {
2232
+ async connect(name, config, internalOptions = false) {
2024
2233
  if (this.servers.has(name)) {
2025
2234
  throw new Error(`Server "${name}" already connected`);
2026
2235
  }
2027
2236
 
2237
+ const internal =
2238
+ internalOptions && typeof internalOptions === "object"
2239
+ ? internalOptions
2240
+ : { authRetryUsed: internalOptions === true };
2241
+ const authRetryUsed = internal.authRetryUsed === true;
2242
+ if (config?.disabled === true || config?.enabled === false) {
2243
+ this._lifecycleAuthority.markDisabled({
2244
+ name,
2245
+ sessionId: this._lifecycleSessionId,
2246
+ config,
2247
+ });
2248
+ const error = new Error(`MCP server "${name}" is disabled`);
2249
+ error.code = MCP_LIFECYCLE_ERROR_CODES.DISABLED;
2250
+ throw error;
2251
+ }
2252
+
2028
2253
  const transportKind = inferTransport(config);
2029
2254
  let stdioExecutionApproval = null;
2030
2255
  let approvedStdioInvocation = null;
@@ -2112,22 +2337,46 @@ export class MCPClient extends EventEmitter {
2112
2337
  };
2113
2338
  }
2114
2339
  }
2340
+ const lifecycleAttempt =
2341
+ internal.lifecycleAttempt ||
2342
+ this._lifecycleAuthority.beginConnection({
2343
+ name,
2344
+ sessionId: this._lifecycleSessionId,
2345
+ config: sourceConfig,
2346
+ reconnect: internal.reconnect === true,
2347
+ explicitEnable: true,
2348
+ });
2349
+ this._lifecycleAttempts.set(name, lifecycleAttempt);
2115
2350
  let tlsMaterial = null;
2116
- if (sourceConfig.tls != null) {
2117
- if (transportKind !== "https" && transportKind !== "wss") {
2118
- throw mcpTransportError(
2119
- "CC_MCP_TLS_TRANSPORT_REQUIRED",
2120
- `MCP TLS configuration requires https or wss (server "${name}")`,
2121
- { transport: transportKind, url: sourceConfig.url },
2122
- );
2351
+ let connectionHeaders;
2352
+ try {
2353
+ if (sourceConfig.tls != null) {
2354
+ // Source provenance is enforced before header helpers, dispatcher
2355
+ // creation, or any transport work. `configScope` alone is not trusted:
2356
+ // the TLS object must carry the opaque managed-loader provenance too.
2357
+ assertManagedMcpTlsConfig(sourceConfig.tls, {
2358
+ configScope: sourceConfig.configScope,
2359
+ });
2360
+ if (transportKind !== "https" && transportKind !== "wss") {
2361
+ throw mcpTransportError(
2362
+ "CC_MCP_TLS_TRANSPORT_REQUIRED",
2363
+ `MCP TLS configuration requires https or wss (server "${name}")`,
2364
+ { transport: transportKind, url: sourceConfig.url },
2365
+ );
2366
+ }
2367
+ tlsMaterial = _deps.loadMcpTlsMaterial(sourceConfig.tls, {
2368
+ configScope: sourceConfig.configScope,
2369
+ });
2123
2370
  }
2124
- tlsMaterial = _deps.loadMcpTlsMaterial(sourceConfig.tls);
2371
+ connectionHeaders = await this._connectionHeaders(
2372
+ name,
2373
+ sourceConfig,
2374
+ transportKind,
2375
+ );
2376
+ } catch (error) {
2377
+ this._failLifecycle({ _lifecycleAttempt: lifecycleAttempt }, error);
2378
+ throw error;
2125
2379
  }
2126
- const connectionHeaders = await this._connectionHeaders(
2127
- name,
2128
- sourceConfig,
2129
- transportKind,
2130
- );
2131
2380
  config = { ...runtimeConfig, headers: connectionHeaders };
2132
2381
  const entry = {
2133
2382
  // Keep the source config (static headers + helper command) so every
@@ -2154,6 +2403,7 @@ export class MCPClient extends EventEmitter {
2154
2403
  _toolMetadataBytes: 0,
2155
2404
  _stdioExecutionApproval: stdioExecutionApproval,
2156
2405
  _stdioExecutableIdentity: stdioExecutableIdentity,
2406
+ _lifecycleAttempt: lifecycleAttempt,
2157
2407
  tools: [],
2158
2408
  resources: [],
2159
2409
  resourceTemplates: [],
@@ -2265,6 +2515,7 @@ export class MCPClient extends EventEmitter {
2265
2515
  });
2266
2516
  entry._webSocketPayloadError = error;
2267
2517
  entry.state = ServerState.ERROR;
2518
+ this._failLifecycle(entry, error);
2268
2519
  rejectPending(error);
2269
2520
  this._cancelServerElicitations(name);
2270
2521
  this.emit("server-error", {
@@ -2289,6 +2540,7 @@ export class MCPClient extends EventEmitter {
2289
2540
  }
2290
2541
  entry._webSocketInboundError = error;
2291
2542
  entry.state = ServerState.ERROR;
2543
+ this._failLifecycle(entry, error);
2292
2544
  rejectPending(error);
2293
2545
  this._cancelServerElicitations(name);
2294
2546
  this.emit("server-error", {
@@ -2366,6 +2618,7 @@ export class MCPClient extends EventEmitter {
2366
2618
  { transport: transportKind, url: config.url },
2367
2619
  );
2368
2620
  entry.state = ServerState.ERROR;
2621
+ this._failLifecycle(entry, error);
2369
2622
  rejectPending(error);
2370
2623
  this._cancelServerElicitations(name);
2371
2624
  this.emit("server-error", {
@@ -2400,7 +2653,7 @@ export class MCPClient extends EventEmitter {
2400
2653
  ) {
2401
2654
  throw new Error("expected one JSON-RPC object");
2402
2655
  }
2403
- this._handleMessage(name, message);
2656
+ this._handleMessage(name, message, entry);
2404
2657
  } catch (cause) {
2405
2658
  if (isMcpJsonBudgetError(cause)) {
2406
2659
  recordInboundFailure(cause);
@@ -2412,6 +2665,7 @@ export class MCPClient extends EventEmitter {
2412
2665
  { transport: transportKind, url: config.url },
2413
2666
  );
2414
2667
  entry.state = ServerState.ERROR;
2668
+ this._failLifecycle(entry, error);
2415
2669
  rejectPending(error);
2416
2670
  this._cancelServerElicitations(name);
2417
2671
  this.emit("server-error", {
@@ -2433,6 +2687,7 @@ export class MCPClient extends EventEmitter {
2433
2687
  const inboundError = entry._webSocketInboundError;
2434
2688
  if (inboundError) {
2435
2689
  entry.state = ServerState.ERROR;
2690
+ this._failLifecycle(entry, inboundError);
2436
2691
  rejectPending(inboundError);
2437
2692
  this._cancelServerElicitations(name);
2438
2693
  this.emit("server-disconnected", {
@@ -2460,6 +2715,7 @@ export class MCPClient extends EventEmitter {
2460
2715
  }
2461
2716
  if (payloadError) {
2462
2717
  entry.state = ServerState.ERROR;
2718
+ this._failLifecycle(entry, payloadError);
2463
2719
  rejectPending(payloadError);
2464
2720
  this._cancelServerElicitations(name);
2465
2721
  this.emit("server-disconnected", {
@@ -2484,6 +2740,7 @@ export class MCPClient extends EventEmitter {
2484
2740
  outcomeUnknown: dispatched,
2485
2741
  },
2486
2742
  );
2743
+ this._failLifecycle(entry, error);
2487
2744
  rejectPending(error);
2488
2745
  this._cancelServerElicitations(name);
2489
2746
  this.emit("server-disconnected", {
@@ -2511,6 +2768,7 @@ export class MCPClient extends EventEmitter {
2511
2768
  outcomeUnknown: dispatched,
2512
2769
  },
2513
2770
  );
2771
+ this._failLifecycle(entry, error);
2514
2772
  rejectPending(error);
2515
2773
  this._cancelServerElicitations(name);
2516
2774
  this.emit("server-error", {
@@ -2626,6 +2884,7 @@ export class MCPClient extends EventEmitter {
2626
2884
  typeof data === "string"
2627
2885
  ? data
2628
2886
  : entry._decoder.decode(data, { stream: true }),
2887
+ entry,
2629
2888
  );
2630
2889
  });
2631
2890
 
@@ -2655,6 +2914,7 @@ export class MCPClient extends EventEmitter {
2655
2914
  if (!entry._stdioFrameError) {
2656
2915
  entry.state = ServerState.DISCONNECTED;
2657
2916
  }
2917
+ this._failLifecycle(entry, { code: "CC_MCP_STDIO_EXIT" });
2658
2918
  failPending(
2659
2919
  `MCP server "${name}" process exited (code ${code}) before responding`,
2660
2920
  );
@@ -2669,6 +2929,7 @@ export class MCPClient extends EventEmitter {
2669
2929
  if (cleanup?.verifiable && !cleanup.confirmed) {
2670
2930
  entry.state = ServerState.ERROR;
2671
2931
  const cleanupError = mcpStdioCleanupError(name, cleanup);
2932
+ this._failLifecycle(entry, cleanupError);
2672
2933
  this.emit("server-error", {
2673
2934
  name,
2674
2935
  error: cleanupError.message,
@@ -2685,6 +2946,7 @@ export class MCPClient extends EventEmitter {
2685
2946
 
2686
2947
  proc.on("error", (err) => {
2687
2948
  entry.state = ServerState.ERROR;
2949
+ this._failLifecycle(entry, err);
2688
2950
  failPending(`MCP server "${name}" process error: ${err.message}`);
2689
2951
  this._cancelServerElicitations(name);
2690
2952
  this.emit("server-error", { name, error: err.message });
@@ -2699,6 +2961,7 @@ export class MCPClient extends EventEmitter {
2699
2961
  if (proc.stdin && typeof proc.stdin.on === "function") {
2700
2962
  proc.stdin.on("error", (err) => {
2701
2963
  entry.state = ServerState.ERROR;
2964
+ this._failLifecycle(entry, err);
2702
2965
  failPending(`MCP server "${name}" stdin error: ${err.message}`);
2703
2966
  this._cancelServerElicitations(name);
2704
2967
  this.emit("server-error", { name, error: err.message });
@@ -2711,6 +2974,9 @@ export class MCPClient extends EventEmitter {
2711
2974
  }
2712
2975
 
2713
2976
  // Initialize MCP protocol (retried on transient network errors).
2977
+ this._transitionLifecycle(entry, MCP_LIFECYCLE_PHASES.INITIALIZING, {
2978
+ tlsIdentityDigest: entry._tlsIdentityDigest,
2979
+ });
2714
2980
  const initResult = await this._requestWithRetry(name, "initialize", {
2715
2981
  protocolVersion: this._protocolVersion,
2716
2982
  capabilities: {
@@ -2730,6 +2996,9 @@ export class MCPClient extends EventEmitter {
2730
2996
  entry.protocolVersion = assertSupportedMcpProtocolVersion(
2731
2997
  initResult?.protocolVersion,
2732
2998
  );
2999
+ this._transitionLifecycle(entry, MCP_LIFECYCLE_PHASES.DISCOVERING, {
3000
+ tlsIdentityDigest: entry._tlsIdentityDigest,
3001
+ });
2733
3002
  // Send initialized notification
2734
3003
  this._sendNotification(name, "notifications/initialized", {});
2735
3004
 
@@ -2744,62 +3013,150 @@ export class MCPClient extends EventEmitter {
2744
3013
  ? initResult.instructions
2745
3014
  : null;
2746
3015
 
2747
- // Fetch available tools. Per MCP a server advertises a `tools` capability
2748
- // in its initialize response; if it does and tools/list then fails, that
2749
- // is a genuine fetch failure we must surface (Claude-Code 2.1.181 — show
2750
- // "Connected · tools fetch failed" rather than a misleading "Tools: 0").
2751
- // A server that did not advertise tools simply has none, so a failure
2752
- // there is expected and stays quiet.
2753
- entry.tools = [];
2754
- entry.toolsError = null;
2755
- const advertisesTools =
2756
- entry.capabilities && entry.capabilities.tools !== undefined;
2757
- try {
2758
- const toolsResult = await this._requestWithRetry(
3016
+ const cachePolicy = discoveryCachePolicy(entry.config);
3017
+ const cacheKey = discoveryCacheKey(name, entry);
3018
+ const cached = cachePolicy.enabled
3019
+ ? this._discoveryCache.get(cacheKey) || null
3020
+ : null;
3021
+ const now = Date.now();
3022
+ const capabilitiesSignature = discoveryCapabilitiesSignature(
3023
+ entry.capabilities,
3024
+ );
3025
+ const cacheCompatible =
3026
+ cached?.capabilitiesSignature === capabilitiesSignature;
3027
+ const cacheAgeMs = cacheCompatible
3028
+ ? Math.max(0, now - cached.savedAt)
3029
+ : 0;
3030
+ entry._discoveryCacheKey = cachePolicy.enabled ? cacheKey : null;
3031
+
3032
+ if (cacheCompatible && cacheAgeMs <= cachePolicy.ttlMs) {
3033
+ this._replaceToolInventory(name, entry, cached.tools);
3034
+ applyDiscoveryCache(entry, cached);
3035
+ entry.discoveryCache = { state: "fresh", ageMs: cacheAgeMs };
3036
+ this.emit("server-discovery-cache", {
2759
3037
  name,
2760
- "tools/list",
2761
- {},
2762
- );
2763
- this._replaceToolInventory(name, entry, toolsResult?.tools || []);
2764
- } catch (err) {
2765
- if (advertisesTools) {
2766
- entry.toolsError = err?.message || String(err);
3038
+ state: "fresh",
3039
+ ageMs: cacheAgeMs,
3040
+ });
3041
+ } else {
3042
+ // Fetch available tools. Per MCP a server advertises a `tools`
3043
+ // capability in its initialize response; if it does and tools/list
3044
+ // fails, that is a genuine fetch failure we must surface. A server that
3045
+ // did not advertise tools simply has none, so a failure stays quiet.
3046
+ entry.tools = [];
3047
+ entry.toolsError = null;
3048
+ const advertisesTools =
3049
+ entry.capabilities && entry.capabilities.tools !== undefined;
3050
+ let toolsDiscoveryFailed = false;
3051
+ try {
3052
+ const toolsResult = await this._requestWithRetry(
3053
+ name,
3054
+ "tools/list",
3055
+ {},
3056
+ );
3057
+ this._replaceToolInventory(name, entry, toolsResult?.tools || []);
3058
+ } catch (err) {
3059
+ if (isMcpFatalDiscoveryError(err)) throw err;
3060
+ if (advertisesTools) {
3061
+ toolsDiscoveryFailed = true;
3062
+ entry.toolsError = err?.message || String(err);
3063
+ }
3064
+ // else: server did not advertise tools — legitimately none.
2767
3065
  }
2768
- // else: server did not advertise tools — legitimately none.
2769
- }
2770
3066
 
2771
- // Fetch available resources
2772
- try {
2773
- const resourcesResult = await this._sendRequest(
2774
- name,
2775
- "resources/list",
2776
- {},
2777
- );
2778
- entry.resources = resourcesResult?.resources || [];
2779
- } catch {
2780
- // Server may not support resources
2781
- }
3067
+ // Fetch available resources
3068
+ try {
3069
+ const resourcesResult = await this._sendRequest(
3070
+ name,
3071
+ "resources/list",
3072
+ {},
3073
+ );
3074
+ entry.resources = resourcesResult?.resources || [];
3075
+ } catch (error) {
3076
+ if (isMcpFatalDiscoveryError(error)) throw error;
3077
+ // Server may not support resources
3078
+ }
2782
3079
 
2783
- // Resource templates are optional even when resources are supported.
2784
- try {
2785
- const templatesResult = await this._sendRequest(
2786
- name,
2787
- "resources/templates/list",
2788
- {},
2789
- );
2790
- entry.resourceTemplates = templatesResult?.resourceTemplates || [];
2791
- } catch {
2792
- // Server may not support resource templates.
3080
+ // Resource templates are optional even when resources are supported.
3081
+ try {
3082
+ const templatesResult = await this._sendRequest(
3083
+ name,
3084
+ "resources/templates/list",
3085
+ {},
3086
+ );
3087
+ entry.resourceTemplates = templatesResult?.resourceTemplates || [];
3088
+ } catch (error) {
3089
+ if (isMcpFatalDiscoveryError(error)) throw error;
3090
+ // Server may not support resource templates.
3091
+ }
3092
+
3093
+ // Fetch available prompts (server-provided slash commands)
3094
+ try {
3095
+ const promptsResult = await this._sendRequest(
3096
+ name,
3097
+ "prompts/list",
3098
+ {},
3099
+ );
3100
+ entry.prompts = promptsResult?.prompts || [];
3101
+ } catch (error) {
3102
+ if (isMcpFatalDiscoveryError(error)) throw error;
3103
+ // Server may not support prompts
3104
+ }
3105
+
3106
+ const staleUsable =
3107
+ cacheCompatible &&
3108
+ cacheAgeMs <= cachePolicy.maxStaleMs &&
3109
+ cached.strikes < cachePolicy.maxStrikes;
3110
+ if (toolsDiscoveryFailed && staleUsable) {
3111
+ cached.strikes += 1;
3112
+ cached.lastFailureAt = now;
3113
+ this._replaceToolInventory(name, entry, cached.tools);
3114
+ applyDiscoveryCache(entry, cached);
3115
+ entry.discoveryCache = {
3116
+ state: "stale",
3117
+ ageMs: cacheAgeMs,
3118
+ strikes: cached.strikes,
3119
+ };
3120
+ this.emit("server-discovery-cache", {
3121
+ name,
3122
+ state: "stale",
3123
+ ageMs: cacheAgeMs,
3124
+ strikes: cached.strikes,
3125
+ });
3126
+ } else if (cachePolicy.enabled && !toolsDiscoveryFailed) {
3127
+ this._discoveryCache.set(cacheKey, {
3128
+ savedAt: now,
3129
+ strikes: 0,
3130
+ capabilitiesSignature,
3131
+ tools: cloneDiscoveryList(entry.tools),
3132
+ resources: cloneDiscoveryList(entry.resources),
3133
+ resourceTemplates: cloneDiscoveryList(entry.resourceTemplates),
3134
+ prompts: cloneDiscoveryList(entry.prompts),
3135
+ });
3136
+ entry.discoveryCache = { state: "miss", ageMs: null };
3137
+ }
2793
3138
  }
2794
3139
 
2795
- // Fetch available prompts (server-provided slash commands)
2796
- try {
2797
- const promptsResult = await this._sendRequest(name, "prompts/list", {});
2798
- entry.prompts = promptsResult?.prompts || [];
2799
- } catch {
2800
- // Server may not support prompts
3140
+ // Desired v2 subscriptions belong to the logical authority, not a
3141
+ // transport object. Restore them after initialize + capability discovery
3142
+ // so both hot reconnect and process restart preserve live updates.
3143
+ for (const uri of this._lifecycleAuthority.desiredSubscriptions(
3144
+ lifecycleAttempt,
3145
+ )) {
3146
+ await this._sendRequest(name, "resources/subscribe", { uri });
3147
+ entry.resourceSubscriptions.add(uri);
3148
+ let desired = this._resourceSubscriptions.get(name);
3149
+ if (!desired) {
3150
+ desired = new Set();
3151
+ this._resourceSubscriptions.set(name, desired);
3152
+ }
3153
+ desired.add(uri);
2801
3154
  }
2802
3155
 
3156
+ this._transitionLifecycle(entry, MCP_LIFECYCLE_PHASES.READY, {
3157
+ tlsIdentityDigest: entry._tlsIdentityDigest,
3158
+ });
3159
+
2803
3160
  this.emit("server-connected", {
2804
3161
  name,
2805
3162
  tools: entry.tools.length,
@@ -2824,6 +3181,16 @@ export class MCPClient extends EventEmitter {
2824
3181
  };
2825
3182
  } catch (err) {
2826
3183
  entry.state = ServerState.ERROR;
3184
+ try {
3185
+ this._transitionLifecycle(
3186
+ entry,
3187
+ MCP_LIFECYCLE_PHASES.FAILED,
3188
+ { reasonCode: err?.code || "CC_MCP_CONNECTION_FAILED" },
3189
+ { tolerateFence: true },
3190
+ );
3191
+ } catch {
3192
+ // The original transport/protocol failure remains authoritative.
3193
+ }
2827
3194
  this._cancelServerElicitations(name);
2828
3195
  // The initialize notification is fire-and-forget. If later capability
2829
3196
  // discovery fails, connect() deletes this entry without going through
@@ -2887,14 +3254,17 @@ export class MCPClient extends EventEmitter {
2887
3254
  this._releaseToolInventory(entry);
2888
3255
  this.servers.delete(name);
2889
3256
  if (
2890
- !_authRetryUsed &&
3257
+ !authRetryUsed &&
2891
3258
  sourceConfig.headersHelper &&
2892
3259
  isMcpAuthenticationError(err)
2893
3260
  ) {
2894
3261
  // A helper is intentionally uncached. One authentication rejection may
2895
3262
  // mean its short-lived token expired between generation and initialize;
2896
3263
  // execute it once more and retry the connection exactly once.
2897
- return this.connect(name, sourceConfig, true);
3264
+ return this.connect(name, sourceConfig, {
3265
+ authRetryUsed: true,
3266
+ reconnect: true,
3267
+ });
2898
3268
  }
2899
3269
  throw err;
2900
3270
  }
@@ -2908,16 +3278,73 @@ export class MCPClient extends EventEmitter {
2908
3278
  this._resourceSubscriptions.delete(name);
2909
3279
  }
2910
3280
  const entry = this.servers.get(name);
3281
+ const lifecycleAttempt =
3282
+ entry?._lifecycleAttempt || this._lifecycleAttempts.get(name) || null;
2911
3283
  if (!entry) {
2912
3284
  this._cancelServerElicitations(name);
3285
+ if (options.preserveResourceSubscriptions !== true && lifecycleAttempt) {
3286
+ try {
3287
+ this._lifecycleAuthority.clearSubscriptions(lifecycleAttempt);
3288
+ const snapshot = this._lifecycleAuthority.snapshot({
3289
+ name,
3290
+ sessionId: this._lifecycleSessionId,
3291
+ });
3292
+ if (
3293
+ snapshot &&
3294
+ snapshot.phase !== MCP_LIFECYCLE_PHASES.IDLE &&
3295
+ snapshot.phase !== MCP_LIFECYCLE_PHASES.DISABLED
3296
+ ) {
3297
+ this._lifecycleAuthority.transition(
3298
+ lifecycleAttempt,
3299
+ MCP_LIFECYCLE_PHASES.DISCONNECTING,
3300
+ );
3301
+ this._lifecycleAuthority.transition(
3302
+ lifecycleAttempt,
3303
+ MCP_LIFECYCLE_PHASES.IDLE,
3304
+ );
3305
+ }
3306
+ } catch (error) {
3307
+ if (error?.code !== MCP_LIFECYCLE_ERROR_CODES.FENCED) {
3308
+ return Promise.reject(error);
3309
+ }
3310
+ } finally {
3311
+ if (this._lifecycleAttempts.get(name) === lifecycleAttempt) {
3312
+ this._lifecycleAttempts.delete(name);
3313
+ }
3314
+ }
3315
+ }
2913
3316
  return Promise.resolve(false);
2914
3317
  }
2915
3318
  if (entry._disconnectPromise) return entry._disconnectPromise;
2916
3319
 
3320
+ let subscriptionClearError = null;
3321
+ if (options.preserveResourceSubscriptions !== true && lifecycleAttempt) {
3322
+ try {
3323
+ this._lifecycleAuthority.clearSubscriptions(lifecycleAttempt);
3324
+ } catch (error) {
3325
+ if (error?.code !== MCP_LIFECYCLE_ERROR_CODES.FENCED) {
3326
+ subscriptionClearError = error;
3327
+ }
3328
+ }
3329
+ }
3330
+
2917
3331
  // Make disconnect visible before any teardown awaits. In particular, an
2918
3332
  // HTTP session DELETE may wait for response headers; no new tool/resource
2919
3333
  // request may enter the transport during that window or trigger a hot
2920
3334
  // reconnect that resurrects a server the caller is intentionally closing.
3335
+ if (options.preserveLifecycleAuthority !== true) {
3336
+ try {
3337
+ this._transitionLifecycle(
3338
+ entry,
3339
+ MCP_LIFECYCLE_PHASES.DISCONNECTING,
3340
+ {},
3341
+ { tolerateFence: true },
3342
+ );
3343
+ } catch {
3344
+ // Local transport teardown must still run if durable state is
3345
+ // temporarily unavailable. No further request is admitted below.
3346
+ }
3347
+ }
2921
3348
  entry.state = ServerState.DISCONNECTED;
2922
3349
  entry._httpDiscardStopping = true;
2923
3350
  this._cancelServerElicitations(name);
@@ -2960,7 +3387,6 @@ export class MCPClient extends EventEmitter {
2960
3387
  // ignore — disconnect is best-effort
2961
3388
  }
2962
3389
  }
2963
- return true;
2964
3390
  } finally {
2965
3391
  try {
2966
3392
  await _deps.closeMcpTlsDispatcher(entry._httpDispatcher);
@@ -2969,11 +3395,32 @@ export class MCPClient extends EventEmitter {
2969
3395
  }
2970
3396
  entry._httpDispatcher = null;
2971
3397
  entry.state = ServerState.DISCONNECTED;
3398
+ if (options.preserveLifecycleAuthority !== true) {
3399
+ try {
3400
+ this._transitionLifecycle(
3401
+ entry,
3402
+ MCP_LIFECYCLE_PHASES.IDLE,
3403
+ {},
3404
+ { tolerateFence: true },
3405
+ );
3406
+ } catch {
3407
+ // The transport is already closed; retain the durable failure for
3408
+ // the next generation's restart recovery.
3409
+ }
3410
+ }
2972
3411
  this._releaseToolInventory(entry);
2973
3412
  // A future connection may reuse the same name. Never let a delayed
2974
3413
  // teardown delete that replacement entry.
2975
3414
  if (this.servers.get(name) === entry) this.servers.delete(name);
3415
+ if (
3416
+ options.preserveResourceSubscriptions !== true &&
3417
+ this._lifecycleAttempts.get(name) === lifecycleAttempt
3418
+ ) {
3419
+ this._lifecycleAttempts.delete(name);
3420
+ }
2976
3421
  }
3422
+ if (subscriptionClearError) throw subscriptionClearError;
3423
+ return true;
2977
3424
  });
2978
3425
  entry._disconnectPromise = disconnecting;
2979
3426
 
@@ -2989,7 +3436,10 @@ export class MCPClient extends EventEmitter {
2989
3436
  * Disconnect from all servers.
2990
3437
  */
2991
3438
  async disconnectAll() {
2992
- const names = [...this.servers.keys()];
3439
+ const names = new Set([
3440
+ ...this.servers.keys(),
3441
+ ...this._lifecycleAttempts.keys(),
3442
+ ]);
2993
3443
  try {
2994
3444
  for (const name of names) {
2995
3445
  await this.disconnect(name);
@@ -3215,15 +3665,58 @@ export class MCPClient extends EventEmitter {
3215
3665
  throw new Error(`Server "${serverName}" is not connected`);
3216
3666
  }
3217
3667
 
3218
- const result = await this._sendRequest(
3219
- serverName,
3220
- "tools/call",
3221
- {
3222
- name: toolName,
3223
- arguments: args,
3224
- },
3225
- options,
3226
- );
3668
+ const timeoutMs = mcpToolIdleTimeoutMs(entry.config, options);
3669
+ const progressToken = `cc:${randomUUID()}`;
3670
+ const controller = new AbortController();
3671
+ let idleError = null;
3672
+ let idleTimer = null;
3673
+ let removeCallerAbort = null;
3674
+ const resetIdleTimer = () => {
3675
+ if (idleTimer) clearTimeout(idleTimer);
3676
+ idleTimer = setTimeout(() => {
3677
+ idleError = mcpToolIdleTimeoutError(serverName, toolName, timeoutMs);
3678
+ controller.abort();
3679
+ }, timeoutMs);
3680
+ idleTimer.unref?.();
3681
+ };
3682
+ const callerSignal = options?.signal || null;
3683
+ if (callerSignal?.addEventListener) {
3684
+ const abortFromCaller = () => controller.abort();
3685
+ callerSignal.addEventListener("abort", abortFromCaller, { once: true });
3686
+ removeCallerAbort = () =>
3687
+ callerSignal.removeEventListener?.("abort", abortFromCaller);
3688
+ if (callerSignal.aborted) controller.abort();
3689
+ }
3690
+ this._toolProgress.set(progressToken, resetIdleTimer);
3691
+ resetIdleTimer();
3692
+
3693
+ let result;
3694
+ try {
3695
+ result = await this._sendRequest(
3696
+ serverName,
3697
+ "tools/call",
3698
+ {
3699
+ name: toolName,
3700
+ arguments: args,
3701
+ _meta: { progressToken },
3702
+ },
3703
+ { ...options, signal: controller.signal },
3704
+ );
3705
+ } catch (error) {
3706
+ // The inner transport supplies cancellation and JSON-RPC
3707
+ // notifications/cancelled. Preserve the idle classification at the host
3708
+ // boundary instead of leaking its generic AbortError/cancelled detail.
3709
+ if (idleError) throw idleError;
3710
+ throw error;
3711
+ } finally {
3712
+ if (idleTimer) clearTimeout(idleTimer);
3713
+ try {
3714
+ removeCallerAbort?.();
3715
+ } catch {
3716
+ // Best-effort cleanup for non-native AbortSignal adapters.
3717
+ }
3718
+ this._toolProgress.delete(progressToken);
3719
+ }
3227
3720
 
3228
3721
  return admitMcpToolResult(serverName, result, entry.config).result;
3229
3722
  }
@@ -3256,8 +3749,18 @@ export class MCPClient extends EventEmitter {
3256
3749
  const resolver = this._reconnectors.get(name);
3257
3750
  const fresh = resolver ? await resolver() : currentConfig;
3258
3751
  if (!fresh) return false;
3752
+ const lifecycleAttempt = this._lifecycleAuthority.beginConnection({
3753
+ name,
3754
+ sessionId: this._lifecycleSessionId,
3755
+ config: fresh,
3756
+ reconnect: true,
3757
+ explicitEnable: true,
3758
+ });
3259
3759
  try {
3260
- await this.disconnect(name, { preserveResourceSubscriptions: true });
3760
+ await this.disconnect(name, {
3761
+ preserveResourceSubscriptions: true,
3762
+ preserveLifecycleAuthority: true,
3763
+ });
3261
3764
  } catch {
3262
3765
  // entry may already be gone — connect() below is what matters
3263
3766
  }
@@ -3270,14 +3773,16 @@ export class MCPClient extends EventEmitter {
3270
3773
  { serverName: name, config: reconnectConfig },
3271
3774
  );
3272
3775
  }
3273
- await this.connect(
3274
- name,
3275
- reconnectConfig,
3276
- options.connectAuthRetryUsed === true,
3277
- );
3776
+ await this.connect(name, reconnectConfig, {
3777
+ authRetryUsed: options.connectAuthRetryUsed === true,
3778
+ lifecycleAttempt,
3779
+ reconnect: true,
3780
+ });
3278
3781
  try {
3279
3782
  for (const uri of resourceSubscriptions) {
3280
- await this.subscribeResource(name, uri);
3783
+ if (!this.servers.get(name)?.resourceSubscriptions?.has(uri)) {
3784
+ await this.subscribeResource(name, uri);
3785
+ }
3281
3786
  }
3282
3787
  } catch {
3283
3788
  // A partially restored connection is unsafe: callers would believe
@@ -3385,6 +3890,13 @@ export class MCPClient extends EventEmitter {
3385
3890
  this._resourceSubscriptions.set(serverName, desired);
3386
3891
  }
3387
3892
  desired.add(normalizedUri);
3893
+ if (entry._lifecycleAttempt) {
3894
+ this._lifecycleAuthority.setSubscription(
3895
+ entry._lifecycleAttempt,
3896
+ normalizedUri,
3897
+ true,
3898
+ );
3899
+ }
3388
3900
  return true;
3389
3901
  }
3390
3902
 
@@ -3405,6 +3917,13 @@ export class MCPClient extends EventEmitter {
3405
3917
  const desired = this._resourceSubscriptions.get(serverName);
3406
3918
  desired?.delete(normalizedUri);
3407
3919
  if (desired?.size === 0) this._resourceSubscriptions.delete(serverName);
3920
+ if (entry._lifecycleAttempt) {
3921
+ this._lifecycleAuthority.setSubscription(
3922
+ entry._lifecycleAttempt,
3923
+ normalizedUri,
3924
+ false,
3925
+ );
3926
+ }
3408
3927
  return true;
3409
3928
  }
3410
3929
 
@@ -3537,6 +4056,7 @@ export class MCPClient extends EventEmitter {
3537
4056
  method,
3538
4057
  params,
3539
4058
  });
4059
+ this._registerLifecycleRpc(entry, id, method);
3540
4060
 
3541
4061
  let timeout = null;
3542
4062
  let removeCallerAbort = null;
@@ -3555,6 +4075,20 @@ export class MCPClient extends EventEmitter {
3555
4075
  if (settled) return false;
3556
4076
  settled = true;
3557
4077
  cleanup();
4078
+ try {
4079
+ const admitted = this._settleLifecycleRpc(
4080
+ entry,
4081
+ id,
4082
+ settler === resolve ? "completed" : "failed-closed",
4083
+ );
4084
+ if (!admitted && settler === resolve) {
4085
+ reject(this._staleLifecycleCallbackError());
4086
+ return true;
4087
+ }
4088
+ } catch (authorityError) {
4089
+ reject(authorityError);
4090
+ return true;
4091
+ }
3558
4092
  settler(value);
3559
4093
  return true;
3560
4094
  };
@@ -3658,6 +4192,7 @@ export class MCPClient extends EventEmitter {
3658
4192
  return new Promise((resolve, reject) => {
3659
4193
  const id = this._nextId++;
3660
4194
  const message = JSON.stringify({ jsonrpc: "2.0", id, method, params });
4195
+ this._registerLifecycleRpc(entry, id, method);
3661
4196
  const longRunning = Boolean(entry.config?.longRunning);
3662
4197
  const timeoutMs = Number.isFinite(entry.config?.requestTimeoutMs)
3663
4198
  ? entry.config.requestTimeoutMs
@@ -3679,6 +4214,20 @@ export class MCPClient extends EventEmitter {
3679
4214
  if (settled) return false;
3680
4215
  settled = true;
3681
4216
  cleanup();
4217
+ try {
4218
+ const admitted = this._settleLifecycleRpc(
4219
+ entry,
4220
+ id,
4221
+ settler === resolve ? "completed" : "failed-closed",
4222
+ );
4223
+ if (!admitted && settler === resolve) {
4224
+ reject(this._staleLifecycleCallbackError());
4225
+ return true;
4226
+ }
4227
+ } catch (authorityError) {
4228
+ reject(authorityError);
4229
+ return true;
4230
+ }
3682
4231
  settler(value);
3683
4232
  return true;
3684
4233
  };
@@ -3923,7 +4472,8 @@ export class MCPClient extends EventEmitter {
3923
4472
  if (error) throw error;
3924
4473
  },
3925
4474
  parseJson: (text) => parseMcpJson(text, serverName, entry),
3926
- onMessage: (message) => this._handleMessage(serverName, message),
4475
+ onMessage: (message) =>
4476
+ this._handleMessage(serverName, message, entry),
3927
4477
  });
3928
4478
  } catch (error) {
3929
4479
  if (stream.stopped) return;
@@ -4006,6 +4556,14 @@ export class MCPClient extends EventEmitter {
4006
4556
 
4007
4557
  const id = this._nextId++;
4008
4558
  const body = JSON.stringify({ jsonrpc: "2.0", id, method, params });
4559
+ this._registerLifecycleRpc(entry, id, method);
4560
+ let lifecycleSettled = false;
4561
+ const settleLifecycle = (outcome) => {
4562
+ if (lifecycleSettled) return true;
4563
+ const admitted = this._settleLifecycleRpc(entry, id, outcome);
4564
+ lifecycleSettled = true;
4565
+ return admitted;
4566
+ };
4009
4567
  const headers = {
4010
4568
  "Content-Type": "application/json",
4011
4569
  Accept: "application/json, text/event-stream",
@@ -4210,7 +4768,7 @@ export class MCPClient extends EventEmitter {
4210
4768
  response,
4211
4769
  id,
4212
4770
  cap,
4213
- (message) => this._handleMessage(serverName, message),
4771
+ (message) => this._handleMessage(serverName, message, entry),
4214
4772
  controller?.signal,
4215
4773
  (text) => parseMcpJson(text, serverName, entry),
4216
4774
  );
@@ -4243,9 +4801,18 @@ export class MCPClient extends EventEmitter {
4243
4801
  throw mcpRpcError(wire.error);
4244
4802
  }
4245
4803
  throwIfAborted();
4804
+ if (!settleLifecycle("completed")) {
4805
+ throw this._staleLifecycleCallbackError();
4806
+ }
4246
4807
  return wire.result;
4247
4808
  } catch (err) {
4248
- throwIfAborted();
4809
+ try {
4810
+ throwIfAborted();
4811
+ } catch (abortError) {
4812
+ settleLifecycle("failed-closed");
4813
+ throw abortError;
4814
+ }
4815
+ settleLifecycle("failed-closed");
4249
4816
  throw err;
4250
4817
  } finally {
4251
4818
  if (timer) clearTimeout(timer);
@@ -4286,8 +4853,9 @@ export class MCPClient extends EventEmitter {
4286
4853
  });
4287
4854
  }
4288
4855
 
4289
- _handleData(serverName, data) {
4290
- const entry = this.servers.get(serverName);
4856
+ _handleData(serverName, data, sourceEntry = null) {
4857
+ const entry = sourceEntry || this.servers.get(serverName);
4858
+ if (sourceEntry && this.servers.get(serverName) !== sourceEntry) return;
4291
4859
  if (!entry || entry._stdioFrameError) return;
4292
4860
 
4293
4861
  const cap = stdioFrameByteLimit(entry.config?.maxBufferChars);
@@ -4346,7 +4914,7 @@ export class MCPClient extends EventEmitter {
4346
4914
  }
4347
4915
  continue;
4348
4916
  }
4349
- this._handleMessage(serverName, msg);
4917
+ this._handleMessage(serverName, msg, entry);
4350
4918
  }
4351
4919
  }
4352
4920
 
@@ -4442,6 +5010,7 @@ export class MCPClient extends EventEmitter {
4442
5010
  entry._buffer = "";
4443
5011
  entry._bufferBytes = 0;
4444
5012
  entry.state = ServerState.ERROR;
5013
+ this._failLifecycle(entry, error);
4445
5014
  if (entry._pending instanceof Map) {
4446
5015
  for (const pending of entry._pending.values()) {
4447
5016
  if (pending.timeout) clearTimeout(pending.timeout);
@@ -4475,12 +5044,21 @@ export class MCPClient extends EventEmitter {
4475
5044
  return error;
4476
5045
  }
4477
5046
 
4478
- _handleMessage(serverName, msg) {
4479
- const entry = this.servers.get(serverName);
5047
+ _handleMessage(serverName, msg, sourceEntry = null) {
5048
+ const currentEntry = this.servers.get(serverName);
5049
+ const entry = sourceEntry || currentEntry;
4480
5050
  if (!entry) return;
5051
+ if (sourceEntry && currentEntry !== sourceEntry) {
5052
+ const wire = snapshotRpcMessage(msg);
5053
+ if (wire?.hasId && !wire.hasMethod) {
5054
+ this._rejectUnexpectedLifecycleRpc(sourceEntry, wire.id, "stale");
5055
+ }
5056
+ return;
5057
+ }
4481
5058
 
4482
5059
  const failMessage = (error) => {
4483
5060
  entry.state = ServerState.ERROR;
5061
+ this._failLifecycle(entry, error);
4484
5062
  if (entry._pending instanceof Map) {
4485
5063
  for (const pending of entry._pending.values()) {
4486
5064
  clearTimeout(pending.timeout);
@@ -4549,6 +5127,13 @@ export class MCPClient extends EventEmitter {
4549
5127
  // one of ours). Previously these fell into the notification branch and
4550
5128
  // never got a response, so a server calling e.g. roots/list hung until its
4551
5129
  // own timeout. Answer the ones our advertised capabilities invite.
5130
+ // A response without a live pending request is never delivered. The
5131
+ // durable authority records whether it was duplicate or uncorrelated.
5132
+ if (wire.hasId && !wire.hasMethod) {
5133
+ this._rejectUnexpectedLifecycleRpc(entry, wire.id, "unmatched");
5134
+ return;
5135
+ }
5136
+
4552
5137
  if (wire.hasId && typeof wire.method === "string" && wire.method) {
4553
5138
  if (
4554
5139
  wire.hasParams &&
@@ -4581,10 +5166,35 @@ export class MCPClient extends EventEmitter {
4581
5166
  // tool-search's prompt-cache stability depends on an append-only,
4582
5167
  // stable-prefix tool list.)
4583
5168
  if (wire.method === "notifications/tools/list_changed") {
5169
+ if (entry._discoveryCacheKey) {
5170
+ this._discoveryCache.delete(entry._discoveryCacheKey);
5171
+ }
4584
5172
  this._refreshServerList(serverName, "tools");
4585
5173
  } else if (wire.method === "notifications/resources/list_changed") {
5174
+ if (entry._discoveryCacheKey) {
5175
+ this._discoveryCache.delete(entry._discoveryCacheKey);
5176
+ }
4586
5177
  this._refreshServerList(serverName, "resources");
4587
5178
  this._refreshServerList(serverName, "resourceTemplates");
5179
+ } else if (wire.method === "notifications/progress") {
5180
+ const progressToken = safeRpcProperty(wire.params, "progressToken");
5181
+ const resetIdle =
5182
+ typeof progressToken === "string"
5183
+ ? this._toolProgress.get(progressToken)
5184
+ : null;
5185
+ if (resetIdle) resetIdle();
5186
+ // Project only numeric progress. Server-controlled status text belongs
5187
+ // to the tool result, not host diagnostics/events.
5188
+ this.emit("tool-progress", {
5189
+ server: serverName,
5190
+ ...(typeof progressToken === "string" ? { progressToken } : {}),
5191
+ ...(Number.isFinite(safeRpcProperty(wire.params, "progress"))
5192
+ ? { progress: safeRpcProperty(wire.params, "progress") }
5193
+ : {}),
5194
+ ...(Number.isFinite(safeRpcProperty(wire.params, "total"))
5195
+ ? { total: safeRpcProperty(wire.params, "total") }
5196
+ : {}),
5197
+ });
4588
5198
  } else if (wire.method === "notifications/resources/updated") {
4589
5199
  this.emit("resource-updated", {
4590
5200
  server: serverName,