@animalabs/agent-framework 0.8.0 → 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.
Files changed (172) hide show
  1. package/dist/src/agent.d.ts +10 -0
  2. package/dist/src/agent.d.ts.map +1 -1
  3. package/dist/src/agent.js +33 -3
  4. package/dist/src/agent.js.map +1 -1
  5. package/dist/src/framework.d.ts +171 -15
  6. package/dist/src/framework.d.ts.map +1 -1
  7. package/dist/src/framework.js +1059 -224
  8. package/dist/src/framework.js.map +1 -1
  9. package/dist/src/gate/event-gate.d.ts +11 -0
  10. package/dist/src/gate/event-gate.d.ts.map +1 -1
  11. package/dist/src/gate/event-gate.js +66 -0
  12. package/dist/src/gate/event-gate.js.map +1 -1
  13. package/dist/src/index.d.ts +1 -0
  14. package/dist/src/index.d.ts.map +1 -1
  15. package/dist/src/index.js +1 -0
  16. package/dist/src/index.js.map +1 -1
  17. package/dist/src/mcpl/capability-grant.d.ts +115 -0
  18. package/dist/src/mcpl/capability-grant.d.ts.map +1 -0
  19. package/dist/src/mcpl/capability-grant.js +291 -0
  20. package/dist/src/mcpl/capability-grant.js.map +1 -0
  21. package/dist/src/mcpl/capability-mask.d.ts +51 -0
  22. package/dist/src/mcpl/capability-mask.d.ts.map +1 -0
  23. package/dist/src/mcpl/capability-mask.js +190 -0
  24. package/dist/src/mcpl/capability-mask.js.map +1 -0
  25. package/dist/src/mcpl/channel-registry.d.ts +3 -1
  26. package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
  27. package/dist/src/mcpl/channel-registry.js +163 -36
  28. package/dist/src/mcpl/channel-registry.js.map +1 -1
  29. package/dist/src/mcpl/checkpoint-manager.d.ts +8 -3
  30. package/dist/src/mcpl/checkpoint-manager.d.ts.map +1 -1
  31. package/dist/src/mcpl/checkpoint-manager.js +28 -8
  32. package/dist/src/mcpl/checkpoint-manager.js.map +1 -1
  33. package/dist/src/mcpl/errors.d.ts +6 -0
  34. package/dist/src/mcpl/errors.d.ts.map +1 -1
  35. package/dist/src/mcpl/errors.js +6 -0
  36. package/dist/src/mcpl/errors.js.map +1 -1
  37. package/dist/src/mcpl/feature-set-manager.d.ts +10 -1
  38. package/dist/src/mcpl/feature-set-manager.d.ts.map +1 -1
  39. package/dist/src/mcpl/feature-set-manager.js +45 -4
  40. package/dist/src/mcpl/feature-set-manager.js.map +1 -1
  41. package/dist/src/mcpl/hook-orchestrator.d.ts +10 -14
  42. package/dist/src/mcpl/hook-orchestrator.d.ts.map +1 -1
  43. package/dist/src/mcpl/hook-orchestrator.js +58 -67
  44. package/dist/src/mcpl/hook-orchestrator.js.map +1 -1
  45. package/dist/src/mcpl/index.d.ts +1 -1
  46. package/dist/src/mcpl/index.d.ts.map +1 -1
  47. package/dist/src/mcpl/index.js.map +1 -1
  48. package/dist/src/mcpl/prose-stream-router.d.ts +9 -0
  49. package/dist/src/mcpl/prose-stream-router.d.ts.map +1 -1
  50. package/dist/src/mcpl/prose-stream-router.js +16 -0
  51. package/dist/src/mcpl/prose-stream-router.js.map +1 -1
  52. package/dist/src/mcpl/push-handler.d.ts.map +1 -1
  53. package/dist/src/mcpl/push-handler.js +20 -2
  54. package/dist/src/mcpl/push-handler.js.map +1 -1
  55. package/dist/src/mcpl/server-connection.d.ts +93 -9
  56. package/dist/src/mcpl/server-connection.d.ts.map +1 -1
  57. package/dist/src/mcpl/server-connection.js +231 -32
  58. package/dist/src/mcpl/server-connection.js.map +1 -1
  59. package/dist/src/mcpl/server-registry.d.ts.map +1 -1
  60. package/dist/src/mcpl/server-registry.js +4 -3
  61. package/dist/src/mcpl/server-registry.js.map +1 -1
  62. package/dist/src/mcpl/tags.d.ts +24 -0
  63. package/dist/src/mcpl/tags.d.ts.map +1 -0
  64. package/dist/src/mcpl/tags.js +52 -0
  65. package/dist/src/mcpl/tags.js.map +1 -0
  66. package/dist/src/mcpl/transport.d.ts.map +1 -1
  67. package/dist/src/mcpl/transport.js +10 -0
  68. package/dist/src/mcpl/transport.js.map +1 -1
  69. package/dist/src/mcpl/types.d.ts +159 -64
  70. package/dist/src/mcpl/types.d.ts.map +1 -1
  71. package/dist/src/mcpl/types.js +16 -2
  72. package/dist/src/mcpl/types.js.map +1 -1
  73. package/dist/src/refusal-reactions.d.ts +27 -0
  74. package/dist/src/refusal-reactions.d.ts.map +1 -0
  75. package/dist/src/refusal-reactions.js +35 -0
  76. package/dist/src/refusal-reactions.js.map +1 -0
  77. package/dist/src/tool-result-history.d.ts +10 -0
  78. package/dist/src/tool-result-history.d.ts.map +1 -1
  79. package/dist/src/tool-result-history.js +16 -5
  80. package/dist/src/tool-result-history.js.map +1 -1
  81. package/dist/src/types/agent.d.ts +35 -0
  82. package/dist/src/types/agent.d.ts.map +1 -1
  83. package/dist/src/types/framework.d.ts +16 -0
  84. package/dist/src/types/framework.d.ts.map +1 -1
  85. package/dist/src/types/trace.d.ts +24 -0
  86. package/dist/src/types/trace.d.ts.map +1 -1
  87. package/dist/test/channel-registry-routing.test.js +38 -10
  88. package/dist/test/channel-registry-routing.test.js.map +1 -1
  89. package/dist/test/channel-registry-typing.test.js +10 -1
  90. package/dist/test/channel-registry-typing.test.js.map +1 -1
  91. package/dist/test/checkpoint-attribution.test.js +168 -8
  92. package/dist/test/checkpoint-attribution.test.js.map +1 -1
  93. package/dist/test/conversation-fork-injection-scoping.test.js +8 -3
  94. package/dist/test/conversation-fork-injection-scoping.test.js.map +1 -1
  95. package/dist/test/event-gate-selfwake.test.d.ts +15 -0
  96. package/dist/test/event-gate-selfwake.test.d.ts.map +1 -0
  97. package/dist/test/event-gate-selfwake.test.js +125 -0
  98. package/dist/test/event-gate-selfwake.test.js.map +1 -0
  99. package/dist/test/machine-close-provenance.test.js +6 -0
  100. package/dist/test/machine-close-provenance.test.js.map +1 -1
  101. package/dist/test/mcpl-awareness-barrier.test.js +10 -0
  102. package/dist/test/mcpl-awareness-barrier.test.js.map +1 -1
  103. package/dist/test/mcpl-capability-grant.test.d.ts +10 -0
  104. package/dist/test/mcpl-capability-grant.test.d.ts.map +1 -0
  105. package/dist/test/mcpl-capability-grant.test.js +109 -0
  106. package/dist/test/mcpl-capability-grant.test.js.map +1 -0
  107. package/dist/test/mcpl-capability-scoping.test.d.ts +2 -0
  108. package/dist/test/mcpl-capability-scoping.test.d.ts.map +1 -0
  109. package/dist/test/mcpl-capability-scoping.test.js +266 -0
  110. package/dist/test/mcpl-capability-scoping.test.js.map +1 -0
  111. package/dist/test/mcpl-gate-roundtrip.test.js +13 -0
  112. package/dist/test/mcpl-gate-roundtrip.test.js.map +1 -1
  113. package/dist/test/mcpl-list-status.test.d.ts +2 -0
  114. package/dist/test/mcpl-list-status.test.d.ts.map +1 -0
  115. package/dist/test/mcpl-list-status.test.js +78 -0
  116. package/dist/test/mcpl-list-status.test.js.map +1 -0
  117. package/dist/test/mcpl-manifest-changed.test.d.ts +2 -0
  118. package/dist/test/mcpl-manifest-changed.test.d.ts.map +1 -0
  119. package/dist/test/mcpl-manifest-changed.test.js +177 -0
  120. package/dist/test/mcpl-manifest-changed.test.js.map +1 -0
  121. package/dist/test/mcpl-reconnect-events.test.js +27 -2
  122. package/dist/test/mcpl-reconnect-events.test.js.map +1 -1
  123. package/dist/test/mcpl-reconnect-reregistration.test.js +53 -17
  124. package/dist/test/mcpl-reconnect-reregistration.test.js.map +1 -1
  125. package/dist/test/mcpl-request-timeout.test.js +1 -1
  126. package/dist/test/mcpl-request-timeout.test.js.map +1 -1
  127. package/dist/test/physical-window-restart.test.d.ts +14 -0
  128. package/dist/test/physical-window-restart.test.d.ts.map +1 -0
  129. package/dist/test/physical-window-restart.test.js +222 -0
  130. package/dist/test/physical-window-restart.test.js.map +1 -0
  131. package/dist/test/present-while-acting.test.js +199 -1
  132. package/dist/test/present-while-acting.test.js.map +1 -1
  133. package/dist/test/refusal-reaction-baseline.test.d.ts +2 -0
  134. package/dist/test/refusal-reaction-baseline.test.d.ts.map +1 -0
  135. package/dist/test/refusal-reaction-baseline.test.js +74 -0
  136. package/dist/test/refusal-reaction-baseline.test.js.map +1 -0
  137. package/dist/test/state-scaling-e2e.test.js +7 -1
  138. package/dist/test/state-scaling-e2e.test.js.map +1 -1
  139. package/dist/test/state-scaling.test.js +9 -1
  140. package/dist/test/state-scaling.test.js.map +1 -1
  141. package/dist/test/tool-result-spill.test.d.ts +13 -0
  142. package/dist/test/tool-result-spill.test.d.ts.map +1 -0
  143. package/dist/test/tool-result-spill.test.js +463 -0
  144. package/dist/test/tool-result-spill.test.js.map +1 -0
  145. package/dist/test/trunk-channel-routing.test.js +45 -0
  146. package/dist/test/trunk-channel-routing.test.js.map +1 -1
  147. package/dist/tsconfig.tsbuildinfo +1 -1
  148. package/package.json +4 -4
  149. package/src/agent.ts +37 -3
  150. package/src/framework.ts +1219 -248
  151. package/src/gate/event-gate.ts +72 -0
  152. package/src/index.ts +1 -0
  153. package/src/mcpl/capability-grant.ts +319 -0
  154. package/src/mcpl/capability-mask.ts +228 -0
  155. package/src/mcpl/channel-registry.ts +167 -39
  156. package/src/mcpl/checkpoint-manager.ts +28 -8
  157. package/src/mcpl/errors.ts +7 -0
  158. package/src/mcpl/feature-set-manager.ts +50 -5
  159. package/src/mcpl/hook-orchestrator.ts +64 -86
  160. package/src/mcpl/index.ts +0 -2
  161. package/src/mcpl/prose-stream-router.ts +17 -0
  162. package/src/mcpl/push-handler.ts +18 -2
  163. package/src/mcpl/server-connection.ts +262 -34
  164. package/src/mcpl/server-registry.ts +4 -3
  165. package/src/mcpl/tags.ts +50 -0
  166. package/src/mcpl/transport.ts +11 -0
  167. package/src/mcpl/types.ts +176 -78
  168. package/src/refusal-reactions.ts +37 -0
  169. package/src/tool-result-history.ts +17 -5
  170. package/src/types/agent.ts +36 -0
  171. package/src/types/framework.ts +17 -0
  172. package/src/types/trace.ts +22 -0
@@ -3,8 +3,9 @@
3
3
  *
4
4
  * This is the bridge between the MCPL protocol and the agent-framework inference
5
5
  * pipeline. It collects ContextInjection[] from MCPL servers via beforeInference
6
- * and feeds them into context-manager's compile(). After inference, it notifies
7
- * servers of the result via afterInference.
6
+ * and feeds them into context-manager's compile(). Turn boundaries are announced
7
+ * via metadata-only `inference/lifecycle` notifications (§10.5) —
8
+ * context/afterInference and its modifiedResponse are removed in 0.5.0.
8
9
  *
9
10
  * Design principles:
10
11
  * - Fail-open: timeouts and errors never block inference
@@ -20,18 +21,24 @@ import type {
20
21
  McplContextInjection,
21
22
  BeforeInferenceParams,
22
23
  BeforeInferenceResult,
23
- AfterInferenceParams,
24
- AfterInferenceResult,
24
+ InferenceLifecycleParams,
25
25
  } from './types.js';
26
26
  import type { McplServerRegistry } from './server-registry.js';
27
27
  import type { McplServerConnection } from './server-connection.js';
28
28
  import type { FeatureSetManager } from './feature-set-manager.js';
29
+ import { CapabilityGrant } from './capability-grant.js';
29
30
 
30
31
  /** Timeout for beforeInference per server (fail-open). */
31
32
  const BEFORE_INFERENCE_TIMEOUT_MS = 5_000;
32
33
 
33
- /** Timeout for blocking afterInference per server. */
34
- const AFTER_INFERENCE_BLOCKING_TIMEOUT_MS = 10_000;
34
+ /** The three injection positions and their §6.2 capability paths. Position
35
+ * is the TYPED field on the injection — authorization never reads the
36
+ * response's featureSet (§5.4). */
37
+ const INJECT_LEAVES: ReadonlyArray<{ position: string; path: string }> = [
38
+ { position: 'system', path: 'contextHooks.beforeInference.inject.system' },
39
+ { position: 'beforeUser', path: 'contextHooks.beforeInference.inject.beforeUser' },
40
+ { position: 'afterUser', path: 'contextHooks.beforeInference.inject.afterUser' },
41
+ ];
35
42
 
36
43
  /**
37
44
  * Races a promise against a timeout. Rejects with a descriptive error on timeout.
@@ -141,7 +148,16 @@ export class HookOrchestrator {
141
148
  * Fail-open: servers that time out or error are silently skipped.
142
149
  */
143
150
  async beforeInference(params: BeforeInferenceParams): Promise<ContextInjection[]> {
144
- const servers = this.registry.getServersWithCapability('contextHooks.beforeInference');
151
+ // §5.4: the GRANT decides fan-out, not the raw advertisement — which
152
+ // also fixes selection for the recursive §5.1 shape, where
153
+ // `beforeInference: {observe: true, …}` is an object, not `true`. A
154
+ // server granted any inject.* leaf but not observe is still CALLED
155
+ // (§10.1 — the hook is how injection happens) and receives
156
+ // `userMessage: null` instead of the user's text.
157
+ const servers = this.registry.getAllServers().filter((s) =>
158
+ CapabilityGrant.of(s).has('contextHooks.beforeInference.observe')
159
+ || INJECT_LEAVES.some((leaf) => CapabilityGrant.of(s).has(leaf.path)),
160
+ );
145
161
  if (servers.length === 0) {
146
162
  return [];
147
163
  }
@@ -155,23 +171,20 @@ export class HookOrchestrator {
155
171
  }
156
172
 
157
173
  /**
158
- * Fan out `context/afterInference` to all capable MCPL servers.
159
- *
160
- * Non-blocking servers receive a notification (fire-and-forget).
161
- * Blocking servers receive a request with a 10s timeout.
162
- * Returns the first valid AfterInferenceResult from a blocking server, or null.
174
+ * Fan out `inference/lifecycle` (§10.5) — the metadata-only replacement
175
+ * for context/afterInference. Notifications, fire-and-forget, gated on the
176
+ * inferenceLifecycle grant. BEST-EFFORT by design: the host attempts one
177
+ * terminal per `started` on every exit path it controls; consumers dedupe
178
+ * by inferenceId and keep a safety timeout. Never blocks the turn.
163
179
  */
164
- async afterInference(params: AfterInferenceParams): Promise<AfterInferenceResult | null> {
165
- const servers = this.registry.getServersWithCapability('contextHooks.afterInference');
166
- if (servers.length === 0) {
167
- return null;
168
- }
169
-
170
- this._isInHook = true;
171
- try {
172
- return await this.fanOutAfterInference(servers, params);
173
- } finally {
174
- this._isInHook = false;
180
+ emitLifecycle(params: InferenceLifecycleParams): void {
181
+ for (const server of this.registry.getAllServers()) {
182
+ if (!CapabilityGrant.of(server).has('inferenceLifecycle')) continue;
183
+ try {
184
+ server.sendInferenceLifecycle(params);
185
+ } catch {
186
+ /* best-effort — a failed notify never disturbs the turn */
187
+ }
175
188
  }
176
189
  }
177
190
 
@@ -184,13 +197,21 @@ export class HookOrchestrator {
184
197
  params: BeforeInferenceParams,
185
198
  ): Promise<ContextInjection[]> {
186
199
  const results = await Promise.allSettled(
187
- servers.map((server) =>
188
- withTimeout(
189
- server.sendBeforeInference(params),
200
+ servers.map((server) => {
201
+ // §10.1: a server not granted observe MUST receive
202
+ // `userMessage: null` — the field is ABSENT authority, not merely
203
+ // discouraged — while the hook is still invoked so granted
204
+ // injection positions keep working (write-without-read).
205
+ const perServer: BeforeInferenceParams =
206
+ CapabilityGrant.of(server).has('contextHooks.beforeInference.observe')
207
+ ? params
208
+ : { ...params, userMessage: null };
209
+ return withTimeout(
210
+ server.sendBeforeInference(perServer),
190
211
  BEFORE_INFERENCE_TIMEOUT_MS,
191
212
  `beforeInference to "${server.id}"`,
192
- ).then((result) => ({ server, result })),
193
- ),
213
+ ).then((result) => ({ server, result }));
214
+ }),
194
215
  );
195
216
 
196
217
  const injections: ContextInjection[] = [];
@@ -203,7 +224,9 @@ export class HookOrchestrator {
203
224
 
204
225
  const { server, result } = settled.value;
205
226
 
206
- // Validate feature set
227
+ // Feature-set discipline (diagnostics; §5.4 forbids using the
228
+ // response-supplied featureSet as AUTHORIZATION — that is the
229
+ // position check below).
207
230
  try {
208
231
  this.featureSetManager.validateInbound(server.id, result.featureSet);
209
232
  } catch {
@@ -211,9 +234,20 @@ export class HookOrchestrator {
211
234
  continue;
212
235
  }
213
236
 
214
- // Convert wire-format injections to context-manager format
237
+ // §5.4/§10.8: authorize EACH injection by its typed position against
238
+ // the grant CURRENT NOW, at response receipt — not when the request
239
+ // was sent. A revocation that landed while the hook was in flight is
240
+ // enforced here without extra machinery.
215
241
  if (result.contextInjections && result.contextInjections.length > 0) {
216
242
  for (const mcplInj of result.contextInjections) {
243
+ const leaf = INJECT_LEAVES.find((l) => l.position === mcplInj.position);
244
+ if (!leaf || !CapabilityGrant.of(server).has(leaf.path)) {
245
+ console.error(
246
+ `[mcpl] ${server.id}: dropped beforeInference injection at position ` +
247
+ `"${mcplInj.position}" — not in the effective grant (§10.8)`,
248
+ );
249
+ continue;
250
+ }
217
251
  injections.push(convertMcplInjection(mcplInj));
218
252
  }
219
253
  }
@@ -222,60 +256,4 @@ export class HookOrchestrator {
222
256
  return injections;
223
257
  }
224
258
 
225
- // ==========================================================================
226
- // Private: afterInference fan-out
227
- // ==========================================================================
228
-
229
- private async fanOutAfterInference(
230
- servers: McplServerConnection[],
231
- params: AfterInferenceParams,
232
- ): Promise<AfterInferenceResult | null> {
233
- const blockingPromises: Promise<AfterInferenceResult | null>[] = [];
234
-
235
- for (const server of servers) {
236
- const isBlocking = this.isBlockingAfterInference(server);
237
-
238
- if (isBlocking) {
239
- // Send as request, await response with timeout
240
- const promise = withTimeout(
241
- server.sendAfterInference(params, true) as Promise<AfterInferenceResult>,
242
- AFTER_INFERENCE_BLOCKING_TIMEOUT_MS,
243
- `afterInference (blocking) to "${server.id}"`,
244
- ).catch((): null => {
245
- // Fail-open: timeout or error — treat as no modification
246
- return null;
247
- });
248
- blockingPromises.push(promise);
249
- } else {
250
- // Send as notification (fire-and-forget)
251
- server.sendAfterInference(params, false);
252
- }
253
- }
254
-
255
- if (blockingPromises.length === 0) {
256
- return null;
257
- }
258
-
259
- // Await all blocking responses and return the first valid result
260
- const results = await Promise.all(blockingPromises);
261
- for (const result of results) {
262
- if (result && result.modifiedResponse) {
263
- return result;
264
- }
265
- }
266
-
267
- return null;
268
- }
269
-
270
- /**
271
- * Check if a server's afterInference capability is blocking.
272
- */
273
- private isBlockingAfterInference(server: McplServerConnection): boolean {
274
- const caps = server.capabilities;
275
- if (!caps?.contextHooks?.afterInference) {
276
- return false;
277
- }
278
- const afterCap = caps.contextHooks.afterInference;
279
- return typeof afterCap === 'object' && afterCap !== null && 'blocking' in afterCap && afterCap.blocking === true;
280
- }
281
259
  }
package/src/mcpl/index.ts CHANGED
@@ -47,8 +47,6 @@ export type {
47
47
  BeforeInferenceParams,
48
48
  McplContextInjection,
49
49
  BeforeInferenceResult,
50
- AfterInferenceParams,
51
- AfterInferenceResult,
52
50
 
53
51
  // Server-initiated inference
54
52
  McplMessage,
@@ -59,6 +59,23 @@ export class ProseStreamRouter {
59
59
  return this.accumulated;
60
60
  }
61
61
 
62
+ /**
63
+ * Drop everything routed so far and return to the constructed state.
64
+ *
65
+ * For a membrane refusal retry (RetryingEvent): the abandoned attempt's
66
+ * prose must not survive into the new one, and `accumulated` feeds
67
+ * outgoing/complete — leaving it would finalize text the model never
68
+ * actually said in the attempt that stands.
69
+ */
70
+ reset(): void {
71
+ this.target = this.opts.initialTarget;
72
+ this.state = this.opts.mode === 'locus' ? 'body' : 'line-start';
73
+ this.held = '';
74
+ this.token = '';
75
+ this.sawBang = false;
76
+ this.accumulated = new Map();
77
+ }
78
+
62
79
  feed(delta: string): RoutedDelta[] {
63
80
  const out: RoutedDelta[] = [];
64
81
  for (const ch of delta) this.step(ch, out);
@@ -16,6 +16,7 @@ import type {
16
16
  } from './types.js';
17
17
  import type { FeatureSetManager } from './feature-set-manager.js';
18
18
  import { McplFeatureSetError } from './feature-set-manager.js';
19
+ import { expandCoreTags } from './tags.js';
19
20
 
20
21
  // ============================================================================
21
22
  // McplPushEvent (the ProcessEvent shape pushed to the queue)
@@ -165,7 +166,18 @@ export class PushHandler {
165
166
  params: PushEventParams,
166
167
  responder?: Responder,
167
168
  ): void {
168
- // 1. Validate feature set
169
+ // §16.3: expand the normative chat:* core closure once, at entry, so
170
+ // every downstream consumer (wake matching, metadata, the queued event)
171
+ // sees the closed set. Producer `implies` edges are NOT consumed —
172
+ // advisory pending acceptance (§16.4). Tags were admitted before this
173
+ // point and grant nothing (§16.6).
174
+ if (params.tags) params.tags = expandCoreTags(params.tags);
175
+ // 1. Validate feature set. §6.6: rejection is diagnostics, not
176
+ // authorization, and MUST be a JSON-RPC error object — not a result
177
+ // carrying a failure flag. (The old `{accepted:false, reason}` result
178
+ // was AUDIT-001's finding: the error factories existed and were never
179
+ // invoked.) Falls back to the result shape only for a legacy responder
180
+ // with no error path.
169
181
  try {
170
182
  this.featureSetManager.validateInbound(serverId, params.featureSet);
171
183
  } catch (err) {
@@ -175,7 +187,11 @@ export class PushHandler {
175
187
  // Loud rejection — a rejected push event is an agent that silently
176
188
  // never hears the message. (2026-07-09 diagnosability pass.)
177
189
  console.error(`[push-event-rejected] server=${serverId} eventId=${params.eventId} reason=${reason}`);
178
- responder?.respond({ accepted: false, reason });
190
+ if (err instanceof McplFeatureSetError && responder?.respondError) {
191
+ responder.respondError(err.code, reason, { featureSet: err.featureSet });
192
+ } else {
193
+ responder?.respond({ accepted: false, reason });
194
+ }
179
195
  return;
180
196
  }
181
197