@animalabs/agent-framework 0.22.1 → 0.22.2
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 +1 -1
- package/dist/src/agent.d.ts +192 -19
- package/dist/src/agent.d.ts.map +1 -1
- package/dist/src/agent.js +404 -65
- package/dist/src/agent.js.map +1 -1
- package/dist/src/api/mcp-server-core.d.ts +41 -0
- package/dist/src/api/mcp-server-core.d.ts.map +1 -0
- package/dist/src/api/mcp-server-core.js +593 -0
- package/dist/src/api/mcp-server-core.js.map +1 -0
- package/dist/src/api/mcp-server.d.ts +2 -36
- package/dist/src/api/mcp-server.d.ts.map +1 -1
- package/dist/src/api/mcp-server.js +15 -595
- package/dist/src/api/mcp-server.js.map +1 -1
- package/dist/src/api/server.d.ts +9 -1
- package/dist/src/api/server.d.ts.map +1 -1
- package/dist/src/api/server.js +50 -26
- package/dist/src/api/server.js.map +1 -1
- package/dist/src/api/types.d.ts +9 -1
- package/dist/src/api/types.d.ts.map +1 -1
- package/dist/src/code-execution/py-runner.d.ts.map +1 -1
- package/dist/src/code-execution/py-runner.js.map +1 -1
- package/dist/src/framework.d.ts +393 -113
- package/dist/src/framework.d.ts.map +1 -1
- package/dist/src/framework.js +1748 -582
- package/dist/src/framework.js.map +1 -1
- package/dist/src/gate/event-gate.d.ts.map +1 -1
- package/dist/src/gate/gate-script.d.ts.map +1 -1
- package/dist/src/gate/gate-script.js.map +1 -1
- package/dist/src/index.d.ts +2 -2
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +2 -2
- package/dist/src/index.js.map +1 -1
- package/dist/src/mcpl/capability-grant.d.ts.map +1 -1
- package/dist/src/mcpl/channel-registry.d.ts +26 -7
- package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
- package/dist/src/mcpl/channel-registry.js +102 -22
- package/dist/src/mcpl/channel-registry.js.map +1 -1
- package/dist/src/mcpl/checkpoint-manager.d.ts.map +1 -1
- package/dist/src/mcpl/checkpoint-manager.js.map +1 -1
- package/dist/src/mcpl/conversation-router.d.ts.map +1 -1
- package/dist/src/mcpl/errors.js.map +1 -1
- package/dist/src/mcpl/feature-set-manager.d.ts.map +1 -1
- package/dist/src/mcpl/feature-set-manager.js +4 -3
- package/dist/src/mcpl/feature-set-manager.js.map +1 -1
- package/dist/src/mcpl/hook-orchestrator.d.ts.map +1 -1
- package/dist/src/mcpl/inference-router.d.ts.map +1 -1
- package/dist/src/mcpl/log-value.d.ts +54 -0
- package/dist/src/mcpl/log-value.d.ts.map +1 -0
- package/dist/src/mcpl/log-value.js +109 -0
- package/dist/src/mcpl/log-value.js.map +1 -0
- package/dist/src/mcpl/prose-stream-router.d.ts +6 -6
- package/dist/src/mcpl/prose-stream-router.d.ts.map +1 -1
- package/dist/src/mcpl/prose-stream-router.js +4 -4
- package/dist/src/mcpl/prose-stream-router.js.map +1 -1
- package/dist/src/mcpl/push-coalescer.d.ts +38 -3
- package/dist/src/mcpl/push-coalescer.d.ts.map +1 -1
- package/dist/src/mcpl/push-coalescer.js +81 -16
- package/dist/src/mcpl/push-coalescer.js.map +1 -1
- package/dist/src/mcpl/push-handler.d.ts +26 -3
- package/dist/src/mcpl/push-handler.d.ts.map +1 -1
- package/dist/src/mcpl/push-handler.js +87 -23
- package/dist/src/mcpl/push-handler.js.map +1 -1
- package/dist/src/mcpl/reference-fetcher.d.ts.map +1 -1
- package/dist/src/mcpl/reference-fetcher.js.map +1 -1
- package/dist/src/mcpl/references.d.ts +2 -2
- package/dist/src/mcpl/references.d.ts.map +1 -1
- package/dist/src/mcpl/references.js.map +1 -1
- package/dist/src/mcpl/refusal-lines.d.ts +37 -0
- package/dist/src/mcpl/refusal-lines.d.ts.map +1 -0
- package/dist/src/mcpl/refusal-lines.js +61 -0
- package/dist/src/mcpl/refusal-lines.js.map +1 -0
- package/dist/src/mcpl/scope-manager.d.ts.map +1 -1
- package/dist/src/mcpl/server-connection.d.ts +18 -2
- package/dist/src/mcpl/server-connection.d.ts.map +1 -1
- package/dist/src/mcpl/server-connection.js +66 -11
- package/dist/src/mcpl/server-connection.js.map +1 -1
- package/dist/src/mcpl/server-registry.d.ts.map +1 -1
- package/dist/src/mcpl/tool-classes.d.ts +1 -1
- package/dist/src/mcpl/tool-classes.d.ts.map +1 -1
- package/dist/src/mcpl/tool-classes.js +3 -2
- package/dist/src/mcpl/tool-classes.js.map +1 -1
- package/dist/src/mcpl/tool-lifecycle.d.ts.map +1 -1
- package/dist/src/mcpl/tool-lifecycle.js.map +1 -1
- package/dist/src/mcpl/transport.d.ts +23 -5
- package/dist/src/mcpl/transport.d.ts.map +1 -1
- package/dist/src/mcpl/transport.js +103 -10
- package/dist/src/mcpl/transport.js.map +1 -1
- package/dist/src/mcpl/types.d.ts +30 -26
- package/dist/src/mcpl/types.d.ts.map +1 -1
- package/dist/src/mcpl/types.js.map +1 -1
- package/dist/src/module-registry.d.ts.map +1 -1
- package/dist/src/module-registry.js.map +1 -1
- package/dist/src/modules/api/index.d.ts.map +1 -1
- package/dist/src/modules/discord/discord-js-client.d.ts.map +1 -1
- package/dist/src/modules/discord/discord-js-client.js.map +1 -1
- package/dist/src/modules/discord/index.d.ts.map +1 -1
- package/dist/src/modules/discord/index.js +4 -2
- package/dist/src/modules/discord/index.js.map +1 -1
- package/dist/src/modules/health/index.d.ts.map +1 -1
- package/dist/src/modules/health/index.js.map +1 -1
- package/dist/src/modules/history/index.d.ts.map +1 -1
- package/dist/src/modules/history/index.js.map +1 -1
- package/dist/src/modules/history/semantic.d.ts.map +1 -1
- package/dist/src/modules/history/semantic.js.map +1 -1
- package/dist/src/modules/workspace/branch-intent.d.ts.map +1 -1
- package/dist/src/modules/workspace/branch-intent.js.map +1 -1
- package/dist/src/modules/workspace/disk-agreement.d.ts +22 -4
- package/dist/src/modules/workspace/disk-agreement.d.ts.map +1 -1
- package/dist/src/modules/workspace/disk-agreement.js +63 -25
- package/dist/src/modules/workspace/disk-agreement.js.map +1 -1
- package/dist/src/modules/workspace/effects.d.ts.map +1 -1
- package/dist/src/modules/workspace/index.d.ts +8 -4
- package/dist/src/modules/workspace/index.d.ts.map +1 -1
- package/dist/src/modules/workspace/index.js +39 -12
- package/dist/src/modules/workspace/index.js.map +1 -1
- package/dist/src/modules/workspace/observe.d.ts +43 -8
- package/dist/src/modules/workspace/observe.d.ts.map +1 -1
- package/dist/src/modules/workspace/observe.js +264 -39
- package/dist/src/modules/workspace/observe.js.map +1 -1
- package/dist/src/modules/workspace/reconcile.d.ts +17 -1
- package/dist/src/modules/workspace/reconcile.d.ts.map +1 -1
- package/dist/src/modules/workspace/reconcile.js +57 -14
- package/dist/src/modules/workspace/reconcile.js.map +1 -1
- package/dist/src/modules/workspace/types.d.ts +27 -2
- 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.map +1 -1
- package/dist/src/modules/workspace/watcher.js.map +1 -1
- package/dist/src/operator-log.d.ts.map +1 -1
- package/dist/src/operator-log.js.map +1 -1
- package/dist/src/provider-waits.d.ts.map +1 -1
- package/dist/src/provider-waits.js.map +1 -1
- package/dist/src/queue.d.ts.map +1 -1
- package/dist/src/record-journal.d.ts.map +1 -1
- package/dist/src/record-journal.js.map +1 -1
- package/dist/src/recovery/discord-awareness-outbox.d.ts.map +1 -1
- package/dist/src/recovery/discord-awareness-outbox.js.map +1 -1
- package/dist/src/recovery/offline-branch.d.ts.map +1 -1
- package/dist/src/recovery/offline-branch.js +29 -24
- package/dist/src/recovery/offline-branch.js.map +1 -1
- package/dist/src/recovery/recover-cli.js.map +1 -1
- package/dist/src/runtime/liveness-watchdog.d.ts.map +1 -1
- package/dist/src/runtime/liveness-watchdog.js.map +1 -1
- package/dist/src/shutdown.d.ts.map +1 -1
- package/dist/src/shutdown.js.map +1 -1
- package/dist/src/stored-removal.d.ts +73 -0
- package/dist/src/stored-removal.d.ts.map +1 -0
- package/dist/src/stored-removal.js +100 -0
- package/dist/src/stored-removal.js.map +1 -0
- package/dist/src/thinking-token-stamp.js.map +1 -1
- package/dist/src/tool-image-ledger.d.ts.map +1 -1
- package/dist/src/tool-image-ledger.js.map +1 -1
- package/dist/src/tool-presentation.d.ts.map +1 -1
- 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.map +1 -1
- package/dist/src/tune-out/coordinator.d.ts.map +1 -1
- package/dist/src/tune-out/coordinator.js.map +1 -1
- package/dist/src/tune-out/tools.d.ts +1 -1
- package/dist/src/tune-out/tools.d.ts.map +1 -1
- package/dist/src/types/agent.d.ts +42 -6
- package/dist/src/types/agent.d.ts.map +1 -1
- package/dist/src/types/framework.d.ts +10 -0
- package/dist/src/types/framework.d.ts.map +1 -1
- package/dist/src/types/module.d.ts +8 -4
- package/dist/src/types/module.d.ts.map +1 -1
- package/dist/src/types/trace.d.ts +61 -10
- package/dist/src/types/trace.d.ts.map +1 -1
- package/dist/src/usage/usage-tracker.d.ts.map +1 -1
- package/dist/test/agent-runtime-settings.test.js +1139 -3
- package/dist/test/agent-runtime-settings.test.js.map +1 -1
- package/dist/test/batched-wake-locus.test.js.map +1 -1
- package/dist/test/channel-label-history.test.js.map +1 -1
- package/dist/test/code-execution.test.js.map +1 -1
- package/dist/test/conversation-fork-injection-scoping.test.js.map +1 -1
- package/dist/test/conversation-router.test.js.map +1 -1
- package/dist/test/conversation-routing.test.js +240 -6
- package/dist/test/conversation-routing.test.js.map +1 -1
- package/dist/test/discord-awareness-outbox.test.js +104 -0
- package/dist/test/discord-awareness-outbox.test.js.map +1 -1
- package/dist/test/discord-typing-keepalive.test.d.ts +2 -0
- package/dist/test/discord-typing-keepalive.test.d.ts.map +1 -0
- package/dist/test/discord-typing-keepalive.test.js +77 -0
- package/dist/test/discord-typing-keepalive.test.js.map +1 -0
- package/dist/test/ephemeral-settle.test.js.map +1 -1
- package/dist/test/event-gate-passthrough.test.js.map +1 -1
- package/dist/test/event-gate-policy-mutation.test.js.map +1 -1
- package/dist/test/event-gate-rate-limit.test.js.map +1 -1
- package/dist/test/event-gate-script.test.js.map +1 -1
- package/dist/test/event-gate-selfwake.test.js.map +1 -1
- package/dist/test/event-gate-tags.test.js.map +1 -1
- package/dist/test/event-gate.test.js.map +1 -1
- package/dist/test/explicit-prose-routing.test.js +363 -42
- package/dist/test/explicit-prose-routing.test.js.map +1 -1
- package/dist/test/framework.test.js.map +1 -1
- package/dist/test/helpers/coalescing-fixture.d.ts +2 -0
- package/dist/test/helpers/coalescing-fixture.d.ts.map +1 -1
- package/dist/test/helpers/coalescing-fixture.js +8 -2
- package/dist/test/helpers/coalescing-fixture.js.map +1 -1
- package/dist/test/helpers/mock-membrane.d.ts.map +1 -1
- package/dist/test/helpers/mock-membrane.js.map +1 -1
- package/dist/test/hide-then-turn.test.d.ts +2 -0
- package/dist/test/hide-then-turn.test.d.ts.map +1 -0
- package/dist/test/hide-then-turn.test.js +101 -0
- package/dist/test/hide-then-turn.test.js.map +1 -0
- package/dist/test/history-module-overview.test.js.map +1 -1
- package/dist/test/history-module-semantic.test.js.map +1 -1
- package/dist/test/history-module.test.js.map +1 -1
- package/dist/test/history-semantic-review-findings.test.js.map +1 -1
- package/dist/test/host-quiesce.test.js +125 -0
- package/dist/test/host-quiesce.test.js.map +1 -1
- package/dist/test/inference-exhaustion-rewind.test.js +81 -3
- package/dist/test/inference-exhaustion-rewind.test.js.map +1 -1
- package/dist/test/inference-failure-bounds.test.js +1 -0
- package/dist/test/inference-failure-bounds.test.js.map +1 -1
- package/dist/test/kv-unified-open-flight-retry.test.js.map +1 -1
- package/dist/test/liveness-watchdog.test.js.map +1 -1
- package/dist/test/maintenance-timer.test.js +1564 -2
- package/dist/test/maintenance-timer.test.js.map +1 -1
- package/dist/test/mcp-server-bin.test.d.ts +2 -0
- package/dist/test/mcp-server-bin.test.d.ts.map +1 -0
- package/dist/test/mcp-server-bin.test.js +142 -0
- package/dist/test/mcp-server-bin.test.js.map +1 -0
- package/dist/test/mcpl-awareness-barrier.test.js.map +1 -1
- package/dist/test/mcpl-child-env.test.js +1 -1
- package/dist/test/mcpl-child-env.test.js.map +1 -1
- package/dist/test/mcpl-coalesced-content-parity.test.d.ts +2 -0
- package/dist/test/mcpl-coalesced-content-parity.test.d.ts.map +1 -0
- package/dist/test/mcpl-coalesced-content-parity.test.js +532 -0
- package/dist/test/mcpl-coalesced-content-parity.test.js.map +1 -0
- package/dist/test/mcpl-coalescing.test.js.map +1 -1
- package/dist/test/mcpl-config-diagnostics.test.js.map +1 -1
- package/dist/test/mcpl-gate-roundtrip.test.js.map +1 -1
- package/dist/test/mcpl-manifest-changed.test.js.map +1 -1
- package/dist/test/mcpl-reconnect-events.test.js.map +1 -1
- package/dist/test/mcpl-reconnect-reregistration.test.js +2 -0
- package/dist/test/mcpl-reconnect-reregistration.test.js.map +1 -1
- package/dist/test/mcpl-reference-fetcher.test.js.map +1 -1
- package/dist/test/mcpl-reference-fields.test.js.map +1 -1
- package/dist/test/mcpl-rejection-log-lines.test.d.ts +2 -0
- package/dist/test/mcpl-rejection-log-lines.test.d.ts.map +1 -0
- package/dist/test/mcpl-rejection-log-lines.test.js +349 -0
- package/dist/test/mcpl-rejection-log-lines.test.js.map +1 -0
- package/dist/test/mcpl-stderr-tail.test.d.ts +2 -0
- package/dist/test/mcpl-stderr-tail.test.d.ts.map +1 -0
- package/dist/test/mcpl-stderr-tail.test.js +210 -0
- package/dist/test/mcpl-stderr-tail.test.js.map +1 -0
- package/dist/test/mcpl-tool-lifecycle-e2e.test.js.map +1 -1
- package/dist/test/mcpl-tool-lifecycle-emitter.test.js.map +1 -1
- package/dist/test/mcpl-tool-lifecycle-script.test.js.map +1 -1
- package/dist/test/mcpl-tool-lifecycle.test.js.map +1 -1
- package/dist/test/mcpl-transport-close.test.d.ts +2 -0
- package/dist/test/mcpl-transport-close.test.d.ts.map +1 -0
- package/dist/test/mcpl-transport-close.test.js +373 -0
- package/dist/test/mcpl-transport-close.test.js.map +1 -0
- package/dist/test/mcpl-transport-spawn.test.d.ts +2 -0
- package/dist/test/mcpl-transport-spawn.test.d.ts.map +1 -0
- package/dist/test/mcpl-transport-spawn.test.js +58 -0
- package/dist/test/mcpl-transport-spawn.test.js.map +1 -0
- package/dist/test/mcpl-ws-transport.test.js +1 -0
- package/dist/test/mcpl-ws-transport.test.js.map +1 -1
- package/dist/test/offline-recovery-branch.test.js +344 -0
- package/dist/test/offline-recovery-branch.test.js.map +1 -1
- package/dist/test/operator-surgery.test.js +197 -0
- package/dist/test/operator-surgery.test.js.map +1 -1
- package/dist/test/overbudget-drain-breaker.test.js +1 -0
- package/dist/test/overbudget-drain-breaker.test.js.map +1 -1
- package/dist/test/pack-installs-alone.test.d.ts +2 -0
- package/dist/test/pack-installs-alone.test.d.ts.map +1 -0
- package/dist/test/pack-installs-alone.test.js +284 -0
- package/dist/test/pack-installs-alone.test.js.map +1 -0
- package/dist/test/physical-window-restart.test.d.ts +6 -4
- package/dist/test/physical-window-restart.test.d.ts.map +1 -1
- package/dist/test/physical-window-restart.test.js +275 -10
- package/dist/test/physical-window-restart.test.js.map +1 -1
- package/dist/test/placed-reaction-baseline.test.js.map +1 -1
- package/dist/test/present-while-acting.test.js +949 -6
- package/dist/test/present-while-acting.test.js.map +1 -1
- package/dist/test/provider-acceleration-cooldown.test.js.map +1 -1
- package/dist/test/provider-waits.test.js +67 -4
- package/dist/test/provider-waits.test.js.map +1 -1
- package/dist/test/puppet-tool-call.test.js +2 -0
- package/dist/test/puppet-tool-call.test.js.map +1 -1
- package/dist/test/reaction-baseline-platform.test.js.map +1 -1
- package/dist/test/record-journal.test.js.map +1 -1
- package/dist/test/refusal-rewind.test.js +9 -0
- package/dist/test/refusal-rewind.test.js.map +1 -1
- package/dist/test/refusal-single-call.test.js.map +1 -1
- package/dist/test/release-bump.test.d.ts +2 -0
- package/dist/test/release-bump.test.d.ts.map +1 -0
- package/dist/test/release-bump.test.js +248 -0
- package/dist/test/release-bump.test.js.map +1 -0
- package/dist/test/release-changelog.test.js.map +1 -1
- package/dist/test/release-holds.test.d.ts +2 -0
- package/dist/test/release-holds.test.d.ts.map +1 -0
- package/dist/test/release-holds.test.js +456 -0
- package/dist/test/release-holds.test.js.map +1 -0
- package/dist/test/same-round-think-policy.test.js.map +1 -1
- package/dist/test/shutdown-errors.test.js.map +1 -1
- package/dist/test/silent-heartbeat-rows.test.js.map +1 -1
- package/dist/test/stop-with-host-operations.test.d.ts +20 -0
- package/dist/test/stop-with-host-operations.test.d.ts.map +1 -0
- package/dist/test/stop-with-host-operations.test.js +1174 -0
- package/dist/test/stop-with-host-operations.test.js.map +1 -0
- package/dist/test/stop-with-work-in-flight.test.js +162 -50
- package/dist/test/stop-with-work-in-flight.test.js.map +1 -1
- package/dist/test/surgery-marks-receipt.test.js.map +1 -1
- package/dist/test/tool-batch-hook.test.js.map +1 -1
- package/dist/test/tool-result-guard.test.js +131 -9
- package/dist/test/tool-result-guard.test.js.map +1 -1
- package/dist/test/tool-result-spill.test.js.map +1 -1
- package/dist/test/tool-wrapper-prose-guard-integration.test.js.map +1 -1
- package/dist/test/trunk-channel-routing.test.js +330 -7
- package/dist/test/trunk-channel-routing.test.js.map +1 -1
- package/dist/test/tune-out-e2e.test.js.map +1 -1
- package/dist/test/unconvertible-tool-result.test.js.map +1 -1
- package/dist/test/unsent-compiles.test.d.ts +2 -0
- package/dist/test/unsent-compiles.test.d.ts.map +1 -0
- package/dist/test/unsent-compiles.test.js +139 -0
- package/dist/test/unsent-compiles.test.js.map +1 -0
- package/dist/test/workspace-branch-continuation.test.js.map +1 -1
- package/dist/test/workspace-materialize-freshness.test.js.map +1 -1
- package/dist/test/workspace-read-text.test.js.map +1 -1
- package/dist/test/workspace-three-way.test.js +907 -7
- package/dist/test/workspace-three-way.test.js.map +1 -1
- package/dist/test/workspace-watcher.test.js.map +1 -1
- package/dist/test/xml-round-persist.test.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +8 -6
- package/src/agent.ts +411 -66
- package/src/api/mcp-server-core.ts +681 -0
- package/src/api/mcp-server.ts +16 -685
- package/src/api/server.ts +51 -29
- package/src/api/types.ts +9 -1
- package/src/framework.ts +1899 -641
- package/src/index.ts +2 -2
- package/src/mcpl/channel-registry.ts +126 -31
- package/src/mcpl/feature-set-manager.ts +4 -3
- package/src/mcpl/log-value.ts +116 -0
- package/src/mcpl/prose-stream-router.ts +6 -6
- package/src/mcpl/push-coalescer.ts +76 -12
- package/src/mcpl/push-handler.ts +95 -23
- package/src/mcpl/refusal-lines.ts +59 -0
- package/src/mcpl/server-connection.ts +70 -11
- package/src/mcpl/tool-classes.ts +3 -2
- package/src/mcpl/transport.ts +105 -14
- package/src/mcpl/types.ts +5 -1
- package/src/modules/discord/index.ts +4 -2
- package/src/modules/workspace/disk-agreement.ts +74 -18
- package/src/modules/workspace/index.ts +41 -11
- package/src/modules/workspace/observe.ts +265 -39
- package/src/modules/workspace/reconcile.ts +71 -14
- package/src/modules/workspace/types.ts +26 -1
- package/src/recovery/offline-branch.ts +32 -28
- package/src/stored-removal.ts +126 -0
- package/src/types/agent.ts +42 -6
- package/src/types/framework.ts +10 -0
- package/src/types/module.ts +8 -4
- package/src/types/trace.ts +62 -11
package/src/mcpl/push-handler.ts
CHANGED
|
@@ -18,8 +18,10 @@ import type {
|
|
|
18
18
|
import type { FeatureSetManager } from './feature-set-manager.js';
|
|
19
19
|
import { McplFeatureSetError } from './feature-set-manager.js';
|
|
20
20
|
import { expandCoreTags } from './tags.js';
|
|
21
|
-
import { EmptyContentError, validateCoalescedContent } from './push-coalescer.js';
|
|
21
|
+
import { coalesceRejectionReason, EmptyContentError, validateCoalescedContent } from './push-coalescer.js';
|
|
22
22
|
import { isSilentHeartbeatMarker, isVisiblyEmptyContent } from './visible-content.js';
|
|
23
|
+
import { logDetail, logValue } from './log-value.js';
|
|
24
|
+
import { RefusalLines } from './refusal-lines.js';
|
|
23
25
|
|
|
24
26
|
// ============================================================================
|
|
25
27
|
// McplPushEvent (the ProcessEvent shape pushed to the queue)
|
|
@@ -55,13 +57,17 @@ export interface McplPushEvent {
|
|
|
55
57
|
// ============================================================================
|
|
56
58
|
|
|
57
59
|
/**
|
|
58
|
-
* Convert a single MCPL wire-format content block to a membrane ContentBlock
|
|
59
|
-
*
|
|
60
|
+
* Convert a single MCPL wire-format content block to a membrane ContentBlock,
|
|
61
|
+
* as the channel lane's convertBlock (channel-registry.ts) does, apart from
|
|
62
|
+
* the provenance its reference stubs name. Hook context injections convert
|
|
63
|
+
* through hook-orchestrator.ts's own.
|
|
60
64
|
*/
|
|
61
65
|
export function convertBlock(block: McplContentBlock): ContentBlock {
|
|
62
66
|
switch (block.type) {
|
|
63
67
|
case 'text':
|
|
64
|
-
|
|
68
|
+
// Text that isn't a string is no text, as the request sanitizer and
|
|
69
|
+
// the visible-content check already read it.
|
|
70
|
+
return { type: 'text', text: typeof block.text === 'string' ? block.text : '' };
|
|
65
71
|
|
|
66
72
|
case 'image':
|
|
67
73
|
if (isInlineContradiction(block)) {
|
|
@@ -69,24 +75,24 @@ export function convertBlock(block: McplContentBlock): ContentBlock {
|
|
|
69
75
|
// closed, withhold the data (checked BEFORE the data branch).
|
|
70
76
|
return { type: 'text', text: INLINE_WITHHELD_TEXT };
|
|
71
77
|
}
|
|
72
|
-
if (block.uri && block.disposition) {
|
|
78
|
+
if (isFilledString(block.uri) && block.disposition) {
|
|
73
79
|
// RFC-005 uri-form media with a disposition claim: stub, do not
|
|
74
80
|
// hand the URI to the provider or inline it.
|
|
75
81
|
return { type: 'text', text: referenceStubOrNull(block, 'attachment on push event') ?? '[reference]' };
|
|
76
82
|
}
|
|
77
|
-
if (block.data && block.mimeType) {
|
|
83
|
+
if (isFilledString(block.data) && isFilledString(block.mimeType)) {
|
|
78
84
|
return {
|
|
79
85
|
type: 'image',
|
|
80
86
|
source: { type: 'base64', data: block.data, mediaType: block.mimeType },
|
|
81
87
|
} as ContentBlock;
|
|
82
88
|
}
|
|
83
|
-
if (block.uri) {
|
|
89
|
+
if (isFilledString(block.uri)) {
|
|
84
90
|
return {
|
|
85
91
|
type: 'image',
|
|
86
92
|
source: { type: 'url', url: block.uri },
|
|
87
93
|
} as ContentBlock;
|
|
88
94
|
}
|
|
89
|
-
return { type: 'text', text: '[Image: no data]' };
|
|
95
|
+
return { type: 'text', text: isFilledString(block.data) ? '[Image: no media type]' : '[Image: no data]' };
|
|
90
96
|
|
|
91
97
|
case 'audio':
|
|
92
98
|
if (isInlineContradiction(block)) {
|
|
@@ -94,31 +100,43 @@ export function convertBlock(block: McplContentBlock): ContentBlock {
|
|
|
94
100
|
// closed, withhold the data (checked BEFORE the data branch).
|
|
95
101
|
return { type: 'text', text: INLINE_WITHHELD_TEXT };
|
|
96
102
|
}
|
|
97
|
-
if (block.uri && block.disposition) {
|
|
103
|
+
if (isFilledString(block.uri) && block.disposition) {
|
|
98
104
|
// RFC-005 uri-form media with a disposition claim: stub, do not
|
|
99
105
|
// hand the URI to the provider or inline it.
|
|
100
106
|
return { type: 'text', text: referenceStubOrNull(block, 'attachment on push event') ?? '[reference]' };
|
|
101
107
|
}
|
|
102
|
-
if (block.data && block.mimeType) {
|
|
108
|
+
if (isFilledString(block.data) && isFilledString(block.mimeType)) {
|
|
103
109
|
return {
|
|
104
110
|
type: 'audio',
|
|
105
111
|
source: { type: 'base64', data: block.data, mediaType: block.mimeType },
|
|
106
112
|
} as ContentBlock;
|
|
107
113
|
}
|
|
108
|
-
return { type: 'text', text: '[Audio: no data]' };
|
|
114
|
+
return { type: 'text', text: isFilledString(block.data) ? '[Audio: no media type]' : '[Audio: no data]' };
|
|
109
115
|
|
|
110
116
|
case 'resource':
|
|
111
117
|
// RFC-005: reference blocks become bounded stubs — never raw URIs
|
|
112
118
|
// (a signed URL is a bearer credential that looks like a location).
|
|
113
119
|
return { type: 'text', text: referenceStubOrNull(block, 'attachment on push event') ?? '[reference]' };
|
|
114
120
|
|
|
115
|
-
default:
|
|
121
|
+
default: {
|
|
116
122
|
// Unknown wire block types previously fell off the exhaustive switch
|
|
117
|
-
// and propagated `undefined` into ContentBlock[]. Fail visibly.
|
|
118
|
-
|
|
123
|
+
// and propagated `undefined` into ContentBlock[]. Fail visibly. A type
|
|
124
|
+
// that isn't a string is no type, and is never converted to one: an
|
|
125
|
+
// object can throw when it is.
|
|
126
|
+
const type: unknown = (block as { type?: unknown }).type;
|
|
127
|
+
return { type: 'text', text: `[unrecognized content block: ${typeof type === 'string' ? type : 'untyped'}]` };
|
|
128
|
+
}
|
|
119
129
|
}
|
|
120
130
|
}
|
|
121
131
|
|
|
132
|
+
/** A string with something in it: the only data, mimeType or uri a block's
|
|
133
|
+
* source is built from, and the only uri that makes it a reference. Anything
|
|
134
|
+
* else a sender puts there leaves the block without it, so conversion never
|
|
135
|
+
* throws and never copies a non-string. */
|
|
136
|
+
function isFilledString(value: unknown): value is string {
|
|
137
|
+
return typeof value === 'string' && value !== '';
|
|
138
|
+
}
|
|
139
|
+
|
|
122
140
|
// ============================================================================
|
|
123
141
|
// LRU Dedup Set
|
|
124
142
|
// ============================================================================
|
|
@@ -171,6 +189,9 @@ export class PushHandler {
|
|
|
171
189
|
private emitTraceFn: (event: { type: string; [key: string]: unknown }) => void;
|
|
172
190
|
private shouldTriggerInference?: (content: string, metadata: Record<string, unknown>) => boolean;
|
|
173
191
|
private dedup = new DedupSet(1000);
|
|
192
|
+
/** Holds back repeats of the line for a channel-scoped push refused because
|
|
193
|
+
* its channel isn't declared (-32023) or may not be used (-32017). */
|
|
194
|
+
private channelRefusalLines = new RefusalLines();
|
|
174
195
|
|
|
175
196
|
constructor(
|
|
176
197
|
featureSetManager: FeatureSetManager,
|
|
@@ -221,7 +242,7 @@ export class PushHandler {
|
|
|
221
242
|
: 'Feature set validation failed';
|
|
222
243
|
// Loud rejection — a rejected push event is an agent that silently
|
|
223
244
|
// never hears the message. (2026-07-09 diagnosability pass.)
|
|
224
|
-
console.error(`[push-event-rejected] server=${serverId} eventId=${params.eventId} reason=${reason}`);
|
|
245
|
+
console.error(`[push-event-rejected] server=${serverId} eventId=${logValue(params.eventId ?? '')} reason=${logValue(reason)}`);
|
|
225
246
|
if (err instanceof McplFeatureSetError && responder?.respondError) {
|
|
226
247
|
responder.respondError(err.code, reason, { featureSet: err.featureSet });
|
|
227
248
|
} else {
|
|
@@ -240,11 +261,13 @@ export class PushHandler {
|
|
|
240
261
|
if (coalesced) {
|
|
241
262
|
// RFC-006 §13: malformed content is a -32602, checked before conversion.
|
|
242
263
|
try {
|
|
243
|
-
validateCoalescedContent(params.payload?.content,
|
|
264
|
+
validateCoalescedContent(params.payload?.content, { allowEmpty: emptyAllowed });
|
|
244
265
|
} catch (error) {
|
|
245
266
|
const err = error as Error & { code?: number; field?: string };
|
|
267
|
+
const code = err.code ?? -32602;
|
|
246
268
|
if (err instanceof EmptyContentError) this.traceEmptyRejection(serverId, params);
|
|
247
|
-
|
|
269
|
+
else this.traceCoalesceRejection(serverId, params, err, code);
|
|
270
|
+
if (responder?.respondError) responder.respondError(code, err.message, { field: err.field });
|
|
248
271
|
else responder?.respond({ accepted: false, reason: err instanceof EmptyContentError ? err.reason : err.message });
|
|
249
272
|
return;
|
|
250
273
|
}
|
|
@@ -265,7 +288,7 @@ export class PushHandler {
|
|
|
265
288
|
// coalescer's receipts instead (RFC-006 §3.1: a retry within the window
|
|
266
289
|
// gets its original result, which this set could not return).
|
|
267
290
|
if (!coalesced && this.dedup.checkAndAdd(params.eventId)) {
|
|
268
|
-
console.error(`[push-event-rejected] server=${serverId} eventId=${params.eventId} reason=duplicate`);
|
|
291
|
+
console.error(`[push-event-rejected] server=${serverId} eventId=${logValue(params.eventId ?? '')} reason=duplicate`);
|
|
269
292
|
responder?.respond({ accepted: false, reason: 'duplicate' });
|
|
270
293
|
return;
|
|
271
294
|
}
|
|
@@ -314,8 +337,9 @@ export class PushHandler {
|
|
|
314
337
|
responder?.respond(result);
|
|
315
338
|
} catch (error) {
|
|
316
339
|
const err = error as Error & { code?: number; field?: string };
|
|
317
|
-
|
|
318
|
-
|
|
340
|
+
const code = typeof err.code === 'number' ? err.code : -32603;
|
|
341
|
+
this.traceCoalesceRejection(serverId, params, err, code);
|
|
342
|
+
if (responder?.respondError) responder.respondError(code, err.message, { field: err.field });
|
|
319
343
|
else responder?.respond({ accepted: false, reason: err.message });
|
|
320
344
|
}
|
|
321
345
|
return;
|
|
@@ -334,13 +358,61 @@ export class PushHandler {
|
|
|
334
358
|
responder?.respond({ accepted: true, inferenceId });
|
|
335
359
|
}
|
|
336
360
|
|
|
337
|
-
/**
|
|
361
|
+
/**
|
|
362
|
+
* A coalesced occurrence the host refused, as loud as every other push
|
|
363
|
+
* rejection: its producer is told on the wire, and until now the host said
|
|
364
|
+
* nothing, so a dropped message left no trace where operators look
|
|
365
|
+
* (agent-framework#266). The producer gets the wire answer this lane gives
|
|
366
|
+
* (`answered`): the thrown error's code and message, or -32602 for a
|
|
367
|
+
* malformed occurrence without one. The reason follows that answer
|
|
368
|
+
* (coalesceRejectionReason): `coalesce-failed` when the host itself failed,
|
|
369
|
+
* `coalesce-invalid` when the producer's occurrence was refused, with the
|
|
370
|
+
* failing `field` and the error's message as `detail`. A missing eventId
|
|
371
|
+
* prints as `""` and traces as `''`, as the channel lane's missing id does.
|
|
372
|
+
* A channel-scoped push refused because its channel isn't declared or may
|
|
373
|
+
* not be used names the channel on its line and its trace, and is refused
|
|
374
|
+
* for as long as that lasts, so its line prints at most once a minute per
|
|
375
|
+
* channel (RefusalLines); every refusal is traced.
|
|
376
|
+
*/
|
|
377
|
+
private traceCoalesceRejection(
|
|
378
|
+
serverId: string,
|
|
379
|
+
params: PushEventParams,
|
|
380
|
+
error: { field?: unknown; message: string },
|
|
381
|
+
answered: number,
|
|
382
|
+
): void {
|
|
383
|
+
const reason = coalesceRejectionReason(answered);
|
|
384
|
+
const field = typeof error.field === 'string' && error.field ? error.field : undefined;
|
|
385
|
+
const scoped = params.coalesce?.channelId;
|
|
386
|
+
const channelId = (answered === -32023 || answered === -32017) && typeof scoped === 'string' ? scoped : undefined;
|
|
387
|
+
const held = channelId === undefined ? 0 : this.channelRefusalLines.admit(serverId, channelId);
|
|
388
|
+
if (held !== null) {
|
|
389
|
+
console.error(
|
|
390
|
+
`[push-event-rejected] server=${serverId} featureSet=${logValue(params.featureSet ?? '')} eventId=${logValue(params.eventId ?? '')} ` +
|
|
391
|
+
(channelId === undefined ? '' : `channel=${logValue(channelId)} `) +
|
|
392
|
+
`reason=${reason}${field ? ` field=${logValue(field)}` : ''}: ${logDetail(error.message)}` +
|
|
393
|
+
(held ? ` (${held} more since the last line)` : ''),
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
this.emitTraceFn({
|
|
397
|
+
type: 'mcpl:push-event-rejected',
|
|
398
|
+
serverId,
|
|
399
|
+
eventId: typeof params.eventId === 'string' ? params.eventId : '',
|
|
400
|
+
featureSet: params.featureSet,
|
|
401
|
+
reason,
|
|
402
|
+
...(field ? { field } : {}),
|
|
403
|
+
...(channelId === undefined ? {} : { channelId }),
|
|
404
|
+
detail: error.message,
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/** Loud, like every other push rejection: the producer's wake went nowhere.
|
|
409
|
+
* A missing eventId prints as `""` and traces as `''`, as on every push line. */
|
|
338
410
|
private traceEmptyRejection(serverId: string, params: PushEventParams): void {
|
|
339
|
-
console.error(`[push-event-rejected] server=${serverId} eventId=${params.eventId} reason=empty-content`);
|
|
411
|
+
console.error(`[push-event-rejected] server=${serverId} eventId=${logValue(params.eventId ?? '')} reason=empty-content`);
|
|
340
412
|
this.emitTraceFn({
|
|
341
413
|
type: 'mcpl:push-event-rejected',
|
|
342
414
|
serverId,
|
|
343
|
-
eventId: params.eventId,
|
|
415
|
+
eventId: typeof params.eventId === 'string' ? params.eventId : '',
|
|
344
416
|
featureSet: params.featureSet,
|
|
345
417
|
reason: 'empty-content',
|
|
346
418
|
});
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { logValue } from './log-value.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* At most one host line a minute for a refusal a server keeps repeating for
|
|
5
|
+
* as long as a standing condition lasts: messages for a channel it never
|
|
6
|
+
* registered, or channel-scoped pushes to a channel it hasn't declared or may
|
|
7
|
+
* not use. A server that never registered a busy channel would otherwise
|
|
8
|
+
* print a line for every message in it. Only the line is held back: every
|
|
9
|
+
* refusal is still answered and traced. Each lane keeps its own records, as
|
|
10
|
+
* its lines are its own.
|
|
11
|
+
*
|
|
12
|
+
* Per server and channel, the first refusal prints. After that a refusal
|
|
13
|
+
* prints only once a minute has passed since that channel's last line, and
|
|
14
|
+
* its line names how many were held back since. The minute is timed with a
|
|
15
|
+
* monotonic clock, so a step of the wall clock can't silence a channel. A
|
|
16
|
+
* refusal within a minute of the channel's last line is counted into its
|
|
17
|
+
* next line, whatever happens to the channel in between (it may be
|
|
18
|
+
* registered, unregistered or reconnected).
|
|
19
|
+
*
|
|
20
|
+
* A record is keyed by the channel as its line prints it (logValue), so two
|
|
21
|
+
* ids share one exactly when their lines name them alike, which only ids
|
|
22
|
+
* over 256 units can. The records cover the 1,000 most recently refused
|
|
23
|
+
* channels: a new channel evicts the least recently refused one, with its
|
|
24
|
+
* minute and any count it held, and that channel's next refusal prints as a
|
|
25
|
+
* first.
|
|
26
|
+
*/
|
|
27
|
+
export class RefusalLines {
|
|
28
|
+
/** By key, from the least recently refused channel to the most. */
|
|
29
|
+
private readonly records = new Map<string, { at: number; since: number }>();
|
|
30
|
+
|
|
31
|
+
constructor(private readonly now: () => number = () => performance.now()) {}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Record one refusal for a server's channel. Returns null when its line is
|
|
35
|
+
* held back, or else how many of the channel's refusals were held back
|
|
36
|
+
* since its last line (0 for its first).
|
|
37
|
+
*/
|
|
38
|
+
admit(serverId: string, channelId: string): number | null {
|
|
39
|
+
// logValue never prints a NUL, so the key's last NUL keeps the two apart
|
|
40
|
+
// whatever a server id holds.
|
|
41
|
+
const key = `${serverId}\u0000${logValue(channelId)}`;
|
|
42
|
+
const now = this.now();
|
|
43
|
+
const record = this.records.get(key);
|
|
44
|
+
if (record) this.records.delete(key);
|
|
45
|
+
else if (this.records.size >= MAX_CHANNELS) this.records.delete(this.records.keys().next().value!);
|
|
46
|
+
if (!record || now - record.at >= EVERY_MS) {
|
|
47
|
+
this.records.set(key, { at: now, since: 0 });
|
|
48
|
+
return record?.since ?? 0;
|
|
49
|
+
}
|
|
50
|
+
record.since++;
|
|
51
|
+
this.records.set(key, record);
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** A channel's line prints at most this often. */
|
|
57
|
+
const EVERY_MS = 60_000;
|
|
58
|
+
/** The channels the records cover at once. */
|
|
59
|
+
const MAX_CHANNELS = 1_000;
|
|
@@ -15,6 +15,7 @@ import { openTransport, type McplTransport, type TransportCloseInfo } from './tr
|
|
|
15
15
|
import { maskNegotiatedCapabilities } from './capability-mask.js';
|
|
16
16
|
import { CapabilityGrant, expandAdvertisementShorthand } from './capability-grant.js';
|
|
17
17
|
import { CAPABILITY_DISABLED } from './errors.js';
|
|
18
|
+
import { logDetail, logValue } from './log-value.js';
|
|
18
19
|
|
|
19
20
|
import type {
|
|
20
21
|
McplServerConfig,
|
|
@@ -65,6 +66,9 @@ export interface McplConnectionCloseInfo {
|
|
|
65
66
|
signal: NodeJS.Signals | null;
|
|
66
67
|
/** WebSocket close reason, or a short description of the cause. */
|
|
67
68
|
reason?: string;
|
|
69
|
+
/** True when `reason` is the peer's own text, a WebSocket server's close
|
|
70
|
+
* reason, rather than the transport's or the host's words. */
|
|
71
|
+
reasonFromPeer?: boolean;
|
|
68
72
|
/** True when the host closed the link (`close()` / `reconnectAfterFailure()`),
|
|
69
73
|
* false when the transport dropped unexpectedly. */
|
|
70
74
|
byHost: boolean;
|
|
@@ -74,6 +78,23 @@ export interface McplConnectionCloseInfo {
|
|
|
74
78
|
* Spring Boot + JDA servers can take 5-10s to boot, so 30s is safe. */
|
|
75
79
|
const INITIALIZE_TIMEOUT_MS = 30_000;
|
|
76
80
|
|
|
81
|
+
/** A failed connect's `stderrTail`, the last of the server's stderr that its
|
|
82
|
+
* handshake's error keeps, when there is one. */
|
|
83
|
+
export function stderrTailOf(error: unknown): string | undefined {
|
|
84
|
+
const tail = (error as { stderrTail?: unknown } | null | undefined)?.stderrTail;
|
|
85
|
+
return typeof tail === 'string' && tail !== '' ? tail : undefined;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** For the host's own log line about a failed connect: the server's stderr
|
|
89
|
+
* that the error keeps as `stderrTail`, one marked line per line of it,
|
|
90
|
+
* each through log-value, since it's the server's text. '' when there's
|
|
91
|
+
* none. */
|
|
92
|
+
export function withStderr(error: unknown): string {
|
|
93
|
+
const tail = stderrTailOf(error);
|
|
94
|
+
if (tail === undefined) return '';
|
|
95
|
+
return '. The last of its stderr:' + tail.split('\n').map((line) => `\n stderr: ${logDetail(line)}`).join('');
|
|
96
|
+
}
|
|
97
|
+
|
|
77
98
|
/** Default per-request timeout in milliseconds (see McplServerConfig.requestTimeoutMs).
|
|
78
99
|
* A live-but-wedged server (accepts requests, never answers, never closes its
|
|
79
100
|
* transport) would otherwise freeze the awaiting agent turn forever — no
|
|
@@ -116,8 +137,13 @@ interface RequestOptions {
|
|
|
116
137
|
* - `'channels-incoming'` — Server sent `channels/incoming`
|
|
117
138
|
* - `'error'` — Connection-level error
|
|
118
139
|
* - `'close'` — Connection closed: `(code, signal, info: McplConnectionCloseInfo)`
|
|
119
|
-
* - `'connect-failed'` — Initial connect failed; background retry scheduled
|
|
120
|
-
*
|
|
140
|
+
* - `'connect-failed'` — Initial connect failed; background retry scheduled:
|
|
141
|
+
* `{ error, stderrTail?, attempt }`
|
|
142
|
+
* - `'reconnect-failed'` — A background reconnect attempt failed; next retry scheduled:
|
|
143
|
+
* `{ error, stderrTail?, attempt }`. `stderrTail` is the last of the
|
|
144
|
+
* server's stderr when its handshake failed by its transport closing, a
|
|
145
|
+
* transport error or its timeout and it had written any; `error` never
|
|
146
|
+
* carries it.
|
|
121
147
|
* - `'reconnect'` — Background reconnect succeeded
|
|
122
148
|
*/
|
|
123
149
|
export class McplServerConnection extends EventEmitter {
|
|
@@ -403,7 +429,7 @@ export class McplServerConnection extends EventEmitter {
|
|
|
403
429
|
if (responder?.respondError) {
|
|
404
430
|
responder.respondError(CAPABILITY_DISABLED, `${name} rejected: capability "${requiredCap}" ${phase}`, { capability: requiredCap });
|
|
405
431
|
} else {
|
|
406
|
-
console.error(`[mcpl] ${this.id}: discarded ${name} — "${requiredCap}" ${phase}`);
|
|
432
|
+
console.error(`[mcpl] ${this.id}: discarded ${logValue(name)} — "${requiredCap}" ${phase}`);
|
|
407
433
|
}
|
|
408
434
|
return true;
|
|
409
435
|
}
|
|
@@ -531,6 +557,15 @@ export class McplServerConnection extends EventEmitter {
|
|
|
531
557
|
hostCapabilities: McplHostCapabilities,
|
|
532
558
|
): Promise<{ transport: McplTransport; capabilities: McplCapabilities | null; droppedCapabilities: ReadonlySet<string>; mcpToolsAdvertised: boolean }> {
|
|
533
559
|
const transport = await openTransport(config);
|
|
560
|
+
// Once the handshake stops listening for 'error', this logs what the
|
|
561
|
+
// transport reports, until the handshake hands the transport on, or for
|
|
562
|
+
// good if the handshake fails. EventEmitter throws an 'error' that has no
|
|
563
|
+
// listener, and from an I/O callback nothing catches it. A malformed
|
|
564
|
+
// WebSocket frame that arrives with the initialize response, or while a
|
|
565
|
+
// failed handshake's close() runs, is such an error.
|
|
566
|
+
const logAfterSettled = (err: Error) => {
|
|
567
|
+
console.error(`[mcpl] ${config.id}: transport error after the handshake settled: ${err.message}`);
|
|
568
|
+
};
|
|
534
569
|
|
|
535
570
|
try {
|
|
536
571
|
const initId = 0; // use id=0 for the handshake
|
|
@@ -548,10 +583,20 @@ export class McplServerConnection extends EventEmitter {
|
|
|
548
583
|
// Await the initialize response, racing an early transport close/error and
|
|
549
584
|
// a timeout. All three paths clean up their listeners.
|
|
550
585
|
const { mcpl: rawCapabilities, mcpToolsAdvertised } = await new Promise<{ mcpl: McplCapabilities | null; mcpToolsAdvertised: boolean }>((resolve, reject) => {
|
|
586
|
+
// When the transport fails, the end of the server's stderr often says
|
|
587
|
+
// why, such as a missing token or a crash at startup. It goes on the
|
|
588
|
+
// error as `stderrTail`, never into the message: an error's message
|
|
589
|
+
// travels to traces and tool results, and a server's stderr is its
|
|
590
|
+
// own text, which can hold a credential.
|
|
591
|
+
const failed = (failure: string): Error & { stderrTail?: string } => {
|
|
592
|
+
const tail = transport.stderrTail.trimEnd();
|
|
593
|
+
return Object.assign(new Error(failure), tail ? { stderrTail: tail } : {});
|
|
594
|
+
};
|
|
551
595
|
const cleanup = () => {
|
|
552
596
|
transport.off('line', onLine);
|
|
553
597
|
transport.off('close', onClose);
|
|
554
598
|
transport.off('error', onError);
|
|
599
|
+
transport.on('error', logAfterSettled);
|
|
555
600
|
clearTimeout(timer);
|
|
556
601
|
};
|
|
557
602
|
const onLine = (line: string) => {
|
|
@@ -580,15 +625,15 @@ export class McplServerConnection extends EventEmitter {
|
|
|
580
625
|
};
|
|
581
626
|
const onClose = (info: TransportCloseInfo) => {
|
|
582
627
|
cleanup();
|
|
583
|
-
reject(
|
|
628
|
+
reject(failed(`MCPL server "${config.id}" transport closed before handshake completed (${info.reason ?? 'unknown'})`));
|
|
584
629
|
};
|
|
585
630
|
const onError = (err: Error) => {
|
|
586
631
|
cleanup();
|
|
587
|
-
reject(
|
|
632
|
+
reject(failed(`MCPL server "${config.id}" transport error during handshake: ${err.message}`));
|
|
588
633
|
};
|
|
589
634
|
const timer = setTimeout(() => {
|
|
590
635
|
cleanup();
|
|
591
|
-
reject(
|
|
636
|
+
reject(failed(`MCPL server "${config.id}" initialize handshake timed out`));
|
|
592
637
|
}, INITIALIZE_TIMEOUT_MS);
|
|
593
638
|
|
|
594
639
|
transport.on('line', onLine);
|
|
@@ -610,9 +655,13 @@ export class McplServerConnection extends EventEmitter {
|
|
|
610
655
|
// had never been advertised (hooks, streaming, queries all read this).
|
|
611
656
|
const { capabilities: scoped, dropped } = maskNegotiatedCapabilities(capabilities, config);
|
|
612
657
|
if (dropped.length > 0) {
|
|
613
|
-
console.error(`MCPL server "${config.id}" capabilities masked by host config: ${dropped.join(', ')}`);
|
|
658
|
+
console.error(`MCPL server "${config.id}" capabilities masked by host config: ${dropped.map(logValue).join(', ')}`);
|
|
614
659
|
}
|
|
615
660
|
|
|
661
|
+
// The caller wires the transport, or keeps it for close() with a
|
|
662
|
+
// listener of its own, in the continuation of this return, before any
|
|
663
|
+
// I/O callback can emit.
|
|
664
|
+
transport.off('error', logAfterSettled);
|
|
616
665
|
return { transport, capabilities: scoped, droppedCapabilities: new Set(dropped), mcpToolsAdvertised };
|
|
617
666
|
} catch (err) {
|
|
618
667
|
// Never leak the child / socket if the handshake fails.
|
|
@@ -639,12 +688,13 @@ export class McplServerConnection extends EventEmitter {
|
|
|
639
688
|
}
|
|
640
689
|
|
|
641
690
|
// Non-blocking start: create a disconnected stub that will reconnect in background
|
|
642
|
-
console.error(`MCPL server "${config.id}" initial connect failed, will retry
|
|
691
|
+
console.error(`MCPL server "${config.id}" initial connect failed, will retry: ${(error as Error).message}${withStderr(error)}`);
|
|
643
692
|
const stub = McplServerConnection.createDisconnectedStub(config, hostCapabilities);
|
|
644
693
|
// Surface the initial failure. The event is buffered by the emit()
|
|
645
694
|
// override until the host wires listeners and calls ready(), so it is
|
|
646
695
|
// not lost in the window before wireMcplEvents runs.
|
|
647
|
-
|
|
696
|
+
const stderrTail = stderrTailOf(error);
|
|
697
|
+
stub.emit('connect-failed', { error: (error as Error).message, ...(stderrTail ? { stderrTail } : {}), attempt: 0 });
|
|
648
698
|
return stub;
|
|
649
699
|
}
|
|
650
700
|
}
|
|
@@ -989,6 +1039,13 @@ export class McplServerConnection extends EventEmitter {
|
|
|
989
1039
|
// The prior transport was disconnected before this retry began. Keep
|
|
990
1040
|
// the fresh, unadmitted transport owned by close(), including on a
|
|
991
1041
|
// failed transport cleanup, instead of reviving a stopped connection.
|
|
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
|
+
});
|
|
992
1049
|
this.transport = transport;
|
|
993
1050
|
return;
|
|
994
1051
|
}
|
|
@@ -1013,9 +1070,10 @@ export class McplServerConnection extends EventEmitter {
|
|
|
1013
1070
|
this.reconnectAttempts = 0;
|
|
1014
1071
|
this.emit('reconnect', { attempts: attempt });
|
|
1015
1072
|
} catch (error) {
|
|
1016
|
-
console.error(`MCPL server "${this.id}" reconnect failed
|
|
1073
|
+
console.error(`MCPL server "${this.id}" reconnect failed: ${(error as Error).message}${withStderr(error)}`);
|
|
1017
1074
|
this.reconnectAttempts = attempt + 1;
|
|
1018
|
-
|
|
1075
|
+
const stderrTail = stderrTailOf(error);
|
|
1076
|
+
this.emit('reconnect-failed', { error: (error as Error).message, ...(stderrTail ? { stderrTail } : {}), attempt });
|
|
1019
1077
|
this.scheduleReconnect();
|
|
1020
1078
|
}
|
|
1021
1079
|
}
|
|
@@ -1355,6 +1413,7 @@ export class McplServerConnection extends EventEmitter {
|
|
|
1355
1413
|
code: info.code ?? null,
|
|
1356
1414
|
signal: info.signal ?? null,
|
|
1357
1415
|
reason: info.reason ?? (byHost ? 'closed by host' : undefined),
|
|
1416
|
+
...(info.reasonFromPeer ? { reasonFromPeer: true } : {}),
|
|
1358
1417
|
byHost,
|
|
1359
1418
|
};
|
|
1360
1419
|
this.emit('close', closeInfo.code, closeInfo.signal, closeInfo);
|
package/src/mcpl/tool-classes.ts
CHANGED
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
23
|
import { globMatch } from './tool-glob.js';
|
|
24
|
+
import { logValue } from './log-value.js';
|
|
24
25
|
|
|
25
26
|
export const TOOL_CLASSES = [
|
|
26
27
|
'comms',
|
|
@@ -116,7 +117,7 @@ export function parseDeclaredClasses(meta: unknown, toolName = '(unnamed)'): Too
|
|
|
116
117
|
const raw = (meta as Record<string, unknown>)[TOOL_CLASS_META_KEY];
|
|
117
118
|
if (raw === undefined) return undefined;
|
|
118
119
|
if (!Array.isArray(raw)) {
|
|
119
|
-
console.error(`[mcpl] ${toolName}: _meta["${TOOL_CLASS_META_KEY}"] is not an array — treated as unclassed`);
|
|
120
|
+
console.error(`[mcpl] ${logValue(toolName)}: _meta["${TOOL_CLASS_META_KEY}"] is not an array — treated as unclassed`);
|
|
120
121
|
return undefined;
|
|
121
122
|
}
|
|
122
123
|
const out: ToolClass[] = [];
|
|
@@ -124,7 +125,7 @@ export function parseDeclaredClasses(meta: unknown, toolName = '(unnamed)'): Too
|
|
|
124
125
|
if (isToolClass(value)) {
|
|
125
126
|
if (!out.includes(value)) out.push(value);
|
|
126
127
|
} else {
|
|
127
|
-
console.error(`[mcpl] ${toolName}: unknown tool class ${
|
|
128
|
+
console.error(`[mcpl] ${logValue(toolName)}: unknown tool class ${logValue(value)} ignored (RFC-008 §3)`);
|
|
128
129
|
}
|
|
129
130
|
}
|
|
130
131
|
return out.length > 0 ? out : undefined;
|