@animalabs/agent-framework 0.22.2 → 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +108 -4
- package/dist/src/agent.d.ts +108 -17
- package/dist/src/agent.d.ts.map +1 -1
- package/dist/src/agent.js +207 -67
- package/dist/src/agent.js.map +1 -1
- package/dist/src/api/mcp-server-core.js +1 -1
- package/dist/src/api/mcp-server-core.js.map +1 -1
- package/dist/src/api/server.d.ts.map +1 -1
- package/dist/src/api/server.js +5 -1
- package/dist/src/api/server.js.map +1 -1
- package/dist/src/api/types.d.ts +1 -1
- package/dist/src/api/types.d.ts.map +1 -1
- package/dist/src/branch-names.d.ts +14 -0
- package/dist/src/branch-names.d.ts.map +1 -0
- package/dist/src/branch-names.js +22 -0
- package/dist/src/branch-names.js.map +1 -0
- package/dist/src/closest-names.d.ts +28 -0
- package/dist/src/closest-names.d.ts.map +1 -0
- package/dist/src/closest-names.js +98 -0
- package/dist/src/closest-names.js.map +1 -0
- package/dist/src/code-execution/index.d.ts +1 -1
- package/dist/src/code-execution/index.d.ts.map +1 -1
- package/dist/src/code-execution/index.js.map +1 -1
- package/dist/src/code-execution/py-runner.d.ts +50 -7
- package/dist/src/code-execution/py-runner.d.ts.map +1 -1
- package/dist/src/code-execution/py-runner.js +258 -90
- package/dist/src/code-execution/py-runner.js.map +1 -1
- package/dist/src/code-execution/runtime-py.d.ts +3 -2
- package/dist/src/code-execution/runtime-py.d.ts.map +1 -1
- package/dist/src/code-execution/runtime-py.js +94 -8
- package/dist/src/code-execution/runtime-py.js.map +1 -1
- package/dist/src/code-execution/tool-definition.d.ts +17 -1
- package/dist/src/code-execution/tool-definition.d.ts.map +1 -1
- package/dist/src/code-execution/tool-definition.js +72 -6
- package/dist/src/code-execution/tool-definition.js.map +1 -1
- package/dist/src/config-checks.d.ts +28 -0
- package/dist/src/config-checks.d.ts.map +1 -0
- package/dist/src/config-checks.js +56 -0
- package/dist/src/config-checks.js.map +1 -0
- package/dist/src/deadline-timer.d.ts +21 -0
- package/dist/src/deadline-timer.d.ts.map +1 -0
- package/dist/src/deadline-timer.js +43 -0
- package/dist/src/deadline-timer.js.map +1 -0
- package/dist/src/error-message.d.ts +30 -0
- package/dist/src/error-message.d.ts.map +1 -0
- package/dist/src/error-message.js +50 -0
- package/dist/src/error-message.js.map +1 -0
- package/dist/src/framework.d.ts +1529 -165
- package/dist/src/framework.d.ts.map +1 -1
- package/dist/src/framework.js +8264 -1939
- package/dist/src/framework.js.map +1 -1
- package/dist/src/gate/event-gate.d.ts +8 -0
- package/dist/src/gate/event-gate.d.ts.map +1 -1
- package/dist/src/gate/event-gate.js +36 -5
- package/dist/src/gate/event-gate.js.map +1 -1
- package/dist/src/held-writes.d.ts +16 -0
- package/dist/src/held-writes.d.ts.map +1 -0
- package/dist/src/held-writes.js +56 -0
- package/dist/src/held-writes.js.map +1 -0
- package/dist/src/index.d.ts +10 -4
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +11 -5
- package/dist/src/index.js.map +1 -1
- package/dist/src/mcpl/capability-grant.d.ts +4 -0
- package/dist/src/mcpl/capability-grant.d.ts.map +1 -1
- package/dist/src/mcpl/capability-grant.js +1 -1
- package/dist/src/mcpl/capability-grant.js.map +1 -1
- package/dist/src/mcpl/channel-registry.d.ts +233 -18
- package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
- package/dist/src/mcpl/channel-registry.js +975 -386
- package/dist/src/mcpl/channel-registry.js.map +1 -1
- package/dist/src/mcpl/checkpoint-manager.d.ts +21 -1
- package/dist/src/mcpl/checkpoint-manager.d.ts.map +1 -1
- package/dist/src/mcpl/checkpoint-manager.js +91 -30
- package/dist/src/mcpl/checkpoint-manager.js.map +1 -1
- package/dist/src/mcpl/conversation-router.d.ts +19 -3
- package/dist/src/mcpl/conversation-router.d.ts.map +1 -1
- package/dist/src/mcpl/conversation-router.js +23 -8
- package/dist/src/mcpl/conversation-router.js.map +1 -1
- package/dist/src/mcpl/convert-block.d.ts +20 -0
- package/dist/src/mcpl/convert-block.d.ts.map +1 -0
- package/dist/src/mcpl/convert-block.js +88 -0
- package/dist/src/mcpl/convert-block.js.map +1 -0
- package/dist/src/mcpl/errors.d.ts +13 -0
- package/dist/src/mcpl/errors.d.ts.map +1 -1
- package/dist/src/mcpl/errors.js +28 -0
- package/dist/src/mcpl/errors.js.map +1 -1
- package/dist/src/mcpl/hook-orchestrator.d.ts +23 -0
- package/dist/src/mcpl/hook-orchestrator.d.ts.map +1 -1
- package/dist/src/mcpl/hook-orchestrator.js +83 -72
- package/dist/src/mcpl/hook-orchestrator.js.map +1 -1
- package/dist/src/mcpl/inbound-source.d.ts +114 -0
- package/dist/src/mcpl/inbound-source.d.ts.map +1 -0
- package/dist/src/mcpl/inbound-source.js +83 -0
- package/dist/src/mcpl/inbound-source.js.map +1 -0
- package/dist/src/mcpl/index.d.ts +7 -1
- package/dist/src/mcpl/index.d.ts.map +1 -1
- package/dist/src/mcpl/index.js +5 -1
- package/dist/src/mcpl/index.js.map +1 -1
- package/dist/src/mcpl/inference-router.d.ts +19 -13
- package/dist/src/mcpl/inference-router.d.ts.map +1 -1
- package/dist/src/mcpl/inference-router.js +155 -87
- package/dist/src/mcpl/inference-router.js.map +1 -1
- package/dist/src/mcpl/log-value.d.ts +7 -0
- package/dist/src/mcpl/log-value.d.ts.map +1 -1
- package/dist/src/mcpl/log-value.js +9 -0
- package/dist/src/mcpl/log-value.js.map +1 -1
- package/dist/src/mcpl/modern-connection.d.ts +227 -0
- package/dist/src/mcpl/modern-connection.d.ts.map +1 -0
- package/dist/src/mcpl/modern-connection.js +1088 -0
- package/dist/src/mcpl/modern-connection.js.map +1 -0
- package/dist/src/mcpl/prefix-rule.d.ts +18 -0
- package/dist/src/mcpl/prefix-rule.d.ts.map +1 -0
- package/dist/src/mcpl/prefix-rule.js +25 -0
- package/dist/src/mcpl/prefix-rule.js.map +1 -0
- package/dist/src/mcpl/protocol-family.d.ts +179 -0
- package/dist/src/mcpl/protocol-family.d.ts.map +1 -0
- package/dist/src/mcpl/protocol-family.js +697 -0
- package/dist/src/mcpl/protocol-family.js.map +1 -0
- package/dist/src/mcpl/push-coalescer.d.ts +58 -17
- package/dist/src/mcpl/push-coalescer.d.ts.map +1 -1
- package/dist/src/mcpl/push-coalescer.js +93 -32
- package/dist/src/mcpl/push-coalescer.js.map +1 -1
- package/dist/src/mcpl/push-handler.d.ts +16 -9
- package/dist/src/mcpl/push-handler.d.ts.map +1 -1
- package/dist/src/mcpl/push-handler.js +18 -83
- package/dist/src/mcpl/push-handler.js.map +1 -1
- package/dist/src/mcpl/reference-fetcher.d.ts +76 -13
- package/dist/src/mcpl/reference-fetcher.d.ts.map +1 -1
- package/dist/src/mcpl/reference-fetcher.js +275 -66
- package/dist/src/mcpl/reference-fetcher.js.map +1 -1
- package/dist/src/mcpl/references.d.ts +98 -18
- package/dist/src/mcpl/references.d.ts.map +1 -1
- package/dist/src/mcpl/references.js +170 -32
- package/dist/src/mcpl/references.js.map +1 -1
- package/dist/src/mcpl/server-connection.d.ts +406 -31
- package/dist/src/mcpl/server-connection.d.ts.map +1 -1
- package/dist/src/mcpl/server-connection.js +1331 -272
- package/dist/src/mcpl/server-connection.js.map +1 -1
- package/dist/src/mcpl/server-registry.d.ts +35 -6
- package/dist/src/mcpl/server-registry.d.ts.map +1 -1
- package/dist/src/mcpl/server-registry.js +63 -13
- package/dist/src/mcpl/server-registry.js.map +1 -1
- package/dist/src/mcpl/skip-reply-wake.d.ts +36 -0
- package/dist/src/mcpl/skip-reply-wake.d.ts.map +1 -0
- package/dist/src/mcpl/skip-reply-wake.js +55 -0
- package/dist/src/mcpl/skip-reply-wake.js.map +1 -0
- package/dist/src/mcpl/tool-result-normalize.d.ts +54 -0
- package/dist/src/mcpl/tool-result-normalize.d.ts.map +1 -0
- package/dist/src/mcpl/tool-result-normalize.js +232 -0
- package/dist/src/mcpl/tool-result-normalize.js.map +1 -0
- package/dist/src/mcpl/transport.d.ts +104 -12
- package/dist/src/mcpl/transport.d.ts.map +1 -1
- package/dist/src/mcpl/transport.js +315 -85
- package/dist/src/mcpl/transport.js.map +1 -1
- package/dist/src/mcpl/types.d.ts +76 -19
- package/dist/src/mcpl/types.d.ts.map +1 -1
- package/dist/src/mcpl/types.js.map +1 -1
- package/dist/src/mcpl/visible-content.d.ts +16 -0
- package/dist/src/mcpl/visible-content.d.ts.map +1 -1
- package/dist/src/mcpl/visible-content.js +26 -0
- package/dist/src/mcpl/visible-content.js.map +1 -1
- package/dist/src/module-registry.d.ts +34 -6
- package/dist/src/module-registry.d.ts.map +1 -1
- package/dist/src/module-registry.js +119 -31
- package/dist/src/module-registry.js.map +1 -1
- package/dist/src/modules/api/index.d.ts +1 -1
- package/dist/src/modules/api/index.d.ts.map +1 -1
- package/dist/src/modules/api/index.js +3 -2
- package/dist/src/modules/api/index.js.map +1 -1
- package/dist/src/modules/discord/discord-js-client.d.ts +43 -2
- package/dist/src/modules/discord/discord-js-client.d.ts.map +1 -1
- package/dist/src/modules/discord/discord-js-client.js +286 -67
- package/dist/src/modules/discord/discord-js-client.js.map +1 -1
- package/dist/src/modules/discord/index.d.ts +53 -5
- package/dist/src/modules/discord/index.d.ts.map +1 -1
- package/dist/src/modules/discord/index.js +712 -186
- package/dist/src/modules/discord/index.js.map +1 -1
- package/dist/src/modules/discord/refusal.d.ts +14 -0
- package/dist/src/modules/discord/refusal.d.ts.map +1 -0
- package/dist/src/modules/discord/refusal.js +14 -0
- package/dist/src/modules/discord/refusal.js.map +1 -0
- package/dist/src/modules/discord/types.d.ts +27 -4
- package/dist/src/modules/discord/types.d.ts.map +1 -1
- package/dist/src/modules/health/index.d.ts.map +1 -1
- package/dist/src/modules/health/index.js +8 -3
- package/dist/src/modules/health/index.js.map +1 -1
- package/dist/src/modules/history/index.d.ts +3 -1
- package/dist/src/modules/history/index.d.ts.map +1 -1
- package/dist/src/modules/history/index.js +359 -127
- package/dist/src/modules/history/index.js.map +1 -1
- package/dist/src/modules/history/semantic.d.ts +28 -1
- package/dist/src/modules/history/semantic.d.ts.map +1 -1
- package/dist/src/modules/history/semantic.js +177 -32
- package/dist/src/modules/history/semantic.js.map +1 -1
- package/dist/src/modules/index.d.ts +1 -1
- package/dist/src/modules/index.d.ts.map +1 -1
- package/dist/src/modules/index.js +1 -1
- package/dist/src/modules/index.js.map +1 -1
- package/dist/src/modules/unknown-tool.d.ts +10 -0
- package/dist/src/modules/unknown-tool.d.ts.map +1 -0
- package/dist/src/modules/unknown-tool.js +17 -0
- package/dist/src/modules/unknown-tool.js.map +1 -0
- package/dist/src/modules/workspace/disk-agreement.d.ts +57 -4
- package/dist/src/modules/workspace/disk-agreement.d.ts.map +1 -1
- package/dist/src/modules/workspace/disk-agreement.js +75 -7
- package/dist/src/modules/workspace/disk-agreement.js.map +1 -1
- package/dist/src/modules/workspace/effects.d.ts +66 -7
- package/dist/src/modules/workspace/effects.d.ts.map +1 -1
- package/dist/src/modules/workspace/effects.js +303 -46
- package/dist/src/modules/workspace/effects.js.map +1 -1
- package/dist/src/modules/workspace/index.d.ts +313 -45
- package/dist/src/modules/workspace/index.d.ts.map +1 -1
- package/dist/src/modules/workspace/index.js +1404 -304
- package/dist/src/modules/workspace/index.js.map +1 -1
- package/dist/src/modules/workspace/observe.d.ts +315 -16
- package/dist/src/modules/workspace/observe.d.ts.map +1 -1
- package/dist/src/modules/workspace/observe.js +727 -37
- package/dist/src/modules/workspace/observe.js.map +1 -1
- package/dist/src/modules/workspace/reconcile.d.ts +58 -7
- package/dist/src/modules/workspace/reconcile.d.ts.map +1 -1
- package/dist/src/modules/workspace/reconcile.js +521 -56
- package/dist/src/modules/workspace/reconcile.js.map +1 -1
- package/dist/src/modules/workspace/types.d.ts +28 -5
- package/dist/src/modules/workspace/types.d.ts.map +1 -1
- package/dist/src/modules/workspace/types.js.map +1 -1
- package/dist/src/modules/workspace/watcher.d.ts +49 -2
- package/dist/src/modules/workspace/watcher.d.ts.map +1 -1
- package/dist/src/modules/workspace/watcher.js +158 -29
- package/dist/src/modules/workspace/watcher.js.map +1 -1
- package/dist/src/prose-drafts.d.ts +240 -0
- package/dist/src/prose-drafts.d.ts.map +1 -0
- package/dist/src/prose-drafts.js +362 -0
- package/dist/src/prose-drafts.js.map +1 -0
- package/dist/src/prose-segments.d.ts +21 -2
- package/dist/src/prose-segments.d.ts.map +1 -1
- package/dist/src/prose-segments.js +46 -9
- package/dist/src/prose-segments.js.map +1 -1
- package/dist/src/recovery/offline-branch.d.ts +1 -0
- package/dist/src/recovery/offline-branch.d.ts.map +1 -1
- package/dist/src/recovery/offline-branch.js +3 -1
- package/dist/src/recovery/offline-branch.js.map +1 -1
- package/dist/src/safe-slice.d.ts +5 -1
- package/dist/src/safe-slice.d.ts.map +1 -1
- package/dist/src/safe-slice.js +9 -1
- package/dist/src/safe-slice.js.map +1 -1
- package/dist/src/silent-heartbeat.d.ts +112 -39
- package/dist/src/silent-heartbeat.d.ts.map +1 -1
- package/dist/src/silent-heartbeat.js +100 -29
- package/dist/src/silent-heartbeat.js.map +1 -1
- package/dist/src/tool-args.d.ts +40 -0
- package/dist/src/tool-args.d.ts.map +1 -0
- package/dist/src/tool-args.js +207 -0
- package/dist/src/tool-args.js.map +1 -0
- package/dist/src/tool-image-ledger.d.ts +87 -19
- package/dist/src/tool-image-ledger.d.ts.map +1 -1
- package/dist/src/tool-image-ledger.js +135 -32
- package/dist/src/tool-image-ledger.js.map +1 -1
- package/dist/src/tool-presentation.d.ts +9 -0
- package/dist/src/tool-presentation.d.ts.map +1 -1
- package/dist/src/tool-presentation.js +266 -62
- package/dist/src/tool-presentation.js.map +1 -1
- package/dist/src/tool-result-guard.d.ts.map +1 -1
- package/dist/src/tool-result-guard.js +7 -10
- package/dist/src/tool-result-guard.js.map +1 -1
- package/dist/src/tool-result-history.d.ts +32 -3
- package/dist/src/tool-result-history.d.ts.map +1 -1
- package/dist/src/tool-result-history.js +106 -19
- package/dist/src/tool-result-history.js.map +1 -1
- package/dist/src/tune-out/coordinator.d.ts +25 -1
- package/dist/src/tune-out/coordinator.d.ts.map +1 -1
- package/dist/src/tune-out/coordinator.js +260 -65
- package/dist/src/tune-out/coordinator.js.map +1 -1
- package/dist/src/types/agent.d.ts +44 -22
- package/dist/src/types/agent.d.ts.map +1 -1
- package/dist/src/types/events.d.ts +25 -0
- package/dist/src/types/events.d.ts.map +1 -1
- package/dist/src/types/framework.d.ts +38 -7
- package/dist/src/types/framework.d.ts.map +1 -1
- package/dist/src/types/inference-log.d.ts +1 -1
- package/dist/src/types/inference-log.d.ts.map +1 -1
- package/dist/src/types/module.d.ts +77 -14
- package/dist/src/types/module.d.ts.map +1 -1
- package/dist/src/types/trace.d.ts +97 -8
- package/dist/src/types/trace.d.ts.map +1 -1
- package/dist/src/usage/usage-tracker.d.ts +5 -0
- package/dist/src/usage/usage-tracker.d.ts.map +1 -1
- package/dist/src/usage/usage-tracker.js +35 -8
- package/dist/src/usage/usage-tracker.js.map +1 -1
- package/package.json +5 -4
- package/src/agent.ts +257 -72
- package/src/api/mcp-server-core.ts +1 -1
- package/src/api/server.ts +4 -1
- package/src/api/types.ts +1 -1
- package/src/branch-names.ts +19 -0
- package/src/closest-names.ts +94 -0
- package/src/code-execution/index.ts +1 -0
- package/src/code-execution/py-runner.ts +304 -93
- package/src/code-execution/runtime-py.ts +94 -8
- package/src/code-execution/tool-definition.ts +85 -7
- package/src/config-checks.ts +50 -0
- package/src/deadline-timer.ts +44 -0
- package/src/error-message.ts +48 -0
- package/src/framework.ts +8625 -1984
- package/src/gate/event-gate.ts +38 -5
- package/src/held-writes.ts +59 -0
- package/src/index.ts +29 -5
- package/src/mcpl/capability-grant.ts +1 -1
- package/src/mcpl/channel-registry.ts +1095 -411
- package/src/mcpl/checkpoint-manager.ts +104 -29
- package/src/mcpl/conversation-router.ts +28 -8
- package/src/mcpl/convert-block.ts +105 -0
- package/src/mcpl/errors.ts +28 -0
- package/src/mcpl/hook-orchestrator.ts +84 -77
- package/src/mcpl/inbound-source.ts +156 -0
- package/src/mcpl/index.ts +23 -1
- package/src/mcpl/inference-router.ts +179 -103
- package/src/mcpl/log-value.ts +10 -0
- package/src/mcpl/modern-connection.ts +1182 -0
- package/src/mcpl/prefix-rule.ts +32 -0
- package/src/mcpl/protocol-family.ts +691 -0
- package/src/mcpl/push-coalescer.ts +110 -36
- package/src/mcpl/push-handler.ts +20 -90
- package/src/mcpl/reference-fetcher.ts +318 -67
- package/src/mcpl/references.ts +183 -34
- package/src/mcpl/server-connection.ts +1404 -280
- package/src/mcpl/server-registry.ts +63 -14
- package/src/mcpl/skip-reply-wake.ts +73 -0
- package/src/mcpl/tool-result-normalize.ts +294 -0
- package/src/mcpl/transport.ts +320 -76
- package/src/mcpl/types.ts +77 -19
- package/src/mcpl/visible-content.ts +28 -0
- package/src/module-registry.ts +126 -36
- package/src/modules/api/index.ts +3 -2
- package/src/modules/discord/discord-js-client.ts +306 -74
- package/src/modules/discord/index.ts +737 -201
- package/src/modules/discord/refusal.ts +13 -0
- package/src/modules/discord/types.ts +27 -6
- package/src/modules/health/index.ts +8 -3
- package/src/modules/history/index.ts +410 -120
- package/src/modules/history/semantic.ts +189 -32
- package/src/modules/index.ts +1 -1
- package/src/modules/unknown-tool.ts +16 -0
- package/src/modules/workspace/disk-agreement.ts +97 -8
- package/src/modules/workspace/effects.ts +291 -36
- package/src/modules/workspace/index.ts +1469 -321
- package/src/modules/workspace/observe.ts +784 -41
- package/src/modules/workspace/reconcile.ts +619 -64
- package/src/modules/workspace/types.ts +28 -5
- package/src/modules/workspace/watcher.ts +154 -30
- package/src/prose-drafts.ts +481 -0
- package/src/prose-segments.ts +51 -8
- package/src/recovery/offline-branch.ts +4 -1
- package/src/safe-slice.ts +9 -1
- package/src/silent-heartbeat.ts +184 -59
- package/src/tool-args.ts +213 -0
- package/src/tool-image-ledger.ts +153 -34
- package/src/tool-presentation.ts +233 -42
- package/src/tool-result-guard.ts +7 -8
- package/src/tool-result-history.ts +124 -20
- package/src/tune-out/coordinator.ts +259 -65
- package/src/types/agent.ts +44 -24
- package/src/types/events.ts +25 -0
- package/src/types/framework.ts +38 -7
- package/src/types/inference-log.ts +1 -1
- package/src/types/module.ts +79 -14
- package/src/types/trace.ts +103 -8
- package/src/usage/usage-tracker.ts +32 -8
- package/dist/test/activation-request-sanitize.test.d.ts +0 -2
- package/dist/test/activation-request-sanitize.test.d.ts.map +0 -1
- package/dist/test/activation-request-sanitize.test.js +0 -89
- package/dist/test/activation-request-sanitize.test.js.map +0 -1
- package/dist/test/agent-cache-ttl.test.d.ts +0 -2
- package/dist/test/agent-cache-ttl.test.d.ts.map +0 -1
- package/dist/test/agent-cache-ttl.test.js +0 -41
- package/dist/test/agent-cache-ttl.test.js.map +0 -1
- package/dist/test/agent-runtime-settings.test.d.ts +0 -2
- package/dist/test/agent-runtime-settings.test.d.ts.map +0 -1
- package/dist/test/agent-runtime-settings.test.js +0 -1598
- package/dist/test/agent-runtime-settings.test.js.map +0 -1
- package/dist/test/api-admin-gate.test.d.ts +0 -2
- package/dist/test/api-admin-gate.test.d.ts.map +0 -1
- package/dist/test/api-admin-gate.test.js +0 -116
- package/dist/test/api-admin-gate.test.js.map +0 -1
- package/dist/test/assistant-persist-refusal.test.d.ts +0 -2
- package/dist/test/assistant-persist-refusal.test.d.ts.map +0 -1
- package/dist/test/assistant-persist-refusal.test.js +0 -63
- package/dist/test/assistant-persist-refusal.test.js.map +0 -1
- package/dist/test/batched-wake-locus.test.d.ts +0 -2
- package/dist/test/batched-wake-locus.test.d.ts.map +0 -1
- package/dist/test/batched-wake-locus.test.js +0 -166
- package/dist/test/batched-wake-locus.test.js.map +0 -1
- package/dist/test/busy-agent-scheduler.test.d.ts +0 -2
- package/dist/test/busy-agent-scheduler.test.d.ts.map +0 -1
- package/dist/test/busy-agent-scheduler.test.js +0 -92
- package/dist/test/busy-agent-scheduler.test.js.map +0 -1
- package/dist/test/channel-incoming-targeting.test.d.ts +0 -2
- package/dist/test/channel-incoming-targeting.test.d.ts.map +0 -1
- package/dist/test/channel-incoming-targeting.test.js +0 -81
- package/dist/test/channel-incoming-targeting.test.js.map +0 -1
- package/dist/test/channel-label-history.test.d.ts +0 -2
- package/dist/test/channel-label-history.test.d.ts.map +0 -1
- package/dist/test/channel-label-history.test.js +0 -424
- package/dist/test/channel-label-history.test.js.map +0 -1
- package/dist/test/channel-lifecycle-provenance.test.d.ts +0 -2
- package/dist/test/channel-lifecycle-provenance.test.d.ts.map +0 -1
- package/dist/test/channel-lifecycle-provenance.test.js +0 -47
- package/dist/test/channel-lifecycle-provenance.test.js.map +0 -1
- package/dist/test/channel-registry-routing.test.d.ts +0 -2
- package/dist/test/channel-registry-routing.test.d.ts.map +0 -1
- package/dist/test/channel-registry-routing.test.js +0 -446
- package/dist/test/channel-registry-routing.test.js.map +0 -1
- package/dist/test/channel-registry-typing.test.d.ts +0 -2
- package/dist/test/channel-registry-typing.test.d.ts.map +0 -1
- package/dist/test/channel-registry-typing.test.js +0 -91
- package/dist/test/channel-registry-typing.test.js.map +0 -1
- package/dist/test/channels-changed-respond-first.test.d.ts +0 -2
- package/dist/test/channels-changed-respond-first.test.d.ts.map +0 -1
- package/dist/test/channels-changed-respond-first.test.js +0 -74
- package/dist/test/channels-changed-respond-first.test.js.map +0 -1
- package/dist/test/checkpoint-attribution.test.d.ts +0 -2
- package/dist/test/checkpoint-attribution.test.d.ts.map +0 -1
- package/dist/test/checkpoint-attribution.test.js +0 -200
- package/dist/test/checkpoint-attribution.test.js.map +0 -1
- package/dist/test/code-execution.test.d.ts +0 -11
- package/dist/test/code-execution.test.d.ts.map +0 -1
- package/dist/test/code-execution.test.js +0 -1272
- package/dist/test/code-execution.test.js.map +0 -1
- package/dist/test/component-defaults.test.d.ts +0 -2
- package/dist/test/component-defaults.test.d.ts.map +0 -1
- package/dist/test/component-defaults.test.js +0 -54
- package/dist/test/component-defaults.test.js.map +0 -1
- package/dist/test/conversation-fork-injection-scoping.test.d.ts +0 -2
- package/dist/test/conversation-fork-injection-scoping.test.d.ts.map +0 -1
- package/dist/test/conversation-fork-injection-scoping.test.js +0 -200
- package/dist/test/conversation-fork-injection-scoping.test.js.map +0 -1
- package/dist/test/conversation-router.test.d.ts +0 -2
- package/dist/test/conversation-router.test.d.ts.map +0 -1
- package/dist/test/conversation-router.test.js +0 -174
- package/dist/test/conversation-router.test.js.map +0 -1
- package/dist/test/conversation-routing.test.d.ts +0 -7
- package/dist/test/conversation-routing.test.d.ts.map +0 -1
- package/dist/test/conversation-routing.test.js +0 -621
- package/dist/test/conversation-routing.test.js.map +0 -1
- package/dist/test/deferred-wake-integrity.test.d.ts +0 -29
- package/dist/test/deferred-wake-integrity.test.d.ts.map +0 -1
- package/dist/test/deferred-wake-integrity.test.js +0 -320
- package/dist/test/deferred-wake-integrity.test.js.map +0 -1
- package/dist/test/discord-awareness-outbox.test.d.ts +0 -2
- package/dist/test/discord-awareness-outbox.test.d.ts.map +0 -1
- package/dist/test/discord-awareness-outbox.test.js +0 -371
- package/dist/test/discord-awareness-outbox.test.js.map +0 -1
- package/dist/test/discord-history-delete-window.test.d.ts +0 -2
- package/dist/test/discord-history-delete-window.test.d.ts.map +0 -1
- package/dist/test/discord-history-delete-window.test.js +0 -81
- package/dist/test/discord-history-delete-window.test.js.map +0 -1
- package/dist/test/discord-history-sync.test.d.ts +0 -2
- package/dist/test/discord-history-sync.test.d.ts.map +0 -1
- package/dist/test/discord-history-sync.test.js +0 -56
- package/dist/test/discord-history-sync.test.js.map +0 -1
- package/dist/test/discord-typing-keepalive.test.d.ts +0 -2
- package/dist/test/discord-typing-keepalive.test.d.ts.map +0 -1
- package/dist/test/discord-typing-keepalive.test.js +0 -77
- package/dist/test/discord-typing-keepalive.test.js.map +0 -1
- package/dist/test/ephemeral-settle.test.d.ts +0 -2
- package/dist/test/ephemeral-settle.test.d.ts.map +0 -1
- package/dist/test/ephemeral-settle.test.js +0 -354
- package/dist/test/ephemeral-settle.test.js.map +0 -1
- package/dist/test/event-gate-passthrough.test.d.ts +0 -15
- package/dist/test/event-gate-passthrough.test.d.ts.map +0 -1
- package/dist/test/event-gate-passthrough.test.js +0 -289
- package/dist/test/event-gate-passthrough.test.js.map +0 -1
- package/dist/test/event-gate-policy-mutation.test.d.ts +0 -2
- package/dist/test/event-gate-policy-mutation.test.d.ts.map +0 -1
- package/dist/test/event-gate-policy-mutation.test.js +0 -137
- package/dist/test/event-gate-policy-mutation.test.js.map +0 -1
- package/dist/test/event-gate-rate-limit.test.d.ts +0 -2
- package/dist/test/event-gate-rate-limit.test.d.ts.map +0 -1
- package/dist/test/event-gate-rate-limit.test.js +0 -627
- package/dist/test/event-gate-rate-limit.test.js.map +0 -1
- package/dist/test/event-gate-script.test.d.ts +0 -2
- package/dist/test/event-gate-script.test.d.ts.map +0 -1
- package/dist/test/event-gate-script.test.js +0 -95
- package/dist/test/event-gate-script.test.js.map +0 -1
- package/dist/test/event-gate-selfwake.test.d.ts +0 -15
- package/dist/test/event-gate-selfwake.test.d.ts.map +0 -1
- package/dist/test/event-gate-selfwake.test.js +0 -125
- package/dist/test/event-gate-selfwake.test.js.map +0 -1
- package/dist/test/event-gate-tags.test.d.ts +0 -2
- package/dist/test/event-gate-tags.test.d.ts.map +0 -1
- package/dist/test/event-gate-tags.test.js +0 -76
- package/dist/test/event-gate-tags.test.js.map +0 -1
- package/dist/test/event-gate.test.d.ts +0 -2
- package/dist/test/event-gate.test.d.ts.map +0 -1
- package/dist/test/event-gate.test.js +0 -919
- package/dist/test/event-gate.test.js.map +0 -1
- package/dist/test/explicit-prose-routing.test.d.ts +0 -11
- package/dist/test/explicit-prose-routing.test.d.ts.map +0 -1
- package/dist/test/explicit-prose-routing.test.js +0 -1715
- package/dist/test/explicit-prose-routing.test.js.map +0 -1
- package/dist/test/framework.test.d.ts +0 -5
- package/dist/test/framework.test.d.ts.map +0 -1
- package/dist/test/framework.test.js +0 -1048
- package/dist/test/framework.test.js.map +0 -1
- package/dist/test/gather-context-timeout.test.d.ts +0 -2
- package/dist/test/gather-context-timeout.test.d.ts.map +0 -1
- package/dist/test/gather-context-timeout.test.js +0 -120
- package/dist/test/gather-context-timeout.test.js.map +0 -1
- package/dist/test/health-snapshot.test.d.ts +0 -2
- package/dist/test/health-snapshot.test.d.ts.map +0 -1
- package/dist/test/health-snapshot.test.js +0 -74
- package/dist/test/health-snapshot.test.js.map +0 -1
- package/dist/test/helpers/coalescing-fixture.d.ts +0 -74
- package/dist/test/helpers/coalescing-fixture.d.ts.map +0 -1
- package/dist/test/helpers/coalescing-fixture.js +0 -152
- package/dist/test/helpers/coalescing-fixture.js.map +0 -1
- package/dist/test/helpers/coalescing-kill-child.d.ts +0 -2
- package/dist/test/helpers/coalescing-kill-child.d.ts.map +0 -1
- package/dist/test/helpers/coalescing-kill-child.js +0 -24
- package/dist/test/helpers/coalescing-kill-child.js.map +0 -1
- package/dist/test/helpers/mock-membrane.d.ts +0 -53
- package/dist/test/helpers/mock-membrane.d.ts.map +0 -1
- package/dist/test/helpers/mock-membrane.js +0 -168
- package/dist/test/helpers/mock-membrane.js.map +0 -1
- package/dist/test/helpers/pytc-stop-rows.d.ts +0 -11
- package/dist/test/helpers/pytc-stop-rows.d.ts.map +0 -1
- package/dist/test/helpers/pytc-stop-rows.js +0 -143
- package/dist/test/helpers/pytc-stop-rows.js.map +0 -1
- package/dist/test/helpers/quiesce-crash-child.d.ts +0 -2
- package/dist/test/helpers/quiesce-crash-child.d.ts.map +0 -1
- package/dist/test/helpers/quiesce-crash-child.js +0 -156
- package/dist/test/helpers/quiesce-crash-child.js.map +0 -1
- package/dist/test/hide-then-turn.test.d.ts +0 -2
- package/dist/test/hide-then-turn.test.d.ts.map +0 -1
- package/dist/test/hide-then-turn.test.js +0 -101
- package/dist/test/hide-then-turn.test.js.map +0 -1
- package/dist/test/history-module-overview.test.d.ts +0 -2
- package/dist/test/history-module-overview.test.d.ts.map +0 -1
- package/dist/test/history-module-overview.test.js +0 -711
- package/dist/test/history-module-overview.test.js.map +0 -1
- package/dist/test/history-module-realstore.test.d.ts +0 -2
- package/dist/test/history-module-realstore.test.d.ts.map +0 -1
- package/dist/test/history-module-realstore.test.js +0 -363
- package/dist/test/history-module-realstore.test.js.map +0 -1
- package/dist/test/history-module-semantic.test.d.ts +0 -2
- package/dist/test/history-module-semantic.test.d.ts.map +0 -1
- package/dist/test/history-module-semantic.test.js +0 -213
- package/dist/test/history-module-semantic.test.js.map +0 -1
- package/dist/test/history-module-ux.test.d.ts +0 -2
- package/dist/test/history-module-ux.test.d.ts.map +0 -1
- package/dist/test/history-module-ux.test.js +0 -304
- package/dist/test/history-module-ux.test.js.map +0 -1
- package/dist/test/history-module.test.d.ts +0 -2
- package/dist/test/history-module.test.d.ts.map +0 -1
- package/dist/test/history-module.test.js +0 -619
- package/dist/test/history-module.test.js.map +0 -1
- package/dist/test/history-semantic-review-findings.test.d.ts +0 -2
- package/dist/test/history-semantic-review-findings.test.d.ts.map +0 -1
- package/dist/test/history-semantic-review-findings.test.js +0 -338
- package/dist/test/history-semantic-review-findings.test.js.map +0 -1
- package/dist/test/history-semantic-sync-converges.test.d.ts +0 -2
- package/dist/test/history-semantic-sync-converges.test.d.ts.map +0 -1
- package/dist/test/history-semantic-sync-converges.test.js +0 -109
- package/dist/test/history-semantic-sync-converges.test.js.map +0 -1
- package/dist/test/host-quiesce.test.d.ts +0 -2
- package/dist/test/host-quiesce.test.d.ts.map +0 -1
- package/dist/test/host-quiesce.test.js +0 -988
- package/dist/test/host-quiesce.test.js.map +0 -1
- package/dist/test/inference-exhaustion-rewind.test.d.ts +0 -2
- package/dist/test/inference-exhaustion-rewind.test.d.ts.map +0 -1
- package/dist/test/inference-exhaustion-rewind.test.js +0 -387
- package/dist/test/inference-exhaustion-rewind.test.js.map +0 -1
- package/dist/test/inference-failure-bounds.test.d.ts +0 -2
- package/dist/test/inference-failure-bounds.test.d.ts.map +0 -1
- package/dist/test/inference-failure-bounds.test.js +0 -1202
- package/dist/test/inference-failure-bounds.test.js.map +0 -1
- package/dist/test/inference-failure-observability.test.d.ts +0 -2
- package/dist/test/inference-failure-observability.test.d.ts.map +0 -1
- package/dist/test/inference-failure-observability.test.js +0 -84
- package/dist/test/inference-failure-observability.test.js.map +0 -1
- package/dist/test/journal-tool.test.d.ts +0 -2
- package/dist/test/journal-tool.test.d.ts.map +0 -1
- package/dist/test/journal-tool.test.js +0 -33
- package/dist/test/journal-tool.test.js.map +0 -1
- package/dist/test/kv-unified-open-flight-retry.test.d.ts +0 -2
- package/dist/test/kv-unified-open-flight-retry.test.d.ts.map +0 -1
- package/dist/test/kv-unified-open-flight-retry.test.js +0 -189
- package/dist/test/kv-unified-open-flight-retry.test.js.map +0 -1
- package/dist/test/kv-unified-wiring.test.d.ts +0 -2
- package/dist/test/kv-unified-wiring.test.d.ts.map +0 -1
- package/dist/test/kv-unified-wiring.test.js +0 -95
- package/dist/test/kv-unified-wiring.test.js.map +0 -1
- package/dist/test/late-events-after-stop.test.d.ts +0 -2
- package/dist/test/late-events-after-stop.test.d.ts.map +0 -1
- package/dist/test/late-events-after-stop.test.js +0 -122
- package/dist/test/late-events-after-stop.test.js.map +0 -1
- package/dist/test/liveness-watchdog.test.d.ts +0 -2
- package/dist/test/liveness-watchdog.test.d.ts.map +0 -1
- package/dist/test/liveness-watchdog.test.js +0 -94
- package/dist/test/liveness-watchdog.test.js.map +0 -1
- package/dist/test/machine-close-provenance.test.d.ts +0 -2
- package/dist/test/machine-close-provenance.test.d.ts.map +0 -1
- package/dist/test/machine-close-provenance.test.js +0 -116
- package/dist/test/machine-close-provenance.test.js.map +0 -1
- package/dist/test/maintenance-timer.test.d.ts +0 -2
- package/dist/test/maintenance-timer.test.d.ts.map +0 -1
- package/dist/test/maintenance-timer.test.js +0 -1827
- package/dist/test/maintenance-timer.test.js.map +0 -1
- package/dist/test/mcp-server-bin.test.d.ts +0 -2
- package/dist/test/mcp-server-bin.test.d.ts.map +0 -1
- package/dist/test/mcp-server-bin.test.js +0 -142
- package/dist/test/mcp-server-bin.test.js.map +0 -1
- package/dist/test/mcpl-access-provider.test.d.ts +0 -2
- package/dist/test/mcpl-access-provider.test.d.ts.map +0 -1
- package/dist/test/mcpl-access-provider.test.js +0 -20
- package/dist/test/mcpl-access-provider.test.js.map +0 -1
- package/dist/test/mcpl-awareness-barrier.test.d.ts +0 -2
- package/dist/test/mcpl-awareness-barrier.test.d.ts.map +0 -1
- package/dist/test/mcpl-awareness-barrier.test.js +0 -769
- package/dist/test/mcpl-awareness-barrier.test.js.map +0 -1
- package/dist/test/mcpl-capability-grant.test.d.ts +0 -10
- package/dist/test/mcpl-capability-grant.test.d.ts.map +0 -1
- package/dist/test/mcpl-capability-grant.test.js +0 -111
- package/dist/test/mcpl-capability-grant.test.js.map +0 -1
- package/dist/test/mcpl-capability-scoping.test.d.ts +0 -2
- package/dist/test/mcpl-capability-scoping.test.d.ts.map +0 -1
- package/dist/test/mcpl-capability-scoping.test.js +0 -266
- package/dist/test/mcpl-capability-scoping.test.js.map +0 -1
- package/dist/test/mcpl-child-env.test.d.ts +0 -2
- package/dist/test/mcpl-child-env.test.d.ts.map +0 -1
- package/dist/test/mcpl-child-env.test.js +0 -158
- package/dist/test/mcpl-child-env.test.js.map +0 -1
- package/dist/test/mcpl-coalesced-content-parity.test.d.ts +0 -2
- package/dist/test/mcpl-coalesced-content-parity.test.d.ts.map +0 -1
- package/dist/test/mcpl-coalesced-content-parity.test.js +0 -532
- package/dist/test/mcpl-coalesced-content-parity.test.js.map +0 -1
- package/dist/test/mcpl-coalescing.test.d.ts +0 -2
- package/dist/test/mcpl-coalescing.test.d.ts.map +0 -1
- package/dist/test/mcpl-coalescing.test.js +0 -1125
- package/dist/test/mcpl-coalescing.test.js.map +0 -1
- package/dist/test/mcpl-config-diagnostics.test.d.ts +0 -2
- package/dist/test/mcpl-config-diagnostics.test.d.ts.map +0 -1
- package/dist/test/mcpl-config-diagnostics.test.js +0 -452
- package/dist/test/mcpl-config-diagnostics.test.js.map +0 -1
- package/dist/test/mcpl-empty-content.test.d.ts +0 -2
- package/dist/test/mcpl-empty-content.test.d.ts.map +0 -1
- package/dist/test/mcpl-empty-content.test.js +0 -368
- package/dist/test/mcpl-empty-content.test.js.map +0 -1
- package/dist/test/mcpl-gate-roundtrip.test.d.ts +0 -2
- package/dist/test/mcpl-gate-roundtrip.test.d.ts.map +0 -1
- package/dist/test/mcpl-gate-roundtrip.test.js +0 -285
- package/dist/test/mcpl-gate-roundtrip.test.js.map +0 -1
- package/dist/test/mcpl-list-status.test.d.ts +0 -2
- package/dist/test/mcpl-list-status.test.d.ts.map +0 -1
- package/dist/test/mcpl-list-status.test.js +0 -82
- package/dist/test/mcpl-list-status.test.js.map +0 -1
- package/dist/test/mcpl-manifest-changed.test.d.ts +0 -2
- package/dist/test/mcpl-manifest-changed.test.d.ts.map +0 -1
- package/dist/test/mcpl-manifest-changed.test.js +0 -178
- package/dist/test/mcpl-manifest-changed.test.js.map +0 -1
- package/dist/test/mcpl-push-convert.test.d.ts +0 -2
- package/dist/test/mcpl-push-convert.test.d.ts.map +0 -1
- package/dist/test/mcpl-push-convert.test.js +0 -42
- package/dist/test/mcpl-push-convert.test.js.map +0 -1
- package/dist/test/mcpl-reconnect-events.test.d.ts +0 -2
- package/dist/test/mcpl-reconnect-events.test.d.ts.map +0 -1
- package/dist/test/mcpl-reconnect-events.test.js +0 -144
- package/dist/test/mcpl-reconnect-events.test.js.map +0 -1
- package/dist/test/mcpl-reconnect-reregistration.test.d.ts +0 -2
- package/dist/test/mcpl-reconnect-reregistration.test.d.ts.map +0 -1
- package/dist/test/mcpl-reconnect-reregistration.test.js +0 -238
- package/dist/test/mcpl-reconnect-reregistration.test.js.map +0 -1
- package/dist/test/mcpl-reference-fetcher.test.d.ts +0 -2
- package/dist/test/mcpl-reference-fetcher.test.d.ts.map +0 -1
- package/dist/test/mcpl-reference-fetcher.test.js +0 -427
- package/dist/test/mcpl-reference-fetcher.test.js.map +0 -1
- package/dist/test/mcpl-reference-fields.test.d.ts +0 -2
- package/dist/test/mcpl-reference-fields.test.d.ts.map +0 -1
- package/dist/test/mcpl-reference-fields.test.js +0 -149
- package/dist/test/mcpl-reference-fields.test.js.map +0 -1
- package/dist/test/mcpl-rejection-log-lines.test.d.ts +0 -2
- package/dist/test/mcpl-rejection-log-lines.test.d.ts.map +0 -1
- package/dist/test/mcpl-rejection-log-lines.test.js +0 -349
- package/dist/test/mcpl-rejection-log-lines.test.js.map +0 -1
- package/dist/test/mcpl-request-timeout.test.d.ts +0 -2
- package/dist/test/mcpl-request-timeout.test.d.ts.map +0 -1
- package/dist/test/mcpl-request-timeout.test.js +0 -138
- package/dist/test/mcpl-request-timeout.test.js.map +0 -1
- package/dist/test/mcpl-runtime-lifecycle.test.d.ts +0 -7
- package/dist/test/mcpl-runtime-lifecycle.test.d.ts.map +0 -1
- package/dist/test/mcpl-runtime-lifecycle.test.js +0 -142
- package/dist/test/mcpl-runtime-lifecycle.test.js.map +0 -1
- package/dist/test/mcpl-stderr-tail.test.d.ts +0 -2
- package/dist/test/mcpl-stderr-tail.test.d.ts.map +0 -1
- package/dist/test/mcpl-stderr-tail.test.js +0 -210
- package/dist/test/mcpl-stderr-tail.test.js.map +0 -1
- package/dist/test/mcpl-tool-class-listing.test.d.ts +0 -2
- package/dist/test/mcpl-tool-class-listing.test.d.ts.map +0 -1
- package/dist/test/mcpl-tool-class-listing.test.js +0 -147
- package/dist/test/mcpl-tool-class-listing.test.js.map +0 -1
- package/dist/test/mcpl-tool-classes.test.d.ts +0 -7
- package/dist/test/mcpl-tool-classes.test.d.ts.map +0 -1
- package/dist/test/mcpl-tool-classes.test.js +0 -80
- package/dist/test/mcpl-tool-classes.test.js.map +0 -1
- package/dist/test/mcpl-tool-lifecycle-e2e.test.d.ts +0 -2
- package/dist/test/mcpl-tool-lifecycle-e2e.test.d.ts.map +0 -1
- package/dist/test/mcpl-tool-lifecycle-e2e.test.js +0 -237
- package/dist/test/mcpl-tool-lifecycle-e2e.test.js.map +0 -1
- package/dist/test/mcpl-tool-lifecycle-emitter.test.d.ts +0 -11
- package/dist/test/mcpl-tool-lifecycle-emitter.test.d.ts.map +0 -1
- package/dist/test/mcpl-tool-lifecycle-emitter.test.js +0 -412
- package/dist/test/mcpl-tool-lifecycle-emitter.test.js.map +0 -1
- package/dist/test/mcpl-tool-lifecycle-script.test.d.ts +0 -2
- package/dist/test/mcpl-tool-lifecycle-script.test.d.ts.map +0 -1
- package/dist/test/mcpl-tool-lifecycle-script.test.js +0 -288
- package/dist/test/mcpl-tool-lifecycle-script.test.js.map +0 -1
- package/dist/test/mcpl-tool-lifecycle.test.d.ts +0 -11
- package/dist/test/mcpl-tool-lifecycle.test.d.ts.map +0 -1
- package/dist/test/mcpl-tool-lifecycle.test.js +0 -270
- package/dist/test/mcpl-tool-lifecycle.test.js.map +0 -1
- package/dist/test/mcpl-transport-close.test.d.ts +0 -2
- package/dist/test/mcpl-transport-close.test.d.ts.map +0 -1
- package/dist/test/mcpl-transport-close.test.js +0 -373
- package/dist/test/mcpl-transport-close.test.js.map +0 -1
- package/dist/test/mcpl-transport-spawn.test.d.ts +0 -2
- package/dist/test/mcpl-transport-spawn.test.d.ts.map +0 -1
- package/dist/test/mcpl-transport-spawn.test.js +0 -58
- package/dist/test/mcpl-transport-spawn.test.js.map +0 -1
- package/dist/test/mcpl-ws-transport.test.d.ts +0 -2
- package/dist/test/mcpl-ws-transport.test.d.ts.map +0 -1
- package/dist/test/mcpl-ws-transport.test.js +0 -259
- package/dist/test/mcpl-ws-transport.test.js.map +0 -1
- package/dist/test/module-context-integration.test.d.ts +0 -18
- package/dist/test/module-context-integration.test.d.ts.map +0 -1
- package/dist/test/module-context-integration.test.js +0 -181
- package/dist/test/module-context-integration.test.js.map +0 -1
- package/dist/test/offline-recovery-branch.test.d.ts +0 -2
- package/dist/test/offline-recovery-branch.test.d.ts.map +0 -1
- package/dist/test/offline-recovery-branch.test.js +0 -570
- package/dist/test/offline-recovery-branch.test.js.map +0 -1
- package/dist/test/operator-surgery.test.d.ts +0 -2
- package/dist/test/operator-surgery.test.d.ts.map +0 -1
- package/dist/test/operator-surgery.test.js +0 -595
- package/dist/test/operator-surgery.test.js.map +0 -1
- package/dist/test/ops-alert.test.d.ts +0 -2
- package/dist/test/ops-alert.test.d.ts.map +0 -1
- package/dist/test/ops-alert.test.js +0 -153
- package/dist/test/ops-alert.test.js.map +0 -1
- package/dist/test/overbudget-drain-breaker.test.d.ts +0 -2
- package/dist/test/overbudget-drain-breaker.test.d.ts.map +0 -1
- package/dist/test/overbudget-drain-breaker.test.js +0 -198
- package/dist/test/overbudget-drain-breaker.test.js.map +0 -1
- package/dist/test/pack-installs-alone.test.d.ts +0 -2
- package/dist/test/pack-installs-alone.test.d.ts.map +0 -1
- package/dist/test/pack-installs-alone.test.js +0 -284
- package/dist/test/pack-installs-alone.test.js.map +0 -1
- package/dist/test/per-agent-delivery.test.d.ts +0 -2
- package/dist/test/per-agent-delivery.test.d.ts.map +0 -1
- package/dist/test/per-agent-delivery.test.js +0 -108
- package/dist/test/per-agent-delivery.test.js.map +0 -1
- package/dist/test/physical-window-restart.test.d.ts +0 -16
- package/dist/test/physical-window-restart.test.d.ts.map +0 -1
- package/dist/test/physical-window-restart.test.js +0 -487
- package/dist/test/physical-window-restart.test.js.map +0 -1
- package/dist/test/placed-reaction-baseline.test.d.ts +0 -2
- package/dist/test/placed-reaction-baseline.test.d.ts.map +0 -1
- package/dist/test/placed-reaction-baseline.test.js +0 -312
- package/dist/test/placed-reaction-baseline.test.js.map +0 -1
- package/dist/test/present-while-acting.test.d.ts +0 -19
- package/dist/test/present-while-acting.test.d.ts.map +0 -1
- package/dist/test/present-while-acting.test.js +0 -1649
- package/dist/test/present-while-acting.test.js.map +0 -1
- package/dist/test/prose-segments.test.d.ts +0 -2
- package/dist/test/prose-segments.test.d.ts.map +0 -1
- package/dist/test/prose-segments.test.js +0 -52
- package/dist/test/prose-segments.test.js.map +0 -1
- package/dist/test/prose-stream-router.test.d.ts +0 -2
- package/dist/test/prose-stream-router.test.d.ts.map +0 -1
- package/dist/test/prose-stream-router.test.js +0 -171
- package/dist/test/prose-stream-router.test.js.map +0 -1
- package/dist/test/provider-acceleration-cooldown.test.d.ts +0 -2
- package/dist/test/provider-acceleration-cooldown.test.d.ts.map +0 -1
- package/dist/test/provider-acceleration-cooldown.test.js +0 -435
- package/dist/test/provider-acceleration-cooldown.test.js.map +0 -1
- package/dist/test/provider-waits.test.d.ts +0 -2
- package/dist/test/provider-waits.test.d.ts.map +0 -1
- package/dist/test/provider-waits.test.js +0 -2259
- package/dist/test/provider-waits.test.js.map +0 -1
- package/dist/test/puppet-tool-call.test.d.ts +0 -2
- package/dist/test/puppet-tool-call.test.d.ts.map +0 -1
- package/dist/test/puppet-tool-call.test.js +0 -226
- package/dist/test/puppet-tool-call.test.js.map +0 -1
- package/dist/test/puppet-turn-reservation.test.d.ts +0 -2
- package/dist/test/puppet-turn-reservation.test.d.ts.map +0 -1
- package/dist/test/puppet-turn-reservation.test.js +0 -139
- package/dist/test/puppet-turn-reservation.test.js.map +0 -1
- package/dist/test/reaction-baseline-platform.test.d.ts +0 -2
- package/dist/test/reaction-baseline-platform.test.d.ts.map +0 -1
- package/dist/test/reaction-baseline-platform.test.js +0 -124
- package/dist/test/reaction-baseline-platform.test.js.map +0 -1
- package/dist/test/record-journal.test.d.ts +0 -6
- package/dist/test/record-journal.test.d.ts.map +0 -1
- package/dist/test/record-journal.test.js +0 -269
- package/dist/test/record-journal.test.js.map +0 -1
- package/dist/test/refusal-reaction-baseline.test.d.ts +0 -2
- package/dist/test/refusal-reaction-baseline.test.d.ts.map +0 -1
- package/dist/test/refusal-reaction-baseline.test.js +0 -81
- package/dist/test/refusal-reaction-baseline.test.js.map +0 -1
- package/dist/test/refusal-rewind.test.d.ts +0 -2
- package/dist/test/refusal-rewind.test.d.ts.map +0 -1
- package/dist/test/refusal-rewind.test.js +0 -117
- package/dist/test/refusal-rewind.test.js.map +0 -1
- package/dist/test/refusal-single-call.test.d.ts +0 -2
- package/dist/test/refusal-single-call.test.d.ts.map +0 -1
- package/dist/test/refusal-single-call.test.js +0 -177
- package/dist/test/refusal-single-call.test.js.map +0 -1
- package/dist/test/release-bump.test.d.ts +0 -2
- package/dist/test/release-bump.test.d.ts.map +0 -1
- package/dist/test/release-bump.test.js +0 -248
- package/dist/test/release-bump.test.js.map +0 -1
- package/dist/test/release-changelog.test.d.ts +0 -2
- package/dist/test/release-changelog.test.d.ts.map +0 -1
- package/dist/test/release-changelog.test.js +0 -159
- package/dist/test/release-changelog.test.js.map +0 -1
- package/dist/test/release-holds.test.d.ts +0 -2
- package/dist/test/release-holds.test.d.ts.map +0 -1
- package/dist/test/release-holds.test.js +0 -456
- package/dist/test/release-holds.test.js.map +0 -1
- package/dist/test/same-round-think-policy.test.d.ts +0 -2
- package/dist/test/same-round-think-policy.test.d.ts.map +0 -1
- package/dist/test/same-round-think-policy.test.js +0 -853
- package/dist/test/same-round-think-policy.test.js.map +0 -1
- package/dist/test/save-recent-image.test.d.ts +0 -26
- package/dist/test/save-recent-image.test.d.ts.map +0 -1
- package/dist/test/save-recent-image.test.js +0 -974
- package/dist/test/save-recent-image.test.js.map +0 -1
- package/dist/test/shutdown-errors.test.d.ts +0 -2
- package/dist/test/shutdown-errors.test.d.ts.map +0 -1
- package/dist/test/shutdown-errors.test.js +0 -662
- package/dist/test/shutdown-errors.test.js.map +0 -1
- package/dist/test/silent-heartbeat-rows.test.d.ts +0 -2
- package/dist/test/silent-heartbeat-rows.test.d.ts.map +0 -1
- package/dist/test/silent-heartbeat-rows.test.js +0 -451
- package/dist/test/silent-heartbeat-rows.test.js.map +0 -1
- package/dist/test/silent-heartbeat.test.d.ts +0 -2
- package/dist/test/silent-heartbeat.test.d.ts.map +0 -1
- package/dist/test/silent-heartbeat.test.js +0 -165
- package/dist/test/silent-heartbeat.test.js.map +0 -1
- package/dist/test/state-scaling-e2e.test.d.ts +0 -22
- package/dist/test/state-scaling-e2e.test.d.ts.map +0 -1
- package/dist/test/state-scaling-e2e.test.js +0 -206
- package/dist/test/state-scaling-e2e.test.js.map +0 -1
- package/dist/test/state-scaling.test.d.ts +0 -12
- package/dist/test/state-scaling.test.d.ts.map +0 -1
- package/dist/test/state-scaling.test.js +0 -251
- package/dist/test/state-scaling.test.js.map +0 -1
- package/dist/test/stop-with-host-operations.test.d.ts +0 -20
- package/dist/test/stop-with-host-operations.test.d.ts.map +0 -1
- package/dist/test/stop-with-host-operations.test.js +0 -1174
- package/dist/test/stop-with-host-operations.test.js.map +0 -1
- package/dist/test/stop-with-work-in-flight.test.d.ts +0 -23
- package/dist/test/stop-with-work-in-flight.test.d.ts.map +0 -1
- package/dist/test/stop-with-work-in-flight.test.js +0 -784
- package/dist/test/stop-with-work-in-flight.test.js.map +0 -1
- package/dist/test/surgery-marks-receipt.test.d.ts +0 -2
- package/dist/test/surgery-marks-receipt.test.d.ts.map +0 -1
- package/dist/test/surgery-marks-receipt.test.js +0 -540
- package/dist/test/surgery-marks-receipt.test.js.map +0 -1
- package/dist/test/thinking-token-stamp.test.d.ts +0 -12
- package/dist/test/thinking-token-stamp.test.d.ts.map +0 -1
- package/dist/test/thinking-token-stamp.test.js +0 -129
- package/dist/test/thinking-token-stamp.test.js.map +0 -1
- package/dist/test/timezone.test.d.ts +0 -2
- package/dist/test/timezone.test.d.ts.map +0 -1
- package/dist/test/timezone.test.js +0 -18
- package/dist/test/timezone.test.js.map +0 -1
- package/dist/test/tool-batch-hook.test.d.ts +0 -8
- package/dist/test/tool-batch-hook.test.d.ts.map +0 -1
- package/dist/test/tool-batch-hook.test.js +0 -275
- package/dist/test/tool-batch-hook.test.js.map +0 -1
- package/dist/test/tool-policy.test.d.ts +0 -2
- package/dist/test/tool-policy.test.d.ts.map +0 -1
- package/dist/test/tool-policy.test.js +0 -51
- package/dist/test/tool-policy.test.js.map +0 -1
- package/dist/test/tool-presentation.test.d.ts +0 -2
- package/dist/test/tool-presentation.test.d.ts.map +0 -1
- package/dist/test/tool-presentation.test.js +0 -268
- package/dist/test/tool-presentation.test.js.map +0 -1
- package/dist/test/tool-result-content.test.d.ts +0 -2
- package/dist/test/tool-result-content.test.d.ts.map +0 -1
- package/dist/test/tool-result-content.test.js +0 -37
- package/dist/test/tool-result-content.test.js.map +0 -1
- package/dist/test/tool-result-guard.test.d.ts +0 -2
- package/dist/test/tool-result-guard.test.d.ts.map +0 -1
- package/dist/test/tool-result-guard.test.js +0 -3729
- package/dist/test/tool-result-guard.test.js.map +0 -1
- package/dist/test/tool-result-history.test.d.ts +0 -7
- package/dist/test/tool-result-history.test.d.ts.map +0 -1
- package/dist/test/tool-result-history.test.js +0 -156
- package/dist/test/tool-result-history.test.js.map +0 -1
- package/dist/test/tool-result-spill.test.d.ts +0 -13
- package/dist/test/tool-result-spill.test.d.ts.map +0 -1
- package/dist/test/tool-result-spill.test.js +0 -586
- package/dist/test/tool-result-spill.test.js.map +0 -1
- package/dist/test/tool-wrapper-prose-guard-integration.test.d.ts +0 -2
- package/dist/test/tool-wrapper-prose-guard-integration.test.d.ts.map +0 -1
- package/dist/test/tool-wrapper-prose-guard-integration.test.js +0 -485
- package/dist/test/tool-wrapper-prose-guard-integration.test.js.map +0 -1
- package/dist/test/tool-wrapper-prose-guard.test.d.ts +0 -2
- package/dist/test/tool-wrapper-prose-guard.test.d.ts.map +0 -1
- package/dist/test/tool-wrapper-prose-guard.test.js +0 -34
- package/dist/test/tool-wrapper-prose-guard.test.js.map +0 -1
- package/dist/test/tools-observe-parser-conformance.test.d.ts +0 -2
- package/dist/test/tools-observe-parser-conformance.test.d.ts.map +0 -1
- package/dist/test/tools-observe-parser-conformance.test.js +0 -129
- package/dist/test/tools-observe-parser-conformance.test.js.map +0 -1
- package/dist/test/trunk-channel-routing.test.d.ts +0 -19
- package/dist/test/trunk-channel-routing.test.d.ts.map +0 -1
- package/dist/test/trunk-channel-routing.test.js +0 -652
- package/dist/test/trunk-channel-routing.test.js.map +0 -1
- package/dist/test/tune-out-e2e.test.d.ts +0 -2
- package/dist/test/tune-out-e2e.test.d.ts.map +0 -1
- package/dist/test/tune-out-e2e.test.js +0 -142
- package/dist/test/tune-out-e2e.test.js.map +0 -1
- package/dist/test/tune-out-gate-composition.test.d.ts +0 -2
- package/dist/test/tune-out-gate-composition.test.d.ts.map +0 -1
- package/dist/test/tune-out-gate-composition.test.js +0 -221
- package/dist/test/tune-out-gate-composition.test.js.map +0 -1
- package/dist/test/tune-out-lifecycle.test.d.ts +0 -2
- package/dist/test/tune-out-lifecycle.test.d.ts.map +0 -1
- package/dist/test/tune-out-lifecycle.test.js +0 -121
- package/dist/test/tune-out-lifecycle.test.js.map +0 -1
- package/dist/test/turn-trigger-provenance.test.d.ts +0 -2
- package/dist/test/turn-trigger-provenance.test.d.ts.map +0 -1
- package/dist/test/turn-trigger-provenance.test.js +0 -137
- package/dist/test/turn-trigger-provenance.test.js.map +0 -1
- package/dist/test/unconvertible-tool-result.test.d.ts +0 -2
- package/dist/test/unconvertible-tool-result.test.d.ts.map +0 -1
- package/dist/test/unconvertible-tool-result.test.js +0 -358
- package/dist/test/unconvertible-tool-result.test.js.map +0 -1
- package/dist/test/unsent-compiles.test.d.ts +0 -2
- package/dist/test/unsent-compiles.test.d.ts.map +0 -1
- package/dist/test/unsent-compiles.test.js +0 -139
- package/dist/test/unsent-compiles.test.js.map +0 -1
- package/dist/test/usage-accounting.test.d.ts +0 -2
- package/dist/test/usage-accounting.test.d.ts.map +0 -1
- package/dist/test/usage-accounting.test.js +0 -13
- package/dist/test/usage-accounting.test.js.map +0 -1
- package/dist/test/usage-tracker.test.d.ts +0 -2
- package/dist/test/usage-tracker.test.d.ts.map +0 -1
- package/dist/test/usage-tracker.test.js +0 -129
- package/dist/test/usage-tracker.test.js.map +0 -1
- package/dist/test/utils-surface.test.d.ts +0 -2
- package/dist/test/utils-surface.test.d.ts.map +0 -1
- package/dist/test/utils-surface.test.js +0 -145
- package/dist/test/utils-surface.test.js.map +0 -1
- package/dist/test/workspace-branch-continuation.test.d.ts +0 -15
- package/dist/test/workspace-branch-continuation.test.d.ts.map +0 -1
- package/dist/test/workspace-branch-continuation.test.js +0 -183
- package/dist/test/workspace-branch-continuation.test.js.map +0 -1
- package/dist/test/workspace-materialize-freshness.test.d.ts +0 -16
- package/dist/test/workspace-materialize-freshness.test.d.ts.map +0 -1
- package/dist/test/workspace-materialize-freshness.test.js +0 -252
- package/dist/test/workspace-materialize-freshness.test.js.map +0 -1
- package/dist/test/workspace-path-containment.test.d.ts +0 -19
- package/dist/test/workspace-path-containment.test.d.ts.map +0 -1
- package/dist/test/workspace-path-containment.test.js +0 -91
- package/dist/test/workspace-path-containment.test.js.map +0 -1
- package/dist/test/workspace-read-file-from-disk.test.d.ts +0 -17
- package/dist/test/workspace-read-file-from-disk.test.d.ts.map +0 -1
- package/dist/test/workspace-read-file-from-disk.test.js +0 -174
- package/dist/test/workspace-read-file-from-disk.test.js.map +0 -1
- package/dist/test/workspace-read-image.test.d.ts +0 -2
- package/dist/test/workspace-read-image.test.d.ts.map +0 -1
- package/dist/test/workspace-read-image.test.js +0 -597
- package/dist/test/workspace-read-image.test.js.map +0 -1
- package/dist/test/workspace-read-text.test.d.ts +0 -2
- package/dist/test/workspace-read-text.test.d.ts.map +0 -1
- package/dist/test/workspace-read-text.test.js +0 -101
- package/dist/test/workspace-read-text.test.js.map +0 -1
- package/dist/test/workspace-restore-order.test.d.ts +0 -16
- package/dist/test/workspace-restore-order.test.d.ts.map +0 -1
- package/dist/test/workspace-restore-order.test.js +0 -140
- package/dist/test/workspace-restore-order.test.js.map +0 -1
- package/dist/test/workspace-three-way.test.d.ts +0 -12
- package/dist/test/workspace-three-way.test.d.ts.map +0 -1
- package/dist/test/workspace-three-way.test.js +0 -4094
- package/dist/test/workspace-three-way.test.js.map +0 -1
- package/dist/test/workspace-watcher.test.d.ts +0 -2
- package/dist/test/workspace-watcher.test.d.ts.map +0 -1
- package/dist/test/workspace-watcher.test.js +0 -281
- package/dist/test/workspace-watcher.test.js.map +0 -1
- package/dist/test/xml-round-persist.test.d.ts +0 -19
- package/dist/test/xml-round-persist.test.d.ts.map +0 -1
- package/dist/test/xml-round-persist.test.js +0 -235
- package/dist/test/xml-round-persist.test.js.map +0 -1
- package/dist/tsconfig.tsbuildinfo +0 -1
|
@@ -11,11 +11,17 @@
|
|
|
11
11
|
|
|
12
12
|
import { EventEmitter } from 'node:events';
|
|
13
13
|
|
|
14
|
-
import { openTransport, type McplTransport, type TransportCloseInfo } from './transport.js';
|
|
14
|
+
import { openTransport, STDERR_END_WAIT_MS, type McplTransport, type TransportCloseInfo } from './transport.js';
|
|
15
15
|
import { maskNegotiatedCapabilities } from './capability-mask.js';
|
|
16
16
|
import { CapabilityGrant, expandAdvertisementShorthand } from './capability-grant.js';
|
|
17
|
-
import { CAPABILITY_DISABLED } from './errors.js';
|
|
18
|
-
import { logDetail, logValue } from './log-value.js';
|
|
17
|
+
import { CAPABILITY_DISABLED, boundedForLog, describeCause } from './errors.js';
|
|
18
|
+
import { logDetail, logValue, logValueRedacted } from './log-value.js';
|
|
19
|
+
import {
|
|
20
|
+
LEGACY_MCP_PROTOCOL_VERSION,
|
|
21
|
+
MODERN_MCP_PROTOCOL_VERSION,
|
|
22
|
+
checkServerConfig,
|
|
23
|
+
redactServerText,
|
|
24
|
+
} from './protocol-family.js';
|
|
19
25
|
|
|
20
26
|
import type {
|
|
21
27
|
McplServerConfig,
|
|
@@ -86,13 +92,29 @@ export function stderrTailOf(error: unknown): string | undefined {
|
|
|
86
92
|
}
|
|
87
93
|
|
|
88
94
|
/** For the host's own log line about a failed connect: the server's stderr
|
|
89
|
-
* that the error keeps as `stderrTail`,
|
|
90
|
-
*
|
|
95
|
+
* that the error keeps as `stderrTail`, with the server's credentials
|
|
96
|
+
* taken out by `redact` (its redactServerText), one marked line per line of
|
|
97
|
+
* it, each through log-value, since it's the server's text. '' when there's
|
|
91
98
|
* none. */
|
|
92
|
-
export function withStderr(error: unknown): string {
|
|
99
|
+
export function withStderr(error: unknown, redact: (text: string) => string): string {
|
|
93
100
|
const tail = stderrTailOf(error);
|
|
94
101
|
if (tail === undefined) return '';
|
|
95
|
-
return '. The last of its stderr:' + tail.split('\n').map((line) => `\n stderr: ${logDetail(line)}`).join('');
|
|
102
|
+
return '. The last of its stderr:' + redact(tail).split('\n').map((line) => `\n stderr: ${logDetail(line)}`).join('');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** A transport close's reason, as the host's log and the handshake's error
|
|
106
|
+
* give it: the transport's own words. A WebSocket peer's close reason is the
|
|
107
|
+
* server's text (`reasonFromPeer`), so they give the words the transport uses
|
|
108
|
+
* for a close without one, as `mcpl:server-closed` leaves that text out. A
|
|
109
|
+
* WebSocket close's code, a number, says what the server meant (4001: a
|
|
110
|
+
* refused credential), with or without a reason, so each gives it too: the
|
|
111
|
+
* log line for a close with requests pending has `code=`, and the
|
|
112
|
+
* handshake's error adds it for any WebSocket close, since a close before the
|
|
113
|
+
* handshake completes reaches no 'close' event. A stdio child's exit reads
|
|
114
|
+
* as before. */
|
|
115
|
+
function closeReasonText(info: TransportCloseInfo): string {
|
|
116
|
+
if (info.reasonFromPeer) return 'websocket closed';
|
|
117
|
+
return info.reason ?? 'unknown';
|
|
96
118
|
}
|
|
97
119
|
|
|
98
120
|
/** Default per-request timeout in milliseconds (see McplServerConfig.requestTimeoutMs).
|
|
@@ -102,8 +124,140 @@ export function withStderr(error: unknown): string {
|
|
|
102
124
|
* tool work while still bounding the hang. */
|
|
103
125
|
const DEFAULT_REQUEST_TIMEOUT_MS = 60_000;
|
|
104
126
|
|
|
105
|
-
/** MCP protocol version used in the initialize handshake
|
|
106
|
-
|
|
127
|
+
/** MCP protocol version used in the initialize handshake: the only revision
|
|
128
|
+
* this legacy engine offers or accepts. */
|
|
129
|
+
const MCP_PROTOCOL_VERSION = LEGACY_MCP_PROTOCOL_VERSION;
|
|
130
|
+
|
|
131
|
+
/** JSON-RPC code for an unsupported protocol version (MCP 2026-07-28 §basic,
|
|
132
|
+
* the code a modern-only server answers our `initialize` with). */
|
|
133
|
+
const UNSUPPORTED_PROTOCOL_VERSION_CODE = -32022;
|
|
134
|
+
|
|
135
|
+
/** Upper bound on `tools/list` pages, against a server whose cursors never
|
|
136
|
+
* end. Generous: real inventories fit in a handful of pages. */
|
|
137
|
+
const MAX_TOOL_LIST_PAGES = 100;
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* The server and this engine have no protocol revision in common, so the
|
|
141
|
+
* connection was never established. This is a protocol-negotiation fact, not
|
|
142
|
+
* a request outcome. Retrying cannot change it: each engine offers one
|
|
143
|
+
* revision, and only configuration can fix the mismatch. A connection that
|
|
144
|
+
* gets this error does not schedule reconnects. The legacy engine and the
|
|
145
|
+
* modern one (ModernMcpConnection, whose pinned revision the server refused)
|
|
146
|
+
* both throw it, so its readers treat them alike.
|
|
147
|
+
*
|
|
148
|
+
* - `rejected`: the server refused the offered revision: the legacy engine's
|
|
149
|
+
* `initialize` answered with error -32022, or a modern server's discovery
|
|
150
|
+
* or error naming no common revision. `supported` is the server's own list,
|
|
151
|
+
* when it gave one.
|
|
152
|
+
* - `mismatch`: the server answered the legacy engine's `initialize` with a
|
|
153
|
+
* revision other than 2024-11-05, or with none.
|
|
154
|
+
*/
|
|
155
|
+
export class McplProtocolVersionError extends Error {
|
|
156
|
+
constructor(
|
|
157
|
+
message: string,
|
|
158
|
+
readonly kind: 'rejected' | 'mismatch',
|
|
159
|
+
/** Revision the server answered with (`mismatch`; null when it gave none). */
|
|
160
|
+
readonly returned: string | null,
|
|
161
|
+
/** Revisions the server says it supports (`rejected`, when stated). */
|
|
162
|
+
readonly supported: readonly string[] | null,
|
|
163
|
+
/** The JSON-RPC error's code and data, verbatim (`rejected`). */
|
|
164
|
+
readonly code?: number,
|
|
165
|
+
readonly data?: unknown,
|
|
166
|
+
/** The revision this client offered: the legacy engine's, unless a
|
|
167
|
+
* modern connection's. */
|
|
168
|
+
readonly offered: string = MCP_PROTOCOL_VERSION,
|
|
169
|
+
) {
|
|
170
|
+
super(message);
|
|
171
|
+
this.name = 'McplProtocolVersionError';
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function protocolVersionRejection(id: string, error: { code: number; message: string; data?: unknown }): McplProtocolVersionError {
|
|
176
|
+
const data = error.data as { supported?: unknown } | undefined;
|
|
177
|
+
const supported = Array.isArray(data?.supported) && data.supported.every((v) => typeof v === 'string')
|
|
178
|
+
? (data.supported as string[])
|
|
179
|
+
: null;
|
|
180
|
+
const modernHint = supported?.includes(MODERN_MCP_PROTOCOL_VERSION)
|
|
181
|
+
? ` It speaks modern MCP: configure it with protocol: 'modern' (stdio) or reach it over http(s).`
|
|
182
|
+
: '';
|
|
183
|
+
return new McplProtocolVersionError(
|
|
184
|
+
`MCPL server "${id}" does not support MCP ${MCP_PROTOCOL_VERSION}` +
|
|
185
|
+
// The server's message can be any JSON value, so it's read without
|
|
186
|
+
// throwing (describeCause) and bounded, as the initialize error is.
|
|
187
|
+
(supported ? ` (it supports: ${supported.join(', ') || 'none listed'}).` : ` (${boundedForLog(describeCause(error.message))}).`) +
|
|
188
|
+
modernHint,
|
|
189
|
+
'rejected',
|
|
190
|
+
null,
|
|
191
|
+
supported,
|
|
192
|
+
error.code,
|
|
193
|
+
error.data,
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const errorOf = (value: unknown): Error => (value instanceof Error ? value : new Error(String(value)));
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* A handshake that failed, and whose cleanup couldn't confirm that the child
|
|
201
|
+
* exited. The transport goes to whoever owns the launch.
|
|
202
|
+
*/
|
|
203
|
+
class HandshakeCleanupError extends Error {
|
|
204
|
+
constructor(
|
|
205
|
+
readonly failure: Error,
|
|
206
|
+
readonly cleanup: Error,
|
|
207
|
+
readonly transport: McplTransport,
|
|
208
|
+
) {
|
|
209
|
+
super(`${failure.message}; its cleanup also failed: ${cleanup.message}`, { cause: failure });
|
|
210
|
+
this.name = 'HandshakeCleanupError';
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* A connect that failed, and whose cleanup couldn't confirm that the child
|
|
216
|
+
* exited: it may still be running. `connection` owns that launch, closed and
|
|
217
|
+
* not reconnecting. Its close() retries the reap and resolves once the exit
|
|
218
|
+
* is confirmed, so the caller can still clean up, and should start nothing
|
|
219
|
+
* else for this server until then. connect() and connectWithReconnect()
|
|
220
|
+
* throw this, and the modern engine's ModernMcpConnection.connect() too.
|
|
221
|
+
* `cause` is the connect's own failure.
|
|
222
|
+
*/
|
|
223
|
+
export class McplUnreapedLaunchError<Connection extends { close(): Promise<void> } = McplServerConnection> extends Error {
|
|
224
|
+
constructor(
|
|
225
|
+
readonly connection: Connection,
|
|
226
|
+
failure: Error,
|
|
227
|
+
/** The cleanup's own failure: the child it couldn't confirm exited. */
|
|
228
|
+
readonly cleanup: Error,
|
|
229
|
+
) {
|
|
230
|
+
super(`${failure.message}; its cleanup also failed: ${cleanup.message}`, { cause: failure });
|
|
231
|
+
this.name = 'McplUnreapedLaunchError';
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Why a request to an MCPL server produced no result, as far as the host can
|
|
237
|
+
* know it. Messages are unchanged from plain errors; the `outcome` makes the
|
|
238
|
+
* difference between "never left the host" and "left, and no answer came
|
|
239
|
+
* back" a fact a caller can act on instead of a string to match:
|
|
240
|
+
* - `not-sent`: the request was refused before anything was written (the
|
|
241
|
+
* connection was already closed);
|
|
242
|
+
* - `error-response`: the server answered with a JSON-RPC error (it may have
|
|
243
|
+
* acted before failing; the error alone does not say);
|
|
244
|
+
* - `no-response`: the request was handed to the transport and no answer
|
|
245
|
+
* came back (timeout, or the connection closed while awaiting). It does
|
|
246
|
+
* not prove the request reached the server — the transport can decline or
|
|
247
|
+
* drop a write — only that the host can no longer say it didn't.
|
|
248
|
+
*/
|
|
249
|
+
export class McplRequestError extends Error {
|
|
250
|
+
constructor(
|
|
251
|
+
message: string,
|
|
252
|
+
readonly outcome: 'not-sent' | 'error-response' | 'no-response',
|
|
253
|
+
/** The JSON-RPC error's `data`, verbatim, when the server supplied one
|
|
254
|
+
* (e.g. which parts of a multi-part send were posted). */
|
|
255
|
+
readonly data?: unknown,
|
|
256
|
+
) {
|
|
257
|
+
super(message);
|
|
258
|
+
this.name = 'McplRequestError';
|
|
259
|
+
}
|
|
260
|
+
}
|
|
107
261
|
|
|
108
262
|
/**
|
|
109
263
|
* Represents a pending JSON-RPC request awaiting a response.
|
|
@@ -123,6 +277,21 @@ interface RequestOptions {
|
|
|
123
277
|
surfaceOrphanedResponse?: boolean;
|
|
124
278
|
}
|
|
125
279
|
|
|
280
|
+
/** A server→host message held while its plane is closed. `kept`: it came on
|
|
281
|
+
* a transport that is gone, and the boundary kept it (keepAcrossBoundary). */
|
|
282
|
+
interface HeldItem {
|
|
283
|
+
event: string;
|
|
284
|
+
args: unknown[];
|
|
285
|
+
kept?: true;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/** A request's method for a log line, whatever the server sent there. */
|
|
289
|
+
function describeMethod(method: unknown): string {
|
|
290
|
+
return typeof method === 'string'
|
|
291
|
+
? `method ${logValue(method)}`
|
|
292
|
+
: `a method of type ${method === null ? 'null' : typeof method}`;
|
|
293
|
+
}
|
|
294
|
+
|
|
126
295
|
/**
|
|
127
296
|
* McplServerConnection manages a single JSON-RPC 2.0 connection to an
|
|
128
297
|
* MCPL server over stdio. Use the static `connect()` factory to create
|
|
@@ -144,7 +313,12 @@ interface RequestOptions {
|
|
|
144
313
|
* server's stderr when its handshake failed by its transport closing, a
|
|
145
314
|
* transport error or its timeout and it had written any; `error` never
|
|
146
315
|
* carries it.
|
|
147
|
-
* - `'reconnect'` — Background reconnect succeeded
|
|
316
|
+
* - `'reconnect'` — Background reconnect succeeded. The new transport
|
|
317
|
+
* starts with an empty grant and both planes
|
|
318
|
+
* closed, as at initial connect: whoever owns the
|
|
319
|
+
* connection runs the §5.3 policy exchange again,
|
|
320
|
+
* then opens the planes (the framework's listener
|
|
321
|
+
* does).
|
|
148
322
|
*/
|
|
149
323
|
export class McplServerConnection extends EventEmitter {
|
|
150
324
|
/** Unique server identifier from config. */
|
|
@@ -164,6 +338,10 @@ export class McplServerConnection extends EventEmitter {
|
|
|
164
338
|
* the sole source of the `tools` capability path). */
|
|
165
339
|
mcpToolsAdvertised = false;
|
|
166
340
|
|
|
341
|
+
/** The MCP revision established by the last successful handshake (always
|
|
342
|
+
* 2024-11-05 for this engine), or null before the first one. */
|
|
343
|
+
protocolVersion: string | null = null;
|
|
344
|
+
|
|
167
345
|
/** Host-owned per-server authority for host/command (config
|
|
168
346
|
* allowHostCommands, default false). No capability path exists and the
|
|
169
347
|
* grant cannot confer it — server params never decide admin authority. */
|
|
@@ -231,6 +409,22 @@ export class McplServerConnection extends EventEmitter {
|
|
|
231
409
|
this.grant = CapabilityGrant.empty();
|
|
232
410
|
this.policyEstablished = false;
|
|
233
411
|
this.transportEpoch++;
|
|
412
|
+
// What the server sent that is still held came on the transport that is
|
|
413
|
+
// now gone. What it said about its own process is dropped: its requests
|
|
414
|
+
// wait on answers that can no longer reach whoever asked (an inference
|
|
415
|
+
// request, a host/command), and its announcements describe a process
|
|
416
|
+
// that is gone (a tools/observe filter, a channel registration), so the
|
|
417
|
+
// next one announces its own. What it reported about the world is kept: a
|
|
418
|
+
// channel's incoming messages and its pushes, which the next process
|
|
419
|
+
// doesn't send again when its bridge records them as forwarded first, as
|
|
420
|
+
// discord-mcpl does with its watermark. They are admitted at the flush
|
|
421
|
+
// against the next transport's grant, like anything held, and an answer
|
|
422
|
+
// to one is never written to the next transport (sameTransport); a
|
|
423
|
+
// connection closed for good, with no next transport, drops them. The
|
|
424
|
+
// host's own reports about the transport stay too: its lifecycle
|
|
425
|
+
// (connect-failed, reconnect-failed), the server's stderr, and
|
|
426
|
+
// orphaned-response audits.
|
|
427
|
+
this.replaceHeld(this.keepAcrossBoundary(this.bufferedEvents));
|
|
234
428
|
// RFC-007 §6.5: a filter is per connection epoch — after a reconnect the
|
|
235
429
|
// server sends it again, and until then receives metadata only.
|
|
236
430
|
this.toolObserveFilter = null;
|
|
@@ -241,6 +435,59 @@ export class McplServerConnection extends EventEmitter {
|
|
|
241
435
|
this.manifestState = { lastValidatedRevision: null, lastFetchedAt: null, lastNegotiatedAt: null };
|
|
242
436
|
}
|
|
243
437
|
|
|
438
|
+
/** What of these held items a transport boundary keeps (see
|
|
439
|
+
* resetPolicyForTransportBoundary), in order, each marked `kept`; the
|
|
440
|
+
* rest is logged and dropped. A connection closed for good has no next
|
|
441
|
+
* transport whose grant could admit what the server reported about the
|
|
442
|
+
* world, so that is dropped too. */
|
|
443
|
+
private keepAcrossBoundary(held: HeldItem[]): HeldItem[] {
|
|
444
|
+
const forGood = this.closedForGood;
|
|
445
|
+
const drops = (item: HeldItem) => McplServerConnection.PROCESS_EVENTS.has(item.event)
|
|
446
|
+
|| (forGood && McplServerConnection.WORLD_EVENTS.has(item.event));
|
|
447
|
+
const kept = held.filter((item) => !drops(item));
|
|
448
|
+
if (kept.length < held.length) {
|
|
449
|
+
// Named by event, with a count, so a dropped request a server was
|
|
450
|
+
// waiting on, which no answer will now reach, can be told apart.
|
|
451
|
+
const counts = new Map<string, number>();
|
|
452
|
+
for (const item of held) if (drops(item)) counts.set(item.event, (counts.get(item.event) ?? 0) + 1);
|
|
453
|
+
const what = [...counts].map(([event, count]) => `${count} ${logValue(event)}`).join(', ');
|
|
454
|
+
console.error(
|
|
455
|
+
`[mcpl] ${this.id}: dropped ${held.length - kept.length} held message(s) from a transport that closed` +
|
|
456
|
+
`${forGood ? ', with no transport to come' : ''}: ${what}`,
|
|
457
|
+
);
|
|
458
|
+
}
|
|
459
|
+
return kept.map((item) => ({ ...item, kept: true as const }));
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/** Hold these items in place of what is held, with heldRequests and
|
|
463
|
+
* keptRequests counting the requests among them: a boundary keeps some of
|
|
464
|
+
* what was held, a flush that crosses one holds the rest of its snapshot
|
|
465
|
+
* again, and a flush starts from nothing held. Notifications need no cap
|
|
466
|
+
* here: hold caps them kept or not, and what this is given never has more
|
|
467
|
+
* than was held. */
|
|
468
|
+
private replaceHeld(items: HeldItem[]): void {
|
|
469
|
+
// The kept bound holds here too: boundaries that come with no flush
|
|
470
|
+
// between them each keep what the next transport held, so it's checked
|
|
471
|
+
// where they're joined. The earliest kept are kept, as at hold.
|
|
472
|
+
let kept = 0;
|
|
473
|
+
let past = 0;
|
|
474
|
+
this.bufferedEvents = items.filter((item) => {
|
|
475
|
+
if (item.kept !== true || !McplServerConnection.hasResponder(item)) return true;
|
|
476
|
+
if (kept < McplServerConnection.MAX_HELD_REQUESTS) { kept++; return true; }
|
|
477
|
+
past++;
|
|
478
|
+
return false;
|
|
479
|
+
});
|
|
480
|
+
this.heldRequests = this.bufferedEvents.filter((item) => !item.kept && McplServerConnection.hasResponder(item)).length;
|
|
481
|
+
this.keptRequests = kept;
|
|
482
|
+
if (past > 0) {
|
|
483
|
+
this.droppedKeptRequests += past;
|
|
484
|
+
console.error(
|
|
485
|
+
`[mcpl] ${this.id}: kept-request bound (${McplServerConnection.MAX_HELD_REQUESTS}) — ` +
|
|
486
|
+
`dropped ${past} request(s) a closed transport left`,
|
|
487
|
+
);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
244
491
|
/** The active transport (stdio child or WebSocket). Null for a disconnected
|
|
245
492
|
* stub awaiting its first background reconnect. */
|
|
246
493
|
private transport: McplTransport | null;
|
|
@@ -255,17 +502,30 @@ export class McplServerConnection extends EventEmitter {
|
|
|
255
502
|
private pendingRequests = new Map<string | number, PendingRequest>();
|
|
256
503
|
/** Metadata retained after selected request deadlines so a late response is
|
|
257
504
|
* observable even though its original promise has already been rejected. */
|
|
258
|
-
private orphanedRequests = new Map<string | number, {
|
|
505
|
+
private orphanedRequests = new Map<string | number, {
|
|
506
|
+
method: string;
|
|
507
|
+
renderParams?: { featureSet: string; key: string; eventId: string; channelId?: string };
|
|
508
|
+
}>();
|
|
259
509
|
private closed = false;
|
|
260
510
|
/** Set by `reconnectAfterFailure()` before it closes a live transport, so
|
|
261
511
|
* the transport close handler reports that teardown as `byHost: true`. */
|
|
262
512
|
private hostCloseRequested = false;
|
|
513
|
+
/** Set by `close()`: this host has stopped the connection, as a removal and
|
|
514
|
+
* a restart both do, so a request after it can't say whether the server
|
|
515
|
+
* comes back. */
|
|
516
|
+
private closedByThisHost = false;
|
|
517
|
+
/** The host's close attempt: in flight, or done. A failed one is cleared,
|
|
518
|
+
* so the next close() tries again. */
|
|
263
519
|
private closePromise: Promise<void> | null = null;
|
|
520
|
+
/** The host closed this connection while it was live, and its teardown
|
|
521
|
+
* hasn't completed yet: 'close' is emitted once it has. */
|
|
264
522
|
private closeIncomplete = false;
|
|
265
|
-
private reconnectPromise: Promise<void> | null = null;
|
|
266
523
|
|
|
267
524
|
/** Per-request timeout in ms (0 disables). See McplServerConfig.requestTimeoutMs. */
|
|
268
525
|
private requestTimeoutMs = DEFAULT_REQUEST_TIMEOUT_MS;
|
|
526
|
+
/** The deadline a channels/publish or channels/open request keeps when
|
|
527
|
+
* requestTimeoutMs is 0 (see sendChannelsPublish). */
|
|
528
|
+
private publishDeadlineMs = DEFAULT_REQUEST_TIMEOUT_MS;
|
|
269
529
|
|
|
270
530
|
// Event buffering: control-plane traffic can be released independently from
|
|
271
531
|
// inference-bearing data-plane traffic. This lets a server finish its own
|
|
@@ -273,16 +533,38 @@ export class McplServerConnection extends EventEmitter {
|
|
|
273
533
|
// reconciliation keeps agent wakes behind a durable barrier.
|
|
274
534
|
private controlPlaneReady = false;
|
|
275
535
|
private dataPlaneReady = false;
|
|
276
|
-
private bufferedEvents:
|
|
277
|
-
/**
|
|
278
|
-
*
|
|
279
|
-
private
|
|
536
|
+
private bufferedEvents: HeldItem[] = [];
|
|
537
|
+
/** How many of bufferedEvents are server→host requests, each holding a
|
|
538
|
+
* responder its server waits on. Every change to bufferedEvents keeps it. */
|
|
539
|
+
private heldRequests = 0;
|
|
540
|
+
/** How many of bufferedEvents are requests a transport boundary kept:
|
|
541
|
+
* incoming messages and pushes whose transport has closed. No server waits
|
|
542
|
+
* on them, since their answers can't reach it (sameTransport), so they
|
|
543
|
+
* stay out of the held-request budget, which bounds what a live server
|
|
544
|
+
* waits on: a dead process's backlog must not refuse the next process's
|
|
545
|
+
* registration. They have their own bound at the same size, past which a
|
|
546
|
+
* kept request is dropped and logged, there being no one to refuse. */
|
|
547
|
+
private keptRequests = 0;
|
|
548
|
+
/** Kept requests dropped at their bound since the planes last opened
|
|
549
|
+
* (ready()), so the first of each run of drops is logged. */
|
|
550
|
+
private droppedKeptRequests = 0;
|
|
551
|
+
/** Cap on held notifications (responder-less events) during a plane pause
|
|
552
|
+
* (issue #122), counted apart from requests: past it, the oldest held
|
|
553
|
+
* notification is dropped. */
|
|
554
|
+
private static readonly MAX_HELD_NOTIFICATIONS = 1000;
|
|
555
|
+
/** Budget of held requests during a plane pause (#387), counted apart from
|
|
556
|
+
* notifications: past it, a request is refused at once, never dropped. It
|
|
557
|
+
* bounds a server that pipelines requests without waiting for answers. A
|
|
558
|
+
* server that doesn't send a refused request again loses it (discord-mcpl
|
|
559
|
+
* doesn't), so the budget sits far above the notifications' cap, to leave
|
|
560
|
+
* a busy server's long pause room. Requests a transport boundary kept are
|
|
561
|
+
* counted apart from it (keptRequests), with a bound of the same size past
|
|
562
|
+
* which they're dropped, since no server waits on them. */
|
|
563
|
+
private static readonly MAX_HELD_REQUESTS = 10_000;
|
|
280
564
|
private droppedBufferedEvents = 0;
|
|
281
|
-
/**
|
|
282
|
-
*
|
|
283
|
-
|
|
284
|
-
* NEW arrivals during a pause, not churn from control-plane flushes). */
|
|
285
|
-
private flushDepth = 0;
|
|
565
|
+
/** Requests refused at the budget since the planes last opened (ready()),
|
|
566
|
+
* so each pause's first refusal is logged. */
|
|
567
|
+
private refusedHeldRequests = 0;
|
|
286
568
|
|
|
287
569
|
// Reconnect state (adapted from Anarchid/agent-framework@mcpl-module-proto)
|
|
288
570
|
private config: McplServerConfig | null = null;
|
|
@@ -294,13 +576,37 @@ export class McplServerConnection extends EventEmitter {
|
|
|
294
576
|
/** Consecutive failed connect attempts since the last successful handshake.
|
|
295
577
|
* Drives the exponential backoff and is reported on reconnect events. */
|
|
296
578
|
private reconnectAttempts = 0;
|
|
579
|
+
/** The reconnect attempt in flight, so close() can abort it and wait for
|
|
580
|
+
* its cleanup. */
|
|
581
|
+
private reconnecting: { abort: AbortController; done: Promise<void> } | null = null;
|
|
582
|
+
/**
|
|
583
|
+
* Launches whose cleanup couldn't confirm that the child exited. While any
|
|
584
|
+
* is held, reconnecting is halted, since another launch could run beside
|
|
585
|
+
* one that's still alive. close() retries their reap.
|
|
586
|
+
*/
|
|
587
|
+
private unreaped: Array<{ transport: McplTransport; failure: Error }> = [];
|
|
297
588
|
|
|
298
589
|
/** Whether a background reconnect loop will revive this connection after a
|
|
299
|
-
* drop. False once close() has been called (explicit close stops retrying)
|
|
590
|
+
* drop. False once close() has been called (explicit close stops retrying),
|
|
591
|
+
* after a protocol-version verdict, and while a launch is unreaped. */
|
|
300
592
|
get willReconnect(): boolean {
|
|
301
593
|
return this.reconnectEnabled;
|
|
302
594
|
}
|
|
303
595
|
|
|
596
|
+
/** Whether a child this connection launched couldn't be confirmed exited.
|
|
597
|
+
* While it's true the connection owns that child, so whatever registered
|
|
598
|
+
* it keeps its id held; close() retries the reap. */
|
|
599
|
+
get holdsUnreapedChild(): boolean {
|
|
600
|
+
return this.unreaped.length > 0;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
/** Closed with no reconnect to come: after close(), whether or not its
|
|
604
|
+
* transport close succeeded, or a drop with reconnect off. A connection
|
|
605
|
+
* between retries keeps reconnectEnabled, so it isn't closed for good. */
|
|
606
|
+
private get closedForGood(): boolean {
|
|
607
|
+
return this.closed && !this.reconnectEnabled;
|
|
608
|
+
}
|
|
609
|
+
|
|
304
610
|
/**
|
|
305
611
|
* Private constructor. Use `McplServerConnection.connect()` instead.
|
|
306
612
|
*/
|
|
@@ -328,60 +634,206 @@ export class McplServerConnection extends EventEmitter {
|
|
|
328
634
|
* Mark the connection as ready — flushes any events that arrived between
|
|
329
635
|
* construction (when setupMessageRouting starts emitting) and now (when
|
|
330
636
|
* the caller has attached listeners via wireMcplEvents).
|
|
637
|
+
*
|
|
638
|
+
* Once the connection is closed for good it readies nothing. Its policy was
|
|
639
|
+
* reset when it closed, so what the server sent that needs a grant would be
|
|
640
|
+
* refused, and a closed connection sends no response, so nothing it held
|
|
641
|
+
* could be answered. It delivers only its own reports of how it ended (see
|
|
642
|
+
* {@link deliverOwnReports}). A connection between retries isn't closed for
|
|
643
|
+
* good: what it held, such as a reconnect stub's connect-failed, reaches
|
|
644
|
+
* its listeners as it does any connection's, except the incoming messages
|
|
645
|
+
* and pushes a transport boundary kept, which wait for the next
|
|
646
|
+
* transport's grant (see {@link flushHeld}).
|
|
331
647
|
*/
|
|
332
648
|
ready(): void {
|
|
649
|
+
if (this.closedForGood) {
|
|
650
|
+
this.deliverOwnReports();
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
333
653
|
this.controlPlaneReady = true;
|
|
334
654
|
this.dataPlaneReady = true;
|
|
335
655
|
const pending = this.bufferedEvents;
|
|
336
|
-
this.
|
|
656
|
+
this.replaceHeld([]);
|
|
657
|
+
// The cap's drops are counted per pause: this flush ends one, so its line
|
|
658
|
+
// reports that pause's drops, and the next pause counts (and logs) its own.
|
|
659
|
+
const dropped = this.droppedBufferedEvents;
|
|
660
|
+
this.droppedBufferedEvents = 0;
|
|
661
|
+
this.refusedHeldRequests = 0;
|
|
662
|
+
this.droppedKeptRequests = 0;
|
|
337
663
|
if (pending.length > 200) {
|
|
338
664
|
console.error(
|
|
339
665
|
`[mcpl] ${this.id}: flushing ${pending.length} buffered event(s) synchronously ` +
|
|
340
|
-
`(long pause backlog${
|
|
666
|
+
`(long pause backlog${dropped > 0 ? `; ${dropped} dropped at cap` : ''})`,
|
|
341
667
|
);
|
|
342
668
|
}
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
669
|
+
// Re-enter the gate for every buffered item. A control handler (notably
|
|
670
|
+
// tools/list_changed) may install a newer data-plane barrier while this
|
|
671
|
+
// snapshot is being flushed; later data items must observe that pause.
|
|
672
|
+
this.flushHeld(pending);
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
/**
|
|
676
|
+
* Re-emit a held snapshot through the admission gate (this.emit), inside
|
|
677
|
+
* the containment boundary every inbound event shares
|
|
678
|
+
* (dispatchInboundEvent), in admission order (admissionOrder). A transport
|
|
679
|
+
* boundary crossed during the flush (a listener that closes the connection,
|
|
680
|
+
* say) leaves the rest of the snapshot to the boundary rule, as if it had
|
|
681
|
+
* still been held: what the server said about its own process is dropped,
|
|
682
|
+
* and the rest is held again, after whatever this flush held.
|
|
683
|
+
*
|
|
684
|
+
* Between transports, the incoming messages and pushes a boundary kept
|
|
685
|
+
* stay held for the next transport's grant: admitted now, they would meet
|
|
686
|
+
* the empty grant the boundary left, with no transport to answer on. The
|
|
687
|
+
* host's own reports about the transport (a stub's connect-failed, say)
|
|
688
|
+
* need no grant and go on.
|
|
689
|
+
*/
|
|
690
|
+
private flushHeld(snapshot: HeldItem[]): void {
|
|
691
|
+
const waits = (item: HeldItem) => item.kept === true && McplServerConnection.WORLD_EVENTS.has(item.event);
|
|
692
|
+
let pending = snapshot;
|
|
693
|
+
if (!this.isConnected && snapshot.some(waits)) {
|
|
694
|
+
this.replaceHeld([...this.bufferedEvents, ...snapshot.filter(waits)]);
|
|
695
|
+
pending = snapshot.filter((item) => !waits(item));
|
|
696
|
+
}
|
|
697
|
+
const ordered = McplServerConnection.admissionOrder(pending);
|
|
698
|
+
const epoch = this.transportEpoch;
|
|
699
|
+
for (let index = 0; index < ordered.length; index++) {
|
|
700
|
+
if (this.transportEpoch !== epoch) {
|
|
701
|
+
this.replaceHeld([...this.bufferedEvents, ...this.keepAcrossBoundary(ordered.slice(index))]);
|
|
702
|
+
return;
|
|
703
|
+
}
|
|
704
|
+
const { event, args, kept } = ordered[index]!;
|
|
705
|
+
// What it can't admit yet is held again, as the admission gate would
|
|
706
|
+
// hold it, with what a boundary kept still marked as kept.
|
|
707
|
+
if (kept && !this.planeReadyFor(event)) this.hold(event, args, kept);
|
|
708
|
+
else this.dispatchInboundEvent(event, ...args);
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
/** Whether the plane an event belongs to is open (lifecycle events always
|
|
713
|
+
* pass), as the admission gate (emit) decides it. */
|
|
714
|
+
private planeReadyFor(event: string): boolean {
|
|
715
|
+
return McplServerConnection.isLifecycleEvent(event)
|
|
716
|
+
|| (McplServerConnection.isControlPlaneEvent(event) ? this.controlPlaneReady : this.dataPlaneReady);
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
/**
|
|
720
|
+
* The order a flush admits a held snapshot in: as it was held, except that
|
|
721
|
+
* when it holds items a transport boundary kept, the current transport's
|
|
722
|
+
* channel registrations (`channels-register`, `channels-changed`) go
|
|
723
|
+
* first, each unless it names a channel that something the transport sent
|
|
724
|
+
* before it, and that stays where it was held, also names (below). The
|
|
725
|
+
* registry refuses a message for a channel it doesn't know
|
|
726
|
+
* (§14.5). It keeps a server's channels across a reconnect, but a
|
|
727
|
+
* registration the old process sent that was still held at the boundary
|
|
728
|
+
* was dropped with it (keepAcrossBoundary), so the next process registers
|
|
729
|
+
* that channel anew, behind what was kept: a kept message for it admitted
|
|
730
|
+
* first would be refused, and lost, since a bridge that recorded it as
|
|
731
|
+
* forwarded doesn't send it again.
|
|
732
|
+
* Nothing else moves, so a snapshot with nothing kept is flushed as it was
|
|
733
|
+
* held. A registration also passes whatever its own transport sent before
|
|
734
|
+
* it, which a server that announces its channels first, as a bridge does,
|
|
735
|
+
* never has. A removal stays where it was held, with whatever else it adds
|
|
736
|
+
* or updates, as everything does that isn't a registration. A registration
|
|
737
|
+
* that names a channel which an earlier removal, or an earlier registration
|
|
738
|
+
* that stays, names stays where it was held too, so the writes to one
|
|
739
|
+
* channel keep their order: a channel's removal and its re-registration,
|
|
740
|
+
* and two registrations or updates of it. In a flush that opens both
|
|
741
|
+
* planes, what the transport sent before a removal is admitted before it,
|
|
742
|
+
* its messages for that channel included. A flush that opens only the control
|
|
743
|
+
* plane, as during a quiesce, admits the control traffic, a removal
|
|
744
|
+
* included, and holds the data plane's again until that plane opens, as
|
|
745
|
+
* live traffic is during a quiesce: a message held for a channel a removal
|
|
746
|
+
* took is then refused as the data plane opens.
|
|
747
|
+
*/
|
|
748
|
+
private static admissionOrder(pending: HeldItem[]): HeldItem[] {
|
|
749
|
+
if (!pending.some((item) => item.kept)) return pending;
|
|
750
|
+
const registrations = new Set<HeldItem>();
|
|
751
|
+
// The channels named so far by a removal or registration that stays
|
|
752
|
+
// where it was held.
|
|
753
|
+
const staying = new Set<string>();
|
|
754
|
+
for (const item of pending) {
|
|
755
|
+
if (item.kept) continue;
|
|
756
|
+
if (item.event !== 'channels-register' && item.event !== 'channels-changed') continue;
|
|
757
|
+
const removes = McplServerConnection.removedChannelIds(item);
|
|
758
|
+
const registers = McplServerConnection.registeredChannelIds(item);
|
|
759
|
+
if (removes.length === 0 && !registers.some((id) => staying.has(id))) {
|
|
760
|
+
registrations.add(item);
|
|
761
|
+
continue;
|
|
350
762
|
}
|
|
351
|
-
|
|
352
|
-
this.flushDepth--;
|
|
763
|
+
for (const id of [...removes, ...registers]) staying.add(id);
|
|
353
764
|
}
|
|
765
|
+
if (registrations.size === 0) return pending;
|
|
766
|
+
return [...registrations, ...pending.filter((item) => !registrations.has(item))];
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
/** The channels a held `channels-changed` removes. The registry takes any
|
|
770
|
+
* entry of `removed` as a channel id, by its string form, so a number
|
|
771
|
+
* removes the channel of its digits; an object is left out here, since
|
|
772
|
+
* its string form can throw. */
|
|
773
|
+
private static removedChannelIds(item: HeldItem): string[] {
|
|
774
|
+
if (item.event !== 'channels-changed') return [];
|
|
775
|
+
const params = item.args[0] as { removed?: unknown } | null | undefined;
|
|
776
|
+
return Array.isArray(params?.removed)
|
|
777
|
+
? params.removed.filter((id) => typeof id !== 'object' || id === null).map((id) => String(id))
|
|
778
|
+
: [];
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
/** The channels a held `channels-register` registers, or a held
|
|
782
|
+
* `channels-changed` adds or updates. */
|
|
783
|
+
private static registeredChannelIds(item: HeldItem): string[] {
|
|
784
|
+
const params = item.args[0] as { channels?: unknown; added?: unknown; updated?: unknown } | null | undefined;
|
|
785
|
+
const lists = item.event === 'channels-register' ? [params?.channels] : [params?.added, params?.updated];
|
|
786
|
+
return lists.flatMap((list) => (Array.isArray(list) ? list : []))
|
|
787
|
+
.map((channel) => (channel as { id?: unknown } | null)?.id)
|
|
788
|
+
.filter((id): id is string => typeof id === 'string');
|
|
354
789
|
}
|
|
355
790
|
|
|
356
791
|
/**
|
|
357
792
|
* Release only server-establishment and lifecycle/control traffic. Responses
|
|
358
793
|
* to host requests are never event-buffered, so tools/list and tools/call
|
|
359
794
|
* responses remain live as well. Inference-bearing requests stay queued in
|
|
360
|
-
* their original relative order until {@link ready} is called.
|
|
795
|
+
* their original relative order until {@link ready} is called. Like
|
|
796
|
+
* {@link ready}, once the connection is closed for good it delivers only
|
|
797
|
+
* its own reports of how it ended.
|
|
361
798
|
*/
|
|
362
799
|
readyControlPlane(): void {
|
|
800
|
+
if (this.closedForGood) {
|
|
801
|
+
this.deliverOwnReports();
|
|
802
|
+
return;
|
|
803
|
+
}
|
|
363
804
|
this.controlPlaneReady = true;
|
|
364
805
|
const pending = this.bufferedEvents;
|
|
365
|
-
this.
|
|
366
|
-
this.
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
806
|
+
this.replaceHeld([]);
|
|
807
|
+
// Re-enter the ADMISSION gate (this.emit), never super.emit: control
|
|
808
|
+
// events include channels-register/changed, which are grant-gated —
|
|
809
|
+
// the old direct flush bypassed positive-grant enforcement entirely
|
|
810
|
+
// (PR #79 review blocker 3). Data-plane items re-buffer in order via
|
|
811
|
+
// the same call, since dataPlaneReady is still false.
|
|
812
|
+
this.flushHeld(pending);
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
/**
|
|
816
|
+
* A connection closed for good still says how it ended. Its own buffered
|
|
817
|
+
* connect-failed and reconnect-failed, such as the permanent failure of a
|
|
818
|
+
* launch held as unreaped before the first gate released it, are the
|
|
819
|
+
* framework's records of it: they need no grant, and its listeners act on
|
|
820
|
+
* them (a trace, and for a permanent failure an alert and its tools leaving
|
|
821
|
+
* the list). What the server sent stays held.
|
|
822
|
+
*/
|
|
823
|
+
private deliverOwnReports(): void {
|
|
824
|
+
const own = (item: { event: string }) => item.event === 'connect-failed' || item.event === 'reconnect-failed';
|
|
825
|
+
const reports = this.bufferedEvents.filter(own);
|
|
826
|
+
if (reports.length === 0) return;
|
|
827
|
+
// The host's own reports have no responder, so the request counts stand.
|
|
828
|
+
this.bufferedEvents = this.bufferedEvents.filter((item) => !own(item));
|
|
829
|
+
for (const { event, args } of reports) super.emit(event, ...args);
|
|
379
830
|
}
|
|
380
831
|
|
|
381
832
|
/**
|
|
382
833
|
* Close the data plane synchronously while leaving an already-released
|
|
383
|
-
* control plane live. Used before
|
|
384
|
-
*
|
|
834
|
+
* control plane live. Used before list-change reconciliation so no new
|
|
835
|
+
* inference event can beat barrier installation. A reconnect closes both
|
|
836
|
+
* planes instead.
|
|
385
837
|
*/
|
|
386
838
|
pauseDataPlane(): void {
|
|
387
839
|
this.dataPlaneReady = false;
|
|
@@ -394,12 +846,7 @@ export class McplServerConnection extends EventEmitter {
|
|
|
394
846
|
*/
|
|
395
847
|
override emit(event: string | symbol, ...args: unknown[]): boolean {
|
|
396
848
|
const name = typeof event === 'string' ? event : '';
|
|
397
|
-
if (
|
|
398
|
-
McplServerConnection.isLifecycleEvent(name)
|
|
399
|
-
|| (McplServerConnection.isControlPlaneEvent(name)
|
|
400
|
-
? this.controlPlaneReady
|
|
401
|
-
: this.dataPlaneReady)
|
|
402
|
-
) {
|
|
849
|
+
if (this.planeReadyFor(name)) {
|
|
403
850
|
// Positive-grant admission (§5.4/§14.1), at the single choke-point
|
|
404
851
|
// every path shares: live routing AND buffer flushes re-enter here, so
|
|
405
852
|
// an event buffered while the connection was staged is authorized
|
|
@@ -433,40 +880,119 @@ export class McplServerConnection extends EventEmitter {
|
|
|
433
880
|
}
|
|
434
881
|
return true;
|
|
435
882
|
}
|
|
883
|
+
// §6.6: a request that will never be answered MUST get an error. A
|
|
884
|
+
// request whose event nothing on this host listens for would never be.
|
|
885
|
+
const request = args[1] as {
|
|
886
|
+
respond?: (result: unknown) => void;
|
|
887
|
+
respondError?: (code: number, message: string, data?: unknown) => void;
|
|
888
|
+
} | undefined;
|
|
889
|
+
if (typeof request?.respondError === 'function' && this.listenerCount(event) === 0) {
|
|
890
|
+
request.respondError(-32601, 'Method not found');
|
|
891
|
+
console.error(`[mcpl] ${this.id}: answered -32601 to ${logValue(name)}, which nothing on this host handles`);
|
|
892
|
+
return true;
|
|
893
|
+
}
|
|
894
|
+
// mcpl/manifestChanged (§17.3) and tools/list_changed are notifications.
|
|
895
|
+
// Sent as a request, either is answered with an empty result as it's
|
|
896
|
+
// admitted, and passed on as the notification is, with no responder:
|
|
897
|
+
// the host acts on it the same way, so the answer doesn't depend on what
|
|
898
|
+
// the refresh does, and it's written before any request the refresh
|
|
899
|
+
// sends. This is where live routing and buffer flushes meet, so a request
|
|
900
|
+
// held while its plane is closed is answered only when it's let through,
|
|
901
|
+
// never by a startup that fails first.
|
|
902
|
+
if ((name === 'manifest-changed' || name === 'tools-list-changed') && typeof request?.respond === 'function') {
|
|
903
|
+
request.respond({});
|
|
904
|
+
return super.emit(event, args[0]);
|
|
905
|
+
}
|
|
436
906
|
return super.emit(event, ...args);
|
|
437
907
|
}
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
908
|
+
return this.hold(name, args);
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
/**
|
|
912
|
+
* Hold an event until its plane opens, `kept` when a transport boundary
|
|
913
|
+
* keeps it for the next transport's grant (afterBoundary).
|
|
914
|
+
*/
|
|
915
|
+
private hold(name: string, args: unknown[], kept?: true): true {
|
|
916
|
+
// Held until the plane opens. A long pause (a quiesce, say) on a busy or a
|
|
917
|
+
// runaway server would otherwise grow this array without bound, so
|
|
918
|
+
// requests and notifications each have a bound, counted apart. A request
|
|
919
|
+
// holds a responder its server waits on, and dropping it would strand that
|
|
920
|
+
// call, so past MAX_HELD_REQUESTS (#387) it's refused at once with -32000,
|
|
921
|
+
// the code of the host's other transient refusals, and its server can send
|
|
922
|
+
// it again. A notification has no one to tell, so past
|
|
923
|
+
// MAX_HELD_NOTIFICATIONS (issue #122) the oldest held notification is
|
|
924
|
+
// dropped to make room. A request a closed transport left (`kept`) has
|
|
925
|
+
// no server waiting on it, so it's counted apart, as keptRequests, and
|
|
926
|
+
// dropped past its own bound. A flush takes the whole buffer and zeroes
|
|
927
|
+
// its counts, so what it holds again was admitted within these bounds
|
|
928
|
+
// and stays within them.
|
|
929
|
+
const responder = args[1] as { respond?: unknown; respondError?: (code: number, message: string) => void } | undefined;
|
|
930
|
+
const isRequest = McplServerConnection.hasResponder({ args });
|
|
931
|
+
if (isRequest && kept) {
|
|
932
|
+
// A request a closed transport left (keptRequests): no server waits on
|
|
933
|
+
// it, so past its own bound it's dropped, with nobody to refuse.
|
|
934
|
+
if (this.keptRequests >= McplServerConnection.MAX_HELD_REQUESTS) {
|
|
935
|
+
this.droppedKeptRequests++;
|
|
936
|
+
if (this.droppedKeptRequests % 100 === 1) {
|
|
452
937
|
console.error(
|
|
453
|
-
`[mcpl] ${this.id}:
|
|
454
|
-
`dropped ${this.
|
|
938
|
+
`[mcpl] ${this.id}: kept-request bound (${McplServerConnection.MAX_HELD_REQUESTS}) — ` +
|
|
939
|
+
`dropped ${this.droppedKeptRequests} request(s) a closed transport left`,
|
|
455
940
|
);
|
|
456
941
|
}
|
|
457
|
-
const dropIndex = this.bufferedEvents.findIndex((item) => !hasResponder(item));
|
|
458
|
-
if (dropIndex >= 0) {
|
|
459
|
-
this.bufferedEvents.splice(dropIndex, 1); // drop-oldest non-request
|
|
460
|
-
this.bufferedEvents.push({ event: name, args });
|
|
461
|
-
}
|
|
462
|
-
// else: buffer is all held requests — drop the incoming event instead.
|
|
463
942
|
return true;
|
|
464
943
|
}
|
|
944
|
+
this.bufferedEvents.push({ event: name, args, kept });
|
|
945
|
+
this.keptRequests++;
|
|
946
|
+
return true;
|
|
465
947
|
}
|
|
466
|
-
|
|
948
|
+
if (isRequest && this.heldRequests >= McplServerConnection.MAX_HELD_REQUESTS) {
|
|
949
|
+
this.refusedHeldRequests++;
|
|
950
|
+
if (this.refusedHeldRequests % 100 === 1) {
|
|
951
|
+
console.error(
|
|
952
|
+
`[mcpl] ${this.id}: held-request budget (${McplServerConnection.MAX_HELD_REQUESTS}) — ` +
|
|
953
|
+
`refused ${this.refusedHeldRequests} request(s) with -32000 while the plane is paused`,
|
|
954
|
+
);
|
|
955
|
+
}
|
|
956
|
+
responder?.respondError?.(
|
|
957
|
+
-32000,
|
|
958
|
+
`the host is busy: ${McplServerConnection.MAX_HELD_REQUESTS} of this server's requests are already waiting; send this one again later`,
|
|
959
|
+
);
|
|
960
|
+
return true;
|
|
961
|
+
}
|
|
962
|
+
if (!isRequest && this.bufferedEvents.length - this.heldRequests - this.keptRequests >= McplServerConnection.MAX_HELD_NOTIFICATIONS) {
|
|
963
|
+
this.droppedBufferedEvents++;
|
|
964
|
+
if (this.droppedBufferedEvents % 100 === 1) {
|
|
965
|
+
console.error(
|
|
966
|
+
`[mcpl] ${this.id}: buffered-event cap (${McplServerConnection.MAX_HELD_NOTIFICATIONS}) — ` +
|
|
967
|
+
`dropped ${this.droppedBufferedEvents} responder-less event(s) while the plane is paused`,
|
|
968
|
+
);
|
|
969
|
+
}
|
|
970
|
+
const oldest = this.bufferedEvents.findIndex((item) => !McplServerConnection.hasResponder(item));
|
|
971
|
+
if (oldest >= 0) this.bufferedEvents.splice(oldest, 1);
|
|
972
|
+
}
|
|
973
|
+
this.bufferedEvents.push(kept ? { event: name, args, kept } : { event: name, args });
|
|
974
|
+
if (isRequest) this.heldRequests++;
|
|
467
975
|
return true;
|
|
468
976
|
}
|
|
469
977
|
|
|
978
|
+
/** A buffered server→host request: it carries a responder its server waits on. */
|
|
979
|
+
private static hasResponder(item: { args: unknown[] }): boolean {
|
|
980
|
+
const responder = item.args[1] as { respond?: unknown } | undefined;
|
|
981
|
+
return typeof responder?.respond === 'function';
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
/** Events routed from a server's transport that report the world, which
|
|
985
|
+
* the next process doesn't send again when its bridge records them as
|
|
986
|
+
* forwarded first: kept at a transport boundary while held. */
|
|
987
|
+
private static readonly WORLD_EVENTS: ReadonlySet<string> = new Set(['channels-incoming', 'push-event']);
|
|
988
|
+
|
|
989
|
+
/** Events routed from a server's transport that speak for its process, not
|
|
990
|
+
* the world: dropped at a transport boundary while held. */
|
|
991
|
+
private static get PROCESS_EVENTS(): ReadonlySet<string> {
|
|
992
|
+
return new Set(Object.values(McplServerConnection.METHOD_TO_EVENT)
|
|
993
|
+
.filter((event) => !McplServerConnection.WORLD_EVENTS.has(event)));
|
|
994
|
+
}
|
|
995
|
+
|
|
470
996
|
/** Event-name form of METHOD_TO_REQUIRED_CAPABILITY, for admission-time
|
|
471
997
|
* enforcement in emit(). */
|
|
472
998
|
private static readonly EVENT_TO_REQUIRED_CAPABILITY: Record<string, string> = {
|
|
@@ -518,24 +1044,55 @@ export class McplServerConnection extends EventEmitter {
|
|
|
518
1044
|
* capability negotiation, and return a ready-to-use connection.
|
|
519
1045
|
*
|
|
520
1046
|
* Works over stdio (spawned child) or WebSocket, selected by `config` (see
|
|
521
|
-
* {@link openTransport}). Throws if the
|
|
522
|
-
*
|
|
1047
|
+
* {@link openTransport}). Throws if the configuration can't be used (before
|
|
1048
|
+
* anything is spawned or dialed), the transport can't be opened, the server
|
|
1049
|
+
* closes/errors before the handshake, or the handshake times out. If the
|
|
1050
|
+
* failed launch's cleanup can't confirm its child exited, it throws
|
|
1051
|
+
* {@link McplUnreapedLaunchError}, whose connection owns that launch.
|
|
523
1052
|
*/
|
|
524
1053
|
static async connect(
|
|
525
1054
|
config: McplServerConfig,
|
|
526
1055
|
hostCapabilities: McplHostCapabilities,
|
|
1056
|
+
signal?: AbortSignal,
|
|
527
1057
|
): Promise<McplServerConnection> {
|
|
528
|
-
|
|
1058
|
+
McplServerConnection.checkConfig(config);
|
|
1059
|
+
return McplServerConnection.connectChecked(config, hostCapabilities, signal);
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
/** {@link connect}, for a configuration its caller has already checked
|
|
1063
|
+
* (and warned about) once. `signal` ends the connect at once, its launch
|
|
1064
|
+
* reaped as for any failed handshake: its caller's stop() or a
|
|
1065
|
+
* disconnect of its id. */
|
|
1066
|
+
private static async connectChecked(
|
|
1067
|
+
config: McplServerConfig,
|
|
1068
|
+
hostCapabilities: McplHostCapabilities,
|
|
1069
|
+
signal?: AbortSignal,
|
|
1070
|
+
): Promise<McplServerConnection> {
|
|
1071
|
+
let established: Awaited<ReturnType<typeof McplServerConnection.handshake>>;
|
|
1072
|
+
try {
|
|
1073
|
+
established = await McplServerConnection.handshake(config, hostCapabilities, signal);
|
|
1074
|
+
} catch (error) {
|
|
1075
|
+
if (!(error instanceof HandshakeCleanupError)) throw error;
|
|
1076
|
+
// The launch may still be running. A connection owns it, closed and
|
|
1077
|
+
// halted, so the caller can retry the reap by closing it.
|
|
1078
|
+
const owner = McplServerConnection.createDisconnectedStub(config, hostCapabilities);
|
|
1079
|
+
owner.holdUnreaped(error.transport, error.cleanup);
|
|
1080
|
+
throw new McplUnreapedLaunchError(owner, error.failure, error.cleanup);
|
|
1081
|
+
}
|
|
1082
|
+
const { transport, capabilities, droppedCapabilities, mcpToolsAdvertised, protocolVersion } = established;
|
|
529
1083
|
|
|
530
1084
|
const connection = new McplServerConnection(config.id, capabilities, transport);
|
|
531
1085
|
connection.droppedCapabilities = droppedCapabilities;
|
|
532
1086
|
connection.mcpToolsAdvertised = mcpToolsAdvertised;
|
|
1087
|
+
connection.protocolVersion = protocolVersion;
|
|
533
1088
|
connection.allowHostCommands = config.allowHostCommands === true;
|
|
534
1089
|
connection.requestTimeoutMs = config.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
535
1090
|
|
|
536
|
-
//
|
|
537
|
-
|
|
538
|
-
|
|
1091
|
+
// Its configuration, which a reconnect reads and every log line of the
|
|
1092
|
+
// server's text redacts with. Only `true` turns reconnect on, as at
|
|
1093
|
+
// every other reader of it.
|
|
1094
|
+
connection.config = config;
|
|
1095
|
+
if (config.reconnect === true) {
|
|
539
1096
|
connection.hostCapabilities = hostCapabilities;
|
|
540
1097
|
connection.reconnectEnabled = true;
|
|
541
1098
|
connection.reconnectIntervalMs = config.reconnectIntervalMs ?? 5000;
|
|
@@ -545,18 +1102,49 @@ export class McplServerConnection extends EventEmitter {
|
|
|
545
1102
|
return connection;
|
|
546
1103
|
}
|
|
547
1104
|
|
|
1105
|
+
/**
|
|
1106
|
+
* This engine's configuration check, made before anything is spawned or
|
|
1107
|
+
* dialed: every problem {@link checkServerConfig} finds, the same check the
|
|
1108
|
+
* framework applies, and the modern family, which belongs to
|
|
1109
|
+
* ModernMcpConnection (an http(s) url is never dialed as a WebSocket).
|
|
1110
|
+
* Every configuration that worked before still passes but the one
|
|
1111
|
+
* serverConfigProblems names.
|
|
1112
|
+
*/
|
|
1113
|
+
private static checkConfig(config: McplServerConfig): void {
|
|
1114
|
+
const { family } = checkServerConfig(config);
|
|
1115
|
+
if (family === 'modern') {
|
|
1116
|
+
throw new Error(
|
|
1117
|
+
`MCP server "${config.id}" is configured for modern MCP (${MODERN_MCP_PROTOCOL_VERSION}); ` +
|
|
1118
|
+
`McplServerConnection speaks the legacy family (${MCP_PROTOCOL_VERSION} + MCPL) only`,
|
|
1119
|
+
);
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
|
|
548
1123
|
/**
|
|
549
1124
|
* Open a fresh transport and run the MCP/MCPL initialize handshake over it,
|
|
550
1125
|
* returning the connected transport plus the negotiated capabilities. Shared
|
|
551
1126
|
* by {@link connect} and {@link attemptReconnect} so both transports handshake
|
|
552
1127
|
* identically. On any failure the transport is closed so a spawned child /
|
|
553
|
-
* open socket never leaks.
|
|
1128
|
+
* open socket never leaks. If that close can't confirm the child exited,
|
|
1129
|
+
* the failure is a {@link HandshakeCleanupError} carrying the transport, so
|
|
1130
|
+
* the caller keeps owning it. `signal` ends a handshake whose connection is
|
|
1131
|
+
* closed meanwhile: it cuts a WebSocket dial short, is checked once the
|
|
1132
|
+
* transport is open, and races the initialize answer.
|
|
554
1133
|
*/
|
|
555
1134
|
private static async handshake(
|
|
556
1135
|
config: McplServerConfig,
|
|
557
1136
|
hostCapabilities: McplHostCapabilities,
|
|
558
|
-
|
|
559
|
-
|
|
1137
|
+
signal?: AbortSignal,
|
|
1138
|
+
): Promise<{ transport: McplTransport; capabilities: McplCapabilities | null; droppedCapabilities: ReadonlySet<string>; mcpToolsAdvertised: boolean; protocolVersion: string }> {
|
|
1139
|
+
const closedWhileConnecting = () => new Error(`MCPL server "${config.id}" was closed while connecting`);
|
|
1140
|
+
if (signal?.aborted) throw closedWhileConnecting();
|
|
1141
|
+
let transport: McplTransport;
|
|
1142
|
+
try {
|
|
1143
|
+
transport = await openTransport(config, signal);
|
|
1144
|
+
} catch (error) {
|
|
1145
|
+
if (signal?.aborted) throw closedWhileConnecting();
|
|
1146
|
+
throw error;
|
|
1147
|
+
}
|
|
560
1148
|
// Once the handshake stops listening for 'error', this logs what the
|
|
561
1149
|
// transport reports, until the handshake hands the transport on, or for
|
|
562
1150
|
// good if the handshake fails. EventEmitter throws an 'error' that has no
|
|
@@ -564,10 +1152,11 @@ export class McplServerConnection extends EventEmitter {
|
|
|
564
1152
|
// WebSocket frame that arrives with the initialize response, or while a
|
|
565
1153
|
// failed handshake's close() runs, is such an error.
|
|
566
1154
|
const logAfterSettled = (err: Error) => {
|
|
567
|
-
console.error(`[mcpl] ${config.id}: transport error after the handshake settled: ${err.message}`);
|
|
1155
|
+
console.error(`[mcpl] ${config.id}: transport error after the handshake settled: ${redactServerText(err.message, config)}`);
|
|
568
1156
|
};
|
|
569
1157
|
|
|
570
1158
|
try {
|
|
1159
|
+
if (signal?.aborted) throw closedWhileConnecting();
|
|
571
1160
|
const initId = 0; // use id=0 for the handshake
|
|
572
1161
|
const initRequest: JsonRpcRequest = {
|
|
573
1162
|
jsonrpc: '2.0',
|
|
@@ -580,9 +1169,9 @@ export class McplServerConnection extends EventEmitter {
|
|
|
580
1169
|
},
|
|
581
1170
|
};
|
|
582
1171
|
|
|
583
|
-
// Await the initialize response, racing an early transport close/error
|
|
584
|
-
// a timeout.
|
|
585
|
-
const { mcpl: rawCapabilities, mcpToolsAdvertised } = await new Promise<{ mcpl: McplCapabilities | null; mcpToolsAdvertised: boolean }>((resolve, reject) => {
|
|
1172
|
+
// Await the initialize response, racing an early transport close/error,
|
|
1173
|
+
// a timeout and the owner's close. Every path cleans up its listeners.
|
|
1174
|
+
const { mcpl: rawCapabilities, mcpToolsAdvertised, protocolVersion } = await new Promise<{ mcpl: McplCapabilities | null; mcpToolsAdvertised: boolean; protocolVersion: string }>((resolve, reject) => {
|
|
586
1175
|
// When the transport fails, the end of the server's stderr often says
|
|
587
1176
|
// why, such as a missing token or a crash at startup. It goes on the
|
|
588
1177
|
// error as `stderrTail`, never into the message: an error's message
|
|
@@ -596,9 +1185,14 @@ export class McplServerConnection extends EventEmitter {
|
|
|
596
1185
|
transport.off('line', onLine);
|
|
597
1186
|
transport.off('close', onClose);
|
|
598
1187
|
transport.off('error', onError);
|
|
1188
|
+
signal?.removeEventListener('abort', onAbort);
|
|
599
1189
|
transport.on('error', logAfterSettled);
|
|
600
1190
|
clearTimeout(timer);
|
|
601
1191
|
};
|
|
1192
|
+
const onAbort = () => {
|
|
1193
|
+
cleanup();
|
|
1194
|
+
reject(closedWhileConnecting());
|
|
1195
|
+
};
|
|
602
1196
|
const onLine = (line: string) => {
|
|
603
1197
|
let msg: JsonRpcResponse;
|
|
604
1198
|
try {
|
|
@@ -606,13 +1200,49 @@ export class McplServerConnection extends EventEmitter {
|
|
|
606
1200
|
} catch {
|
|
607
1201
|
return; // Ignore non-JSON lines (e.g. logback output from Java servers)
|
|
608
1202
|
}
|
|
1203
|
+
if (msg === null || typeof msg !== 'object' || Array.isArray(msg)) return;
|
|
1204
|
+
// A request or a notification from the server is never the
|
|
1205
|
+
// initialize response, whatever its id: a server's own ids can start
|
|
1206
|
+
// at 0, this handshake's. MCP lets a server ping before
|
|
1207
|
+
// initialization completes, and the ping gets its empty result, as
|
|
1208
|
+
// on a connected transport. Anything else it sends now is ignored.
|
|
1209
|
+
if ('method' in msg) {
|
|
1210
|
+
const request = msg as unknown as JsonRpcRequest;
|
|
1211
|
+
if (request.method === 'ping' && request.id != null) {
|
|
1212
|
+
transport.writeLine(JSON.stringify({ jsonrpc: '2.0', id: request.id, result: {} }));
|
|
1213
|
+
}
|
|
1214
|
+
return;
|
|
1215
|
+
}
|
|
609
1216
|
if (msg.id !== initId) return;
|
|
610
1217
|
cleanup();
|
|
611
1218
|
if (msg.error) {
|
|
612
|
-
|
|
1219
|
+
// describeCause rather than the template's own conversion: the
|
|
1220
|
+
// server's `message` can be any JSON value, and one that has no
|
|
1221
|
+
// primitive form, such as {"toString":1}, would throw out of this
|
|
1222
|
+
// listener after cleanup() has cleared the timeout: an uncaught
|
|
1223
|
+
// exception, which ends a host that doesn't catch them, and a
|
|
1224
|
+
// handshake left to wait forever. It's the server's text in an
|
|
1225
|
+
// error's message, so it's bounded as errors.ts says.
|
|
1226
|
+
reject(msg.error.code === UNSUPPORTED_PROTOCOL_VERSION_CODE
|
|
1227
|
+
? protocolVersionRejection(config.id, msg.error)
|
|
1228
|
+
: new Error(`MCPL server "${config.id}" initialize error: ${boundedForLog(describeCause(msg.error.message))}`));
|
|
613
1229
|
return;
|
|
614
1230
|
}
|
|
615
1231
|
const result = msg.result as Record<string, unknown> | undefined;
|
|
1232
|
+
// The server's answer is the established revision. This engine
|
|
1233
|
+
// implements exactly one, so any other answer (or none) means the
|
|
1234
|
+
// two sides never agreed on what they're speaking.
|
|
1235
|
+
const returned = typeof result?.protocolVersion === 'string' ? result.protocolVersion : null;
|
|
1236
|
+
if (returned !== MCP_PROTOCOL_VERSION) {
|
|
1237
|
+
reject(new McplProtocolVersionError(
|
|
1238
|
+
`MCPL server "${config.id}" answered initialize with protocolVersion ` +
|
|
1239
|
+
`${returned === null ? '(none)' : JSON.stringify(returned)}; this client's legacy engine speaks only ${MCP_PROTOCOL_VERSION}`,
|
|
1240
|
+
'mismatch',
|
|
1241
|
+
returned,
|
|
1242
|
+
null,
|
|
1243
|
+
));
|
|
1244
|
+
return;
|
|
1245
|
+
}
|
|
616
1246
|
const caps = result?.capabilities as Record<string, unknown> | undefined;
|
|
617
1247
|
const experimental = caps?.experimental as Record<string, unknown> | undefined;
|
|
618
1248
|
// §5.1: `tools` capability is the OUTER standard MCP member — the
|
|
@@ -621,11 +1251,19 @@ export class McplServerConnection extends EventEmitter {
|
|
|
621
1251
|
resolve({
|
|
622
1252
|
mcpl: (experimental?.mcpl as McplCapabilities) ?? null,
|
|
623
1253
|
mcpToolsAdvertised: caps?.tools !== undefined,
|
|
1254
|
+
protocolVersion: returned,
|
|
624
1255
|
});
|
|
625
1256
|
};
|
|
626
1257
|
const onClose = (info: TransportCloseInfo) => {
|
|
627
1258
|
cleanup();
|
|
628
|
-
|
|
1259
|
+
const code = transport.kind === 'websocket' && typeof info.code === 'number' ? `, code ${info.code}` : '';
|
|
1260
|
+
const failure = `MCPL server "${config.id}" transport closed before handshake completed (${closeReasonText(info)}${code})`;
|
|
1261
|
+
// The close comes at a stdio child's exit, which Node can emit before
|
|
1262
|
+
// it has read the last of the child's stderr, or when the host closes
|
|
1263
|
+
// the transport, which then ends the child. The tail is taken once
|
|
1264
|
+
// stderr has ended, waiting at most STDERR_END_WAIT_MS. A WebSocket
|
|
1265
|
+
// transport has no stderr to wait for.
|
|
1266
|
+
void transport.stderrEnded(STDERR_END_WAIT_MS).then(() => reject(failed(failure)));
|
|
629
1267
|
};
|
|
630
1268
|
const onError = (err: Error) => {
|
|
631
1269
|
cleanup();
|
|
@@ -636,6 +1274,7 @@ export class McplServerConnection extends EventEmitter {
|
|
|
636
1274
|
reject(failed(`MCPL server "${config.id}" initialize handshake timed out`));
|
|
637
1275
|
}, INITIALIZE_TIMEOUT_MS);
|
|
638
1276
|
|
|
1277
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
639
1278
|
transport.on('line', onLine);
|
|
640
1279
|
transport.on('close', onClose);
|
|
641
1280
|
transport.on('error', onError);
|
|
@@ -662,10 +1301,16 @@ export class McplServerConnection extends EventEmitter {
|
|
|
662
1301
|
// listener of its own, in the continuation of this return, before any
|
|
663
1302
|
// I/O callback can emit.
|
|
664
1303
|
transport.off('error', logAfterSettled);
|
|
665
|
-
return { transport, capabilities: scoped, droppedCapabilities: new Set(dropped), mcpToolsAdvertised };
|
|
1304
|
+
return { transport, capabilities: scoped, droppedCapabilities: new Set(dropped), mcpToolsAdvertised, protocolVersion };
|
|
666
1305
|
} catch (err) {
|
|
667
|
-
// Never leak the child / socket if the handshake fails.
|
|
668
|
-
|
|
1306
|
+
// Never leak the child / socket if the handshake fails. A cleanup that
|
|
1307
|
+
// can't confirm the child exited is part of the failure, never
|
|
1308
|
+
// swallowed: the launch may still be running, so the caller keeps it.
|
|
1309
|
+
try {
|
|
1310
|
+
await transport.close();
|
|
1311
|
+
} catch (cleanup) {
|
|
1312
|
+
throw new HandshakeCleanupError(errorOf(err), errorOf(cleanup), transport);
|
|
1313
|
+
}
|
|
669
1314
|
throw err;
|
|
670
1315
|
}
|
|
671
1316
|
}
|
|
@@ -674,34 +1319,48 @@ export class McplServerConnection extends EventEmitter {
|
|
|
674
1319
|
* Connect with reconnect support.
|
|
675
1320
|
* When `config.reconnect` is true and the initial connection fails,
|
|
676
1321
|
* resolves immediately with null capabilities and retries in the background.
|
|
1322
|
+
* A configuration this engine can't use, or a protocol-version verdict,
|
|
1323
|
+
* still throws: retrying can't change either. So does a failed launch that
|
|
1324
|
+
* can't be reaped, as in {@link connect}: no retry starts beside a child
|
|
1325
|
+
* that may still be running, and the {@link McplUnreapedLaunchError}
|
|
1326
|
+
* carries the connection that owns it, whose close() retries the reap.
|
|
677
1327
|
* Adapted from Anarchid/agent-framework@mcpl-module-proto.
|
|
678
1328
|
*/
|
|
679
1329
|
static async connectWithReconnect(
|
|
680
1330
|
config: McplServerConfig,
|
|
681
1331
|
hostCapabilities: McplHostCapabilities,
|
|
1332
|
+
signal?: AbortSignal,
|
|
682
1333
|
): Promise<McplServerConnection> {
|
|
1334
|
+
// Outside the retry path below: a configuration error is never retried.
|
|
1335
|
+
McplServerConnection.checkConfig(config);
|
|
683
1336
|
try {
|
|
684
|
-
return await McplServerConnection.
|
|
1337
|
+
return await McplServerConnection.connectChecked(config, hostCapabilities, signal);
|
|
685
1338
|
} catch (error) {
|
|
686
|
-
|
|
1339
|
+
// No common protocol revision is a configuration fact, not an outage:
|
|
1340
|
+
// a retry stub would restart the server forever for nothing. A connect
|
|
1341
|
+
// its caller ended (stop(), or a disconnect of its id) isn't retried
|
|
1342
|
+
// either, nor is a launch that couldn't be reaped.
|
|
1343
|
+
if (config.reconnect !== true || error instanceof McplProtocolVersionError || error instanceof McplUnreapedLaunchError || signal?.aborted) {
|
|
687
1344
|
throw error;
|
|
688
1345
|
}
|
|
689
1346
|
|
|
690
|
-
// Non-blocking start: create a disconnected stub that will reconnect in background
|
|
691
|
-
console.error(`MCPL server "${config.id}" initial connect failed, will retry: ${(error as Error).message}${withStderr(error)}`);
|
|
692
|
-
const stub = McplServerConnection.createDisconnectedStub(config, hostCapabilities);
|
|
693
1347
|
// Surface the initial failure. The event is buffered by the emit()
|
|
694
1348
|
// override until the host wires listeners and calls ready(), so it is
|
|
695
1349
|
// not lost in the window before wireMcplEvents runs.
|
|
1350
|
+
// Non-blocking start: create a disconnected stub that will reconnect in background
|
|
1351
|
+
console.error(`MCPL server "${config.id}" initial connect failed, will retry: ${redactServerText((error as Error).message, config)}${withStderr(error, (text) => redactServerText(text, config))}`);
|
|
1352
|
+
const stub = McplServerConnection.createDisconnectedStub(config, hostCapabilities);
|
|
696
1353
|
const stderrTail = stderrTailOf(error);
|
|
697
1354
|
stub.emit('connect-failed', { error: (error as Error).message, ...(stderrTail ? { stderrTail } : {}), attempt: 0 });
|
|
1355
|
+
stub.scheduleReconnect();
|
|
698
1356
|
return stub;
|
|
699
1357
|
}
|
|
700
1358
|
}
|
|
701
1359
|
|
|
702
1360
|
/**
|
|
703
|
-
* Create a disconnected stub connection
|
|
704
|
-
*
|
|
1361
|
+
* Create a disconnected stub connection, for a failed initial connect: one
|
|
1362
|
+
* that will reconnect in the background (its caller schedules that), or one
|
|
1363
|
+
* that owns a launch that couldn't be reaped.
|
|
705
1364
|
* @internal
|
|
706
1365
|
*/
|
|
707
1366
|
private static createDisconnectedStub(
|
|
@@ -714,15 +1373,31 @@ export class McplServerConnection extends EventEmitter {
|
|
|
714
1373
|
stub.requestTimeoutMs = config.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
715
1374
|
stub.config = config;
|
|
716
1375
|
stub.hostCapabilities = hostCapabilities;
|
|
717
|
-
stub.reconnectEnabled = true;
|
|
1376
|
+
stub.reconnectEnabled = config.reconnect === true;
|
|
718
1377
|
stub.reconnectIntervalMs = config.reconnectIntervalMs ?? 5000;
|
|
719
1378
|
stub.reconnectMaxIntervalMs = config.reconnectMaxIntervalMs ?? 300_000;
|
|
720
1379
|
stub.reconnectAttempts = 1; // the initial connect already failed
|
|
1380
|
+
return stub;
|
|
1381
|
+
}
|
|
721
1382
|
|
|
722
|
-
|
|
723
|
-
|
|
1383
|
+
/**
|
|
1384
|
+
* Keep a launch whose cleanup couldn't confirm its child exited, and halt
|
|
1385
|
+
* reconnecting: another launch could run beside one that's still alive.
|
|
1386
|
+
* close() retries the reap.
|
|
1387
|
+
*/
|
|
1388
|
+
private holdUnreaped(transport: McplTransport, failure: Error): void {
|
|
1389
|
+
this.reconnectEnabled = false;
|
|
1390
|
+
if (this.reconnectTimer) {
|
|
1391
|
+
clearTimeout(this.reconnectTimer);
|
|
1392
|
+
this.reconnectTimer = null;
|
|
1393
|
+
}
|
|
1394
|
+
if (!this.unreaped.some((held) => held.transport === transport)) this.unreaped.push({ transport, failure });
|
|
1395
|
+
}
|
|
724
1396
|
|
|
725
|
-
|
|
1397
|
+
/** The halt, as a reconnect event's failure text. */
|
|
1398
|
+
private unreapedFailure(): string {
|
|
1399
|
+
const held = this.unreaped[0];
|
|
1400
|
+
return `the previous launch could not be reaped (${held?.failure.message ?? 'unknown'}); reconnecting is halted`;
|
|
726
1401
|
}
|
|
727
1402
|
|
|
728
1403
|
// ==========================================================================
|
|
@@ -766,8 +1441,11 @@ export class McplServerConnection extends EventEmitter {
|
|
|
766
1441
|
) as Promise<FeatureSetsUpdateResult>;
|
|
767
1442
|
}
|
|
768
1443
|
|
|
769
|
-
/** Send `inference/chunk` notification
|
|
770
|
-
|
|
1444
|
+
/** Send `inference/chunk` notification, on the transport `epoch` names: a
|
|
1445
|
+
* chunk of a request from a transport that has closed goes nowhere, as
|
|
1446
|
+
* that request's answer does. */
|
|
1447
|
+
sendInferenceChunk(params: InferenceChunkParams, epoch = this.transportEpoch): void {
|
|
1448
|
+
if (epoch !== this.transportEpoch) return;
|
|
771
1449
|
this.sendNotification(McplMethod.InferenceChunk, params as unknown as Record<string, unknown>);
|
|
772
1450
|
}
|
|
773
1451
|
|
|
@@ -798,9 +1476,13 @@ export class McplServerConnection extends EventEmitter {
|
|
|
798
1476
|
return this.sendRequest(McplMethod.StateRollback, params as unknown as Record<string, unknown>) as Promise<StateRollbackResult>;
|
|
799
1477
|
}
|
|
800
1478
|
|
|
801
|
-
/** Send `channels/open` request and await result.
|
|
1479
|
+
/** Send `channels/open` request and await result. It keeps a deadline as
|
|
1480
|
+
* channels/publish does: a delivery opens a closed channel before it
|
|
1481
|
+
* publishes, so an open that is never answered would hold the delivery,
|
|
1482
|
+
* and the turn that waits for it, for good. */
|
|
802
1483
|
sendChannelsOpen(params: ChannelsOpenParams): Promise<ChannelsOpenResult> {
|
|
803
|
-
|
|
1484
|
+
const timeoutMs = this.requestTimeoutMs > 0 ? this.requestTimeoutMs : this.publishDeadlineMs;
|
|
1485
|
+
return this.sendRequest(McplMethod.ChannelsOpen, params as unknown as Record<string, unknown>, { timeoutMs }) as Promise<ChannelsOpenResult>;
|
|
804
1486
|
}
|
|
805
1487
|
|
|
806
1488
|
/** Send `channels/close` request and await result. */
|
|
@@ -825,13 +1507,20 @@ export class McplServerConnection extends EventEmitter {
|
|
|
825
1507
|
* Send `channels/publish`.
|
|
826
1508
|
* May be sent as a request (if an ACK is desired) or notification.
|
|
827
1509
|
* When `params.stream` is true, sends as a notification (no ACK).
|
|
1510
|
+
*
|
|
1511
|
+
* A request always has a deadline: the server's requestTimeoutMs, or
|
|
1512
|
+
* publishDeadlineMs when that is 0 (disabled). A publish is a side effect
|
|
1513
|
+
* whose outcome the resident is told and a held draft's attempt records,
|
|
1514
|
+
* so one that is never answered would hold both open for good. Its expiry
|
|
1515
|
+
* is a no-response, which the caller reads as unknown.
|
|
828
1516
|
*/
|
|
829
1517
|
sendChannelsPublish(params: ChannelsPublishParams): Promise<ChannelsPublishResult | void> {
|
|
830
1518
|
if (params.stream) {
|
|
831
1519
|
this.sendNotification(McplMethod.ChannelsPublish, params as unknown as Record<string, unknown>);
|
|
832
1520
|
return Promise.resolve();
|
|
833
1521
|
}
|
|
834
|
-
|
|
1522
|
+
const timeoutMs = this.requestTimeoutMs > 0 ? this.requestTimeoutMs : this.publishDeadlineMs;
|
|
1523
|
+
return this.sendRequest(McplMethod.ChannelsPublish, params as unknown as Record<string, unknown>, { timeoutMs }) as Promise<ChannelsPublishResult>;
|
|
835
1524
|
}
|
|
836
1525
|
|
|
837
1526
|
/** Send `channels/outgoing/chunk` notification. */
|
|
@@ -865,9 +1554,46 @@ export class McplServerConnection extends EventEmitter {
|
|
|
865
1554
|
// Standard MCP methods
|
|
866
1555
|
// ==========================================================================
|
|
867
1556
|
|
|
868
|
-
/**
|
|
869
|
-
|
|
870
|
-
|
|
1557
|
+
/**
|
|
1558
|
+
* Send `tools/list` and return the server's complete tool inventory,
|
|
1559
|
+
* following `nextCursor` across pages (2024-11-05 pagination). Cursors are
|
|
1560
|
+
* opaque strings, `''` included: only an absent or null `nextCursor` ends
|
|
1561
|
+
* the inventory (a null one is how some servers say "no more", and main
|
|
1562
|
+
* read it as a complete page). A page that isn't well formed (no `tools` array, a cursor
|
|
1563
|
+
* that isn't a string), or cursors that don't terminate (a repeat, or more
|
|
1564
|
+
* than {@link MAX_TOOL_LIST_PAGES} pages), are errors, never a partial or
|
|
1565
|
+
* empty inventory published as complete.
|
|
1566
|
+
*/
|
|
1567
|
+
async sendToolsList(): Promise<{ tools: McpToolDefinition[] }> {
|
|
1568
|
+
const tools: McpToolDefinition[] = [];
|
|
1569
|
+
const seen = new Set<string>();
|
|
1570
|
+
let cursor: string | undefined;
|
|
1571
|
+
let first: Record<string, unknown> | undefined;
|
|
1572
|
+
for (let page = 0; page < MAX_TOOL_LIST_PAGES; page++) {
|
|
1573
|
+
const result = await this.sendRequest('tools/list', cursor === undefined ? {} : { cursor }) as Record<string, unknown> | null;
|
|
1574
|
+
const malformed = (why: string) =>
|
|
1575
|
+
new Error(`MCPL server "${this.id}" sent a malformed tools/list page ${page + 1}: ${why}`);
|
|
1576
|
+
if (!result || typeof result !== 'object' || !Array.isArray(result.tools)) {
|
|
1577
|
+
throw malformed('no tools array');
|
|
1578
|
+
}
|
|
1579
|
+
first ??= result;
|
|
1580
|
+
tools.push(...(result.tools as McpToolDefinition[]));
|
|
1581
|
+
if (result.nextCursor === undefined || result.nextCursor === null) {
|
|
1582
|
+
// The first page's other fields are kept as they came, so a
|
|
1583
|
+
// single-page answer is returned exactly as before.
|
|
1584
|
+
const { nextCursor: _done, ...rest } = first as { nextCursor?: unknown };
|
|
1585
|
+
return { ...rest, tools };
|
|
1586
|
+
}
|
|
1587
|
+
if (typeof result.nextCursor !== 'string') {
|
|
1588
|
+
throw malformed(`nextCursor must be a string, got ${JSON.stringify(result.nextCursor)}`);
|
|
1589
|
+
}
|
|
1590
|
+
if (seen.has(result.nextCursor)) {
|
|
1591
|
+
throw new Error(`MCPL server "${this.id}" repeated tools/list cursor ${JSON.stringify(result.nextCursor)}`);
|
|
1592
|
+
}
|
|
1593
|
+
seen.add(result.nextCursor);
|
|
1594
|
+
cursor = result.nextCursor;
|
|
1595
|
+
}
|
|
1596
|
+
throw new Error(`MCPL server "${this.id}" tools/list did not finish within ${MAX_TOOL_LIST_PAGES} pages`);
|
|
871
1597
|
}
|
|
872
1598
|
|
|
873
1599
|
/** Send `tools/call` and return the result. Optionally includes state/checkpoint for stateful tools. */
|
|
@@ -906,41 +1632,66 @@ export class McplServerConnection extends EventEmitter {
|
|
|
906
1632
|
// Lifecycle
|
|
907
1633
|
// ==========================================================================
|
|
908
1634
|
|
|
909
|
-
/**
|
|
910
|
-
|
|
1635
|
+
/** The requests still waiting for an answer, for the operator's log line
|
|
1636
|
+
* when the connection goes: their methods and ids. */
|
|
1637
|
+
private unansweredRequests(): string {
|
|
1638
|
+
const list = [...this.pendingRequests].map(([id, pending]) => `${pending.method} (id=${id})`).join(', ');
|
|
1639
|
+
return `${this.pendingRequests.size} request(s) unanswered: ${list}`;
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1642
|
+
/**
|
|
1643
|
+
* Close the connection for good: disable reconnect, kill the child
|
|
1644
|
+
* process, and clean up resources. At once, as before, reconnecting stops
|
|
1645
|
+
* and pending requests fail; a reconnect handshake in flight is aborted.
|
|
1646
|
+
*
|
|
1647
|
+
* Calls share one attempt: while it is pending, and once it has
|
|
1648
|
+
* succeeded, close() returns it. The attempt waits for the aborted
|
|
1649
|
+
* reconnect's own cleanup, closes the transport (its close is the reaping
|
|
1650
|
+
* verdict) and retries each launch an earlier cleanup couldn't reap. It
|
|
1651
|
+
* rejects while any child can't be confirmed exited, with that failure
|
|
1652
|
+
* itself, or an AggregateError when there are several. A failed attempt is
|
|
1653
|
+
* cleared, so the next close() retries what it couldn't reap. 'close' is
|
|
1654
|
+
* emitted once, when the teardown of a connection the host closed while
|
|
1655
|
+
* live completes.
|
|
1656
|
+
*/
|
|
1657
|
+
close(): Promise<void> {
|
|
911
1658
|
// Disable reconnect before closing — explicit close means stop retrying
|
|
912
1659
|
this.reconnectEnabled = false;
|
|
1660
|
+
this.closedByThisHost = true;
|
|
913
1661
|
if (this.reconnectTimer) {
|
|
914
1662
|
clearTimeout(this.reconnectTimer);
|
|
915
1663
|
this.reconnectTimer = null;
|
|
916
1664
|
}
|
|
917
|
-
|
|
1665
|
+
this.reconnecting?.abort.abort();
|
|
918
1666
|
if (this.closePromise) return this.closePromise;
|
|
919
|
-
if (this.closed && !this.closeIncomplete && !this.reconnectPromise) return;
|
|
920
|
-
this.closed = true;
|
|
921
|
-
this.closeIncomplete = true;
|
|
922
|
-
this.resetPolicyForTransportBoundary();
|
|
923
1667
|
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
1668
|
+
if (!this.closed) {
|
|
1669
|
+
this.closed = true;
|
|
1670
|
+
this.closeIncomplete = true;
|
|
1671
|
+
this.resetPolicyForTransportBoundary();
|
|
1672
|
+
|
|
1673
|
+
// Reject all pending requests. Each message is the whole of what an
|
|
1674
|
+
// agent reads for a tools/call, and what follows a channel tool's own
|
|
1675
|
+
// words ("Failed to publish to channel: …") for the requests a channel
|
|
1676
|
+
// tool makes, so it names no method; the log line does. It doesn't say
|
|
1677
|
+
// whether this host reconnects: a restart closes the connection and then
|
|
1678
|
+
// connects again.
|
|
1679
|
+
if (this.pendingRequests.size > 0) {
|
|
1680
|
+
console.error(`MCPL server "${this.id}": this host closed its connection with ${this.unansweredRequests()}`);
|
|
1681
|
+
}
|
|
1682
|
+
for (const pending of this.pendingRequests.values()) {
|
|
1683
|
+
if (pending.timer) clearTimeout(pending.timer);
|
|
1684
|
+
pending.reject(new McplRequestError(
|
|
1685
|
+
`The "${this.id}" server didn't answer before this host closed its connection, so whether the request took effect is unknown. ` +
|
|
1686
|
+
'Check its effect before trying again.',
|
|
1687
|
+
'no-response',
|
|
1688
|
+
));
|
|
1689
|
+
}
|
|
1690
|
+
this.pendingRequests.clear();
|
|
1691
|
+
this.orphanedRequests.clear();
|
|
928
1692
|
}
|
|
929
|
-
|
|
930
|
-
this.
|
|
931
|
-
|
|
932
|
-
// Tear down the transport (kills the child / closes the socket). The
|
|
933
|
-
// transport's own 'close' event is short-circuited by `this.closed` in
|
|
934
|
-
// setupLifecycle, so this is the single source of the connection 'close'.
|
|
935
|
-
const attempt = Promise.resolve().then(async () => {
|
|
936
|
-
// An already opened handshake owns a transport too. It hands that
|
|
937
|
-
// transport back without admission once reconnect has been disabled.
|
|
938
|
-
// The registry bounds this wait; retry reuses this real close attempt.
|
|
939
|
-
if (this.reconnectPromise) await this.reconnectPromise;
|
|
940
|
-
if (this.transport) await this.transport.close();
|
|
941
|
-
this.closeIncomplete = false;
|
|
942
|
-
this.emit('close', null, null, this.closeInfoByHost());
|
|
943
|
-
});
|
|
1693
|
+
|
|
1694
|
+
const attempt = this.closeStep(this.transport, this.unreaped.map((held) => held.transport));
|
|
944
1695
|
this.closePromise = attempt;
|
|
945
1696
|
void attempt.catch(() => {
|
|
946
1697
|
if (this.closePromise === attempt) this.closePromise = null;
|
|
@@ -948,14 +1699,57 @@ export class McplServerConnection extends EventEmitter {
|
|
|
948
1699
|
return attempt;
|
|
949
1700
|
}
|
|
950
1701
|
|
|
1702
|
+
/** One close attempt. `heldBefore` are the launches earlier cleanups
|
|
1703
|
+
* couldn't reap, which it retries. */
|
|
1704
|
+
private async closeStep(transport: McplTransport | null, heldBefore: McplTransport[]): Promise<void> {
|
|
1705
|
+
// An aborted reconnect cleans up its own launch, holding it if it can't.
|
|
1706
|
+
// That cleanup was this attempt's try at it, so its failure is this
|
|
1707
|
+
// attempt's, not retried here.
|
|
1708
|
+
await this.reconnecting?.done;
|
|
1709
|
+
const heldMeanwhile = this.unreaped.filter((held) => !heldBefore.includes(held.transport));
|
|
1710
|
+
const failures: Error[] = heldMeanwhile.map((held) => held.failure);
|
|
1711
|
+
const targets: McplTransport[] = [];
|
|
1712
|
+
for (const target of [transport, ...heldBefore]) {
|
|
1713
|
+
if (target && !targets.includes(target) && !heldMeanwhile.some((held) => held.transport === target)) targets.push(target);
|
|
1714
|
+
}
|
|
1715
|
+
for (const target of targets) {
|
|
1716
|
+
try {
|
|
1717
|
+
// Kills the child / closes the socket.
|
|
1718
|
+
await target.close();
|
|
1719
|
+
this.unreaped = this.unreaped.filter((held) => held.transport !== target);
|
|
1720
|
+
} catch (error) {
|
|
1721
|
+
const failure = errorOf(error);
|
|
1722
|
+
failures.push(failure);
|
|
1723
|
+
if (!this.unreaped.some((held) => held.transport === target)) this.unreaped.push({ transport: target, failure });
|
|
1724
|
+
}
|
|
1725
|
+
}
|
|
1726
|
+
if (failures.length === 1) throw failures[0];
|
|
1727
|
+
if (failures.length > 1) {
|
|
1728
|
+
throw new AggregateError(failures, `MCPL server "${this.id}" did not close cleanly: ${failures.map((f) => f.message).join('; ')}`);
|
|
1729
|
+
}
|
|
1730
|
+
// The transport's own 'close' event is short-circuited by `this.closed`
|
|
1731
|
+
// in setupLifecycle, so this is the single source of the connection
|
|
1732
|
+
// 'close' when the host tears down a live transport.
|
|
1733
|
+
if (this.closeIncomplete) {
|
|
1734
|
+
this.closeIncomplete = false;
|
|
1735
|
+
this.emit('close', null, null, this.closeInfoByHost());
|
|
1736
|
+
}
|
|
1737
|
+
}
|
|
1738
|
+
|
|
951
1739
|
/**
|
|
952
1740
|
* Tear down a live transport after an awareness-accounting failure without
|
|
953
1741
|
* disabling configured auto-reconnect. The normal transport close handler
|
|
954
1742
|
* rejects requests, emits lifecycle state, and schedules the next bounded
|
|
955
|
-
* reconnect attempt. With reconnect disabled
|
|
1743
|
+
* reconnect attempt. With reconnect disabled the recycle ends the
|
|
1744
|
+
* connection for good, as close() does: 'close' comes once the child's
|
|
1745
|
+
* exit is confirmed, and a child it can't reap stays owned, holding the id.
|
|
956
1746
|
*/
|
|
957
1747
|
async reconnectAfterFailure(): Promise<void> {
|
|
958
1748
|
this.pauseDataPlane();
|
|
1749
|
+
if (!this.reconnectEnabled) {
|
|
1750
|
+
await this.close();
|
|
1751
|
+
return;
|
|
1752
|
+
}
|
|
959
1753
|
if (this.closed) {
|
|
960
1754
|
this.scheduleReconnect();
|
|
961
1755
|
return;
|
|
@@ -1009,73 +1803,118 @@ export class McplServerConnection extends EventEmitter {
|
|
|
1009
1803
|
* Attempt to reconnect by opening a fresh transport and performing the
|
|
1010
1804
|
* handshake, then adopting that transport in place. Unlike the previous
|
|
1011
1805
|
* approach this builds no throwaway connection instance, so there are no
|
|
1012
|
-
* leaked listeners on a dead object.
|
|
1806
|
+
* leaked listeners on a dead object. The attempt is close()'s to abort,
|
|
1807
|
+
* and it settles only once its own launch is adopted, failed or cleaned up.
|
|
1808
|
+
* One attempt at a time: a request that arrives while one is in flight (a
|
|
1809
|
+
* recycle, say) starts nothing, since that attempt either adopts its
|
|
1810
|
+
* launch or schedules the next retry itself. A second attempt would take
|
|
1811
|
+
* close()'s ownership of the first (Iris-1827, room-293 #55410).
|
|
1013
1812
|
*/
|
|
1014
|
-
private attemptReconnect(): Promise<void> {
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1813
|
+
private async attemptReconnect(): Promise<void> {
|
|
1814
|
+
// A queued recovery timer may outlive an earlier successful handshake:
|
|
1815
|
+
// only a connection that is down reconnects, so it never replaces a live
|
|
1816
|
+
// transport.
|
|
1817
|
+
if (this.reconnecting || !this.closed || !this.reconnectEnabled || !this.config || !this.hostCapabilities) return;
|
|
1818
|
+
const abort = new AbortController();
|
|
1819
|
+
const done = this.reconnectOnce(this.config, this.hostCapabilities, abort.signal);
|
|
1820
|
+
this.reconnecting = { abort, done };
|
|
1821
|
+
await done;
|
|
1822
|
+
if (this.reconnecting?.done === done) this.reconnecting = null;
|
|
1021
1823
|
}
|
|
1022
1824
|
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1825
|
+
/** One reconnect attempt. It never rejects: each outcome is an event, a
|
|
1826
|
+
* scheduled retry, or a launch held as unreaped. */
|
|
1827
|
+
private async reconnectOnce(
|
|
1828
|
+
config: McplServerConfig,
|
|
1829
|
+
hostCapabilities: McplHostCapabilities,
|
|
1830
|
+
signal: AbortSignal,
|
|
1831
|
+
): Promise<void> {
|
|
1028
1832
|
// Ordinal of this attempt: 0 was the initial connect, so the Nth retry
|
|
1029
1833
|
// reports attempt N (reconnectAttempts counts failures so far).
|
|
1030
1834
|
const attempt = Math.max(1, this.reconnectAttempts);
|
|
1031
1835
|
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
// It isn't wired, and the handshake has removed its own listeners, so
|
|
1043
|
-
// an error it reports from here on, such as a malformed frame from a
|
|
1044
|
-
// WebSocket server while close() closes it, is logged here rather than
|
|
1045
|
-
// thrown where nothing catches it.
|
|
1046
|
-
transport.on('error', (err: Error) => {
|
|
1047
|
-
console.error(`[mcpl] ${this.id}: a transport kept for close() reported an error: ${err.message}`);
|
|
1048
|
-
});
|
|
1049
|
-
this.transport = transport;
|
|
1836
|
+
// The previous launch must be gone before another starts. A transport
|
|
1837
|
+
// the host closed itself (reconnectAfterFailure) may still be reaping its
|
|
1838
|
+
// child; one whose child already exited resolves at once.
|
|
1839
|
+
const previous = this.transport;
|
|
1840
|
+
if (previous) {
|
|
1841
|
+
try {
|
|
1842
|
+
await previous.close();
|
|
1843
|
+
} catch (error) {
|
|
1844
|
+
this.holdUnreaped(previous, errorOf(error));
|
|
1845
|
+
if (!signal.aborted) this.emit('reconnect-failed', { error: this.unreapedFailure(), attempt, permanent: true });
|
|
1050
1846
|
return;
|
|
1051
1847
|
}
|
|
1848
|
+
}
|
|
1849
|
+
if (signal.aborted) return;
|
|
1052
1850
|
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
// to establish the server while data remains held.
|
|
1057
|
-
this.pauseDataPlane();
|
|
1058
|
-
this.resetPolicyForTransportBoundary();
|
|
1059
|
-
this.transport = transport;
|
|
1060
|
-
this.capabilities = capabilities;
|
|
1061
|
-
this.droppedCapabilities = droppedCapabilities;
|
|
1062
|
-
this.mcpToolsAdvertised = mcpToolsAdvertised;
|
|
1063
|
-
this.allowHostCommands = this.config?.allowHostCommands === true;
|
|
1064
|
-
this.closed = false;
|
|
1065
|
-
this.nextRequestId = 1;
|
|
1066
|
-
this.pendingRequests.clear();
|
|
1067
|
-
this.wireTransport(transport);
|
|
1068
|
-
|
|
1069
|
-
console.error(`MCPL server "${this.id}" reconnected successfully`);
|
|
1070
|
-
this.reconnectAttempts = 0;
|
|
1071
|
-
this.emit('reconnect', { attempts: attempt });
|
|
1851
|
+
let established: Awaited<ReturnType<typeof McplServerConnection.handshake>>;
|
|
1852
|
+
try {
|
|
1853
|
+
established = await McplServerConnection.handshake(config, hostCapabilities, signal);
|
|
1072
1854
|
} catch (error) {
|
|
1073
|
-
|
|
1855
|
+
const unreaped = error instanceof HandshakeCleanupError;
|
|
1856
|
+
if (unreaped) this.holdUnreaped(error.transport, error.cleanup);
|
|
1857
|
+
// Closed by the host meanwhile: not a failed attempt.
|
|
1858
|
+
if (signal.aborted) return;
|
|
1859
|
+
const failure = unreaped ? error.failure : errorOf(error);
|
|
1860
|
+
const redact = (text: string) => redactServerText(text, this.config ?? {});
|
|
1861
|
+
console.error(`MCPL server "${this.id}" reconnect failed: ${redact(failure.message)}${withStderr(failure, redact)}`);
|
|
1074
1862
|
this.reconnectAttempts = attempt + 1;
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1863
|
+
// No common protocol revision: every further attempt would restart the
|
|
1864
|
+
// server to hear the same answer. Stop retrying; the configuration is
|
|
1865
|
+
// what has to change. A launch that couldn't be reaped halts retrying
|
|
1866
|
+
// for good: close() can confirm its exit, but close() is terminal, so
|
|
1867
|
+
// nothing resumes this connection. Every other failure keeps the
|
|
1868
|
+
// backoff loop.
|
|
1869
|
+
if (error instanceof McplProtocolVersionError) this.reconnectEnabled = false;
|
|
1870
|
+
const permanent = unreaped || error instanceof McplProtocolVersionError;
|
|
1871
|
+
const message = unreaped ? `${failure.message}; ${this.unreapedFailure()}` : failure.message;
|
|
1872
|
+
const stderrTail = stderrTailOf(failure);
|
|
1873
|
+
this.emit('reconnect-failed', { error: message, ...(stderrTail ? { stderrTail } : {}), attempt, ...(permanent ? { permanent: true } : {}) });
|
|
1874
|
+
if (!permanent) this.scheduleReconnect();
|
|
1875
|
+
return;
|
|
1078
1876
|
}
|
|
1877
|
+
const { transport, capabilities, droppedCapabilities, mcpToolsAdvertised, protocolVersion } = established;
|
|
1878
|
+
|
|
1879
|
+
if (signal.aborted) {
|
|
1880
|
+
// Closed by the host while the handshake finished: this launch never
|
|
1881
|
+
// serves. close() is waiting on this attempt, so it hears the verdict.
|
|
1882
|
+
// The transport isn't wired, and the handshake has taken its own
|
|
1883
|
+
// listeners off, so an error it reports while it closes, such as a
|
|
1884
|
+
// malformed frame from a WebSocket server, is logged here rather than
|
|
1885
|
+
// thrown where nothing catches it.
|
|
1886
|
+
transport.on('error', (err: Error) => {
|
|
1887
|
+
console.error(`[mcpl] ${this.id}: a transport closed before it served reported an error: ${redactServerText(err.message, config)}`);
|
|
1888
|
+
});
|
|
1889
|
+
await transport.close().catch((error: unknown) => this.holdUnreaped(transport, errorOf(error)));
|
|
1890
|
+
return;
|
|
1891
|
+
}
|
|
1892
|
+
|
|
1893
|
+
// Close both planes before the new transport can emit, as at initial
|
|
1894
|
+
// connect: its traffic is admitted against the grant, which this
|
|
1895
|
+
// boundary resets, so control traffic too (a server's channels/register
|
|
1896
|
+
// right after initialize) must wait for the new grant rather than be
|
|
1897
|
+
// refused against the empty one. The reconnect lifecycle event bypasses
|
|
1898
|
+
// buffering; its framework listener installs the awareness barrier,
|
|
1899
|
+
// re-establishes the grant (§5.3 policy responses are never buffered),
|
|
1900
|
+
// and then opens the planes.
|
|
1901
|
+
this.controlPlaneReady = false;
|
|
1902
|
+
this.dataPlaneReady = false;
|
|
1903
|
+
this.resetPolicyForTransportBoundary();
|
|
1904
|
+
this.transport = transport;
|
|
1905
|
+
this.capabilities = capabilities;
|
|
1906
|
+
this.droppedCapabilities = droppedCapabilities;
|
|
1907
|
+
this.mcpToolsAdvertised = mcpToolsAdvertised;
|
|
1908
|
+
this.protocolVersion = protocolVersion;
|
|
1909
|
+
this.allowHostCommands = this.config?.allowHostCommands === true;
|
|
1910
|
+
this.closed = false;
|
|
1911
|
+
this.nextRequestId = 1;
|
|
1912
|
+
this.pendingRequests.clear();
|
|
1913
|
+
this.wireTransport(transport);
|
|
1914
|
+
|
|
1915
|
+
console.error(`MCPL server "${this.id}" reconnected successfully`);
|
|
1916
|
+
this.reconnectAttempts = 0;
|
|
1917
|
+
this.emit('reconnect', { attempts: attempt });
|
|
1079
1918
|
}
|
|
1080
1919
|
|
|
1081
1920
|
// ==========================================================================
|
|
@@ -1099,7 +1938,34 @@ export class McplServerConnection extends EventEmitter {
|
|
|
1099
1938
|
options: RequestOptions = {},
|
|
1100
1939
|
): Promise<unknown> {
|
|
1101
1940
|
if (this.closed) {
|
|
1102
|
-
|
|
1941
|
+
// After this host's own close, which a restart makes before connecting
|
|
1942
|
+
// again and a removal makes for good, only that this host closed it
|
|
1943
|
+
// holds, and one next step holds for both: calling again reaches the
|
|
1944
|
+
// restarted server, or fails where it's gone.
|
|
1945
|
+
return Promise.reject(new McplRequestError(this.closedByThisHost
|
|
1946
|
+
? `The request wasn't sent: this host closed its connection to the "${this.id}" server, so nothing ran. ` +
|
|
1947
|
+
'If calling it again fails, ask whoever runs this host.'
|
|
1948
|
+
: `The request wasn't sent: the connection to the "${this.id}" server is closed, so nothing ran. ` +
|
|
1949
|
+
(this.willReconnect
|
|
1950
|
+
? 'This host is reconnecting to it; try again later.'
|
|
1951
|
+
: "This host won't reconnect to it on its own. If you need it, ask whoever runs this host."),
|
|
1952
|
+
'not-sent',
|
|
1953
|
+
));
|
|
1954
|
+
}
|
|
1955
|
+
// Once the child's stdin or stdout has failed, the transport is ending
|
|
1956
|
+
// it and writes nothing more to it, so a request is refused here as not
|
|
1957
|
+
// sent rather than left to wait out its timeout. The method and the
|
|
1958
|
+
// pipe's error are the operator's, so they go to the log.
|
|
1959
|
+
const pipeFailure = this.transport?.pipeFailure;
|
|
1960
|
+
if (pipeFailure) {
|
|
1961
|
+
console.error(`MCPL server "${this.id}": ${method} not sent, since its stdio pipe failed: ${logDetail(pipeFailure.message)}`);
|
|
1962
|
+
return Promise.reject(new McplRequestError(
|
|
1963
|
+
`The request wasn't sent: this host can no longer write to the "${this.id}" server, so nothing ran. ` +
|
|
1964
|
+
(this.willReconnect
|
|
1965
|
+
? 'This host is reconnecting to it; try again later.'
|
|
1966
|
+
: "This host won't reconnect to it on its own. If you need it, ask whoever runs this host."),
|
|
1967
|
+
'not-sent',
|
|
1968
|
+
));
|
|
1103
1969
|
}
|
|
1104
1970
|
|
|
1105
1971
|
const id = this.nextRequestId++;
|
|
@@ -1117,23 +1983,55 @@ export class McplServerConnection extends EventEmitter {
|
|
|
1117
1983
|
pending.timer = setTimeout(() => {
|
|
1118
1984
|
this.pendingRequests.delete(id);
|
|
1119
1985
|
if (options.surfaceOrphanedResponse) {
|
|
1120
|
-
this.orphanedRequests.set(id, {
|
|
1986
|
+
this.orphanedRequests.set(id, {
|
|
1987
|
+
method,
|
|
1988
|
+
...(method === 'push/render'
|
|
1989
|
+
? {
|
|
1990
|
+
renderParams: {
|
|
1991
|
+
featureSet: params.featureSet as string,
|
|
1992
|
+
key: params.key as string,
|
|
1993
|
+
eventId: params.eventId as string,
|
|
1994
|
+
...(typeof params.channelId === 'string' ? { channelId: params.channelId } : {}),
|
|
1995
|
+
},
|
|
1996
|
+
}
|
|
1997
|
+
: {}),
|
|
1998
|
+
});
|
|
1121
1999
|
if (this.orphanedRequests.size > 4096) this.orphanedRequests.delete(this.orphanedRequests.keys().next().value!);
|
|
1122
2000
|
}
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
`
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
2001
|
+
// The method and id are the operator's, so they go to the log.
|
|
2002
|
+
console.error(
|
|
2003
|
+
`MCPL server "${this.id}" did not respond to ${method} (id=${id}) within ${timeoutMs}ms; ` +
|
|
2004
|
+
'the request was abandoned here, not cancelled',
|
|
2005
|
+
);
|
|
2006
|
+
const seconds = timeoutMs / 1000;
|
|
2007
|
+
reject(new McplRequestError(
|
|
2008
|
+
`The "${this.id}" server didn't answer within ${seconds} second${seconds === 1 ? '' : 's'}, ` +
|
|
2009
|
+
'so whether the request took effect is unknown: ' +
|
|
2010
|
+
'it was abandoned here, not cancelled, and may still complete on the server. ' +
|
|
2011
|
+
'Check its effect before trying again, since repeating something that changes state may do it twice.',
|
|
2012
|
+
'no-response',
|
|
1130
2013
|
));
|
|
1131
2014
|
}, timeoutMs);
|
|
1132
2015
|
// Don't hold the event loop open for the watchdog alone.
|
|
1133
2016
|
pending.timer.unref?.();
|
|
1134
2017
|
}
|
|
1135
2018
|
this.pendingRequests.set(id, pending);
|
|
1136
|
-
|
|
2019
|
+
try {
|
|
2020
|
+
this.transport!.writeLine(JSON.stringify(request));
|
|
2021
|
+
} catch (err) {
|
|
2022
|
+
// A synchronous write rejection sent no request. Remove its pending
|
|
2023
|
+
// state and watchdog before reporting that it is safe to retry. The
|
|
2024
|
+
// method, id and the transport's words are the operator's.
|
|
2025
|
+
this.pendingRequests.delete(id);
|
|
2026
|
+
if (pending.timer) clearTimeout(pending.timer);
|
|
2027
|
+
console.error(
|
|
2028
|
+
`MCPL server "${this.id}": writing ${method} (id=${id}) failed: ${logDetail(err instanceof Error ? err.message : err)}`,
|
|
2029
|
+
);
|
|
2030
|
+
reject(new McplRequestError(
|
|
2031
|
+
`The request wasn't sent: this host couldn't write it to the "${this.id}" server, so nothing ran. Try again later.`,
|
|
2032
|
+
'not-sent',
|
|
2033
|
+
));
|
|
2034
|
+
}
|
|
1137
2035
|
});
|
|
1138
2036
|
}
|
|
1139
2037
|
|
|
@@ -1223,13 +2121,18 @@ export class McplServerConnection extends EventEmitter {
|
|
|
1223
2121
|
this.setupLifecycle(transport);
|
|
1224
2122
|
// Surface transport diagnostics (stdio child stderr) as 'stderr' events,
|
|
1225
2123
|
// preserving the `{ line }` payload existing consumers expect.
|
|
1226
|
-
|
|
2124
|
+
// Through the same containment boundary as every inbound event, so a
|
|
2125
|
+
// stderr listener that throws is reported, not thrown into the transport.
|
|
2126
|
+
transport.on('stderr', (line: string) => this.dispatchInboundEvent('stderr', { line }));
|
|
1227
2127
|
}
|
|
1228
2128
|
|
|
1229
2129
|
/**
|
|
1230
2130
|
* Wire up the transport's line stream to route incoming JSON-RPC messages.
|
|
1231
2131
|
*/
|
|
1232
2132
|
private setupMessageRouting(transport: McplTransport): void {
|
|
2133
|
+
// The boundary this transport serves within: a line read once it has
|
|
2134
|
+
// passed is the dead process's (afterBoundary).
|
|
2135
|
+
const epoch = this.transportEpoch;
|
|
1233
2136
|
transport.on('line', (line: string) => {
|
|
1234
2137
|
let msg: JsonRpcRequest | JsonRpcResponse;
|
|
1235
2138
|
try {
|
|
@@ -1239,68 +2142,203 @@ export class McplServerConnection extends EventEmitter {
|
|
|
1239
2142
|
return;
|
|
1240
2143
|
}
|
|
1241
2144
|
|
|
1242
|
-
//
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
2145
|
+
// JSON-RPC messages must be objects. Ignore unrelated stdout/log values
|
|
2146
|
+
// before property access, just as malformed JSON lines are ignored.
|
|
2147
|
+
if (msg === null || typeof msg !== 'object' || Array.isArray(msg)) return;
|
|
2148
|
+
try {
|
|
2149
|
+
this.dispatchMessage(msg, epoch);
|
|
2150
|
+
} catch (error) {
|
|
2151
|
+
const request = msg as { method?: unknown; id?: unknown };
|
|
2152
|
+
this.reportInboundFailure(error, {
|
|
2153
|
+
...(typeof request.method === 'string' ? { event: request.method } : {}),
|
|
2154
|
+
...(typeof request.id === 'string' || typeof request.id === 'number' ? { requestId: request.id } : {}),
|
|
2155
|
+
});
|
|
1246
2156
|
}
|
|
2157
|
+
});
|
|
2158
|
+
}
|
|
1247
2159
|
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
2160
|
+
/** Contain synchronous dispatch failures; async RPC handlers own their promises. */
|
|
2161
|
+
private dispatchMessage(msg: JsonRpcRequest | JsonRpcResponse, epoch: number): void {
|
|
2162
|
+
// Is this a response to one of our outbound requests?
|
|
2163
|
+
if ('id' in msg && msg.id != null && !('method' in msg)) {
|
|
2164
|
+
this.handleResponse(msg as JsonRpcResponse, epoch !== this.transportEpoch);
|
|
2165
|
+
return;
|
|
2166
|
+
}
|
|
2167
|
+
|
|
2168
|
+
// It is an inbound request or notification from the server
|
|
2169
|
+
const request = msg as JsonRpcRequest;
|
|
2170
|
+
|
|
2171
|
+
// Read after its transport's boundary, it is the dead process's: the
|
|
2172
|
+
// boundary's rule applies (afterBoundary).
|
|
2173
|
+
if (epoch !== this.transportEpoch) {
|
|
2174
|
+
this.afterBoundary(request, epoch);
|
|
2175
|
+
return;
|
|
2176
|
+
}
|
|
2177
|
+
|
|
2178
|
+
// MCP's ping, which either side may send: its receiver answers promptly
|
|
2179
|
+
// with an empty result. It's answered as it arrives, before admission and
|
|
2180
|
+
// buffering, since it asks only whether the connection is alive, and a
|
|
2181
|
+
// paused plane mustn't hold that answer. A ping sent as a notification
|
|
2182
|
+
// has nothing to answer. The handshake answers one too.
|
|
2183
|
+
if (request.method === 'ping') {
|
|
2184
|
+
if (request.id != null) this.sendResponse(request.id, {});
|
|
2185
|
+
return;
|
|
2186
|
+
}
|
|
2187
|
+
|
|
2188
|
+
const eventName = typeof request.method === 'string' && Object.hasOwn(McplServerConnection.METHOD_TO_EVENT, request.method)
|
|
2189
|
+
? McplServerConnection.METHOD_TO_EVENT[request.method] : undefined;
|
|
2190
|
+
|
|
2191
|
+
// §7 is removed in 0.5.0: scope/elevate is no longer a method. Answer
|
|
2192
|
+
// rather than hang (§6.6 — a method that will never be answered MUST
|
|
2193
|
+
// return an error).
|
|
2194
|
+
// featureSets/changed is REMOVED in 0.5.0 (§6.7 — it carried a
|
|
2195
|
+
// server-authored change payload, the self-attestation defect; the
|
|
2196
|
+
// need is met by §17's host-diffed manifest fetch). It was still
|
|
2197
|
+
// routed, control-plane classified, and mutating declarations — a
|
|
2198
|
+
// live ungated self-attestation path (PR #79 review blocker 4).
|
|
2199
|
+
if (request.method === McplMethod.FeatureSetsChanged) {
|
|
2200
|
+
if (request.id != null) {
|
|
2201
|
+
this.sendErrorResponse(request.id, -32601, 'featureSets/changed removed in MCPL 0.5.0 (superseded by mcpl/manifestChanged, SPEC §17)');
|
|
2202
|
+
} else {
|
|
2203
|
+
console.error(`[mcpl] ${this.id}: discarded featureSets/changed — removed in 0.5.0 (§6.7/§17)`);
|
|
1267
2204
|
}
|
|
2205
|
+
return;
|
|
2206
|
+
}
|
|
1268
2207
|
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
}
|
|
1273
|
-
return;
|
|
2208
|
+
if (request.method === McplMethod.ScopeElevate) {
|
|
2209
|
+
if (request.id != null) {
|
|
2210
|
+
this.sendErrorResponse(request.id, -32601, 'scope/elevate removed in MCPL 0.5.0 (SPEC §7)');
|
|
1274
2211
|
}
|
|
2212
|
+
return;
|
|
2213
|
+
}
|
|
1275
2214
|
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
this.sendErrorResponse(request.id!, code, message, data),
|
|
1290
|
-
});
|
|
1291
|
-
} else {
|
|
1292
|
-
// Notification — no response expected
|
|
1293
|
-
this.emit(eventName, request.params);
|
|
1294
|
-
}
|
|
2215
|
+
// Positive-grant enforcement (§5.4, §14.1) happens at ADMISSION, in
|
|
2216
|
+
// the emit() override below — the one choke-point that live routing
|
|
2217
|
+
// and staged-buffer flushes share. See the comment there for why
|
|
2218
|
+
// rejecting here, at line receipt, was wrong for staged connections.
|
|
2219
|
+
|
|
2220
|
+
if (!eventName) {
|
|
2221
|
+
// §6.6: a request that will never be answered MUST get an error, so a
|
|
2222
|
+
// request (it has an id) whose method isn't a string, or isn't one this
|
|
2223
|
+
// host handles, is answered -32601. A notification nobody handles is
|
|
2224
|
+
// ignored, as JSON-RPC asks.
|
|
2225
|
+
if (request.id != null) {
|
|
2226
|
+
this.sendErrorResponse(request.id, -32601, 'Method not found');
|
|
2227
|
+
console.error(`[mcpl] ${this.id}: answered -32601 to a request for ${describeMethod(request.method)}, which this host doesn't handle`);
|
|
1295
2228
|
}
|
|
1296
|
-
|
|
2229
|
+
return;
|
|
2230
|
+
}
|
|
2231
|
+
|
|
2232
|
+
// Emit the typed event with params and (for requests) a respond callback
|
|
2233
|
+
if (request.id != null) {
|
|
2234
|
+
// Server expects a response — provide a respond helper
|
|
2235
|
+
this.dispatchInboundEvent(eventName, request.params, this.responderFor(request.id, epoch));
|
|
2236
|
+
} else {
|
|
2237
|
+
// Notification — no response expected
|
|
2238
|
+
this.dispatchInboundEvent(eventName, request.params);
|
|
2239
|
+
}
|
|
2240
|
+
}
|
|
2241
|
+
|
|
2242
|
+
/**
|
|
2243
|
+
* A request's responder, bound to the transport it came on (`epoch`): an
|
|
2244
|
+
* answer, or a streamed inference's chunk, sent after that transport has
|
|
2245
|
+
* closed reaches nothing, never a server that didn't ask it.
|
|
2246
|
+
*/
|
|
2247
|
+
private responderFor(id: string | number, epoch: number) {
|
|
2248
|
+
return {
|
|
2249
|
+
id,
|
|
2250
|
+
respond: (result: unknown) => this.sendResponse(id, result, epoch),
|
|
2251
|
+
respondError: (code: number, message: string, data?: unknown) =>
|
|
2252
|
+
this.sendErrorResponse(id, code, message, data, epoch),
|
|
2253
|
+
sendChunk: (params: InferenceChunkParams) => this.sendInferenceChunk(params, epoch),
|
|
2254
|
+
};
|
|
2255
|
+
}
|
|
2256
|
+
|
|
2257
|
+
/**
|
|
2258
|
+
* A request or notification read from a transport after its boundary has
|
|
2259
|
+
* passed: the boundary's rule (resetPolicyForTransportBoundary). An
|
|
2260
|
+
* incoming message or push is held as kept, for the next transport's
|
|
2261
|
+
* grant, and any other is dropped and logged. (A response read then goes to
|
|
2262
|
+
* handleResponse, flagged late.) A stdio
|
|
2263
|
+
* server's transport closes on the child's 'exit', when its stdout may
|
|
2264
|
+
* still be open, as it is while a process the server started holds it, and
|
|
2265
|
+
* it is read until the next reconnect attempt closes its reader. No
|
|
2266
|
+
* transport serves meanwhile, so what is held waits for the next grant
|
|
2267
|
+
* (flushHeld). A connection closed for good, which won't reconnect, reads
|
|
2268
|
+
* it while its writer lives, with no grant to come, so it drops and logs
|
|
2269
|
+
* all of it; hold's bounds cap what any other keeps.
|
|
2270
|
+
*/
|
|
2271
|
+
private afterBoundary(request: JsonRpcRequest, epoch: number): void {
|
|
2272
|
+
const event = typeof request.method === 'string' && Object.hasOwn(McplServerConnection.METHOD_TO_EVENT, request.method)
|
|
2273
|
+
? McplServerConnection.METHOD_TO_EVENT[request.method] : undefined;
|
|
2274
|
+
if (this.closedForGood || event === undefined || !McplServerConnection.WORLD_EVENTS.has(event)) {
|
|
2275
|
+
console.error(
|
|
2276
|
+
`[mcpl] ${this.id}: dropped a message read after its transport closed` +
|
|
2277
|
+
`${this.closedForGood ? ', with no transport to come' : ''}: ${describeMethod(request.method)}`,
|
|
2278
|
+
);
|
|
2279
|
+
return;
|
|
2280
|
+
}
|
|
2281
|
+
this.hold(event, request.id != null ? [request.params, this.responderFor(request.id, epoch)] : [request.params], true);
|
|
2282
|
+
}
|
|
2283
|
+
|
|
2284
|
+
/** Live delivery and delayed plane flushes share the same containment boundary. */
|
|
2285
|
+
private dispatchInboundEvent(event: string, ...args: unknown[]): void {
|
|
2286
|
+
try {
|
|
2287
|
+
this.emit(event, ...args);
|
|
2288
|
+
} catch (error) {
|
|
2289
|
+
const requestId = (args[1] as { id?: unknown } | undefined)?.id;
|
|
2290
|
+
this.reportInboundFailure(error, {
|
|
2291
|
+
event,
|
|
2292
|
+
...(typeof requestId === 'string' || typeof requestId === 'number' ? { requestId } : {}),
|
|
2293
|
+
});
|
|
2294
|
+
}
|
|
2295
|
+
}
|
|
2296
|
+
|
|
2297
|
+
/** Report a failure the containment boundary caught, naming the event (or
|
|
2298
|
+
* method) and request it was dispatching, and what failed. The message is
|
|
2299
|
+
* what the framework's 'error' listener traces. */
|
|
2300
|
+
private reportInboundFailure(cause: unknown, context: { event?: string; requestId?: string | number } = {}): void {
|
|
2301
|
+
const where = (show: (text: string) => string): string => (context.event === undefined ? '' : ` for ${show(context.event)}`) +
|
|
2302
|
+
(context.requestId === undefined ? '' : ` (request ${show(String(context.requestId))})`);
|
|
2303
|
+
// The error's message is what the 'error' listener traces, as text a
|
|
2304
|
+
// reader parses, so it is bounded rather than escaped (boundedForLog);
|
|
2305
|
+
// the log line prints the same through log-value.
|
|
2306
|
+
const error = new Error(`MCPL server "${this.id}" inbound dispatch failed${where(boundedForLog)}: ${boundedForLog(describeCause(cause))}`, { cause });
|
|
2307
|
+
console.error(`[mcpl] ${this.id}: inbound dispatch failed${where(logValue)}: ${logDetail(describeCause(cause))}`);
|
|
2308
|
+
this.emitToErrorListeners(error);
|
|
2309
|
+
}
|
|
2310
|
+
|
|
2311
|
+
/**
|
|
2312
|
+
* Give an error to the connection's 'error' listeners, and say whether it
|
|
2313
|
+
* has any. EventEmitter throws an 'error' that nothing listens for, and
|
|
2314
|
+
* from a transport's callback, where these errors arise, nothing catches
|
|
2315
|
+
* that throw, so it ends the process. A bare connection has no listener:
|
|
2316
|
+
* the class is exported, and a probe, a playtest or a harness often attaches
|
|
2317
|
+
* none. So with no listener, the error is the caller's to log, unless it
|
|
2318
|
+
* already has. A listener that throws is caught and logged, with the error
|
|
2319
|
+
* it was handling.
|
|
2320
|
+
*/
|
|
2321
|
+
private emitToErrorListeners(error: Error): boolean {
|
|
2322
|
+
if (this.listenerCount('error') === 0) return false;
|
|
2323
|
+
try { this.emit('error', error); }
|
|
2324
|
+
catch (listenerError) {
|
|
2325
|
+
console.error(
|
|
2326
|
+
`[mcpl] ${this.id}: an 'error' listener failed: ${logDetail(describeCause(listenerError))}` +
|
|
2327
|
+
`, handling ${logDetail(error.message)}`,
|
|
2328
|
+
);
|
|
2329
|
+
}
|
|
2330
|
+
return true;
|
|
1297
2331
|
}
|
|
1298
2332
|
|
|
1299
2333
|
/**
|
|
1300
2334
|
* Handle a JSON-RPC response by resolving/rejecting the corresponding pending request.
|
|
1301
2335
|
*/
|
|
1302
|
-
private handleResponse(response: JsonRpcResponse): void {
|
|
1303
|
-
|
|
2336
|
+
private handleResponse(response: JsonRpcResponse, late = false): void {
|
|
2337
|
+
// One read after its transport's boundary answers a request the close
|
|
2338
|
+
// ended. It's matched against none of this connection's request records,
|
|
2339
|
+
// since the next transport numbers its requests afresh, and goes the way
|
|
2340
|
+
// of any late answer: a stateful one is surfaced as an orphaned response.
|
|
2341
|
+
const pending = late ? undefined : this.pendingRequests.get(response.id);
|
|
1304
2342
|
if (!pending) {
|
|
1305
2343
|
// Orphaned response — the request already timed out (or was settled) and
|
|
1306
2344
|
// was removed from pendingRequests. A late STATEFUL result must be
|
|
@@ -1308,12 +2346,19 @@ export class McplServerConnection extends EventEmitter {
|
|
|
1308
2346
|
// Selected side-effecting calls are also surfaced even without state so
|
|
1309
2347
|
// their outcome-unknown receipt remains visible. We cannot safely
|
|
1310
2348
|
// re-inject either result because the original dispatch context is gone.
|
|
1311
|
-
const orphaned = this.orphanedRequests.get(response.id);
|
|
1312
|
-
this.orphanedRequests.delete(response.id);
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
2349
|
+
const orphaned = late ? undefined : this.orphanedRequests.get(response.id);
|
|
2350
|
+
if (!late) this.orphanedRequests.delete(response.id);
|
|
2351
|
+
// A push/render answered after its timeout (RFC-006 §5.3) is reported
|
|
2352
|
+
// with the render's featureSet, key, eventId and, for a channel-scoped
|
|
2353
|
+
// subject, channelId, kept from its request when the timeout fired, so
|
|
2354
|
+
// its discarded answer can be recorded beside its batch's other
|
|
2355
|
+
// coalescing records (§11). It is only reported: the fallback the
|
|
2356
|
+
// timeout materialized stands (§5.4 rule 3). A late JSON-RPC error is
|
|
2357
|
+
// passed on as `error`, so its record can be told from a late result's,
|
|
2358
|
+
// and so is one beside a result, which JSON-RPC forbids: the live path
|
|
2359
|
+
// rejects such an answer as an error.
|
|
2360
|
+
const render = orphaned?.renderParams ?? {};
|
|
2361
|
+
const failure = response.error ? { error: { code: response.error.code, message: response.error.message } } : {};
|
|
1317
2362
|
const result = response.result;
|
|
1318
2363
|
if (result && typeof result === 'object') {
|
|
1319
2364
|
const r = result as { state?: unknown; checkpoint?: unknown };
|
|
@@ -1323,6 +2368,8 @@ export class McplServerConnection extends EventEmitter {
|
|
|
1323
2368
|
method: orphaned?.method,
|
|
1324
2369
|
hadState: r.state !== undefined,
|
|
1325
2370
|
hadCheckpoint: r.checkpoint !== undefined,
|
|
2371
|
+
...render,
|
|
2372
|
+
...failure,
|
|
1326
2373
|
});
|
|
1327
2374
|
}
|
|
1328
2375
|
} else if (orphaned) {
|
|
@@ -1331,6 +2378,8 @@ export class McplServerConnection extends EventEmitter {
|
|
|
1331
2378
|
method: orphaned.method,
|
|
1332
2379
|
hadState: false,
|
|
1333
2380
|
hadCheckpoint: false,
|
|
2381
|
+
...render,
|
|
2382
|
+
...failure,
|
|
1334
2383
|
});
|
|
1335
2384
|
}
|
|
1336
2385
|
return; // Orphaned response — ignore
|
|
@@ -1340,8 +2389,32 @@ export class McplServerConnection extends EventEmitter {
|
|
|
1340
2389
|
if (pending.timer) clearTimeout(pending.timer);
|
|
1341
2390
|
|
|
1342
2391
|
if (response.error) {
|
|
1343
|
-
|
|
1344
|
-
|
|
2392
|
+
// The server's own words, after a word on whose they are, bounded as
|
|
2393
|
+
// errors.ts says, since they're the server's text. Only a string is
|
|
2394
|
+
// words: a message of any other JSON value reads as none. The method,
|
|
2395
|
+
// id and JSON-RPC code are the operator's, so they go to the log, which
|
|
2396
|
+
// prints any value a server can send without throwing (logValue). The
|
|
2397
|
+
// caller's text keeps the server's words unredacted, as a JSON-RPC
|
|
2398
|
+
// answer passes (redactServerText's scope); the log is a record of the
|
|
2399
|
+
// server's text, so the message is redacted there, whatever JSON value
|
|
2400
|
+
// it is, as every other line this connection writes is.
|
|
2401
|
+
const said = typeof response.error.message === 'string' ? boundedForLog(response.error.message.trim()) : '';
|
|
2402
|
+
// The call is settled before its log line: its entry is gone and its
|
|
2403
|
+
// timer cleared, so nothing else would settle it, and a host's logger
|
|
2404
|
+
// can throw. The dispatch's containment reports such a throw. (A
|
|
2405
|
+
// configuration that can't be read withholds the message rather than
|
|
2406
|
+
// throw: redactServerText.)
|
|
2407
|
+
pending.reject(new McplRequestError(
|
|
2408
|
+
said
|
|
2409
|
+
? `The "${this.id}" server answered with an error: ${said}`
|
|
2410
|
+
: `The "${this.id}" server answered with an error without saying what went wrong, so whether the request took effect is unknown. ` +
|
|
2411
|
+
'Check its effect before trying again.',
|
|
2412
|
+
'error-response',
|
|
2413
|
+
(response.error as { data?: unknown }).data,
|
|
2414
|
+
));
|
|
2415
|
+
console.error(
|
|
2416
|
+
`MCPL server "${this.id}" answered ${pending.method} (id=${response.id}) with error ${logValue(response.error.code)}: ` +
|
|
2417
|
+
logValueRedacted(response.error.message, (text) => redactServerText(text, this.config ?? {})),
|
|
1345
2418
|
);
|
|
1346
2419
|
} else {
|
|
1347
2420
|
pending.resolve(response.result);
|
|
@@ -1351,17 +2424,38 @@ export class McplServerConnection extends EventEmitter {
|
|
|
1351
2424
|
/**
|
|
1352
2425
|
* Send a successful JSON-RPC response back to the server.
|
|
1353
2426
|
*/
|
|
1354
|
-
private sendResponse(id: string | number, result: unknown): void {
|
|
1355
|
-
|
|
2427
|
+
private sendResponse(id: string | number, result: unknown, epoch = this.transportEpoch): void {
|
|
2428
|
+
// A closed connection's epoch is past every request's, so a refusal
|
|
2429
|
+
// while it is closed is logged too.
|
|
2430
|
+
if (!this.sameTransport(id, epoch) || this.closed) return;
|
|
1356
2431
|
const response: JsonRpcResponse = { jsonrpc: '2.0', id, result };
|
|
1357
2432
|
this.transport?.writeLine(JSON.stringify(response));
|
|
1358
2433
|
}
|
|
1359
2434
|
|
|
2435
|
+
/** Whether a response to a request received on `epoch` may go out now.
|
|
2436
|
+
* The id is the server's, so it's logged as a field; `answer`, which the
|
|
2437
|
+
* caller has formatted for the log, ends the line. */
|
|
2438
|
+
private sameTransport(id: string | number, epoch: number, answer?: string): boolean {
|
|
2439
|
+
if (epoch === this.transportEpoch) return true;
|
|
2440
|
+
console.error(
|
|
2441
|
+
`[mcpl] ${this.id}: not answering request ${logValue(id)}: the transport it came on has closed${answer ? `, with ${answer}` : ''}`,
|
|
2442
|
+
);
|
|
2443
|
+
return false;
|
|
2444
|
+
}
|
|
2445
|
+
|
|
1360
2446
|
/**
|
|
1361
2447
|
* Send a JSON-RPC error response back to the server.
|
|
1362
2448
|
*/
|
|
1363
|
-
private sendErrorResponse(
|
|
1364
|
-
|
|
2449
|
+
private sendErrorResponse(
|
|
2450
|
+
id: string | number,
|
|
2451
|
+
code: number,
|
|
2452
|
+
message: string,
|
|
2453
|
+
data?: unknown,
|
|
2454
|
+
epoch = this.transportEpoch,
|
|
2455
|
+
): void {
|
|
2456
|
+
// A refusal that can't be answered still says what it refused: a held
|
|
2457
|
+
// message admitted under the next transport's grant is refused here.
|
|
2458
|
+
if (!this.sameTransport(id, epoch, `error ${code} ${logDetail(message)}`) || this.closed) return;
|
|
1365
2459
|
const response: JsonRpcResponse = {
|
|
1366
2460
|
jsonrpc: '2.0',
|
|
1367
2461
|
id,
|
|
@@ -1382,20 +2476,38 @@ export class McplServerConnection extends EventEmitter {
|
|
|
1382
2476
|
private setupLifecycle(transport: McplTransport): void {
|
|
1383
2477
|
this.hostCloseRequested = false;
|
|
1384
2478
|
transport.on('error', (err: Error) => {
|
|
1385
|
-
|
|
2479
|
+
const error = new Error(`MCPL server "${this.id}" transport error: ${err.message}`);
|
|
2480
|
+
if (!this.emitToErrorListeners(error)) console.error(`[mcpl] ${this.id}: transport error: ${logDetail(err.message)}`);
|
|
1386
2481
|
});
|
|
1387
2482
|
|
|
1388
|
-
|
|
2483
|
+
const lost = (info: TransportCloseInfo) => {
|
|
1389
2484
|
if (!this.closed) {
|
|
1390
2485
|
this.closed = true;
|
|
1391
2486
|
this.resetPolicyForTransportBoundary();
|
|
2487
|
+
// A teardown the host asked for via reconnectAfterFailure() arrives
|
|
2488
|
+
// here too; it's reported as such below.
|
|
2489
|
+
const byHost = this.hostCloseRequested;
|
|
2490
|
+
this.hostCloseRequested = false;
|
|
1392
2491
|
|
|
1393
|
-
// Reject all pending requests
|
|
1394
|
-
|
|
2492
|
+
// Reject all pending requests. The words hold whether the server
|
|
2493
|
+
// dropped or this host closed the transport to recover; how it closed
|
|
2494
|
+
// is the operator's, so it goes to the log, not into the messages.
|
|
2495
|
+
if (this.pendingRequests.size > 0) {
|
|
2496
|
+
// Whatever the host asked for, the close's own details are kept, so
|
|
2497
|
+
// a drop that races a recycle loses nothing.
|
|
2498
|
+
const how = `${byHost ? 'closed by this host, ' : ''}` +
|
|
2499
|
+
`code=${info.code ?? 'n/a'}, signal=${info.signal ?? 'n/a'}, reason=${closeReasonText(info)}`;
|
|
2500
|
+
console.error(`MCPL server "${this.id}" disconnected (${how}) with ${this.unansweredRequests()}`);
|
|
2501
|
+
}
|
|
2502
|
+
for (const pending of this.pendingRequests.values()) {
|
|
1395
2503
|
if (pending.timer) clearTimeout(pending.timer);
|
|
1396
2504
|
pending.reject(
|
|
1397
|
-
new
|
|
1398
|
-
`
|
|
2505
|
+
new McplRequestError(
|
|
2506
|
+
`The connection to the "${this.id}" server closed before it answered, so whether the request took effect is unknown. ` +
|
|
2507
|
+
(this.reconnectEnabled
|
|
2508
|
+
? 'Check its effect before trying again; this host is reconnecting to it.'
|
|
2509
|
+
: "This host won't reconnect to it on its own. If you need it, ask whoever runs this host."),
|
|
2510
|
+
'no-response',
|
|
1399
2511
|
),
|
|
1400
2512
|
);
|
|
1401
2513
|
}
|
|
@@ -1404,10 +2516,7 @@ export class McplServerConnection extends EventEmitter {
|
|
|
1404
2516
|
|
|
1405
2517
|
// Forward the transport's close info so listeners can tell a server
|
|
1406
2518
|
// that refused the credential (WebSocket 4001) from a crash, and act
|
|
1407
|
-
// before the reconnect below redials.
|
|
1408
|
-
// via reconnectAfterFailure() arrives here too; report it as such.
|
|
1409
|
-
const byHost = this.hostCloseRequested;
|
|
1410
|
-
this.hostCloseRequested = false;
|
|
2519
|
+
// before the reconnect below redials.
|
|
1411
2520
|
const closeInfo: McplConnectionCloseInfo = {
|
|
1412
2521
|
transport: transport.kind,
|
|
1413
2522
|
code: info.code ?? null,
|
|
@@ -1423,6 +2532,21 @@ export class McplServerConnection extends EventEmitter {
|
|
|
1423
2532
|
this.scheduleReconnect();
|
|
1424
2533
|
}
|
|
1425
2534
|
}
|
|
2535
|
+
};
|
|
2536
|
+
transport.on('close', lost);
|
|
2537
|
+
// A stdio child whose stdin or stdout failed, and that outlived SIGKILL
|
|
2538
|
+
// too: the transport stays open, owning the child. The connection holds
|
|
2539
|
+
// it as a launch it can't reap, as a reconnect does, which halts
|
|
2540
|
+
// reconnecting, and is lost as on a close, with the permanent failure
|
|
2541
|
+
// that raises the alert and takes its tools off the list. Its id stays
|
|
2542
|
+
// held until close() confirms the exit. A close the host has begun
|
|
2543
|
+
// reports the failure itself. During a handshake nothing listens yet:
|
|
2544
|
+
// the handshake's own cleanup holds the launch, once the handshake fails.
|
|
2545
|
+
transport.on('unreaped', (failure: Error) => {
|
|
2546
|
+
if (this.closed || this.transport !== transport) return;
|
|
2547
|
+
this.holdUnreaped(transport, failure);
|
|
2548
|
+
lost({ reason: `its stdio pipe failed with ${transport.pipeFailure?.message ?? 'an error'}, and its child outlived SIGKILL` });
|
|
2549
|
+
this.emit('reconnect-failed', { error: this.unreapedFailure(), attempt: Math.max(1, this.reconnectAttempts), permanent: true });
|
|
1426
2550
|
});
|
|
1427
2551
|
}
|
|
1428
2552
|
}
|