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.
- package/package.json +2 -1
- package/src/assets/web-panel/.build-hash +1 -1
- package/src/assets/web-panel/assets/{AIOps-vfoDb6HZ.js → AIOps-DxwjrnEA.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-Cm5f7p93.js → ActionButton-JdbYowzn.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-yRuvgWxa.js → Analytics-C06GCahv.js} +3 -3
- package/src/assets/web-panel/assets/{AppLayout-BROsC4h1.js → AppLayout-BUVEEyi1.js} +4 -4
- package/src/assets/web-panel/assets/{Artifacts-CimHApb1.js → Artifacts-D_ORzkJd.js} +1 -1
- package/src/assets/web-panel/assets/{Audit-DI8kr8zI.js → Audit-8o_Y5ejG.js} +1 -1
- package/src/assets/web-panel/assets/{BackgroundAgents-DSLzKy-n.js → BackgroundAgents-DrkHd5VU.js} +1 -1
- package/src/assets/web-panel/assets/{Backup-DkH_n2r1.js → Backup-CKBZCX11.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-CKSrUoRA.js → BaseInput-C8HkI399.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-ClwfMXUS.js → Chat-zb2H2Wux.js} +6 -6
- package/src/assets/web-panel/assets/{ChatBubbleRenderer-CjMaqfL7.js → ChatBubbleRenderer-BDcNpU_F.js} +1 -1
- package/src/assets/web-panel/assets/{Checkbox-BYTehcQG.js → Checkbox-MwPtQmNz.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-B8AMcp6J.js → Codegen-3p5QCkFK.js} +1 -1
- package/src/assets/web-panel/assets/{Col-BNSogPo3.js → Col-BPLlhn52.js} +1 -1
- package/src/assets/web-panel/assets/{Community-JxPvGb8O.js → Community-C-3dBNOM.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-DF7bFAgE.js → Compact-BUcy-F_b.js} +1 -1
- package/src/assets/web-panel/assets/{Compliance-Bvhqnwrq.js → Compliance-CI8NxT8m.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-BDS0CJvM.js → Cowork-DaDEBS-Q.js} +3 -3
- package/src/assets/web-panel/assets/{Cron-DqvRfxoQ.js → Cron-BSv9HXu5.js} +2 -2
- package/src/assets/web-panel/assets/{Crosschain-CDcIvnqS.js → Crosschain-yuiKlJmv.js} +1 -1
- package/src/assets/web-panel/assets/{DID-D7jaUxU3.js → DID-D6LfldB0.js} +2 -2
- package/src/assets/web-panel/assets/{Dashboard-B5A18wEV.js → Dashboard-CBhLqG61.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-_6IVkGag.js → Dropdown-BG8pIl3O.js} +1 -1
- package/src/assets/web-panel/assets/{EmailListRenderer-C9PeiKCG.js → EmailListRenderer-CY92VIE6.js} +1 -1
- package/src/assets/web-panel/assets/{FamilyGuardDashboard-DXjuEHrX.js → FamilyGuardDashboard-1Q_ehauM.js} +1 -1
- package/src/assets/web-panel/assets/{Federation-B4IAR9zv.js → Federation-D3XirJIU.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-CxQeSNx1.js → FormItemContext-DfI9UuGZ.js} +1 -1
- package/src/assets/web-panel/assets/{GenericCardRenderer-B8GS-Y3N.js → GenericCardRenderer-IeCZk2m7.js} +1 -1
- package/src/assets/web-panel/assets/{Git-BS8SouUX.js → Git-DzK4IJAZ.js} +2 -2
- package/src/assets/web-panel/assets/{Governance-Dq9aYb5Q.js → Governance-BHf5KfmQ.js} +1 -1
- package/src/assets/web-panel/assets/{Inference-BrCEe9AJ.js → Inference-BmaxSALS.js} +1 -1
- package/src/assets/web-panel/assets/{KnowledgeGraph-DybOpiY2.js → KnowledgeGraph-BgePFLA9.js} +1 -1
- package/src/assets/web-panel/assets/{Logs-DBpzbRfx.js → Logs-nqgxpOc4.js} +2 -2
- package/src/assets/web-panel/assets/MarkdownRenderer-xkX6izBt.js +1 -0
- package/src/assets/web-panel/assets/{Marketplace-Cc7CCSCu.js → Marketplace-Bu8to-86.js} +1 -1
- package/src/assets/web-panel/assets/McpTools-BFWTzEk0.js +4 -0
- package/src/assets/web-panel/assets/{Memory-KCqlp9ol.js → Memory-B026G2GA.js} +2 -2
- package/src/assets/web-panel/assets/{MobileBridge-Bys84ySN.js → MobileBridge-CM2Eeg6I.js} +1 -1
- package/src/assets/web-panel/assets/{MobileProjects-B-6qU-je.js → MobileProjects-B79Ob3hy.js} +1 -1
- package/src/assets/web-panel/assets/{Mtc-BgY-fvCq.js → Mtc-CxevSZuV.js} +4 -4
- package/src/assets/web-panel/assets/{MtcAudit-B8pCmqQJ.js → MtcAudit-X396cPEX.js} +4 -4
- package/src/assets/web-panel/assets/{Multisig-BbOmYutA.js → Multisig-HWIB_aZ1.js} +3 -3
- package/src/assets/web-panel/assets/{NLProgramming-BocLQz_M.js → NLProgramming-gnYD1Xwy.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-CSwbxEop.js → Notes-Dn2Gf_J1.js} +3 -3
- package/src/assets/web-panel/assets/{NotificationSettings-CqFNSYG3.js → NotificationSettings-BRKBQ5rq.js} +1 -1
- package/src/assets/web-panel/assets/{OrderTableRenderer-C6gMfAk7.js → OrderTableRenderer-Dy___Cgo.js} +1 -1
- package/src/assets/web-panel/assets/{Organization-QrDXVdMJ.js → Organization-cqfccxnt.js} +4 -4
- package/src/assets/web-panel/assets/{Overflow-Cxntotjt.js → Overflow-CSX4FHN3.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-CIdBOwNq.js → P2P-Bwa4aghp.js} +2 -2
- package/src/assets/web-panel/assets/{PdhVaultBrowser-CT5aP8rD.js → PdhVaultBrowser-Dhszyjig.js} +3 -3
- package/src/assets/web-panel/assets/{Permissions-DH7OM8XU.js → Permissions-CXzTyk6e.js} +4 -4
- package/src/assets/web-panel/assets/{PersonalDataHub-CnJqbWce.js → PersonalDataHub-CFCAAJbb.js} +4 -4
- package/src/assets/web-panel/assets/{Pipeline-5C3HG1kH.js → Pipeline-BPJbx6xt.js} +1 -1
- package/src/assets/web-panel/assets/{Privacy-B4yIp7xK.js → Privacy-BbiXvEHz.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectInit-BMvt-2M0.js → ProjectInit-B-s7-wSD.js} +2 -2
- package/src/assets/web-panel/assets/{ProjectSettings-CvSTEMRG.js → ProjectSettings-Dyug206y.js} +2 -2
- package/src/assets/web-panel/assets/{Projects-BKBJxEek.js → Projects-BtAI5Fwo.js} +1 -1
- package/src/assets/web-panel/assets/{Providers-BdwUqFWN.js → Providers-JpIPmz6N.js} +1 -1
- package/src/assets/web-panel/assets/{QrScannerModal-DtnCKlTm.js → QrScannerModal-CZdoN64A.js} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-sTbgwyNr.js → QuickAsk-BwvnoRIT.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend-Bzr14gSb.js → Recommend-ASMlMQrD.js} +1 -1
- package/src/assets/web-panel/assets/{RemoteSession-C_TF5IaJ.js → RemoteSession-BiJzKFKi.js} +3 -3
- package/src/assets/web-panel/assets/{Reputation-GCAM4H2K.js → Reputation-C80TWLIO.js} +1 -1
- package/src/assets/web-panel/assets/{Row-Dir0r6OQ.js → Row-C1VNmjmX.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-CpGnlecX.js → RssFeed-T2MyGUlG.js} +3 -3
- package/src/assets/web-panel/assets/{Search-DMMamC6u.js → Search-DXM6O83L.js} +1 -1
- package/src/assets/web-panel/assets/{Security-yqay5yG-.js → Security-nv69kU7W.js} +3 -3
- package/src/assets/web-panel/assets/{Services-C5i2wh9G.js → Services-AyHWe7Qj.js} +2 -2
- package/src/assets/web-panel/assets/{Skeleton-vUqk35yj.js → Skeleton-D6PWLxFI.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-DzcSkhot.js → Skills-BqVeOmNQ.js} +1 -1
- package/src/assets/web-panel/assets/{Sla-CB6qbYNr.js → Sla-BegIPl9M.js} +1 -1
- package/src/assets/web-panel/assets/{SpeechSettings-BHUI7cox.js → SpeechSettings-CKJn3xk-.js} +1 -1
- package/src/assets/web-panel/assets/{SyncSettings-D-RrsxTJ.js → SyncSettings-Th4cnAMl.js} +2 -2
- package/src/assets/web-panel/assets/{Tasks-DFxdOJiq.js → Tasks-BuKfBroh.js} +1 -1
- package/src/assets/web-panel/assets/{Templates-SKAob_Aq.js → Templates-CMuWN8Ac.js} +1 -1
- package/src/assets/web-panel/assets/{Tenant-BPfsfHQ-.js → Tenant-C4je8E8Q.js} +1 -1
- package/src/assets/web-panel/assets/{Terminal-8iw7FpUf.js → Terminal-agJESOcO.js} +2 -2
- package/src/assets/web-panel/assets/{TimelineRenderer-C4Gjj5Su.js → TimelineRenderer-D2qDuVZ_.js} +1 -1
- package/src/assets/web-panel/assets/{Tokens-COYhN18e.js → Tokens-Cvmn99rH.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-B-JzXcf6.js → Trigger-D_QYIHH8.js} +1 -1
- package/src/assets/web-panel/assets/{Trust-BJ5bg0nI.js → Trust-BmzvQLnX.js} +1 -1
- package/src/assets/web-panel/assets/{UkeySign-BG_ByRMf.js → UkeySign-D_LopHwH.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-B3kOnSiU.js → VideoEditing-DoWl3PhQ.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-CcoGFnK7.js → Wallet-5NYcAkxe.js} +4 -4
- package/src/assets/web-panel/assets/{WebAuthn-DdT8vRC_.js → WebAuthn-B-_Q3ewp.js} +4 -4
- package/src/assets/web-panel/assets/{WorkflowEditor-CZu3M9N8.js → WorkflowEditor-CHET-gi2.js} +1 -1
- package/src/assets/web-panel/assets/{chat-DxJt3Qba.js → chat-C6nvb-n6.js} +1 -1
- package/src/assets/web-panel/assets/{colors-DdYfwJaB.js → colors-CTDE9LTX.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-BG3SUzwj.js → compact-item-KnemdF8D.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-By51z2-1.js → createContext-DUJR3Uek.js} +1 -1
- package/src/assets/web-panel/assets/devWarning-1DqLH1KX.js +1 -0
- package/src/assets/web-panel/assets/{hasIn-B0apZ3KJ.js → hasIn-CFNTaby8.js} +1 -1
- package/src/assets/web-panel/assets/{index-ZxtP8jG7.js → index-65HO56Bv.js} +1 -1
- package/src/assets/web-panel/assets/{index-Da_WBdLX.js → index-B8RtbSkV.js} +1 -1
- package/src/assets/web-panel/assets/{index-BBPf3TpM.js → index-BHqSqnW7.js} +1 -1
- package/src/assets/web-panel/assets/{index-BP94CrBV.js → index-BTetEUWd.js} +1 -1
- package/src/assets/web-panel/assets/{index-p1_8SbI1.js → index-BXWg1crv.js} +1 -1
- package/src/assets/web-panel/assets/{index-BWF8_mkx.js → index-BdiC4LOs.js} +1 -1
- package/src/assets/web-panel/assets/index-BhJtcPWo.js +1 -0
- package/src/assets/web-panel/assets/{index-SQqOlR3d.js → index-BuizJo2E.js} +1 -1
- package/src/assets/web-panel/assets/{index-CpjZhgNd.js → index-BwZM1rod.js} +1 -1
- package/src/assets/web-panel/assets/{index-CzmQElZy.js → index-C96N9xeL.js} +1 -1
- package/src/assets/web-panel/assets/{index-bP_VlDb4.js → index-CHfUdnJH.js} +1 -1
- package/src/assets/web-panel/assets/{index-ZFbps9po.js → index-CKvBF3Ov.js} +1 -1
- package/src/assets/web-panel/assets/{index-DaIz0NAW.js → index-CSD9_I2_.js} +1 -1
- package/src/assets/web-panel/assets/{index-BewKYrX9.js → index-Cc7dbl_p.js} +1 -1
- package/src/assets/web-panel/assets/{index-CNIxXMmN.js → index-CcF0D7UZ.js} +1 -1
- package/src/assets/web-panel/assets/{index-Q2SCX-MI.js → index-Ceu92a8D.js} +1 -1
- package/src/assets/web-panel/assets/{index-DE76D-kA.js → index-CnLpuTAU.js} +1 -1
- package/src/assets/web-panel/assets/index-Cq8MWPDT.js +1 -0
- package/src/assets/web-panel/assets/{index-BkvcEo4p.js → index-CquKnw5r.js} +1 -1
- package/src/assets/web-panel/assets/{index-Cp6e-hGw.js → index-CyEtHvEE.js} +1 -1
- package/src/assets/web-panel/assets/{index-BLIbGOLX.js → index-DF-fcTvQ.js} +1 -1
- package/src/assets/web-panel/assets/{index-DCn8IcxV.js → index-DF1Y0Vuw.js} +1 -1
- package/src/assets/web-panel/assets/{index-Bissefox.js → index-DJyPeBBk.js} +3 -3
- package/src/assets/web-panel/assets/{index-BZ_0qm4g.js → index-DP-PbO11.js} +1 -1
- package/src/assets/web-panel/assets/{index-kf57LJ93.js → index-DZgCRMxk.js} +1 -1
- package/src/assets/web-panel/assets/{index-BYINoRLK.js → index-DfJzS3Qa.js} +1 -1
- package/src/assets/web-panel/assets/{index-3jWkIu5j.js → index-DqPpJkte.js} +1 -1
- package/src/assets/web-panel/assets/{index-B5rLiEo0.js → index-DzReoLyh.js} +1 -1
- package/src/assets/web-panel/assets/{index-CW93tZDv.js → index-JGTiDeOW.js} +1 -1
- package/src/assets/web-panel/assets/{index-C1C9i79A.js → index-NUR0If7O.js} +1 -1
- package/src/assets/web-panel/assets/{index-CAhHirVH.js → index-NtAcnJYW.js} +1 -1
- package/src/assets/web-panel/assets/{index-RD6O-0C6.js → index-OmuhFYKE.js} +1 -1
- package/src/assets/web-panel/assets/{index-oxRrmFO9.js → index-Wlc7o_C8.js} +1 -1
- package/src/assets/web-panel/assets/{index-B3rWuPMW.js → index-bxTPfdri.js} +1 -1
- package/src/assets/web-panel/assets/{index-BIrJfqO4.js → index-k6uorYmf.js} +1 -1
- package/src/assets/web-panel/assets/{index-BfWYsB9t.js → index-ox6mIwE2.js} +1 -1
- package/src/assets/web-panel/assets/{index-Bos5_aMV.js → index-rVDD7AAR.js} +1 -1
- package/src/assets/web-panel/assets/{index-CWEuZWCK.js → index-u0h6QqVV.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dz49VOyQ.js → index-v-mtxmWH.js} +1 -1
- package/src/assets/web-panel/assets/{initDefaultProps-BWJktcg8.js → initDefaultProps-qEt8dI-t.js} +1 -1
- package/src/assets/web-panel/assets/{motion-BEXBzUFV.js → motion-DetKPWy-.js} +1 -1
- package/src/assets/web-panel/assets/{move-BUKwn2sx.js → move-BeNYeDV2.js} +1 -1
- package/src/assets/web-panel/assets/{mtc-parser-FTI7btqQ.js → mtc-parser-C0q0MK_T.js} +1 -1
- package/src/assets/web-panel/assets/{omit-CsLtSPH3.js → omit-DLKkrKpE.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-DWZxsT_S.js → pickAttrs-Bw1tWsY8.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-Bcfs43CX.js → placementArrow-DeesBLbd.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-a9bcC1p-.js → responsiveObserve-bqLhxHhn.js} +1 -1
- package/src/assets/web-panel/assets/{slide-BDTzgOXN.js → slide-DGCXRoQm.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-Iq1j_HUg.js → statusUtils-H-re3Egl.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-DAZEYxeh.js → styleChecker-BXg4qGX_.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-DVkjyDrh.js → useFlexGapSupport-CKLb2KA6.js} +1 -1
- package/src/assets/web-panel/assets/{useFs-wXTpNTHt.js → useFs-CTAhHeyw.js} +1 -1
- package/src/assets/web-panel/assets/{usePersonalDataHub-CtQM_aZr.js → usePersonalDataHub-BQ4pDh5g.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-hjEoG85v.js → vnode-jrSK6j4j.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-BHVpyKg_.js → zoom-fOQE24pw.js} +1 -1
- package/src/assets/web-panel/index.html +1 -1
- package/src/command-help-index.json +3 -3
- package/src/commands/agent.js +91 -60
- package/src/commands/git.js +39 -0
- package/src/commands/plugin.js +17 -2
- package/src/commands/session-group.js +131 -0
- package/src/commands/session-location.js +48 -0
- package/src/commands/session.js +11 -0
- package/src/commands/sync-providers.js +4 -1
- package/src/commands/update.js +1 -0
- package/src/data/changelog.json +40 -1
- package/src/gateways/ws/mcp-topic-handlers.js +167 -0
- package/src/gateways/ws/ws-agent-handler.js +20 -12
- package/src/harness/jsonl-session-store.js +279 -37
- package/src/harness/mcp-client.js +701 -91
- package/src/harness/session-index.js +15 -4
- package/src/lib/agent-network.js +166 -29
- package/src/lib/agent-router.js +62 -6
- package/src/lib/agent-sandbox.js +7 -1
- package/src/lib/agent-worktree.js +8 -1
- package/src/lib/auto-mode-safety-classifier.js +3 -17
- package/src/lib/background-agent-keeper-protocol.js +26 -6
- package/src/lib/background-agent-supervisor.js +17 -1
- package/src/lib/browser-evidence.js +1405 -0
- package/src/lib/chrome-connector.js +465 -79
- package/src/lib/claude-code-bridge.js +159 -54
- package/src/lib/claude-project-auto-memory.js +481 -0
- package/src/lib/claude-project-storage-layout.js +104 -0
- package/src/lib/cloud/cloud-client.js +40 -0
- package/src/lib/cowork-workflow.js +69 -6
- package/src/lib/execution-location-local-supervisor.mjs +226 -0
- package/src/lib/execution-location-runner-lifecycle.js +991 -0
- package/src/lib/execution-location-target-preflight.js +333 -0
- package/src/lib/execution-location-target.js +783 -13
- package/src/lib/external-agent-adapters.js +269 -0
- package/src/lib/file-lines.js +6 -1
- package/src/lib/gateway-client-resilience.js +571 -0
- package/src/lib/gitlab-mr-worktree.js +176 -0
- package/src/lib/hooks-v2-producers.js +119 -15
- package/src/lib/hooks-v2-workspace-context.js +44 -1
- package/src/lib/host-resource-budget.js +295 -0
- package/src/lib/ide-context.js +15 -2
- package/src/lib/ipfs-storage.js +127 -25
- package/src/lib/llm-config-defaults.js +43 -0
- package/src/lib/mcp-lifecycle-authority.js +887 -0
- package/src/lib/mcp-oauth.js +101 -20
- package/src/lib/mcp-tls.js +103 -3
- package/src/lib/orchestrator.js +47 -0
- package/src/lib/paths.js +260 -32
- package/src/lib/permission-request.js +48 -0
- package/src/lib/plugin-runtime/bin.js +3 -1
- package/src/lib/plugin-runtime/hooks.js +3 -1
- package/src/lib/plugin-runtime/install.js +117 -1
- package/src/lib/plugin-runtime/lsp.js +4 -4
- package/src/lib/plugin-runtime/marketplace-archive-source.js +40 -5
- package/src/lib/plugin-runtime/marketplace-catalog.js +13 -2
- package/src/lib/plugin-runtime/marketplace-command-descriptor.js +343 -0
- package/src/lib/plugin-runtime/marketplace-command-source.js +580 -0
- package/src/lib/plugin-runtime/marketplace-source-adapter.js +33 -8
- package/src/lib/plugin-runtime/mcp.js +3 -1
- package/src/lib/plugin-runtime/monitors.js +3 -1
- package/src/lib/plugin-runtime/remote-source.js +151 -6
- package/src/lib/plugin-runtime/settings.js +3 -1
- package/src/lib/plugin-runtime/trust.js +274 -13
- package/src/lib/process-execution-broker/credential-agent.js +210 -41
- package/src/lib/process-execution-broker/index.js +15 -1
- package/src/lib/process-execution-broker/linux-cgroup-v2.js +391 -0
- package/src/lib/process-execution-broker/platform-sandbox.js +50 -28
- package/src/lib/project-mcp-trust.js +165 -10
- package/src/lib/resume-session.js +9 -6
- package/src/lib/session-core-singletons.js +26 -11
- package/src/lib/session-host-lease.js +52 -16
- package/src/lib/session-lifecycle-scope.js +153 -0
- package/src/lib/session-message-fabric.js +168 -21
- package/src/lib/session-projection.js +244 -4
- package/src/lib/session-store-guard.js +3 -1
- package/src/lib/session-workbench-store.js +466 -0
- package/src/lib/shell-approval.js +7 -5
- package/src/lib/spellcheck.js +211 -0
- package/src/lib/sub-agent-context.js +9 -9
- package/src/lib/sync-cli-db.js +485 -129
- package/src/lib/sync-credentials.js +31 -20
- package/src/lib/web-fetch.js +112 -40
- package/src/lib/web-search.js +127 -61
- package/src/lib/workflow-engine.js +42 -11
- package/src/lib/workspace-trust.js +793 -0
- package/src/repl/agent-repl.js +124 -5
- package/src/repl/permission-prompt.js +10 -12
- package/src/runtime/agent-core.js +311 -18
- package/src/runtime/agent-runtime.js +12 -1
- package/src/runtime/bootstrap.js +37 -13
- package/src/runtime/coding-agent-contract-shared.cjs +11 -4
- package/src/runtime/headless-runner.js +234 -36
- package/src/runtime/headless-stream.js +181 -6
- package/src/runtime/mcp-config.js +298 -8
- package/src/runtime/policies/agent-policy.js +8 -0
- package/src/runtime/status-diagnostics-lite.js +9 -1
- package/src/assets/web-panel/assets/MarkdownRenderer-CFyt8iUd.js +0 -1
- package/src/assets/web-panel/assets/McpTools-DFaIhOfu.js +0 -7
- package/src/assets/web-panel/assets/devWarning-DorRZcI9.js +0 -1
- package/src/assets/web-panel/assets/index-BETiL0a3.js +0 -1
- 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,
|
|
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
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
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
|
-
"
|
|
2761
|
-
|
|
2762
|
-
);
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
if
|
|
2766
|
-
|
|
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
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
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
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
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
|
-
//
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
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
|
-
!
|
|
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,
|
|
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 = [
|
|
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
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
|
|
3223
|
-
|
|
3224
|
-
|
|
3225
|
-
|
|
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, {
|
|
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
|
-
|
|
3275
|
-
|
|
3276
|
-
|
|
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
|
-
|
|
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) =>
|
|
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
|
-
|
|
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
|
|
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,
|