chainlesschain 0.165.5 → 0.165.8
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/assets/{AIOps-CEeYTeDB.js → AIOps-B3gWmXjo.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-BeRUQ3V2.js → ActionButton-G9pdT-0e.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-DrV718XF.js → Analytics-Ca30J7m2.js} +3 -3
- package/src/assets/web-panel/assets/{AppLayout-Cd2Frt7Q.js → AppLayout-BtVTm8PU.js} +5 -5
- package/src/assets/web-panel/assets/{Artifacts-CUJMlORU.js → Artifacts-BhViNa6u.js} +1 -1
- package/src/assets/web-panel/assets/{Audit-CXJcfT6p.js → Audit-DhKq-FnY.js} +1 -1
- package/src/assets/web-panel/assets/{BackgroundAgents-DOv6Z7H9.js → BackgroundAgents-BL0SVbnE.js} +1 -1
- package/src/assets/web-panel/assets/{Backup-BwdDyBI0.js → Backup-BWkblnFi.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-BmLn4kYH.js → BaseInput-ClaRNrF8.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-COg8pAW2.js → Chat-sZE2Z77o.js} +4 -4
- package/src/assets/web-panel/assets/ChatBubbleRenderer-BAfKOnRe.js +1 -0
- package/src/assets/web-panel/assets/{Checkbox-BCP4uoU8.js → Checkbox-Cvhay5WM.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-pRt6Y9JI.js → Codegen-BpejL-4N.js} +1 -1
- package/src/assets/web-panel/assets/{Col-DLaprKhS.js → Col-g2Ki6g_1.js} +1 -1
- package/src/assets/web-panel/assets/{Community-DyATsz5f.js → Community-D2xKbNo9.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-BXAaupZK.js → Compact-DbL1Pds3.js} +1 -1
- package/src/assets/web-panel/assets/{Compliance-DpcJBELr.js → Compliance-DGtZCV4c.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-DG4EFWyd.js → Cowork-4kQpoNsI.js} +3 -3
- package/src/assets/web-panel/assets/{Cron-F6Oy5n2P.js → Cron-BxTYXXAN.js} +2 -2
- package/src/assets/web-panel/assets/{Crosschain-BOdgtalX.js → Crosschain-k0J0LT2N.js} +1 -1
- package/src/assets/web-panel/assets/{DID-DT1dSkjH.js → DID-BaqhoPgN.js} +2 -2
- package/src/assets/web-panel/assets/{Dashboard-NBUIqWzC.js → Dashboard-T4RVPIM1.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-CSpHeMYB.js → Dropdown-DszOToWq.js} +1 -1
- package/src/assets/web-panel/assets/{EmailListRenderer-CEG0xISr.js → EmailListRenderer-C9KJU7HF.js} +1 -1
- package/src/assets/web-panel/assets/{FamilyGuardDashboard-BTZ0BZna.js → FamilyGuardDashboard-Cmr0JnsA.js} +1 -1
- package/src/assets/web-panel/assets/{Federation-BuuCTn2V.js → Federation-BI-_2xZX.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-KAVceevk.js → FormItemContext-DsnDEPsl.js} +1 -1
- package/src/assets/web-panel/assets/{GenericCardRenderer-DnHpcQvW.js → GenericCardRenderer-BLncAKKK.js} +1 -1
- package/src/assets/web-panel/assets/{Git-C_B_0HwB.js → Git-BPQhGXDn.js} +2 -2
- package/src/assets/web-panel/assets/{Governance-DPSZuzuu.js → Governance-CyElbUQW.js} +1 -1
- package/src/assets/web-panel/assets/{Inference-BV-06V7q.js → Inference-CcSntBQD.js} +1 -1
- package/src/assets/web-panel/assets/{KnowledgeGraph-wpDuupn6.js → KnowledgeGraph-p6e7HZ2r.js} +1 -1
- package/src/assets/web-panel/assets/{Logs-Bseca1_I.js → Logs-CNEtr-wz.js} +2 -2
- package/src/assets/web-panel/assets/MarkdownRenderer-Bj9xI0YH.js +1 -0
- package/src/assets/web-panel/assets/{Marketplace-d3kM4DDL.js → Marketplace-Dd2notAJ.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-DQ_xZ23i.js → McpTools-BBsN2CSW.js} +4 -4
- package/src/assets/web-panel/assets/{Memory-CRIeYO4L.js → Memory-Dg2uTvwx.js} +2 -2
- package/src/assets/web-panel/assets/{MobileBridge-BsllIa9l.js → MobileBridge-r1YMRiJV.js} +1 -1
- package/src/assets/web-panel/assets/{MobileProjects-BUuQy30X.js → MobileProjects-C-89kSm4.js} +1 -1
- package/src/assets/web-panel/assets/{Mtc-BOFDIzrC.js → Mtc-CrPZcVgf.js} +5 -5
- package/src/assets/web-panel/assets/{MtcAudit-BQg-vyvG.js → MtcAudit-sxi7hqJb.js} +5 -5
- package/src/assets/web-panel/assets/{Multisig-Bo1jrPdN.js → Multisig-CAgOMfF1.js} +3 -3
- package/src/assets/web-panel/assets/{NLProgramming-Bmod8alw.js → NLProgramming-DafaPts3.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-CGGFL8w-.js → Notes-CJM-xUW_.js} +3 -3
- package/src/assets/web-panel/assets/{NotificationSettings-BsZvCJxC.js → NotificationSettings-L0ZjfAh_.js} +1 -1
- package/src/assets/web-panel/assets/{OrderTableRenderer-C1-Quo_h.js → OrderTableRenderer-BB1CvCK0.js} +1 -1
- package/src/assets/web-panel/assets/{Organization-BjOeDYOi.js → Organization-DeLtgAD8.js} +4 -4
- package/src/assets/web-panel/assets/{Overflow-DwVV3l5A.js → Overflow-CKC2b10z.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-Cu3DQT9W.js → P2P-eyf2rzY1.js} +2 -2
- package/src/assets/web-panel/assets/{PdhVaultBrowser-Cg0gR7dH.js → PdhVaultBrowser-5upNIOW8.js} +3 -3
- package/src/assets/web-panel/assets/{Permissions-CsAXxRN5.js → Permissions-B8p3W6JK.js} +4 -4
- package/src/assets/web-panel/assets/{PersonalDataHub-C-nNmnYp.js → PersonalDataHub-BnRMxTq3.js} +5 -5
- package/src/assets/web-panel/assets/{Pipeline-BOnW7nNc.js → Pipeline-DjCNI1li.js} +1 -1
- package/src/assets/web-panel/assets/{Privacy-ClLSyGKt.js → Privacy-u6gXLbWT.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectInit-DuLvjjq7.js → ProjectInit-BZAw4tkM.js} +2 -2
- package/src/assets/web-panel/assets/{ProjectSettings-DQYYbDD9.js → ProjectSettings-B1Tr72q7.js} +2 -2
- package/src/assets/web-panel/assets/{Projects-Drw8MBGK.js → Projects-DO7HFuBQ.js} +1 -1
- package/src/assets/web-panel/assets/{Providers-ckqHQe6K.js → Providers-BjCztyhj.js} +1 -1
- package/src/assets/web-panel/assets/{QrScannerModal-DW5BXk-G.js → QrScannerModal-ZWyCUbR1.js} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-DDLnhNTp.js → QuickAsk-DhDjeW4G.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend-PJQ2C91U.js → Recommend-R_CN2MuU.js} +1 -1
- package/src/assets/web-panel/assets/{RemoteSession-s6wmda34.js → RemoteSession-uxFcujeu.js} +2 -2
- package/src/assets/web-panel/assets/{Reputation-DC2sb-I3.js → Reputation-DX8D7rks.js} +1 -1
- package/src/assets/web-panel/assets/{Row-hPGk1eGj.js → Row-BYahQILD.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-RIvfZql0.js → RssFeed-CUSQ2tm-.js} +3 -3
- package/src/assets/web-panel/assets/{Search-DFg1G1eR.js → Search-CnN_S8Cs.js} +1 -1
- package/src/assets/web-panel/assets/{Security-D6H5p8Dp.js → Security-DcZlh-O4.js} +4 -4
- package/src/assets/web-panel/assets/{Services-nxYiFV_w.js → Services-ZhBOjzIX.js} +2 -2
- package/src/assets/web-panel/assets/{Skeleton-BmgFNAdB.js → Skeleton-D5Qnq_lI.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-CsaaD2av.js → Skills-BC9cZ3Be.js} +1 -1
- package/src/assets/web-panel/assets/{Sla-yM5y9yvK.js → Sla-DauJ3E8_.js} +1 -1
- package/src/assets/web-panel/assets/SpeechSettings-DbNeZ_fh.js +1 -0
- package/src/assets/web-panel/assets/{SyncSettings-CFVhEyyr.js → SyncSettings-DUvJbSct.js} +2 -2
- package/src/assets/web-panel/assets/{Tasks-DdO02Inn.js → Tasks-BXa-5noQ.js} +1 -1
- package/src/assets/web-panel/assets/{Templates-Eo2d8Llu.js → Templates-JxaiUt_3.js} +1 -1
- package/src/assets/web-panel/assets/{Tenant-ChESOXgy.js → Tenant-BW4ERQN8.js} +1 -1
- package/src/assets/web-panel/assets/{Terminal-BJ7oDxTI.js → Terminal-DltUbdtv.js} +2 -2
- package/src/assets/web-panel/assets/{TimelineRenderer-hWX0fUvz.js → TimelineRenderer-CMoZz0Tc.js} +1 -1
- package/src/assets/web-panel/assets/{Tokens-DVBZQnd-.js → Tokens-D_VEV60K.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-D89cQfVq.js → Trigger-CCXTCs0R.js} +1 -1
- package/src/assets/web-panel/assets/{Trust-BIVSb4-V.js → Trust-lkGRIPcG.js} +1 -1
- package/src/assets/web-panel/assets/{UkeySign-B58gWopl.js → UkeySign-BfGeQ9HX.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-DhHI7bmG.js → VideoEditing-BEC7pYvc.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-C4FheWS_.js → Wallet-BZdG24o0.js} +4 -4
- package/src/assets/web-panel/assets/{WebAuthn-DnPRq-Bx.js → WebAuthn-yoEddedp.js} +3 -3
- package/src/assets/web-panel/assets/{WorkflowEditor-BerRPjpJ.js → WorkflowEditor-BXZEprOW.js} +1 -1
- package/src/assets/web-panel/assets/{chat-BzZ52iKr.js → chat-gg0bzZal.js} +1 -1
- package/src/assets/web-panel/assets/{colors-BI_E4BAn.js → colors-DjRVRuin.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-d50mr3yk.js → compact-item-su-dieAc.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-C6FsQ1IH.js → createContext-Ci3S8oKF.js} +1 -1
- package/src/assets/web-panel/assets/devWarning-CxbFX9uH.js +1 -0
- package/src/assets/web-panel/assets/{hasIn-DLZEMgLP.js → hasIn-D8F10heH.js} +1 -1
- package/src/assets/web-panel/assets/{index-D-aBumtY.js → index-11wtYQiF.js} +1 -1
- package/src/assets/web-panel/assets/{index-CjuKZBqJ.js → index-1IgxgALJ.js} +1 -1
- package/src/assets/web-panel/assets/{index-9H_M2jju.js → index-1agckWSq.js} +1 -1
- package/src/assets/web-panel/assets/{index-DE0V6cp-.js → index-5JqhujnR.js} +1 -1
- package/src/assets/web-panel/assets/{index-Y2-q7QLm.js → index-6Tniqk1s.js} +1 -1
- package/src/assets/web-panel/assets/{index-DoFtCVZ7.js → index-8Z-8BIlN.js} +1 -1
- package/src/assets/web-panel/assets/{index-KNGgO4Wm.js → index-BCI5v0yB.js} +1 -1
- package/src/assets/web-panel/assets/{index-DY4j5zkF.js → index-BDJntYmJ.js} +1 -1
- package/src/assets/web-panel/assets/{index-BZjjdQ62.js → index-BGzixOGZ.js} +1 -1
- package/src/assets/web-panel/assets/{index-BTpIOTfR.js → index-BMMOVoLD.js} +1 -1
- package/src/assets/web-panel/assets/{index-BB9SqVSt.js → index-BTFpdBtt.js} +1 -1
- package/src/assets/web-panel/assets/{index-AHr7ryWf.js → index-Ba_HyPnE.js} +1 -1
- package/src/assets/web-panel/assets/{index-Pvf30m8Q.js → index-BbF1IHof.js} +1 -1
- package/src/assets/web-panel/assets/{index-CRN5TRTQ.js → index-BgT9su_e.js} +1 -1
- package/src/assets/web-panel/assets/{index-CUaEHfQ0.js → index-BzPB6kGZ.js} +1 -1
- package/src/assets/web-panel/assets/{index-CdKt9fOd.js → index-C6ygzplr.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dec0Kf_Z.js → index-CC3ZDjzY.js} +1 -1
- package/src/assets/web-panel/assets/{index-IPkwSAW_.js → index-CEmXRhAE.js} +1 -1
- package/src/assets/web-panel/assets/index-CJGOlduY.js +1 -0
- package/src/assets/web-panel/assets/{index-C3k8kevG.js → index-CMPqd5Cv.js} +1 -1
- package/src/assets/web-panel/assets/{index-A5CDWP7y.js → index-COWd8uGx.js} +1 -1
- package/src/assets/web-panel/assets/{index-CO8DAF7U.js → index-CPIYSp4H.js} +1 -1
- package/src/assets/web-panel/assets/{index-CVzuawCX.js → index-CR0h6seT.js} +1 -1
- package/src/assets/web-panel/assets/{index-Co7s9LsB.js → index-CUmDhC4j.js} +1 -1
- package/src/assets/web-panel/assets/index-CkBCN5yH.js +1 -0
- package/src/assets/web-panel/assets/{index-CLoTQr0W.js → index-CusgZPtc.js} +1 -1
- package/src/assets/web-panel/assets/{index-B81mo1Et.js → index-DBAETXft.js} +1 -1
- package/src/assets/web-panel/assets/{index-BcSUl9fA.js → index-DIJ2qFOC.js} +1 -1
- package/src/assets/web-panel/assets/{index-Q5Tn735s.js → index-Dbq4NMET.js} +1 -1
- package/src/assets/web-panel/assets/{index-GXE0D8Nx.js → index-DkMEF5VT.js} +1 -1
- package/src/assets/web-panel/assets/{index-TWG-_D1M.js → index-DkfQR3hM.js} +1 -1
- package/src/assets/web-panel/assets/{index-BAE3hwev.js → index-DnLucOn9.js} +1 -1
- package/src/assets/web-panel/assets/{index-CgkoH3_g.js → index-DugB_aKa.js} +1 -1
- package/src/assets/web-panel/assets/{index-BRGx3w6T.js → index-DyLUarhZ.js} +3 -3
- package/src/assets/web-panel/assets/{index-BeM4hfht.js → index-T6_6ygJZ.js} +1 -1
- package/src/assets/web-panel/assets/{index-DsscwxPE.js → index-Tu8eWqh-.js} +1 -1
- package/src/assets/web-panel/assets/{index-Bmr_vxXD.js → index-W7XUgdVL.js} +1 -1
- package/src/assets/web-panel/assets/{index-BoNZlzhQ.js → index-dHZinfko.js} +1 -1
- package/src/assets/web-panel/assets/{index-DqkbHHg8.js → index-jyNgp5FI.js} +1 -1
- package/src/assets/web-panel/assets/{initDefaultProps-D-4Hzgej.js → initDefaultProps-Cv4p8e4w.js} +1 -1
- package/src/assets/web-panel/assets/{motion-D4Wl4xU7.js → motion-B8EwmMOw.js} +1 -1
- package/src/assets/web-panel/assets/{move-Cd5eQvL0.js → move-B8R9F8TK.js} +1 -1
- package/src/assets/web-panel/assets/{mtc-parser-B4ilslCW.js → mtc-parser-DoFYz5Bp.js} +1 -1
- package/src/assets/web-panel/assets/{omit-Ckarx-tf.js → omit-CHWYLBjI.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-QJ0bpCmc.js → pickAttrs-CXuPDO77.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-CKHm-tq8.js → placementArrow-BaAtUUon.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-CV6a_w_Y.js → responsiveObserve-DCc4zcDD.js} +1 -1
- package/src/assets/web-panel/assets/{slide-mHpeQFgk.js → slide-B_lx89oZ.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-DIaQNQWA.js → statusUtils-DE8DPmR0.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-PJpzavV8.js → styleChecker-1ktXOsL_.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-BH7zTw8S.js → useFlexGapSupport-xH89-wjh.js} +1 -1
- package/src/assets/web-panel/assets/{useFs-BDNG3JDd.js → useFs-DscvfW5p.js} +1 -1
- package/src/assets/web-panel/assets/{usePersonalDataHub-CY2VWdUg.js → usePersonalDataHub-RdMKQta7.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-D0kw9jhz.js → vnode-Z3wTni2T.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-B5g7TN7r.js → zoom-B0iChCsm.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 +105 -62
- package/src/commands/background-session.js +100 -10
- package/src/commands/git.js +39 -0
- package/src/commands/mcp.js +5 -0
- package/src/commands/plugin.js +160 -29
- package/src/commands/remote-control.js +295 -110
- package/src/commands/session-group.js +131 -0
- package/src/commands/session-location.js +48 -0
- package/src/commands/session-message.js +171 -0
- package/src/commands/session.js +22 -0
- package/src/commands/sync-providers.js +4 -1
- package/src/commands/update.js +1 -0
- package/src/data/changelog.json +41 -2
- 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 +832 -91
- package/src/harness/session-index.js +15 -4
- 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/background-session-transport.js +153 -27
- package/src/lib/browser-evidence.js +1405 -0
- package/src/lib/chrome-connector.js +465 -79
- 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/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/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/llm-config-defaults.js +43 -0
- package/src/lib/mcp-client.js +2 -0
- package/src/lib/mcp-headers-helper.js +47 -5
- package/src/lib/mcp-lifecycle-authority.js +887 -0
- package/src/lib/mcp-oauth.js +156 -26
- package/src/lib/mcp-tls.js +330 -0
- package/src/lib/output-styles.js +12 -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 +730 -71
- package/src/lib/plugin-runtime/lsp.js +4 -4
- package/src/lib/plugin-runtime/marketplace-archive-source.js +366 -0
- package/src/lib/plugin-runtime/marketplace-catalog.js +63 -6
- 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-remote-artifacts.js +1 -0
- package/src/lib/plugin-runtime/marketplace-source-adapter.js +188 -0
- package/src/lib/plugin-runtime/marketplace-source-cache.js +11 -1
- 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 +583 -58
- package/src/lib/plugin-runtime/settings.js +3 -1
- package/src/lib/plugin-runtime/trust.js +274 -13
- package/src/lib/plugin-security.js +393 -41
- package/src/lib/plugin-source-identity.js +850 -0
- 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/remote-approval-bridge.js +136 -46
- package/src/lib/remote-control.js +217 -31
- 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 +1252 -0
- package/src/lib/session-projection.js +286 -3
- package/src/lib/session-runtime-retention.js +155 -0
- package/src/lib/session-store-guard.js +3 -1
- package/src/lib/session-workbench-store.js +466 -0
- package/src/lib/settings-loader.cjs +29 -0
- 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/workspace-trust.js +793 -0
- package/src/repl/agent-repl.js +175 -19
- package/src/repl/permission-prompt.js +29 -8
- package/src/repl/repl-keybindings.js +55 -0
- package/src/runtime/agent-core.js +362 -20
- package/src/runtime/bootstrap.js +37 -13
- package/src/runtime/coding-agent-contract-shared.cjs +11 -4
- package/src/runtime/headless-runner.js +281 -102
- package/src/runtime/headless-stream.js +199 -30
- package/src/runtime/mcp-config.js +298 -6
- package/src/runtime/policies/agent-policy.js +10 -0
- package/src/runtime/status-diagnostics-lite.js +9 -1
- package/src/assets/web-panel/assets/ChatBubbleRenderer-4gGkdGgA.js +0 -1
- package/src/assets/web-panel/assets/MarkdownRenderer-B4RLp2zs.js +0 -1
- package/src/assets/web-panel/assets/SpeechSettings-DPy1XMYh.js +0 -1
- package/src/assets/web-panel/assets/devWarning-CagEcIex.js +0 -1
- package/src/assets/web-panel/assets/index-pzT9PpOU.js +0 -1
- package/src/assets/web-panel/assets/index-z1kFadaX.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";
|
|
@@ -45,6 +46,17 @@ import {
|
|
|
45
46
|
prepareMcpStdioExecutableIdentity,
|
|
46
47
|
} from "../lib/mcp-stdio-executable-identity.js";
|
|
47
48
|
import { resolveMcpStdioSandboxContext } from "../lib/mcp-stdio-workspace-authority.js";
|
|
49
|
+
import {
|
|
50
|
+
assertManagedMcpTlsConfig,
|
|
51
|
+
closeMcpTlsDispatcher,
|
|
52
|
+
createMcpTlsDispatcher,
|
|
53
|
+
loadMcpTlsMaterial,
|
|
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";
|
|
48
60
|
|
|
49
61
|
/**
|
|
50
62
|
* Injectable dependencies — overridable from tests.
|
|
@@ -63,6 +75,9 @@ export const _deps = {
|
|
|
63
75
|
verifyMcpStdioApprovedWorkingDirectory,
|
|
64
76
|
prepareMcpStdioExecutableIdentity,
|
|
65
77
|
resolveMcpStdioSandboxContext,
|
|
78
|
+
loadMcpTlsMaterial,
|
|
79
|
+
createMcpTlsDispatcher,
|
|
80
|
+
closeMcpTlsDispatcher,
|
|
66
81
|
// Backoff sleep seam (tests override with a no-op so retries don't wait).
|
|
67
82
|
sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
68
83
|
};
|
|
@@ -229,7 +244,124 @@ const MCP_HTTP_RESPONSE_MAX_BYTES = 16 * 1024 * 1024;
|
|
|
229
244
|
* hard 30s regardless of its config — an inconsistency with HTTP servers.
|
|
230
245
|
*/
|
|
231
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;
|
|
232
255
|
const MCP_PROTOCOL_VERSION = "2025-11-25";
|
|
256
|
+
export const SUPPORTED_MCP_PROTOCOL_VERSIONS = Object.freeze([
|
|
257
|
+
"2024-11-05",
|
|
258
|
+
"2025-03-26",
|
|
259
|
+
"2025-06-18",
|
|
260
|
+
MCP_PROTOCOL_VERSION,
|
|
261
|
+
]);
|
|
262
|
+
const supportedMcpProtocolVersions = new Set(SUPPORTED_MCP_PROTOCOL_VERSIONS);
|
|
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
|
+
|
|
355
|
+
export function assertSupportedMcpProtocolVersion(value) {
|
|
356
|
+
if (typeof value !== "string" || !supportedMcpProtocolVersions.has(value)) {
|
|
357
|
+
const error = new Error(
|
|
358
|
+
"MCP server selected an unsupported protocol version",
|
|
359
|
+
);
|
|
360
|
+
error.code = "CC_MCP_PROTOCOL_VERSION_UNSUPPORTED";
|
|
361
|
+
throw error;
|
|
362
|
+
}
|
|
363
|
+
return value;
|
|
364
|
+
}
|
|
233
365
|
const URL_ELICITATION_REQUIRED = -32042;
|
|
234
366
|
const MCP_RPC_ERROR_CODE = "CC_MCP_RPC_ERROR";
|
|
235
367
|
const mcpRpcControlData = new WeakMap();
|
|
@@ -885,6 +1017,10 @@ function isMcpNonRetryableProtocolError(error) {
|
|
|
885
1017
|
);
|
|
886
1018
|
}
|
|
887
1019
|
|
|
1020
|
+
function isMcpFatalDiscoveryError(error) {
|
|
1021
|
+
return isMcpNonRetryableProtocolError(error) || isMcpJsonBudgetError(error);
|
|
1022
|
+
}
|
|
1023
|
+
|
|
888
1024
|
/**
|
|
889
1025
|
* Heuristic: does this error look like the server went away (vs. the tool
|
|
890
1026
|
* itself failing)? Used to gate reconnect-and-retry for servers that have a
|
|
@@ -1287,8 +1423,27 @@ export class MCPClient extends EventEmitter {
|
|
|
1287
1423
|
this._nextId = 1;
|
|
1288
1424
|
this._reconnectors = new Map(); // name → async () => config|null
|
|
1289
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();
|
|
1437
|
+
// Desired resource subscriptions outlive a single transport entry. Keeping
|
|
1438
|
+
// them on the client lets a later reconnect retry restoration even when a
|
|
1439
|
+
// previous partial reconnect was torn down fail-closed.
|
|
1440
|
+
this._resourceSubscriptions = new Map(); // name → Set<uri>
|
|
1441
|
+
this._lifecycleAttempts = new Map(); // name → latest durable generation token
|
|
1290
1442
|
this._sessionId =
|
|
1291
1443
|
options && options.sessionId != null ? String(options.sessionId) : null;
|
|
1444
|
+
this._lifecycleSessionId = this._sessionId || "default";
|
|
1445
|
+
this._lifecycleAuthority =
|
|
1446
|
+
options?.lifecycleAuthority || new McpLifecycleAuthority();
|
|
1292
1447
|
this._workspaceBinding =
|
|
1293
1448
|
options?.workspaceBinding || currentHostHooksV2WorkspaceBinding();
|
|
1294
1449
|
// MCP roots capability (Claude-Code 2.1.203 parity): servers may ask the
|
|
@@ -1848,6 +2003,90 @@ export class MCPClient extends EventEmitter {
|
|
|
1848
2003
|
*/
|
|
1849
2004
|
setSessionId(id) {
|
|
1850
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
|
+
}
|
|
1851
2090
|
}
|
|
1852
2091
|
|
|
1853
2092
|
/**
|
|
@@ -1990,11 +2229,27 @@ export class MCPClient extends EventEmitter {
|
|
|
1990
2229
|
* @param {string} name - Server name
|
|
1991
2230
|
* @param {object} config - { command?, args?, env?, url?, transport? }
|
|
1992
2231
|
*/
|
|
1993
|
-
async connect(name, config,
|
|
2232
|
+
async connect(name, config, internalOptions = false) {
|
|
1994
2233
|
if (this.servers.has(name)) {
|
|
1995
2234
|
throw new Error(`Server "${name}" already connected`);
|
|
1996
2235
|
}
|
|
1997
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
|
+
|
|
1998
2253
|
const transportKind = inferTransport(config);
|
|
1999
2254
|
let stdioExecutionApproval = null;
|
|
2000
2255
|
let approvedStdioInvocation = null;
|
|
@@ -2082,11 +2337,46 @@ export class MCPClient extends EventEmitter {
|
|
|
2082
2337
|
};
|
|
2083
2338
|
}
|
|
2084
2339
|
}
|
|
2085
|
-
const
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
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);
|
|
2350
|
+
let tlsMaterial = null;
|
|
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
|
+
});
|
|
2370
|
+
}
|
|
2371
|
+
connectionHeaders = await this._connectionHeaders(
|
|
2372
|
+
name,
|
|
2373
|
+
sourceConfig,
|
|
2374
|
+
transportKind,
|
|
2375
|
+
);
|
|
2376
|
+
} catch (error) {
|
|
2377
|
+
this._failLifecycle({ _lifecycleAttempt: lifecycleAttempt }, error);
|
|
2378
|
+
throw error;
|
|
2379
|
+
}
|
|
2090
2380
|
config = { ...runtimeConfig, headers: connectionHeaders };
|
|
2091
2381
|
const entry = {
|
|
2092
2382
|
// Keep the source config (static headers + helper command) so every
|
|
@@ -2104,6 +2394,8 @@ export class MCPClient extends EventEmitter {
|
|
|
2104
2394
|
_httpRequestControllers: new Set(),
|
|
2105
2395
|
_httpDiscardControllers: new Set(),
|
|
2106
2396
|
_httpDiscardStopping: false,
|
|
2397
|
+
_httpDispatcher: null,
|
|
2398
|
+
_tlsIdentityDigest: tlsMaterial?.identityDigest || null,
|
|
2107
2399
|
_disconnectPromise: null,
|
|
2108
2400
|
_webSocketPayloadError: null,
|
|
2109
2401
|
_webSocketInboundError: null,
|
|
@@ -2111,6 +2403,7 @@ export class MCPClient extends EventEmitter {
|
|
|
2111
2403
|
_toolMetadataBytes: 0,
|
|
2112
2404
|
_stdioExecutionApproval: stdioExecutionApproval,
|
|
2113
2405
|
_stdioExecutableIdentity: stdioExecutableIdentity,
|
|
2406
|
+
_lifecycleAttempt: lifecycleAttempt,
|
|
2114
2407
|
tools: [],
|
|
2115
2408
|
resources: [],
|
|
2116
2409
|
resourceTemplates: [],
|
|
@@ -2157,6 +2450,9 @@ export class MCPClient extends EventEmitter {
|
|
|
2157
2450
|
}
|
|
2158
2451
|
entry.httpUrl = config.url;
|
|
2159
2452
|
entry.httpHeaders = connectionHeaders;
|
|
2453
|
+
entry._httpDispatcher = tlsMaterial
|
|
2454
|
+
? _deps.createMcpTlsDispatcher(tlsMaterial)
|
|
2455
|
+
: null;
|
|
2160
2456
|
} else if (isWebSocketTransport(transportKind)) {
|
|
2161
2457
|
if (!config.url) {
|
|
2162
2458
|
throw mcpTransportError(
|
|
@@ -2203,6 +2499,7 @@ export class MCPClient extends EventEmitter {
|
|
|
2203
2499
|
...(transportKind === "wss" && config.rejectUnauthorized === false
|
|
2204
2500
|
? { rejectUnauthorized: false }
|
|
2205
2501
|
: {}),
|
|
2502
|
+
...(tlsMaterial?.connectOptions || {}),
|
|
2206
2503
|
});
|
|
2207
2504
|
entry.socket = socket;
|
|
2208
2505
|
|
|
@@ -2218,6 +2515,7 @@ export class MCPClient extends EventEmitter {
|
|
|
2218
2515
|
});
|
|
2219
2516
|
entry._webSocketPayloadError = error;
|
|
2220
2517
|
entry.state = ServerState.ERROR;
|
|
2518
|
+
this._failLifecycle(entry, error);
|
|
2221
2519
|
rejectPending(error);
|
|
2222
2520
|
this._cancelServerElicitations(name);
|
|
2223
2521
|
this.emit("server-error", {
|
|
@@ -2242,6 +2540,7 @@ export class MCPClient extends EventEmitter {
|
|
|
2242
2540
|
}
|
|
2243
2541
|
entry._webSocketInboundError = error;
|
|
2244
2542
|
entry.state = ServerState.ERROR;
|
|
2543
|
+
this._failLifecycle(entry, error);
|
|
2245
2544
|
rejectPending(error);
|
|
2246
2545
|
this._cancelServerElicitations(name);
|
|
2247
2546
|
this.emit("server-error", {
|
|
@@ -2319,6 +2618,7 @@ export class MCPClient extends EventEmitter {
|
|
|
2319
2618
|
{ transport: transportKind, url: config.url },
|
|
2320
2619
|
);
|
|
2321
2620
|
entry.state = ServerState.ERROR;
|
|
2621
|
+
this._failLifecycle(entry, error);
|
|
2322
2622
|
rejectPending(error);
|
|
2323
2623
|
this._cancelServerElicitations(name);
|
|
2324
2624
|
this.emit("server-error", {
|
|
@@ -2353,7 +2653,7 @@ export class MCPClient extends EventEmitter {
|
|
|
2353
2653
|
) {
|
|
2354
2654
|
throw new Error("expected one JSON-RPC object");
|
|
2355
2655
|
}
|
|
2356
|
-
this._handleMessage(name, message);
|
|
2656
|
+
this._handleMessage(name, message, entry);
|
|
2357
2657
|
} catch (cause) {
|
|
2358
2658
|
if (isMcpJsonBudgetError(cause)) {
|
|
2359
2659
|
recordInboundFailure(cause);
|
|
@@ -2365,6 +2665,7 @@ export class MCPClient extends EventEmitter {
|
|
|
2365
2665
|
{ transport: transportKind, url: config.url },
|
|
2366
2666
|
);
|
|
2367
2667
|
entry.state = ServerState.ERROR;
|
|
2668
|
+
this._failLifecycle(entry, error);
|
|
2368
2669
|
rejectPending(error);
|
|
2369
2670
|
this._cancelServerElicitations(name);
|
|
2370
2671
|
this.emit("server-error", {
|
|
@@ -2386,6 +2687,7 @@ export class MCPClient extends EventEmitter {
|
|
|
2386
2687
|
const inboundError = entry._webSocketInboundError;
|
|
2387
2688
|
if (inboundError) {
|
|
2388
2689
|
entry.state = ServerState.ERROR;
|
|
2690
|
+
this._failLifecycle(entry, inboundError);
|
|
2389
2691
|
rejectPending(inboundError);
|
|
2390
2692
|
this._cancelServerElicitations(name);
|
|
2391
2693
|
this.emit("server-disconnected", {
|
|
@@ -2413,6 +2715,7 @@ export class MCPClient extends EventEmitter {
|
|
|
2413
2715
|
}
|
|
2414
2716
|
if (payloadError) {
|
|
2415
2717
|
entry.state = ServerState.ERROR;
|
|
2718
|
+
this._failLifecycle(entry, payloadError);
|
|
2416
2719
|
rejectPending(payloadError);
|
|
2417
2720
|
this._cancelServerElicitations(name);
|
|
2418
2721
|
this.emit("server-disconnected", {
|
|
@@ -2437,6 +2740,7 @@ export class MCPClient extends EventEmitter {
|
|
|
2437
2740
|
outcomeUnknown: dispatched,
|
|
2438
2741
|
},
|
|
2439
2742
|
);
|
|
2743
|
+
this._failLifecycle(entry, error);
|
|
2440
2744
|
rejectPending(error);
|
|
2441
2745
|
this._cancelServerElicitations(name);
|
|
2442
2746
|
this.emit("server-disconnected", {
|
|
@@ -2464,6 +2768,7 @@ export class MCPClient extends EventEmitter {
|
|
|
2464
2768
|
outcomeUnknown: dispatched,
|
|
2465
2769
|
},
|
|
2466
2770
|
);
|
|
2771
|
+
this._failLifecycle(entry, error);
|
|
2467
2772
|
rejectPending(error);
|
|
2468
2773
|
this._cancelServerElicitations(name);
|
|
2469
2774
|
this.emit("server-error", {
|
|
@@ -2579,6 +2884,7 @@ export class MCPClient extends EventEmitter {
|
|
|
2579
2884
|
typeof data === "string"
|
|
2580
2885
|
? data
|
|
2581
2886
|
: entry._decoder.decode(data, { stream: true }),
|
|
2887
|
+
entry,
|
|
2582
2888
|
);
|
|
2583
2889
|
});
|
|
2584
2890
|
|
|
@@ -2608,6 +2914,7 @@ export class MCPClient extends EventEmitter {
|
|
|
2608
2914
|
if (!entry._stdioFrameError) {
|
|
2609
2915
|
entry.state = ServerState.DISCONNECTED;
|
|
2610
2916
|
}
|
|
2917
|
+
this._failLifecycle(entry, { code: "CC_MCP_STDIO_EXIT" });
|
|
2611
2918
|
failPending(
|
|
2612
2919
|
`MCP server "${name}" process exited (code ${code}) before responding`,
|
|
2613
2920
|
);
|
|
@@ -2622,6 +2929,7 @@ export class MCPClient extends EventEmitter {
|
|
|
2622
2929
|
if (cleanup?.verifiable && !cleanup.confirmed) {
|
|
2623
2930
|
entry.state = ServerState.ERROR;
|
|
2624
2931
|
const cleanupError = mcpStdioCleanupError(name, cleanup);
|
|
2932
|
+
this._failLifecycle(entry, cleanupError);
|
|
2625
2933
|
this.emit("server-error", {
|
|
2626
2934
|
name,
|
|
2627
2935
|
error: cleanupError.message,
|
|
@@ -2638,6 +2946,7 @@ export class MCPClient extends EventEmitter {
|
|
|
2638
2946
|
|
|
2639
2947
|
proc.on("error", (err) => {
|
|
2640
2948
|
entry.state = ServerState.ERROR;
|
|
2949
|
+
this._failLifecycle(entry, err);
|
|
2641
2950
|
failPending(`MCP server "${name}" process error: ${err.message}`);
|
|
2642
2951
|
this._cancelServerElicitations(name);
|
|
2643
2952
|
this.emit("server-error", { name, error: err.message });
|
|
@@ -2652,6 +2961,7 @@ export class MCPClient extends EventEmitter {
|
|
|
2652
2961
|
if (proc.stdin && typeof proc.stdin.on === "function") {
|
|
2653
2962
|
proc.stdin.on("error", (err) => {
|
|
2654
2963
|
entry.state = ServerState.ERROR;
|
|
2964
|
+
this._failLifecycle(entry, err);
|
|
2655
2965
|
failPending(`MCP server "${name}" stdin error: ${err.message}`);
|
|
2656
2966
|
this._cancelServerElicitations(name);
|
|
2657
2967
|
this.emit("server-error", { name, error: err.message });
|
|
@@ -2664,6 +2974,9 @@ export class MCPClient extends EventEmitter {
|
|
|
2664
2974
|
}
|
|
2665
2975
|
|
|
2666
2976
|
// Initialize MCP protocol (retried on transient network errors).
|
|
2977
|
+
this._transitionLifecycle(entry, MCP_LIFECYCLE_PHASES.INITIALIZING, {
|
|
2978
|
+
tlsIdentityDigest: entry._tlsIdentityDigest,
|
|
2979
|
+
});
|
|
2667
2980
|
const initResult = await this._requestWithRetry(name, "initialize", {
|
|
2668
2981
|
protocolVersion: this._protocolVersion,
|
|
2669
2982
|
capabilities: {
|
|
@@ -2680,9 +2993,12 @@ export class MCPClient extends EventEmitter {
|
|
|
2680
2993
|
clientInfo: { name: "chainlesschain-cli", version: "0.37.9" },
|
|
2681
2994
|
});
|
|
2682
2995
|
|
|
2683
|
-
entry.protocolVersion =
|
|
2684
|
-
initResult?.protocolVersion
|
|
2996
|
+
entry.protocolVersion = assertSupportedMcpProtocolVersion(
|
|
2997
|
+
initResult?.protocolVersion,
|
|
2685
2998
|
);
|
|
2999
|
+
this._transitionLifecycle(entry, MCP_LIFECYCLE_PHASES.DISCOVERING, {
|
|
3000
|
+
tlsIdentityDigest: entry._tlsIdentityDigest,
|
|
3001
|
+
});
|
|
2686
3002
|
// Send initialized notification
|
|
2687
3003
|
this._sendNotification(name, "notifications/initialized", {});
|
|
2688
3004
|
|
|
@@ -2697,62 +3013,150 @@ export class MCPClient extends EventEmitter {
|
|
|
2697
3013
|
? initResult.instructions
|
|
2698
3014
|
: null;
|
|
2699
3015
|
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
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", {
|
|
2712
3037
|
name,
|
|
2713
|
-
"
|
|
2714
|
-
|
|
2715
|
-
);
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
if
|
|
2719
|
-
|
|
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.
|
|
2720
3065
|
}
|
|
2721
|
-
// else: server did not advertise tools — legitimately none.
|
|
2722
|
-
}
|
|
2723
3066
|
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
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
|
+
}
|
|
2735
3079
|
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
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
|
+
}
|
|
2746
3138
|
}
|
|
2747
3139
|
|
|
2748
|
-
//
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
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);
|
|
2754
3154
|
}
|
|
2755
3155
|
|
|
3156
|
+
this._transitionLifecycle(entry, MCP_LIFECYCLE_PHASES.READY, {
|
|
3157
|
+
tlsIdentityDigest: entry._tlsIdentityDigest,
|
|
3158
|
+
});
|
|
3159
|
+
|
|
2756
3160
|
this.emit("server-connected", {
|
|
2757
3161
|
name,
|
|
2758
3162
|
tools: entry.tools.length,
|
|
@@ -2777,6 +3181,16 @@ export class MCPClient extends EventEmitter {
|
|
|
2777
3181
|
};
|
|
2778
3182
|
} catch (err) {
|
|
2779
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
|
+
}
|
|
2780
3194
|
this._cancelServerElicitations(name);
|
|
2781
3195
|
// The initialize notification is fire-and-forget. If later capability
|
|
2782
3196
|
// discovery fails, connect() deletes this entry without going through
|
|
@@ -2831,17 +3245,26 @@ export class MCPClient extends EventEmitter {
|
|
|
2831
3245
|
// teardown is best-effort
|
|
2832
3246
|
}
|
|
2833
3247
|
}
|
|
3248
|
+
try {
|
|
3249
|
+
await _deps.closeMcpTlsDispatcher(entry._httpDispatcher);
|
|
3250
|
+
} catch {
|
|
3251
|
+
// Preserve the connection failure; dispatcher cleanup is best-effort.
|
|
3252
|
+
}
|
|
3253
|
+
entry._httpDispatcher = null;
|
|
2834
3254
|
this._releaseToolInventory(entry);
|
|
2835
3255
|
this.servers.delete(name);
|
|
2836
3256
|
if (
|
|
2837
|
-
!
|
|
3257
|
+
!authRetryUsed &&
|
|
2838
3258
|
sourceConfig.headersHelper &&
|
|
2839
3259
|
isMcpAuthenticationError(err)
|
|
2840
3260
|
) {
|
|
2841
3261
|
// A helper is intentionally uncached. One authentication rejection may
|
|
2842
3262
|
// mean its short-lived token expired between generation and initialize;
|
|
2843
3263
|
// execute it once more and retry the connection exactly once.
|
|
2844
|
-
return this.connect(name, sourceConfig,
|
|
3264
|
+
return this.connect(name, sourceConfig, {
|
|
3265
|
+
authRetryUsed: true,
|
|
3266
|
+
reconnect: true,
|
|
3267
|
+
});
|
|
2845
3268
|
}
|
|
2846
3269
|
throw err;
|
|
2847
3270
|
}
|
|
@@ -2850,18 +3273,78 @@ export class MCPClient extends EventEmitter {
|
|
|
2850
3273
|
/**
|
|
2851
3274
|
* Disconnect from an MCP server.
|
|
2852
3275
|
*/
|
|
2853
|
-
disconnect(name) {
|
|
3276
|
+
disconnect(name, options = {}) {
|
|
3277
|
+
if (options.preserveResourceSubscriptions !== true) {
|
|
3278
|
+
this._resourceSubscriptions.delete(name);
|
|
3279
|
+
}
|
|
2854
3280
|
const entry = this.servers.get(name);
|
|
3281
|
+
const lifecycleAttempt =
|
|
3282
|
+
entry?._lifecycleAttempt || this._lifecycleAttempts.get(name) || null;
|
|
2855
3283
|
if (!entry) {
|
|
2856
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
|
+
}
|
|
2857
3316
|
return Promise.resolve(false);
|
|
2858
3317
|
}
|
|
2859
3318
|
if (entry._disconnectPromise) return entry._disconnectPromise;
|
|
2860
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
|
+
|
|
2861
3331
|
// Make disconnect visible before any teardown awaits. In particular, an
|
|
2862
3332
|
// HTTP session DELETE may wait for response headers; no new tool/resource
|
|
2863
3333
|
// request may enter the transport during that window or trigger a hot
|
|
2864
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
|
+
}
|
|
2865
3348
|
entry.state = ServerState.DISCONNECTED;
|
|
2866
3349
|
entry._httpDiscardStopping = true;
|
|
2867
3350
|
this._cancelServerElicitations(name);
|
|
@@ -2904,14 +3387,40 @@ export class MCPClient extends EventEmitter {
|
|
|
2904
3387
|
// ignore — disconnect is best-effort
|
|
2905
3388
|
}
|
|
2906
3389
|
}
|
|
2907
|
-
return true;
|
|
2908
3390
|
} finally {
|
|
3391
|
+
try {
|
|
3392
|
+
await _deps.closeMcpTlsDispatcher(entry._httpDispatcher);
|
|
3393
|
+
} catch {
|
|
3394
|
+
// Transport teardown remains best-effort.
|
|
3395
|
+
}
|
|
3396
|
+
entry._httpDispatcher = null;
|
|
2909
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
|
+
}
|
|
2910
3411
|
this._releaseToolInventory(entry);
|
|
2911
3412
|
// A future connection may reuse the same name. Never let a delayed
|
|
2912
3413
|
// teardown delete that replacement entry.
|
|
2913
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
|
+
}
|
|
2914
3421
|
}
|
|
3422
|
+
if (subscriptionClearError) throw subscriptionClearError;
|
|
3423
|
+
return true;
|
|
2915
3424
|
});
|
|
2916
3425
|
entry._disconnectPromise = disconnecting;
|
|
2917
3426
|
|
|
@@ -2927,9 +3436,19 @@ export class MCPClient extends EventEmitter {
|
|
|
2927
3436
|
* Disconnect from all servers.
|
|
2928
3437
|
*/
|
|
2929
3438
|
async disconnectAll() {
|
|
2930
|
-
const names = [
|
|
2931
|
-
|
|
2932
|
-
|
|
3439
|
+
const names = new Set([
|
|
3440
|
+
...this.servers.keys(),
|
|
3441
|
+
...this._lifecycleAttempts.keys(),
|
|
3442
|
+
]);
|
|
3443
|
+
try {
|
|
3444
|
+
for (const name of names) {
|
|
3445
|
+
await this.disconnect(name);
|
|
3446
|
+
}
|
|
3447
|
+
} finally {
|
|
3448
|
+
// A failed reconnect may have already removed its transport entry while
|
|
3449
|
+
// retaining desired subscriptions for another retry. disconnectAll()
|
|
3450
|
+
// is the explicit terminal teardown and must clear those tombstones too.
|
|
3451
|
+
this._resourceSubscriptions.clear();
|
|
2933
3452
|
}
|
|
2934
3453
|
}
|
|
2935
3454
|
|
|
@@ -3146,15 +3665,58 @@ export class MCPClient extends EventEmitter {
|
|
|
3146
3665
|
throw new Error(`Server "${serverName}" is not connected`);
|
|
3147
3666
|
}
|
|
3148
3667
|
|
|
3149
|
-
const
|
|
3150
|
-
|
|
3151
|
-
|
|
3152
|
-
|
|
3153
|
-
|
|
3154
|
-
|
|
3155
|
-
|
|
3156
|
-
|
|
3157
|
-
|
|
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
|
+
}
|
|
3158
3720
|
|
|
3159
3721
|
return admitMcpToolResult(serverName, result, entry.config).result;
|
|
3160
3722
|
}
|
|
@@ -3173,11 +3735,32 @@ export class MCPClient extends EventEmitter {
|
|
|
3173
3735
|
try {
|
|
3174
3736
|
const currentEntry = this.servers.get(name) || null;
|
|
3175
3737
|
const currentConfig = currentEntry?.config || null;
|
|
3738
|
+
// Resource subscriptions belong to the logical client/server
|
|
3739
|
+
// relationship, not to one transport connection. Snapshot them before
|
|
3740
|
+
// disconnect() removes the old entry so a hot reconnect cannot
|
|
3741
|
+
// silently stop delivering resource update notifications.
|
|
3742
|
+
const resourceSubscriptions = new Set([
|
|
3743
|
+
...(this._resourceSubscriptions.get(name) || []),
|
|
3744
|
+
...(currentEntry?.resourceSubscriptions || []),
|
|
3745
|
+
]);
|
|
3746
|
+
if (resourceSubscriptions.size > 0) {
|
|
3747
|
+
this._resourceSubscriptions.set(name, new Set(resourceSubscriptions));
|
|
3748
|
+
}
|
|
3176
3749
|
const resolver = this._reconnectors.get(name);
|
|
3177
3750
|
const fresh = resolver ? await resolver() : currentConfig;
|
|
3178
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
|
+
});
|
|
3179
3759
|
try {
|
|
3180
|
-
await this.disconnect(name
|
|
3760
|
+
await this.disconnect(name, {
|
|
3761
|
+
preserveResourceSubscriptions: true,
|
|
3762
|
+
preserveLifecycleAuthority: true,
|
|
3763
|
+
});
|
|
3181
3764
|
} catch {
|
|
3182
3765
|
// entry may already be gone — connect() below is what matters
|
|
3183
3766
|
}
|
|
@@ -3190,11 +3773,32 @@ export class MCPClient extends EventEmitter {
|
|
|
3190
3773
|
{ serverName: name, config: reconnectConfig },
|
|
3191
3774
|
);
|
|
3192
3775
|
}
|
|
3193
|
-
await this.connect(
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
);
|
|
3776
|
+
await this.connect(name, reconnectConfig, {
|
|
3777
|
+
authRetryUsed: options.connectAuthRetryUsed === true,
|
|
3778
|
+
lifecycleAttempt,
|
|
3779
|
+
reconnect: true,
|
|
3780
|
+
});
|
|
3781
|
+
try {
|
|
3782
|
+
for (const uri of resourceSubscriptions) {
|
|
3783
|
+
if (!this.servers.get(name)?.resourceSubscriptions?.has(uri)) {
|
|
3784
|
+
await this.subscribeResource(name, uri);
|
|
3785
|
+
}
|
|
3786
|
+
}
|
|
3787
|
+
} catch {
|
|
3788
|
+
// A partially restored connection is unsafe: callers would believe
|
|
3789
|
+
// the reconnect recovered their live subscriptions while some were
|
|
3790
|
+
// already lost. Tear it down and let the original operation surface
|
|
3791
|
+
// its connection error.
|
|
3792
|
+
try {
|
|
3793
|
+
await this.disconnect(name, {
|
|
3794
|
+
preserveResourceSubscriptions: true,
|
|
3795
|
+
});
|
|
3796
|
+
} catch {
|
|
3797
|
+
// The outer reconnect result remains fail-closed even if cleanup
|
|
3798
|
+
// itself reports an error.
|
|
3799
|
+
}
|
|
3800
|
+
return false;
|
|
3801
|
+
}
|
|
3198
3802
|
this.emit("server-reconnected", { name, url: fresh.url || null });
|
|
3199
3803
|
return true;
|
|
3200
3804
|
} catch {
|
|
@@ -3269,9 +3873,30 @@ export class MCPClient extends EventEmitter {
|
|
|
3269
3873
|
async subscribeResource(serverName, uri) {
|
|
3270
3874
|
const entry = this.servers.get(serverName);
|
|
3271
3875
|
if (!entry) throw new Error(`Server "${serverName}" not found`);
|
|
3272
|
-
|
|
3876
|
+
if (typeof uri !== "string" || !uri.trim()) {
|
|
3877
|
+
throw new TypeError(
|
|
3878
|
+
"MCP resource subscription URI must be a non-empty string",
|
|
3879
|
+
);
|
|
3880
|
+
}
|
|
3881
|
+
const normalizedUri = uri;
|
|
3882
|
+
await this._sendRequest(serverName, "resources/subscribe", {
|
|
3883
|
+
uri: normalizedUri,
|
|
3884
|
+
});
|
|
3273
3885
|
entry.resourceSubscriptions ||= new Set();
|
|
3274
|
-
entry.resourceSubscriptions.add(
|
|
3886
|
+
entry.resourceSubscriptions.add(normalizedUri);
|
|
3887
|
+
let desired = this._resourceSubscriptions.get(serverName);
|
|
3888
|
+
if (!desired) {
|
|
3889
|
+
desired = new Set();
|
|
3890
|
+
this._resourceSubscriptions.set(serverName, desired);
|
|
3891
|
+
}
|
|
3892
|
+
desired.add(normalizedUri);
|
|
3893
|
+
if (entry._lifecycleAttempt) {
|
|
3894
|
+
this._lifecycleAuthority.setSubscription(
|
|
3895
|
+
entry._lifecycleAttempt,
|
|
3896
|
+
normalizedUri,
|
|
3897
|
+
true,
|
|
3898
|
+
);
|
|
3899
|
+
}
|
|
3275
3900
|
return true;
|
|
3276
3901
|
}
|
|
3277
3902
|
|
|
@@ -3279,8 +3904,26 @@ export class MCPClient extends EventEmitter {
|
|
|
3279
3904
|
async unsubscribeResource(serverName, uri) {
|
|
3280
3905
|
const entry = this.servers.get(serverName);
|
|
3281
3906
|
if (!entry) throw new Error(`Server "${serverName}" not found`);
|
|
3282
|
-
|
|
3283
|
-
|
|
3907
|
+
if (typeof uri !== "string" || !uri.trim()) {
|
|
3908
|
+
throw new TypeError(
|
|
3909
|
+
"MCP resource subscription URI must be a non-empty string",
|
|
3910
|
+
);
|
|
3911
|
+
}
|
|
3912
|
+
const normalizedUri = uri;
|
|
3913
|
+
await this._sendRequest(serverName, "resources/unsubscribe", {
|
|
3914
|
+
uri: normalizedUri,
|
|
3915
|
+
});
|
|
3916
|
+
entry.resourceSubscriptions?.delete(normalizedUri);
|
|
3917
|
+
const desired = this._resourceSubscriptions.get(serverName);
|
|
3918
|
+
desired?.delete(normalizedUri);
|
|
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
|
+
}
|
|
3284
3927
|
return true;
|
|
3285
3928
|
}
|
|
3286
3929
|
|
|
@@ -3413,6 +4056,7 @@ export class MCPClient extends EventEmitter {
|
|
|
3413
4056
|
method,
|
|
3414
4057
|
params,
|
|
3415
4058
|
});
|
|
4059
|
+
this._registerLifecycleRpc(entry, id, method);
|
|
3416
4060
|
|
|
3417
4061
|
let timeout = null;
|
|
3418
4062
|
let removeCallerAbort = null;
|
|
@@ -3431,6 +4075,20 @@ export class MCPClient extends EventEmitter {
|
|
|
3431
4075
|
if (settled) return false;
|
|
3432
4076
|
settled = true;
|
|
3433
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
|
+
}
|
|
3434
4092
|
settler(value);
|
|
3435
4093
|
return true;
|
|
3436
4094
|
};
|
|
@@ -3534,6 +4192,7 @@ export class MCPClient extends EventEmitter {
|
|
|
3534
4192
|
return new Promise((resolve, reject) => {
|
|
3535
4193
|
const id = this._nextId++;
|
|
3536
4194
|
const message = JSON.stringify({ jsonrpc: "2.0", id, method, params });
|
|
4195
|
+
this._registerLifecycleRpc(entry, id, method);
|
|
3537
4196
|
const longRunning = Boolean(entry.config?.longRunning);
|
|
3538
4197
|
const timeoutMs = Number.isFinite(entry.config?.requestTimeoutMs)
|
|
3539
4198
|
? entry.config.requestTimeoutMs
|
|
@@ -3555,6 +4214,20 @@ export class MCPClient extends EventEmitter {
|
|
|
3555
4214
|
if (settled) return false;
|
|
3556
4215
|
settled = true;
|
|
3557
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
|
+
}
|
|
3558
4231
|
settler(value);
|
|
3559
4232
|
return true;
|
|
3560
4233
|
};
|
|
@@ -3752,6 +4425,9 @@ export class MCPClient extends EventEmitter {
|
|
|
3752
4425
|
...(stream.controller
|
|
3753
4426
|
? { signal: stream.controller.signal }
|
|
3754
4427
|
: {}),
|
|
4428
|
+
...(entry._httpDispatcher
|
|
4429
|
+
? { dispatcher: entry._httpDispatcher }
|
|
4430
|
+
: {}),
|
|
3755
4431
|
}),
|
|
3756
4432
|
stream.controller?.signal,
|
|
3757
4433
|
);
|
|
@@ -3796,7 +4472,8 @@ export class MCPClient extends EventEmitter {
|
|
|
3796
4472
|
if (error) throw error;
|
|
3797
4473
|
},
|
|
3798
4474
|
parseJson: (text) => parseMcpJson(text, serverName, entry),
|
|
3799
|
-
onMessage: (message) =>
|
|
4475
|
+
onMessage: (message) =>
|
|
4476
|
+
this._handleMessage(serverName, message, entry),
|
|
3800
4477
|
});
|
|
3801
4478
|
} catch (error) {
|
|
3802
4479
|
if (stream.stopped) return;
|
|
@@ -3879,6 +4556,14 @@ export class MCPClient extends EventEmitter {
|
|
|
3879
4556
|
|
|
3880
4557
|
const id = this._nextId++;
|
|
3881
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
|
+
};
|
|
3882
4567
|
const headers = {
|
|
3883
4568
|
"Content-Type": "application/json",
|
|
3884
4569
|
Accept: "application/json, text/event-stream",
|
|
@@ -3997,6 +4682,9 @@ export class MCPClient extends EventEmitter {
|
|
|
3997
4682
|
headers,
|
|
3998
4683
|
body,
|
|
3999
4684
|
...(controller ? { signal: controller.signal } : {}),
|
|
4685
|
+
...(entry._httpDispatcher
|
|
4686
|
+
? { dispatcher: entry._httpDispatcher }
|
|
4687
|
+
: {}),
|
|
4000
4688
|
});
|
|
4001
4689
|
},
|
|
4002
4690
|
);
|
|
@@ -4080,7 +4768,7 @@ export class MCPClient extends EventEmitter {
|
|
|
4080
4768
|
response,
|
|
4081
4769
|
id,
|
|
4082
4770
|
cap,
|
|
4083
|
-
(message) => this._handleMessage(serverName, message),
|
|
4771
|
+
(message) => this._handleMessage(serverName, message, entry),
|
|
4084
4772
|
controller?.signal,
|
|
4085
4773
|
(text) => parseMcpJson(text, serverName, entry),
|
|
4086
4774
|
);
|
|
@@ -4113,9 +4801,18 @@ export class MCPClient extends EventEmitter {
|
|
|
4113
4801
|
throw mcpRpcError(wire.error);
|
|
4114
4802
|
}
|
|
4115
4803
|
throwIfAborted();
|
|
4804
|
+
if (!settleLifecycle("completed")) {
|
|
4805
|
+
throw this._staleLifecycleCallbackError();
|
|
4806
|
+
}
|
|
4116
4807
|
return wire.result;
|
|
4117
4808
|
} catch (err) {
|
|
4118
|
-
|
|
4809
|
+
try {
|
|
4810
|
+
throwIfAborted();
|
|
4811
|
+
} catch (abortError) {
|
|
4812
|
+
settleLifecycle("failed-closed");
|
|
4813
|
+
throw abortError;
|
|
4814
|
+
}
|
|
4815
|
+
settleLifecycle("failed-closed");
|
|
4119
4816
|
throw err;
|
|
4120
4817
|
} finally {
|
|
4121
4818
|
if (timer) clearTimeout(timer);
|
|
@@ -4156,8 +4853,9 @@ export class MCPClient extends EventEmitter {
|
|
|
4156
4853
|
});
|
|
4157
4854
|
}
|
|
4158
4855
|
|
|
4159
|
-
_handleData(serverName, data) {
|
|
4160
|
-
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;
|
|
4161
4859
|
if (!entry || entry._stdioFrameError) return;
|
|
4162
4860
|
|
|
4163
4861
|
const cap = stdioFrameByteLimit(entry.config?.maxBufferChars);
|
|
@@ -4216,7 +4914,7 @@ export class MCPClient extends EventEmitter {
|
|
|
4216
4914
|
}
|
|
4217
4915
|
continue;
|
|
4218
4916
|
}
|
|
4219
|
-
this._handleMessage(serverName, msg);
|
|
4917
|
+
this._handleMessage(serverName, msg, entry);
|
|
4220
4918
|
}
|
|
4221
4919
|
}
|
|
4222
4920
|
|
|
@@ -4312,6 +5010,7 @@ export class MCPClient extends EventEmitter {
|
|
|
4312
5010
|
entry._buffer = "";
|
|
4313
5011
|
entry._bufferBytes = 0;
|
|
4314
5012
|
entry.state = ServerState.ERROR;
|
|
5013
|
+
this._failLifecycle(entry, error);
|
|
4315
5014
|
if (entry._pending instanceof Map) {
|
|
4316
5015
|
for (const pending of entry._pending.values()) {
|
|
4317
5016
|
if (pending.timeout) clearTimeout(pending.timeout);
|
|
@@ -4345,12 +5044,21 @@ export class MCPClient extends EventEmitter {
|
|
|
4345
5044
|
return error;
|
|
4346
5045
|
}
|
|
4347
5046
|
|
|
4348
|
-
_handleMessage(serverName, msg) {
|
|
4349
|
-
const
|
|
5047
|
+
_handleMessage(serverName, msg, sourceEntry = null) {
|
|
5048
|
+
const currentEntry = this.servers.get(serverName);
|
|
5049
|
+
const entry = sourceEntry || currentEntry;
|
|
4350
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
|
+
}
|
|
4351
5058
|
|
|
4352
5059
|
const failMessage = (error) => {
|
|
4353
5060
|
entry.state = ServerState.ERROR;
|
|
5061
|
+
this._failLifecycle(entry, error);
|
|
4354
5062
|
if (entry._pending instanceof Map) {
|
|
4355
5063
|
for (const pending of entry._pending.values()) {
|
|
4356
5064
|
clearTimeout(pending.timeout);
|
|
@@ -4419,6 +5127,13 @@ export class MCPClient extends EventEmitter {
|
|
|
4419
5127
|
// one of ours). Previously these fell into the notification branch and
|
|
4420
5128
|
// never got a response, so a server calling e.g. roots/list hung until its
|
|
4421
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
|
+
|
|
4422
5137
|
if (wire.hasId && typeof wire.method === "string" && wire.method) {
|
|
4423
5138
|
if (
|
|
4424
5139
|
wire.hasParams &&
|
|
@@ -4451,10 +5166,35 @@ export class MCPClient extends EventEmitter {
|
|
|
4451
5166
|
// tool-search's prompt-cache stability depends on an append-only,
|
|
4452
5167
|
// stable-prefix tool list.)
|
|
4453
5168
|
if (wire.method === "notifications/tools/list_changed") {
|
|
5169
|
+
if (entry._discoveryCacheKey) {
|
|
5170
|
+
this._discoveryCache.delete(entry._discoveryCacheKey);
|
|
5171
|
+
}
|
|
4454
5172
|
this._refreshServerList(serverName, "tools");
|
|
4455
5173
|
} else if (wire.method === "notifications/resources/list_changed") {
|
|
5174
|
+
if (entry._discoveryCacheKey) {
|
|
5175
|
+
this._discoveryCache.delete(entry._discoveryCacheKey);
|
|
5176
|
+
}
|
|
4456
5177
|
this._refreshServerList(serverName, "resources");
|
|
4457
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
|
+
});
|
|
4458
5198
|
} else if (wire.method === "notifications/resources/updated") {
|
|
4459
5199
|
this.emit("resource-updated", {
|
|
4460
5200
|
server: serverName,
|
|
@@ -4729,6 +5469,7 @@ async function _fetchAndDiscardHttpResponse(entry, options) {
|
|
|
4729
5469
|
const response = await _deps.fetch(entry.httpUrl, {
|
|
4730
5470
|
...options,
|
|
4731
5471
|
...(controller ? { signal: controller.signal } : {}),
|
|
5472
|
+
...(entry._httpDispatcher ? { dispatcher: entry._httpDispatcher } : {}),
|
|
4732
5473
|
});
|
|
4733
5474
|
_cancelHttpResponseBody(response);
|
|
4734
5475
|
if (timedOut) {
|