@animalabs/agent-framework 0.7.4 → 0.9.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/dist/src/agent.d.ts +10 -0
- package/dist/src/agent.d.ts.map +1 -1
- package/dist/src/agent.js +33 -3
- package/dist/src/agent.js.map +1 -1
- 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 +23 -1
- package/dist/src/code-execution/py-runner.d.ts.map +1 -1
- package/dist/src/code-execution/py-runner.js +40 -4
- package/dist/src/code-execution/py-runner.js.map +1 -1
- package/dist/src/code-execution/runtime-py.d.ts +15 -2
- package/dist/src/code-execution/runtime-py.d.ts.map +1 -1
- package/dist/src/code-execution/runtime-py.js +156 -7
- package/dist/src/code-execution/runtime-py.js.map +1 -1
- package/dist/src/code-execution/tool-definition.d.ts.map +1 -1
- package/dist/src/code-execution/tool-definition.js +27 -2
- package/dist/src/code-execution/tool-definition.js.map +1 -1
- package/dist/src/framework.d.ts +234 -5
- package/dist/src/framework.d.ts.map +1 -1
- package/dist/src/framework.js +1818 -188
- package/dist/src/framework.js.map +1 -1
- package/dist/src/gate/event-gate.d.ts +11 -0
- package/dist/src/gate/event-gate.d.ts.map +1 -1
- package/dist/src/gate/event-gate.js +66 -0
- package/dist/src/gate/event-gate.js.map +1 -1
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +1 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/mcpl/capability-grant.d.ts +115 -0
- package/dist/src/mcpl/capability-grant.d.ts.map +1 -0
- package/dist/src/mcpl/capability-grant.js +291 -0
- package/dist/src/mcpl/capability-grant.js.map +1 -0
- package/dist/src/mcpl/capability-mask.d.ts +51 -0
- package/dist/src/mcpl/capability-mask.d.ts.map +1 -0
- package/dist/src/mcpl/capability-mask.js +190 -0
- package/dist/src/mcpl/capability-mask.js.map +1 -0
- package/dist/src/mcpl/channel-registry.d.ts +30 -2
- package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
- package/dist/src/mcpl/channel-registry.js +216 -43
- package/dist/src/mcpl/channel-registry.js.map +1 -1
- package/dist/src/mcpl/checkpoint-manager.d.ts +8 -3
- package/dist/src/mcpl/checkpoint-manager.d.ts.map +1 -1
- package/dist/src/mcpl/checkpoint-manager.js +28 -8
- package/dist/src/mcpl/checkpoint-manager.js.map +1 -1
- package/dist/src/mcpl/errors.d.ts +6 -0
- package/dist/src/mcpl/errors.d.ts.map +1 -1
- package/dist/src/mcpl/errors.js +6 -0
- package/dist/src/mcpl/errors.js.map +1 -1
- package/dist/src/mcpl/feature-set-manager.d.ts +10 -1
- package/dist/src/mcpl/feature-set-manager.d.ts.map +1 -1
- package/dist/src/mcpl/feature-set-manager.js +45 -4
- package/dist/src/mcpl/feature-set-manager.js.map +1 -1
- package/dist/src/mcpl/hook-orchestrator.d.ts +10 -14
- package/dist/src/mcpl/hook-orchestrator.d.ts.map +1 -1
- package/dist/src/mcpl/hook-orchestrator.js +58 -67
- package/dist/src/mcpl/hook-orchestrator.js.map +1 -1
- package/dist/src/mcpl/index.d.ts +1 -1
- package/dist/src/mcpl/index.d.ts.map +1 -1
- package/dist/src/mcpl/index.js.map +1 -1
- package/dist/src/mcpl/prose-stream-router.d.ts +9 -0
- package/dist/src/mcpl/prose-stream-router.d.ts.map +1 -1
- package/dist/src/mcpl/prose-stream-router.js +16 -0
- package/dist/src/mcpl/prose-stream-router.js.map +1 -1
- package/dist/src/mcpl/push-handler.d.ts.map +1 -1
- package/dist/src/mcpl/push-handler.js +20 -2
- package/dist/src/mcpl/push-handler.js.map +1 -1
- package/dist/src/mcpl/server-connection.d.ts +93 -9
- package/dist/src/mcpl/server-connection.d.ts.map +1 -1
- package/dist/src/mcpl/server-connection.js +231 -32
- 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/server-registry.js +4 -3
- package/dist/src/mcpl/server-registry.js.map +1 -1
- package/dist/src/mcpl/tags.d.ts +24 -0
- package/dist/src/mcpl/tags.d.ts.map +1 -0
- package/dist/src/mcpl/tags.js +52 -0
- package/dist/src/mcpl/tags.js.map +1 -0
- package/dist/src/mcpl/transport.d.ts +1 -1
- package/dist/src/mcpl/transport.d.ts.map +1 -1
- package/dist/src/mcpl/transport.js +35 -7
- package/dist/src/mcpl/transport.js.map +1 -1
- package/dist/src/mcpl/types.d.ts +169 -64
- package/dist/src/mcpl/types.d.ts.map +1 -1
- package/dist/src/mcpl/types.js +16 -2
- package/dist/src/mcpl/types.js.map +1 -1
- package/dist/src/module-registry.d.ts +12 -0
- package/dist/src/module-registry.d.ts.map +1 -1
- package/dist/src/module-registry.js +26 -0
- package/dist/src/module-registry.js.map +1 -1
- package/dist/src/modules/workspace/index.d.ts +20 -0
- package/dist/src/modules/workspace/index.d.ts.map +1 -1
- package/dist/src/modules/workspace/index.js +48 -15
- package/dist/src/modules/workspace/index.js.map +1 -1
- package/dist/src/modules/workspace/sync.d.ts +4 -0
- package/dist/src/modules/workspace/sync.d.ts.map +1 -1
- package/dist/src/modules/workspace/sync.js +1 -1
- package/dist/src/modules/workspace/sync.js.map +1 -1
- package/dist/src/refusal-reactions.d.ts +27 -0
- package/dist/src/refusal-reactions.d.ts.map +1 -0
- package/dist/src/refusal-reactions.js +35 -0
- package/dist/src/refusal-reactions.js.map +1 -0
- package/dist/src/tool-result-history.d.ts +10 -0
- package/dist/src/tool-result-history.d.ts.map +1 -1
- package/dist/src/tool-result-history.js +16 -5
- package/dist/src/tool-result-history.js.map +1 -1
- package/dist/src/types/agent.d.ts +35 -0
- package/dist/src/types/agent.d.ts.map +1 -1
- package/dist/src/types/framework.d.ts +26 -0
- package/dist/src/types/framework.d.ts.map +1 -1
- package/dist/src/types/module.d.ts +22 -0
- package/dist/src/types/module.d.ts.map +1 -1
- package/dist/src/types/trace.d.ts +24 -0
- package/dist/src/types/trace.d.ts.map +1 -1
- package/dist/test/busy-agent-scheduler.test.js +1 -0
- package/dist/test/busy-agent-scheduler.test.js.map +1 -1
- package/dist/test/channel-registry-routing.test.js +38 -10
- package/dist/test/channel-registry-routing.test.js.map +1 -1
- package/dist/test/channel-registry-typing.test.js +10 -1
- package/dist/test/channel-registry-typing.test.js.map +1 -1
- package/dist/test/checkpoint-attribution.test.js +168 -8
- package/dist/test/checkpoint-attribution.test.js.map +1 -1
- package/dist/test/code-execution.test.js +327 -1
- package/dist/test/code-execution.test.js.map +1 -1
- package/dist/test/conversation-fork-injection-scoping.test.js +8 -3
- package/dist/test/conversation-fork-injection-scoping.test.js.map +1 -1
- package/dist/test/deferred-wake-integrity.test.d.ts +29 -0
- package/dist/test/deferred-wake-integrity.test.d.ts.map +1 -0
- package/dist/test/deferred-wake-integrity.test.js +320 -0
- package/dist/test/deferred-wake-integrity.test.js.map +1 -0
- package/dist/test/event-gate-selfwake.test.d.ts +15 -0
- package/dist/test/event-gate-selfwake.test.d.ts.map +1 -0
- package/dist/test/event-gate-selfwake.test.js +125 -0
- package/dist/test/event-gate-selfwake.test.js.map +1 -0
- package/dist/test/machine-close-provenance.test.d.ts +2 -0
- package/dist/test/machine-close-provenance.test.d.ts.map +1 -0
- package/dist/test/machine-close-provenance.test.js +116 -0
- package/dist/test/machine-close-provenance.test.js.map +1 -0
- package/dist/test/mcpl-access-provider.test.d.ts +2 -0
- package/dist/test/mcpl-access-provider.test.d.ts.map +1 -0
- package/dist/test/mcpl-access-provider.test.js +20 -0
- package/dist/test/mcpl-access-provider.test.js.map +1 -0
- package/dist/test/mcpl-awareness-barrier.test.js +10 -0
- package/dist/test/mcpl-awareness-barrier.test.js.map +1 -1
- package/dist/test/mcpl-capability-grant.test.d.ts +10 -0
- package/dist/test/mcpl-capability-grant.test.d.ts.map +1 -0
- package/dist/test/mcpl-capability-grant.test.js +109 -0
- package/dist/test/mcpl-capability-grant.test.js.map +1 -0
- package/dist/test/mcpl-capability-scoping.test.d.ts +2 -0
- package/dist/test/mcpl-capability-scoping.test.d.ts.map +1 -0
- package/dist/test/mcpl-capability-scoping.test.js +266 -0
- package/dist/test/mcpl-capability-scoping.test.js.map +1 -0
- package/dist/test/mcpl-gate-roundtrip.test.js +13 -0
- package/dist/test/mcpl-gate-roundtrip.test.js.map +1 -1
- package/dist/test/mcpl-list-status.test.d.ts +2 -0
- package/dist/test/mcpl-list-status.test.d.ts.map +1 -0
- package/dist/test/mcpl-list-status.test.js +78 -0
- package/dist/test/mcpl-list-status.test.js.map +1 -0
- package/dist/test/mcpl-manifest-changed.test.d.ts +2 -0
- package/dist/test/mcpl-manifest-changed.test.d.ts.map +1 -0
- package/dist/test/mcpl-manifest-changed.test.js +177 -0
- package/dist/test/mcpl-manifest-changed.test.js.map +1 -0
- package/dist/test/mcpl-reconnect-events.test.js +27 -2
- package/dist/test/mcpl-reconnect-events.test.js.map +1 -1
- package/dist/test/mcpl-reconnect-reregistration.test.js +53 -17
- package/dist/test/mcpl-reconnect-reregistration.test.js.map +1 -1
- package/dist/test/mcpl-request-timeout.test.js +1 -1
- package/dist/test/mcpl-request-timeout.test.js.map +1 -1
- package/dist/test/module-context-integration.test.d.ts +18 -0
- package/dist/test/module-context-integration.test.d.ts.map +1 -0
- package/dist/test/module-context-integration.test.js +181 -0
- package/dist/test/module-context-integration.test.js.map +1 -0
- package/dist/test/physical-window-restart.test.d.ts +14 -0
- package/dist/test/physical-window-restart.test.d.ts.map +1 -0
- package/dist/test/physical-window-restart.test.js +222 -0
- package/dist/test/physical-window-restart.test.js.map +1 -0
- package/dist/test/present-while-acting.test.js +270 -0
- package/dist/test/present-while-acting.test.js.map +1 -1
- package/dist/test/refusal-reaction-baseline.test.d.ts +2 -0
- package/dist/test/refusal-reaction-baseline.test.d.ts.map +1 -0
- package/dist/test/refusal-reaction-baseline.test.js +74 -0
- package/dist/test/refusal-reaction-baseline.test.js.map +1 -0
- package/dist/test/state-scaling-e2e.test.js +7 -1
- package/dist/test/state-scaling-e2e.test.js.map +1 -1
- package/dist/test/state-scaling.test.js +9 -1
- package/dist/test/state-scaling.test.js.map +1 -1
- package/dist/test/tool-result-spill.test.d.ts +13 -0
- package/dist/test/tool-result-spill.test.d.ts.map +1 -0
- package/dist/test/tool-result-spill.test.js +463 -0
- package/dist/test/tool-result-spill.test.js.map +1 -0
- package/dist/test/trunk-channel-routing.test.js +45 -0
- package/dist/test/trunk-channel-routing.test.js.map +1 -1
- package/dist/test/utils-surface.test.d.ts +2 -0
- package/dist/test/utils-surface.test.d.ts.map +1 -0
- package/dist/test/utils-surface.test.js +145 -0
- package/dist/test/utils-surface.test.js.map +1 -0
- package/dist/test/workspace-read-image.test.js +71 -0
- package/dist/test/workspace-read-image.test.js.map +1 -1
- package/dist/test/workspace-restore-order.test.d.ts +16 -0
- package/dist/test/workspace-restore-order.test.d.ts.map +1 -0
- package/dist/test/workspace-restore-order.test.js +140 -0
- package/dist/test/workspace-restore-order.test.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +4 -4
- package/src/agent.ts +37 -3
- package/src/code-execution/index.ts +1 -0
- package/src/code-execution/py-runner.ts +56 -4
- package/src/code-execution/runtime-py.ts +156 -7
- package/src/code-execution/tool-definition.ts +27 -2
- package/src/framework.ts +2080 -203
- package/src/gate/event-gate.ts +72 -0
- package/src/index.ts +1 -0
- package/src/mcpl/capability-grant.ts +319 -0
- package/src/mcpl/capability-mask.ts +228 -0
- package/src/mcpl/channel-registry.ts +265 -43
- package/src/mcpl/checkpoint-manager.ts +28 -8
- package/src/mcpl/errors.ts +7 -0
- package/src/mcpl/feature-set-manager.ts +50 -5
- package/src/mcpl/hook-orchestrator.ts +64 -86
- package/src/mcpl/index.ts +0 -2
- package/src/mcpl/prose-stream-router.ts +17 -0
- package/src/mcpl/push-handler.ts +18 -2
- package/src/mcpl/server-connection.ts +262 -34
- package/src/mcpl/server-registry.ts +4 -3
- package/src/mcpl/tags.ts +50 -0
- package/src/mcpl/transport.ts +37 -7
- package/src/mcpl/types.ts +187 -78
- package/src/module-registry.ts +34 -0
- package/src/modules/workspace/index.ts +49 -15
- package/src/modules/workspace/sync.ts +1 -1
- package/src/refusal-reactions.ts +37 -0
- package/src/tool-result-history.ts +17 -5
- package/src/types/agent.ts +36 -0
- package/src/types/framework.ts +27 -0
- package/src/types/module.ts +24 -0
- package/src/types/trace.ts +22 -0
|
@@ -12,6 +12,9 @@
|
|
|
12
12
|
import { EventEmitter } from 'node:events';
|
|
13
13
|
|
|
14
14
|
import { openTransport, type McplTransport, type TransportCloseInfo } from './transport.js';
|
|
15
|
+
import { maskNegotiatedCapabilities } from './capability-mask.js';
|
|
16
|
+
import { CapabilityGrant, expandAdvertisementShorthand } from './capability-grant.js';
|
|
17
|
+
import { CAPABILITY_DISABLED } from './errors.js';
|
|
15
18
|
|
|
16
19
|
import type {
|
|
17
20
|
McplServerConfig,
|
|
@@ -21,10 +24,10 @@ import type {
|
|
|
21
24
|
JsonRpcResponse,
|
|
22
25
|
BeforeInferenceParams,
|
|
23
26
|
BeforeInferenceResult,
|
|
24
|
-
AfterInferenceParams,
|
|
25
|
-
AfterInferenceResult,
|
|
26
27
|
FeatureSetsUpdateParams,
|
|
28
|
+
FeatureSetsUpdateResult,
|
|
27
29
|
InferenceChunkParams,
|
|
30
|
+
InferenceLifecycleParams,
|
|
28
31
|
StateRollbackParams,
|
|
29
32
|
StateRollbackResult,
|
|
30
33
|
ChannelsOpenParams,
|
|
@@ -88,7 +91,6 @@ interface RequestOptions {
|
|
|
88
91
|
* - `'channels-register'` — Server sent `channels/register`
|
|
89
92
|
* - `'channels-changed'` — Server sent `channels/changed`
|
|
90
93
|
* - `'channels-incoming'` — Server sent `channels/incoming`
|
|
91
|
-
* - `'feature-sets-changed'` — Server sent `featureSets/changed`
|
|
92
94
|
* - `'error'` — Connection-level error
|
|
93
95
|
* - `'close'` — Connection closed
|
|
94
96
|
* - `'connect-failed'` — Initial connect failed; background retry scheduled
|
|
@@ -99,9 +101,77 @@ export class McplServerConnection extends EventEmitter {
|
|
|
99
101
|
/** Unique server identifier from config. */
|
|
100
102
|
readonly id: string;
|
|
101
103
|
|
|
102
|
-
/** MCPL capabilities negotiated during the initialize handshake
|
|
104
|
+
/** MCPL capabilities negotiated during the initialize handshake — after
|
|
105
|
+
* host-side capability scoping (enabledCapabilities/disabledCapabilities)
|
|
106
|
+
* — or null if the server does not support MCPL. */
|
|
103
107
|
capabilities: McplCapabilities | null;
|
|
104
108
|
|
|
109
|
+
/** Dotted paths of advertised capabilities the host config masked at the
|
|
110
|
+
* last handshake (empty when no scoping is configured). Inbound methods
|
|
111
|
+
* gated by a masked capability are rejected in setupMessageRouting. */
|
|
112
|
+
droppedCapabilities: ReadonlySet<string> = new Set();
|
|
113
|
+
|
|
114
|
+
/** OUTER standard MCP capabilities.tools was present at handshake (§5.1 —
|
|
115
|
+
* the sole source of the `tools` capability path). */
|
|
116
|
+
mcpToolsAdvertised = false;
|
|
117
|
+
|
|
118
|
+
/** Host-owned per-server authority for host/command (config
|
|
119
|
+
* allowHostCommands, default false). No capability path exists and the
|
|
120
|
+
* grant cannot confer it — server params never decide admin authority. */
|
|
121
|
+
allowHostCommands = false;
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* The effective capability grant (§5.4) — the sole authorization
|
|
125
|
+
* allowlist for this connection. Starts EMPTY: until the initial policy
|
|
126
|
+
* exchange completes (§5.3) nothing is granted and every privileged
|
|
127
|
+
* inbound method is rejected. The framework computes the grant from the
|
|
128
|
+
* masked advertisement and activates it via establishGrant() only after
|
|
129
|
+
* the featureSets/update Request is answered (expansion activates on
|
|
130
|
+
* receipt, §6.7); revocations re-call establishGrant() BEFORE the
|
|
131
|
+
* corresponding Request is sent (reduction applies first, §6.7).
|
|
132
|
+
*/
|
|
133
|
+
grant: CapabilityGrant = CapabilityGrant.empty();
|
|
134
|
+
|
|
135
|
+
/** True once the §5.3 initial policy Request has been answered. */
|
|
136
|
+
policyEstablished = false;
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* §17 manifest tracking (host side of RFC-003): what this host actually
|
|
140
|
+
* fetched and acted on — never the server's announcement log (§17.10:
|
|
141
|
+
* those are different facts). Read by the legibility surface.
|
|
142
|
+
*/
|
|
143
|
+
manifestState: {
|
|
144
|
+
/** revision of the last VALIDATED fetched manifest (server-authored,
|
|
145
|
+
* untrusted, equality-only) — null until a §17.4 fetch succeeds. */
|
|
146
|
+
lastValidatedRevision: string | null;
|
|
147
|
+
lastFetchedAt: number | null;
|
|
148
|
+
/** epoch ms of the last §6.7 receipt that activated a grant change. */
|
|
149
|
+
lastNegotiatedAt: number | null;
|
|
150
|
+
} = { lastValidatedRevision: null, lastFetchedAt: null, lastNegotiatedAt: null };
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Activate (or replace) the effective grant. Ordering is the caller's
|
|
154
|
+
* contract per §6.7: call BEFORE sending a reducing featureSets/update,
|
|
155
|
+
* AFTER the receipt for an expanding one.
|
|
156
|
+
*/
|
|
157
|
+
establishGrant(grant: CapabilityGrant): void {
|
|
158
|
+
this.grant = grant;
|
|
159
|
+
this.policyEstablished = true;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** A §5.3 grant belongs to one initialized transport epoch. Reconnecting
|
|
163
|
+
* performs a fresh initialize handshake, so authority from the dead epoch
|
|
164
|
+
* must be revoked before the replacement transport is exposed. */
|
|
165
|
+
private resetPolicyForTransportBoundary(): void {
|
|
166
|
+
this.grant = CapabilityGrant.empty();
|
|
167
|
+
this.policyEstablished = false;
|
|
168
|
+
// §17 tracking is per-epoch for the same reason the grant is: these are
|
|
169
|
+
// facts about what THIS host fetched/negotiated on THIS initialized
|
|
170
|
+
// transport, and a fresh initialize starts a fresh manifest history
|
|
171
|
+
// (Sol, PR #84 review — they previously survived into the new epoch).
|
|
172
|
+
this.manifestState = { lastValidatedRevision: null, lastFetchedAt: null, lastNegotiatedAt: null };
|
|
173
|
+
}
|
|
174
|
+
|
|
105
175
|
/** The active transport (stdio child or WebSocket). Null for a disconnected
|
|
106
176
|
* stub awaiting its first background reconnect. */
|
|
107
177
|
private transport: McplTransport | null;
|
|
@@ -197,18 +267,15 @@ export class McplServerConnection extends EventEmitter {
|
|
|
197
267
|
readyControlPlane(): void {
|
|
198
268
|
this.controlPlaneReady = true;
|
|
199
269
|
const pending = this.bufferedEvents;
|
|
200
|
-
const deferred: Array<{ event: string; args: unknown[] }> = [];
|
|
201
270
|
this.bufferedEvents = [];
|
|
202
271
|
for (const item of pending) {
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
272
|
+
// Re-enter the ADMISSION gate (this.emit), never super.emit: control
|
|
273
|
+
// events include channels-register/changed, which are grant-gated —
|
|
274
|
+
// the old direct flush bypassed positive-grant enforcement entirely
|
|
275
|
+
// (PR #79 review blocker 3). Data-plane items re-buffer in order via
|
|
276
|
+
// the same call, since dataPlaneReady is still false.
|
|
277
|
+
this.emit(item.event, ...item.args);
|
|
208
278
|
}
|
|
209
|
-
// Data events emitted by control handlers while the snapshot was flushing
|
|
210
|
-
// were appended to the new buffer. Preserve their order behind older data.
|
|
211
|
-
this.bufferedEvents = [...deferred, ...this.bufferedEvents];
|
|
212
279
|
}
|
|
213
280
|
|
|
214
281
|
/**
|
|
@@ -233,18 +300,62 @@ export class McplServerConnection extends EventEmitter {
|
|
|
233
300
|
? this.controlPlaneReady
|
|
234
301
|
: this.dataPlaneReady)
|
|
235
302
|
) {
|
|
303
|
+
// Positive-grant admission (§5.4/§14.1), at the single choke-point
|
|
304
|
+
// every path shares: live routing AND buffer flushes re-enter here, so
|
|
305
|
+
// an event buffered while the connection was staged is authorized
|
|
306
|
+
// against the grant current at ADMISSION — by which time the §5.3
|
|
307
|
+
// policy exchange has settled. Rejecting earlier (at line receipt)
|
|
308
|
+
// answered staged traffic with -32002 before the grant existed, which
|
|
309
|
+
// both bounced conforming servers' startup pushes and made a FAILED
|
|
310
|
+
// startup observable to the server (a response where the ledger said
|
|
311
|
+
// nothing was released).
|
|
312
|
+
// host/command: host-owned authority, not a capability — no §6.2 path
|
|
313
|
+
// exists and none is invented here. Default DENY; only the operator's
|
|
314
|
+
// config confers it (PR #79 review blocker 9: any server could
|
|
315
|
+
// request undo/hide/unstick).
|
|
316
|
+
if (name === 'host-command' && !this.allowHostCommands) {
|
|
317
|
+
const responder = args[1] as { respondError?: (code: number, message: string, data?: unknown) => void } | undefined;
|
|
318
|
+
if (responder?.respondError) {
|
|
319
|
+
responder.respondError(CAPABILITY_DISABLED, 'host/command requires host-owned authority (McplServerConfig.allowHostCommands)', {});
|
|
320
|
+
} else {
|
|
321
|
+
console.error(`[mcpl] ${this.id}: discarded host/command — allowHostCommands not set`);
|
|
322
|
+
}
|
|
323
|
+
return true;
|
|
324
|
+
}
|
|
325
|
+
const requiredCap = McplServerConnection.EVENT_TO_REQUIRED_CAPABILITY[name];
|
|
326
|
+
if (requiredCap && !this.grant.has(requiredCap)) {
|
|
327
|
+
const phase = this.policyEstablished ? 'not in the effective grant' : 'initial policy not yet established (§5.3)';
|
|
328
|
+
const responder = args[1] as { respondError?: (code: number, message: string, data?: unknown) => void } | undefined;
|
|
329
|
+
if (responder?.respondError) {
|
|
330
|
+
responder.respondError(CAPABILITY_DISABLED, `${name} rejected: capability "${requiredCap}" ${phase}`, { capability: requiredCap });
|
|
331
|
+
} else {
|
|
332
|
+
console.error(`[mcpl] ${this.id}: discarded ${name} — "${requiredCap}" ${phase}`);
|
|
333
|
+
}
|
|
334
|
+
return true;
|
|
335
|
+
}
|
|
236
336
|
return super.emit(event, ...args);
|
|
237
337
|
}
|
|
238
338
|
this.bufferedEvents.push({ event: name, args });
|
|
239
339
|
return true;
|
|
240
340
|
}
|
|
241
341
|
|
|
342
|
+
/** Event-name form of METHOD_TO_REQUIRED_CAPABILITY, for admission-time
|
|
343
|
+
* enforcement in emit(). */
|
|
344
|
+
private static readonly EVENT_TO_REQUIRED_CAPABILITY: Record<string, string> = {
|
|
345
|
+
'push-event': 'pushEvents',
|
|
346
|
+
'inference-request': 'inferenceRequest',
|
|
347
|
+
'model-info': 'modelInfo',
|
|
348
|
+
'channels-register': 'channels.register',
|
|
349
|
+
'channels-changed': 'channels.register',
|
|
350
|
+
'channels-incoming': 'channels.incoming',
|
|
351
|
+
'channels-list': 'channels.register',
|
|
352
|
+
};
|
|
353
|
+
|
|
242
354
|
private static isControlPlaneEvent(event: string): boolean {
|
|
243
355
|
return event === 'stderr'
|
|
244
356
|
|| event === 'scope-elevate'
|
|
245
357
|
|| event === 'channels-register'
|
|
246
358
|
|| event === 'channels-changed'
|
|
247
|
-
|| event === 'feature-sets-changed'
|
|
248
359
|
|| event === 'tools-list-changed'
|
|
249
360
|
|| event === 'connect-failed'
|
|
250
361
|
|| event === 'reconnect-failed'
|
|
@@ -273,9 +384,12 @@ export class McplServerConnection extends EventEmitter {
|
|
|
273
384
|
config: McplServerConfig,
|
|
274
385
|
hostCapabilities: McplHostCapabilities,
|
|
275
386
|
): Promise<McplServerConnection> {
|
|
276
|
-
const { transport, capabilities } = await McplServerConnection.handshake(config, hostCapabilities);
|
|
387
|
+
const { transport, capabilities, droppedCapabilities, mcpToolsAdvertised } = await McplServerConnection.handshake(config, hostCapabilities);
|
|
277
388
|
|
|
278
389
|
const connection = new McplServerConnection(config.id, capabilities, transport);
|
|
390
|
+
connection.droppedCapabilities = droppedCapabilities;
|
|
391
|
+
connection.mcpToolsAdvertised = mcpToolsAdvertised;
|
|
392
|
+
connection.allowHostCommands = config.allowHostCommands === true;
|
|
279
393
|
connection.requestTimeoutMs = config.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
280
394
|
|
|
281
395
|
// Store config for reconnection
|
|
@@ -300,7 +414,7 @@ export class McplServerConnection extends EventEmitter {
|
|
|
300
414
|
private static async handshake(
|
|
301
415
|
config: McplServerConfig,
|
|
302
416
|
hostCapabilities: McplHostCapabilities,
|
|
303
|
-
): Promise<{ transport: McplTransport; capabilities: McplCapabilities | null }> {
|
|
417
|
+
): Promise<{ transport: McplTransport; capabilities: McplCapabilities | null; droppedCapabilities: ReadonlySet<string>; mcpToolsAdvertised: boolean }> {
|
|
304
418
|
const transport = await openTransport(config);
|
|
305
419
|
|
|
306
420
|
try {
|
|
@@ -318,7 +432,7 @@ export class McplServerConnection extends EventEmitter {
|
|
|
318
432
|
|
|
319
433
|
// Await the initialize response, racing an early transport close/error and
|
|
320
434
|
// a timeout. All three paths clean up their listeners.
|
|
321
|
-
const
|
|
435
|
+
const { mcpl: rawCapabilities, mcpToolsAdvertised } = await new Promise<{ mcpl: McplCapabilities | null; mcpToolsAdvertised: boolean }>((resolve, reject) => {
|
|
322
436
|
const cleanup = () => {
|
|
323
437
|
transport.off('line', onLine);
|
|
324
438
|
transport.off('close', onClose);
|
|
@@ -341,7 +455,13 @@ export class McplServerConnection extends EventEmitter {
|
|
|
341
455
|
const result = msg.result as Record<string, unknown> | undefined;
|
|
342
456
|
const caps = result?.capabilities as Record<string, unknown> | undefined;
|
|
343
457
|
const experimental = caps?.experimental as Record<string, unknown> | undefined;
|
|
344
|
-
|
|
458
|
+
// §5.1: `tools` capability is the OUTER standard MCP member — the
|
|
459
|
+
// experimental manifest cannot mint it. Carried alongside so the
|
|
460
|
+
// grant can join it into §6.4 derivation.
|
|
461
|
+
resolve({
|
|
462
|
+
mcpl: (experimental?.mcpl as McplCapabilities) ?? null,
|
|
463
|
+
mcpToolsAdvertised: caps?.tools !== undefined,
|
|
464
|
+
});
|
|
345
465
|
};
|
|
346
466
|
const onClose = (info: TransportCloseInfo) => {
|
|
347
467
|
cleanup();
|
|
@@ -365,7 +485,20 @@ export class McplServerConnection extends EventEmitter {
|
|
|
365
485
|
// Send `initialized` notification (no id)
|
|
366
486
|
transport.writeLine(JSON.stringify({ jsonrpc: '2.0', method: 'notifications/initialized' }));
|
|
367
487
|
|
|
368
|
-
|
|
488
|
+
// §5.1 boolean shorthand expands to explicit leaves BEFORE the config
|
|
489
|
+
// mask runs, so sub-leaf policy is addressable — `channels: true` with
|
|
490
|
+
// `disabledCapabilities: ['channels.streaming']` must actually drop
|
|
491
|
+
// streaming (PR #79 review blocker 1).
|
|
492
|
+
const capabilities = expandAdvertisementShorthand(rawCapabilities);
|
|
493
|
+
// Host-side capability scoping: intersect the server's advertisement
|
|
494
|
+
// with config policy so a masked capability behaves exactly as if it
|
|
495
|
+
// had never been advertised (hooks, streaming, queries all read this).
|
|
496
|
+
const { capabilities: scoped, dropped } = maskNegotiatedCapabilities(capabilities, config);
|
|
497
|
+
if (dropped.length > 0) {
|
|
498
|
+
console.error(`MCPL server "${config.id}" capabilities masked by host config: ${dropped.join(', ')}`);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
return { transport, capabilities: scoped, droppedCapabilities: new Set(dropped), mcpToolsAdvertised };
|
|
369
502
|
} catch (err) {
|
|
370
503
|
// Never leak the child / socket if the handshake fails.
|
|
371
504
|
await transport.close().catch(() => { /* best effort */ });
|
|
@@ -436,29 +569,53 @@ export class McplServerConnection extends EventEmitter {
|
|
|
436
569
|
return this.sendRequest(McplMethod.BeforeInference, params as unknown as Record<string, unknown>) as Promise<BeforeInferenceResult>;
|
|
437
570
|
}
|
|
438
571
|
|
|
439
|
-
/**
|
|
440
|
-
* Send `context/afterInference`.
|
|
441
|
-
* When `blocking` is true, sends as a request and awaits the result.
|
|
442
|
-
* When `blocking` is false (or omitted), sends as a notification.
|
|
443
|
-
*/
|
|
444
|
-
sendAfterInference(params: AfterInferenceParams, blocking?: boolean): Promise<AfterInferenceResult | void> {
|
|
445
|
-
if (blocking) {
|
|
446
|
-
return this.sendRequest(McplMethod.AfterInference, params as unknown as Record<string, unknown>) as Promise<AfterInferenceResult>;
|
|
447
|
-
}
|
|
448
|
-
this.sendNotification(McplMethod.AfterInference, params as unknown as Record<string, unknown>);
|
|
449
|
-
return Promise.resolve();
|
|
450
|
-
}
|
|
451
572
|
|
|
452
|
-
/** Send `featureSets/update`
|
|
573
|
+
/** Send `featureSets/update` as a Notification. §6.7: valid ONLY for
|
|
574
|
+
* purely descriptive metadata that does not alter the grant. Any grant
|
|
575
|
+
* change — including the §5.3 initial policy — MUST use the Request form
|
|
576
|
+
* below. */
|
|
453
577
|
sendFeatureSetsUpdate(params: FeatureSetsUpdateParams): void {
|
|
454
578
|
this.sendNotification(McplMethod.FeatureSetsUpdate, params as unknown as Record<string, unknown>);
|
|
455
579
|
}
|
|
456
580
|
|
|
581
|
+
/**
|
|
582
|
+
* Send `featureSets/update` as a Request (§5.3, §6.7) and await the
|
|
583
|
+
* degradation receipt. A short timeout, distinct from tool-call timeouts:
|
|
584
|
+
* a pre-0.5 server treats the Request as a notification and never
|
|
585
|
+
* answers, and per §6.7 an unanswered expansion simply does not activate
|
|
586
|
+
* — the caller leaves the grant empty and the connection MCP-only.
|
|
587
|
+
*/
|
|
588
|
+
sendFeatureSetsUpdateRequest(
|
|
589
|
+
params: FeatureSetsUpdateParams,
|
|
590
|
+
timeoutMs = 15_000,
|
|
591
|
+
): Promise<FeatureSetsUpdateResult> {
|
|
592
|
+
return this.sendRequest(
|
|
593
|
+
McplMethod.FeatureSetsUpdate,
|
|
594
|
+
params as unknown as Record<string, unknown>,
|
|
595
|
+
{ timeoutMs },
|
|
596
|
+
) as Promise<FeatureSetsUpdateResult>;
|
|
597
|
+
}
|
|
598
|
+
|
|
457
599
|
/** Send `inference/chunk` notification. */
|
|
458
600
|
sendInferenceChunk(params: InferenceChunkParams): void {
|
|
459
601
|
this.sendNotification(McplMethod.InferenceChunk, params as unknown as Record<string, unknown>);
|
|
460
602
|
}
|
|
461
603
|
|
|
604
|
+
/** Send `inference/lifecycle` notification (§10.5). Best-effort. */
|
|
605
|
+
sendInferenceLifecycle(params: InferenceLifecycleParams): void {
|
|
606
|
+
this.sendNotification(McplMethod.InferenceLifecycle, params as unknown as Record<string, unknown>);
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
/**
|
|
610
|
+
* Fetch the server's complete current manifest (§17.4) — the
|
|
611
|
+
* experimental.mcpl object in the same shape initialize carries, never a
|
|
612
|
+
* delta. The fetched CONTENT is authoritative for every decision; the
|
|
613
|
+
* revision is only the cheap path (§17.1).
|
|
614
|
+
*/
|
|
615
|
+
sendManifestRequest(timeoutMs = 15_000): Promise<McplCapabilities | null> {
|
|
616
|
+
return this.sendRequest(McplMethod.Manifest, {}, { timeoutMs }) as Promise<McplCapabilities | null>;
|
|
617
|
+
}
|
|
618
|
+
|
|
462
619
|
/** Send `state/rollback` request and await result. */
|
|
463
620
|
sendStateRollback(params: StateRollbackParams): Promise<StateRollbackResult> {
|
|
464
621
|
return this.sendRequest(McplMethod.StateRollback, params as unknown as Record<string, unknown>) as Promise<StateRollbackResult>;
|
|
@@ -585,6 +742,7 @@ export class McplServerConnection extends EventEmitter {
|
|
|
585
742
|
return;
|
|
586
743
|
}
|
|
587
744
|
this.closed = true;
|
|
745
|
+
this.resetPolicyForTransportBoundary();
|
|
588
746
|
|
|
589
747
|
// Reject all pending requests
|
|
590
748
|
for (const [id, pending] of this.pendingRequests) {
|
|
@@ -663,7 +821,7 @@ export class McplServerConnection extends EventEmitter {
|
|
|
663
821
|
const attempt = Math.max(1, this.reconnectAttempts);
|
|
664
822
|
|
|
665
823
|
try {
|
|
666
|
-
const { transport, capabilities } = await McplServerConnection.handshake(
|
|
824
|
+
const { transport, capabilities, droppedCapabilities, mcpToolsAdvertised } = await McplServerConnection.handshake(
|
|
667
825
|
this.config,
|
|
668
826
|
this.hostCapabilities,
|
|
669
827
|
);
|
|
@@ -673,8 +831,12 @@ export class McplServerConnection extends EventEmitter {
|
|
|
673
831
|
// awareness barrier synchronously, then releases control traffic needed
|
|
674
832
|
// to establish the server while data remains held.
|
|
675
833
|
this.pauseDataPlane();
|
|
834
|
+
this.resetPolicyForTransportBoundary();
|
|
676
835
|
this.transport = transport;
|
|
677
836
|
this.capabilities = capabilities;
|
|
837
|
+
this.droppedCapabilities = droppedCapabilities;
|
|
838
|
+
this.mcpToolsAdvertised = mcpToolsAdvertised;
|
|
839
|
+
this.allowHostCommands = this.config?.allowHostCommands === true;
|
|
678
840
|
this.closed = false;
|
|
679
841
|
this.nextRequestId = 1;
|
|
680
842
|
this.pendingRequests.clear();
|
|
@@ -779,7 +941,16 @@ export class McplServerConnection extends EventEmitter {
|
|
|
779
941
|
[McplMethod.ChannelsRegister]: 'channels-register',
|
|
780
942
|
[McplMethod.ChannelsChanged]: 'channels-changed',
|
|
781
943
|
[McplMethod.ChannelsIncoming]: 'channels-incoming',
|
|
782
|
-
|
|
944
|
+
// §6.6/§12: a request bearing an id MUST get a result or an error —
|
|
945
|
+
// neither of these had an entry, so callers hung forever (AUDIT-001
|
|
946
|
+
// item 4, which also poisoned the audit's own usage evidence).
|
|
947
|
+
[McplMethod.ModelInfo]: 'model-info',
|
|
948
|
+
// §17.3: deliberately UNGATED (not in EVENT_TO_REQUIRED_CAPABILITY) —
|
|
949
|
+
// announcing conveys no authority, and gating it would silence exactly
|
|
950
|
+
// the servers whose grants just narrowed. The cost is the host's
|
|
951
|
+
// re-fetch, which the framework rate-limits (§17.8).
|
|
952
|
+
[McplMethod.ManifestChanged]: 'manifest-changed',
|
|
953
|
+
[McplMethod.ChannelsList]: 'channels-list',
|
|
783
954
|
'notifications/tools/list_changed': 'tools-list-changed',
|
|
784
955
|
// Host-level admin commands initiated from a surface (e.g. a Discord
|
|
785
956
|
// slash command). Params: { command, ... }; host responds with a
|
|
@@ -787,6 +958,33 @@ export class McplServerConnection extends EventEmitter {
|
|
|
787
958
|
'host/command': 'host-command',
|
|
788
959
|
};
|
|
789
960
|
|
|
961
|
+
/**
|
|
962
|
+
* Inbound (server-initiated) method → required capability path, per SPEC
|
|
963
|
+
* §14.1's table plus the non-channel privileged methods. **Positive-grant
|
|
964
|
+
* enforcement** (§5.4): the method is admitted only when the effective
|
|
965
|
+
* grant contains the named path — absence is denial, and before the
|
|
966
|
+
* initial policy exchange completes the grant is empty, so everything
|
|
967
|
+
* privileged is rejected (§5.3).
|
|
968
|
+
*
|
|
969
|
+
* This replaces the earlier `droppedCapabilities` deny-list check, which
|
|
970
|
+
* could only deny what had been advertised-then-masked: a server that
|
|
971
|
+
* never advertised a capability at all was never denied. (Adjudicated in
|
|
972
|
+
* review of PR #75; the mask itself is retained as the config-policy
|
|
973
|
+
* layer feeding the grant.)
|
|
974
|
+
*
|
|
975
|
+
* Channel methods key on their specific sub-capabilities, not the bare
|
|
976
|
+
* `channels` parent — a bare parent grants nothing (§5.4).
|
|
977
|
+
*/
|
|
978
|
+
private static readonly METHOD_TO_REQUIRED_CAPABILITY: Record<string, string> = {
|
|
979
|
+
[McplMethod.PushEvent]: 'pushEvents',
|
|
980
|
+
[McplMethod.InferenceRequest]: 'inferenceRequest',
|
|
981
|
+
[McplMethod.ModelInfo]: 'modelInfo',
|
|
982
|
+
[McplMethod.ChannelsRegister]: 'channels.register',
|
|
983
|
+
[McplMethod.ChannelsChanged]: 'channels.register',
|
|
984
|
+
[McplMethod.ChannelsIncoming]: 'channels.incoming',
|
|
985
|
+
[McplMethod.ChannelsList]: 'channels.register',
|
|
986
|
+
};
|
|
987
|
+
|
|
790
988
|
/**
|
|
791
989
|
* Bind all transport-level handlers (inbound routing, lifecycle, stderr) to a
|
|
792
990
|
* transport. Called from the constructor and again from attemptReconnect when
|
|
@@ -823,6 +1021,35 @@ export class McplServerConnection extends EventEmitter {
|
|
|
823
1021
|
const request = msg as JsonRpcRequest;
|
|
824
1022
|
const eventName = McplServerConnection.METHOD_TO_EVENT[request.method];
|
|
825
1023
|
|
|
1024
|
+
// §7 is removed in 0.5.0: scope/elevate is no longer a method. Answer
|
|
1025
|
+
// rather than hang (§6.6 — a method that will never be answered MUST
|
|
1026
|
+
// return an error).
|
|
1027
|
+
// featureSets/changed is REMOVED in 0.5.0 (§6.7 — it carried a
|
|
1028
|
+
// server-authored change payload, the self-attestation defect; the
|
|
1029
|
+
// need is met by §17's host-diffed manifest fetch). It was still
|
|
1030
|
+
// routed, control-plane classified, and mutating declarations — a
|
|
1031
|
+
// live ungated self-attestation path (PR #79 review blocker 4).
|
|
1032
|
+
if (request.method === McplMethod.FeatureSetsChanged) {
|
|
1033
|
+
if (request.id != null) {
|
|
1034
|
+
this.sendErrorResponse(request.id, -32601, 'featureSets/changed removed in MCPL 0.5.0 (superseded by mcpl/manifestChanged, SPEC §17)');
|
|
1035
|
+
} else {
|
|
1036
|
+
console.error(`[mcpl] ${this.id}: discarded featureSets/changed — removed in 0.5.0 (§6.7/§17)`);
|
|
1037
|
+
}
|
|
1038
|
+
return;
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
if (request.method === McplMethod.ScopeElevate) {
|
|
1042
|
+
if (request.id != null) {
|
|
1043
|
+
this.sendErrorResponse(request.id, -32601, 'scope/elevate removed in MCPL 0.5.0 (SPEC §7)');
|
|
1044
|
+
}
|
|
1045
|
+
return;
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
// Positive-grant enforcement (§5.4, §14.1) happens at ADMISSION, in
|
|
1049
|
+
// the emit() override below — the one choke-point that live routing
|
|
1050
|
+
// and staged-buffer flushes share. See the comment there for why
|
|
1051
|
+
// rejecting here, at line receipt, was wrong for staged connections.
|
|
1052
|
+
|
|
826
1053
|
if (eventName) {
|
|
827
1054
|
// Emit the typed event with params and (for requests) a respond callback
|
|
828
1055
|
if (request.id != null) {
|
|
@@ -928,6 +1155,7 @@ export class McplServerConnection extends EventEmitter {
|
|
|
928
1155
|
transport.on('close', (info: TransportCloseInfo) => {
|
|
929
1156
|
if (!this.closed) {
|
|
930
1157
|
this.closed = true;
|
|
1158
|
+
this.resetPolicyForTransportBoundary();
|
|
931
1159
|
|
|
932
1160
|
// Reject all pending requests
|
|
933
1161
|
for (const [id, pending] of this.pendingRequests) {
|
|
@@ -107,9 +107,10 @@ export class McplServerRegistry {
|
|
|
107
107
|
case 'pushEvents':
|
|
108
108
|
return caps.pushEvents === true;
|
|
109
109
|
case 'contextHooks.beforeInference':
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
110
|
+
// Truthy, not ===true: §5.1 shorthand is expanded to the object
|
|
111
|
+
// form at handshake. (Prefer connection.grant for authorization —
|
|
112
|
+
// this query is advertisement-level only.)
|
|
113
|
+
return !!caps.contextHooks?.beforeInference;
|
|
113
114
|
case 'inferenceRequest':
|
|
114
115
|
return !!caps.inferenceRequest;
|
|
115
116
|
case 'modelInfo':
|
package/src/mcpl/tags.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Event-tag core closure (SPEC §16.3) and the addressed/ambient resolution.
|
|
3
|
+
*
|
|
4
|
+
* The `chat:*` core implications are NORMATIVE and spec-defined — a host
|
|
5
|
+
* expands them itself and never needs the producer's ontology to do so.
|
|
6
|
+
* Producer `implies` edges are advisory pending explicit acceptance (§16.4)
|
|
7
|
+
* and are NOT consumed here. Producer `suggestedTreatment` MUST NOT be
|
|
8
|
+
* auto-applied (§16.5) — nothing in this module reads it, deliberately.
|
|
9
|
+
*
|
|
10
|
+
* Tags are never authority (§16.6): expansion happens after admission, and
|
|
11
|
+
* nothing downstream may grant, admit, or authorize based on a tag.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/** Normative core closure edges (§16.3). Closed vocabulary — chat:* only. */
|
|
15
|
+
const CORE_IMPLIES: Readonly<Record<string, readonly string[]>> = {
|
|
16
|
+
'chat:mention': ['chat:addressed'],
|
|
17
|
+
'chat:reply': ['chat:addressed'],
|
|
18
|
+
'chat:dm': ['chat:addressed', 'chat:private'],
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Expand a raw tag list over the normative core closure, then resolve the
|
|
23
|
+
* §16.2 mutual exclusion: `chat:addressed` and `chat:ambient` cannot both
|
|
24
|
+
* stand after expansion, and addressed wins — a message that is both
|
|
25
|
+
* specifically-to-you and background noise is specifically-to-you.
|
|
26
|
+
*
|
|
27
|
+
* Pure; input order is preserved for the tags that survive, with implied
|
|
28
|
+
* tags appended in first-cause order. Unknown and producer-namespaced tags
|
|
29
|
+
* pass through untouched.
|
|
30
|
+
*/
|
|
31
|
+
export function expandCoreTags(tags: readonly string[] | undefined): string[] {
|
|
32
|
+
if (!tags || tags.length === 0) return [];
|
|
33
|
+
const out: string[] = [];
|
|
34
|
+
const seen = new Set<string>();
|
|
35
|
+
const add = (t: string): void => {
|
|
36
|
+
if (!seen.has(t)) {
|
|
37
|
+
seen.add(t);
|
|
38
|
+
out.push(t);
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
for (const t of tags) {
|
|
42
|
+
add(t);
|
|
43
|
+
for (const implied of CORE_IMPLIES[t] ?? []) add(implied);
|
|
44
|
+
}
|
|
45
|
+
if (seen.has('chat:addressed') && seen.has('chat:ambient')) {
|
|
46
|
+
const i = out.indexOf('chat:ambient');
|
|
47
|
+
if (i >= 0) out.splice(i, 1);
|
|
48
|
+
}
|
|
49
|
+
return out;
|
|
50
|
+
}
|
package/src/mcpl/transport.ts
CHANGED
|
@@ -200,18 +200,46 @@ export class WebSocketTransport extends McplTransport {
|
|
|
200
200
|
* when `config.token` is set. Rejects on connect error / timeout (and tears
|
|
201
201
|
* the socket down) so a failed dial never leaks an open handle.
|
|
202
202
|
*/
|
|
203
|
-
static open(config: McplServerConfig): Promise<WebSocketTransport> {
|
|
204
|
-
|
|
203
|
+
static async open(config: McplServerConfig): Promise<WebSocketTransport> {
|
|
204
|
+
// Resolve the per-dial credential first: the provider runs on every
|
|
205
|
+
// connect AND reconnect, so rotation happens at the transport layer and
|
|
206
|
+
// nothing above it ever holds a credential.
|
|
207
|
+
let token: string | null = config.token ?? null;
|
|
208
|
+
if (config.accessProvider) {
|
|
209
|
+
try {
|
|
210
|
+
token = (await config.accessProvider()) ?? token;
|
|
211
|
+
} catch (err) {
|
|
212
|
+
// Fall back to the static token (if any) rather than failing the dial
|
|
213
|
+
// outright — the server will refuse a stale credential loudly anyway.
|
|
214
|
+
console.error(
|
|
215
|
+
`[mcpl] server "${config.id}": access provider failed (${err instanceof Error ? err.message : err}) — dialing with configured fallback`,
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
const url = buildWebSocketUrl(config, token);
|
|
220
|
+
// Error strings travel: traces, logs, tool results the model reads. They
|
|
221
|
+
// get the endpoint, never the credential.
|
|
222
|
+
const redacted = buildWebSocketUrl({ ...config, token: undefined }, null);
|
|
205
223
|
const ws = new WebSocket(url);
|
|
206
224
|
|
|
225
|
+
// A failed dial can emit 'error' MORE THAN ONCE on the raw socket (seen
|
|
226
|
+
// live 2026-08-03: an HTTP 502 upgrade-reject emits the "Expected 101"
|
|
227
|
+
// error and then a teardown error). WebSocket is an EventEmitter, so any
|
|
228
|
+
// 'error' with no listener throws at the top level and KILLS THE WHOLE
|
|
229
|
+
// AGENT PROCESS — a crashloop, since the dial happens at boot. Every
|
|
230
|
+
// settle path below must therefore leave a swallow listener behind; a
|
|
231
|
+
// remote server being down must never be able to take the host down.
|
|
232
|
+
const swallowLateErrors = () => { /* settled; retry loop owns recovery */ };
|
|
233
|
+
|
|
207
234
|
return new Promise<WebSocketTransport>((resolve, reject) => {
|
|
208
235
|
let settled = false;
|
|
209
236
|
const timer = setTimeout(() => {
|
|
210
237
|
if (settled) return;
|
|
211
238
|
settled = true;
|
|
212
239
|
ws.removeAllListeners();
|
|
240
|
+
ws.on('error', swallowLateErrors);
|
|
213
241
|
try { ws.terminate(); } catch { /* already gone */ }
|
|
214
|
-
reject(new Error(`MCPL server "${config.id}" WebSocket connect timed out after ${WS_OPEN_TIMEOUT_MS}ms (${
|
|
242
|
+
reject(new Error(`MCPL server "${config.id}" WebSocket connect timed out after ${WS_OPEN_TIMEOUT_MS}ms (${redacted})`));
|
|
215
243
|
}, WS_OPEN_TIMEOUT_MS);
|
|
216
244
|
|
|
217
245
|
ws.once('open', () => {
|
|
@@ -227,8 +255,9 @@ export class WebSocketTransport extends McplTransport {
|
|
|
227
255
|
settled = true;
|
|
228
256
|
clearTimeout(timer);
|
|
229
257
|
ws.removeAllListeners();
|
|
258
|
+
ws.on('error', swallowLateErrors);
|
|
230
259
|
try { ws.terminate(); } catch { /* already gone */ }
|
|
231
|
-
reject(new Error(`MCPL server "${config.id}" WebSocket connect failed: ${err.message} (${
|
|
260
|
+
reject(new Error(`MCPL server "${config.id}" WebSocket connect failed: ${err.message} (${redacted})`));
|
|
232
261
|
});
|
|
233
262
|
});
|
|
234
263
|
}
|
|
@@ -261,7 +290,7 @@ export class WebSocketTransport extends McplTransport {
|
|
|
261
290
|
* Build the dial URL: validate it's a ws(s):// URL and append the auth token as
|
|
262
291
|
* a `token` query param when provided (preserving any existing query).
|
|
263
292
|
*/
|
|
264
|
-
export function buildWebSocketUrl(config: McplServerConfig): string {
|
|
293
|
+
export function buildWebSocketUrl(config: McplServerConfig, tokenOverride?: string | null): string {
|
|
265
294
|
if (!config.url) {
|
|
266
295
|
throw new Error(`MCPL server "${config.id}": websocket transport requires "url" (none provided)`);
|
|
267
296
|
}
|
|
@@ -276,8 +305,9 @@ export function buildWebSocketUrl(config: McplServerConfig): string {
|
|
|
276
305
|
`MCPL server "${config.id}": websocket url must be ws:// or wss://, got "${parsed.protocol}"`,
|
|
277
306
|
);
|
|
278
307
|
}
|
|
279
|
-
|
|
280
|
-
|
|
308
|
+
const token = tokenOverride !== undefined ? tokenOverride : config.token;
|
|
309
|
+
if (token) {
|
|
310
|
+
parsed.searchParams.set('token', token);
|
|
281
311
|
}
|
|
282
312
|
return parsed.toString();
|
|
283
313
|
}
|