mcp-event-intelligence 0.3.1 → 0.4.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 (31) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +25 -13
  3. package/bin/mcp-event-intelligence.mjs +2 -2
  4. package/dist/src/composite/matching.d.ts +1 -1
  5. package/dist/src/composite/matching.js +21 -1
  6. package/dist/src/intelligenceProtocol/schemas.d.ts +2 -0
  7. package/dist/src/intelligenceProtocol/schemas.js +1 -0
  8. package/dist/src/intelligenceProtocol/triggerPlanSchemas.d.ts +2 -0
  9. package/dist/src/intelligenceProtocol/triggerPlanSchemas.js +1 -0
  10. package/dist/src/intelligenceProtocol/triggerSchemas.d.ts +6 -0
  11. package/dist/src/intelligenceProtocol/triggerSchemas.js +3 -0
  12. package/dist/src/mcpEvents/consumer.d.ts +1 -0
  13. package/dist/src/mcpEvents/consumer.js +1 -0
  14. package/dist/src/mcpEvents/erpnextProvider.d.ts +0 -2
  15. package/dist/src/mcpEvents/erpnextProvider.js +0 -3
  16. package/dist/src/mcpEvents/provider.d.ts +24 -10
  17. package/dist/src/mcpEvents/provider.js +124 -55
  18. package/dist/src/mcpEvents/server.d.ts +3 -1
  19. package/dist/src/mcpEvents/server.js +20 -19
  20. package/dist/src/protocol/types.d.ts +1 -0
  21. package/package.json +1 -1
  22. package/scripts/host-integration.d.mts +40 -0
  23. package/scripts/lib/composite-event-consumer.mjs +2 -0
  24. package/scripts/lib/json-schema.mjs +134 -0
  25. package/scripts/lib/local-event-intelligence-runtime.mjs +1 -0
  26. package/scripts/lib/mcp-events-client.mjs +760 -253
  27. package/scripts/lib/persistent-event-store.mjs +68 -9
  28. package/scripts/lib/trigger-control-plane.mjs +26 -0
  29. package/scripts/lib/trigger-planner.mjs +23 -0
  30. package/scripts/mcp-stdio-server.mjs +1 -0
  31. package/server.json +2 -2
@@ -1,3 +1,4 @@
1
+ import { createHash } from 'node:crypto';
1
2
  import * as z from 'zod/v4';
2
3
  import {
3
4
  McpEventOccurrenceSchema,
@@ -6,8 +7,11 @@ import {
6
7
  DEFAULT_EVENT_SCOPE_ID,
7
8
  normalizeEventScopeId,
8
9
  } from './persistent-event-store.mjs';
10
+ import { assertJsonSchemaValue } from './json-schema.mjs';
9
11
 
10
- const EVENTS_CAPABILITY = 'io.modelcontextprotocol.experimental/events';
12
+ export { assertJsonSchemaValue } from './json-schema.mjs';
13
+
14
+ export const MCP_EVENTS_EXTENSION_ID = 'io.modelcontextprotocol/events';
11
15
  const UnknownResultSchema = z.unknown();
12
16
 
13
17
  function assertObject(value, message) {
@@ -17,6 +21,32 @@ function assertObject(value, message) {
17
21
  return value;
18
22
  }
19
23
 
24
+ function stableValue(value) {
25
+ if (Array.isArray(value)) return value.map(stableValue);
26
+ if (value && typeof value === 'object') {
27
+ return Object.fromEntries(
28
+ Object.keys(value)
29
+ .sort()
30
+ .map((key) => [key, stableValue(value[key])]),
31
+ );
32
+ }
33
+ return value;
34
+ }
35
+
36
+ function stableJson(value) {
37
+ return JSON.stringify(stableValue(value ?? {}));
38
+ }
39
+
40
+ export function eventSubscriptionId(connectionId, eventName, args = {}) {
41
+ const digest = createHash('sha256')
42
+ .update(
43
+ `${String(connectionId)}\u0000${String(eventName)}\u0000${stableJson(args)}`,
44
+ )
45
+ .digest('hex')
46
+ .slice(0, 24);
47
+ return `mcp_sub_${digest}`;
48
+ }
49
+
20
50
  function normalizePositiveInteger(value, fallback, min, max, name) {
21
51
  const normalized = Number(value ?? fallback);
22
52
  if (!Number.isInteger(normalized) || normalized < min || normalized > max) {
@@ -36,18 +66,38 @@ function normalizeConnection(connection) {
36
66
  `Host-managed MCP Events connection ${connectionId} requires request(method, params)`,
37
67
  );
38
68
  }
69
+ if (typeof connection.getCapabilities !== 'function') {
70
+ throw new Error(
71
+ `Host-managed MCP Events connection ${connectionId} requires getCapabilities()`,
72
+ );
73
+ }
74
+
75
+ const preferredDelivery = connection.preferredDelivery
76
+ ? String(connection.preferredDelivery)
77
+ : null;
78
+ if (
79
+ preferredDelivery &&
80
+ !['poll', 'push', 'webhook'].includes(preferredDelivery)
81
+ ) {
82
+ throw new Error(
83
+ `Host-managed MCP Events connection ${connectionId} has invalid preferredDelivery`,
84
+ );
85
+ }
39
86
 
40
87
  return {
41
88
  connectionId,
42
- scopeId: normalizeEventScopeId(connection.scopeId ?? DEFAULT_EVENT_SCOPE_ID),
89
+ scopeId: normalizeEventScopeId(
90
+ connection.scopeId ?? DEFAULT_EVENT_SCOPE_ID,
91
+ ),
43
92
  serverId: String(connection.serverId || `host-mcp:${connectionId}`),
44
93
  request: connection.request,
94
+ getCapabilities: connection.getCapabilities,
45
95
  identity: connection.identity ?? connection.request,
46
96
  enabled: connection.enabled !== false,
47
97
  pollIntervalMs: normalizePositiveInteger(
48
98
  connection.pollIntervalMs,
49
99
  5000,
50
- 1000,
100
+ 250,
51
101
  300000,
52
102
  'pollIntervalMs',
53
103
  ),
@@ -65,120 +115,157 @@ function normalizeConnection(connection) {
65
115
  32,
66
116
  'maxPollBatches',
67
117
  ),
118
+ preferredDelivery,
119
+ openEventStream:
120
+ typeof connection.openEventStream === 'function'
121
+ ? connection.openEventStream
122
+ : null,
123
+ createWebhookSubscription:
124
+ typeof connection.createWebhookSubscription === 'function'
125
+ ? connection.createWebhookSubscription
126
+ : null,
68
127
  };
69
128
  }
70
129
 
71
130
  /**
72
- * Adapt an MCP client that is already connected and authorized by the host.
131
+ * Adapt an MCP client already connected and authorized by the host.
73
132
  *
74
- * Event Intelligence never receives the provider URL, OAuth token, API key,
75
- * or transport. It reuses the host's existing client instance.
133
+ * Poll works with the ordinary MCP request surface. Push and webhook remain
134
+ * host-owned transport concerns: a host can provide openEventStream() and/or
135
+ * createWebhookSubscription() adapters without giving Event Intelligence
136
+ * transport credentials or callback infrastructure.
76
137
  */
77
138
  export function createHostMcpEventsConnection({
78
139
  connectionId,
79
140
  serverId,
80
141
  client,
81
142
  request,
143
+ getCapabilities,
144
+ capabilities,
82
145
  enabled = true,
83
146
  pollIntervalMs = 5000,
84
147
  maxEvents = 100,
85
148
  maxPollBatches = 4,
86
149
  scopeId = DEFAULT_EVENT_SCOPE_ID,
150
+ preferredDelivery,
151
+ openEventStream,
152
+ createWebhookSubscription,
87
153
  } = {}) {
88
154
  const directRequest =
89
155
  typeof request === 'function'
90
156
  ? request
91
157
  : client && typeof client.request === 'function'
92
158
  ? async (method, params) => {
93
- if (
94
- method === 'server/discover' &&
95
- typeof client.getServerCapabilities === 'function'
96
- ) {
97
- const capabilities = client.getServerCapabilities();
98
- if (capabilities) return { capabilities };
99
- }
100
-
101
159
  const message = {
102
160
  method,
103
161
  ...(params === undefined ? {} : { params }),
104
162
  };
105
-
106
- // Events is currently an experimental/custom surface, so the
107
- // official SDK requires an explicit result schema for those calls.
108
- if (method === 'server/discover') {
109
- return client.request(message);
110
- }
111
163
  return client.request(message, UnknownResultSchema);
112
164
  }
113
165
  : null;
114
166
 
167
+ const directCapabilities =
168
+ typeof getCapabilities === 'function'
169
+ ? getCapabilities
170
+ : client && typeof client.getServerCapabilities === 'function'
171
+ ? async () => client.getServerCapabilities()
172
+ : capabilities && typeof capabilities === 'object'
173
+ ? async () => capabilities
174
+ : null;
175
+
115
176
  return normalizeConnection({
116
177
  connectionId,
117
178
  serverId,
118
179
  request: directRequest,
180
+ getCapabilities: directCapabilities,
119
181
  identity: client ?? request ?? directRequest,
120
182
  enabled,
121
183
  pollIntervalMs,
122
184
  maxEvents,
123
185
  maxPollBatches,
124
186
  scopeId,
187
+ preferredDelivery,
188
+ openEventStream,
189
+ createWebhookSubscription,
125
190
  });
126
191
  }
127
192
 
128
- function schemaTypes(schema) {
129
- if (!schema || typeof schema !== 'object') return [];
130
- if (Array.isArray(schema.type)) return schema.type.map(String);
131
- return schema.type ? [String(schema.type)] : [];
193
+ function sourceDescriptor(raw) {
194
+ const descriptor = assertObject(raw, 'Invalid MCP event descriptor');
195
+ const name = String(descriptor.name || '').trim();
196
+ if (!name) throw new Error('MCP event descriptor requires name');
197
+ const delivery = Array.isArray(descriptor.delivery)
198
+ ? descriptor.delivery.map(String)
199
+ : [];
200
+ if (
201
+ delivery.length === 0 ||
202
+ delivery.some((mode) => !['poll', 'push', 'webhook'].includes(mode))
203
+ ) {
204
+ throw new Error(`MCP event descriptor ${name} has invalid delivery modes`);
205
+ }
206
+ return {
207
+ name,
208
+ description: String(descriptor.description || ''),
209
+ delivery,
210
+ inputSchema:
211
+ descriptor.inputSchema &&
212
+ !Array.isArray(descriptor.inputSchema) &&
213
+ typeof descriptor.inputSchema === 'object'
214
+ ? descriptor.inputSchema
215
+ : {},
216
+ payloadSchema:
217
+ descriptor.payloadSchema &&
218
+ !Array.isArray(descriptor.payloadSchema) &&
219
+ typeof descriptor.payloadSchema === 'object'
220
+ ? descriptor.payloadSchema
221
+ : {},
222
+ ...(descriptor._meta &&
223
+ !Array.isArray(descriptor._meta) &&
224
+ typeof descriptor._meta === 'object'
225
+ ? { _meta: descriptor._meta }
226
+ : {}),
227
+ };
132
228
  }
133
229
 
134
- export function assertJsonSchemaValue(schema, value, path = '$') {
135
- if (!schema || typeof schema !== 'object') return;
230
+ function normalizeCursor(cursor, label) {
231
+ if (cursor === null) return null;
232
+ if (typeof cursor === 'string') return cursor;
233
+ throw new Error(`${label} returned invalid cursor; expected string|null`);
234
+ }
136
235
 
137
- if (Array.isArray(schema.enum) && !schema.enum.some((item) => Object.is(item, value))) {
138
- throw new Error(`MCP event payload violates schema at ${path}: value not in enum`);
236
+ function normalizeNextPollMs(value, fallback) {
237
+ const result = Number(value ?? fallback);
238
+ if (!Number.isInteger(result) || result < 0 || result > 300000) {
239
+ throw new Error('events/poll returned invalid nextPollMs');
139
240
  }
241
+ return result;
242
+ }
140
243
 
141
- const types = schemaTypes(schema);
142
- if (types.length) {
143
- const matches = types.some((type) => {
144
- if (type === 'null') return value === null;
145
- if (type === 'array') return Array.isArray(value);
146
- if (type === 'object') return value !== null && !Array.isArray(value) && typeof value === 'object';
147
- if (type === 'integer') return Number.isInteger(value);
148
- if (type === 'number') return typeof value === 'number' && Number.isFinite(value);
149
- return typeof value === type;
150
- });
151
- if (!matches) {
152
- throw new Error(
153
- `MCP event payload violates schema at ${path}: expected ${types.join('|')}`,
154
- );
155
- }
244
+ function supportsMode(connection, mode) {
245
+ if (mode === 'poll') return true;
246
+ if (mode === 'push') return typeof connection.openEventStream === 'function';
247
+ if (mode === 'webhook') {
248
+ return typeof connection.createWebhookSubscription === 'function';
156
249
  }
250
+ return false;
251
+ }
157
252
 
158
- if (value && !Array.isArray(value) && typeof value === 'object') {
159
- const required = Array.isArray(schema.required) ? schema.required.map(String) : [];
160
- for (const key of required) {
161
- if (!(key in value)) {
162
- throw new Error(
163
- `MCP event payload violates schema at ${path}: missing required property ${key}`,
164
- );
165
- }
166
- }
167
- const properties = schema.properties && typeof schema.properties === 'object'
168
- ? schema.properties
169
- : {};
170
- for (const [key, childSchema] of Object.entries(properties)) {
171
- if (key in value) {
172
- assertJsonSchemaValue(childSchema, value[key], `${path}.${key}`);
173
- }
174
- }
253
+ function chooseDelivery(connection, descriptor) {
254
+ const advertised = descriptor.delivery;
255
+ if (
256
+ connection.preferredDelivery &&
257
+ advertised.includes(connection.preferredDelivery) &&
258
+ supportsMode(connection, connection.preferredDelivery)
259
+ ) {
260
+ return connection.preferredDelivery;
175
261
  }
176
262
 
177
- if (Array.isArray(value) && schema.items) {
178
- value.forEach((item, index) =>
179
- assertJsonSchemaValue(schema.items, item, `${path}[${index}]`)
180
- );
263
+ for (const mode of ['push', 'webhook', 'poll']) {
264
+ if (advertised.includes(mode) && supportsMode(connection, mode)) {
265
+ return mode;
266
+ }
181
267
  }
268
+ return null;
182
269
  }
183
270
 
184
271
  export class McpEventsClientManager {
@@ -197,10 +284,11 @@ export class McpEventsClientManager {
197
284
  this.connections = new Map();
198
285
  this.now = now;
199
286
  this.descriptors = new Map();
200
- this.intervals = new Map();
287
+ this.timers = new Map();
201
288
  this.lastErrors = new Map();
202
289
  this.scopeContexts = new Map();
203
290
  this.pollInFlight = new Map();
291
+ this.deliverySessions = new Map();
204
292
  this.started = false;
205
293
 
206
294
  for (const connection of connections) {
@@ -222,11 +310,7 @@ export class McpEventsClientManager {
222
310
  triggerControl: null,
223
311
  };
224
312
 
225
- if (
226
- !context ||
227
- !context.store ||
228
- !context.compositeEventConsumer
229
- ) {
313
+ if (!context || !context.store || !context.compositeEventConsumer) {
230
314
  throw new Error(
231
315
  `Invalid Event Intelligence scope context for ${connection.scopeId}`,
232
316
  );
@@ -255,7 +339,7 @@ export class McpEventsClientManager {
255
339
  events = await this.discoverConnection(connection.connectionId);
256
340
  }
257
341
  if (connection.enabled !== false && this.started) {
258
- this.scheduleConnection(connection);
342
+ this.scheduleConnection(connection, 0);
259
343
  }
260
344
  return {
261
345
  connectionId: connection.connectionId,
@@ -263,34 +347,60 @@ export class McpEventsClientManager {
263
347
  };
264
348
  }
265
349
 
350
+ async closeSession(subscriptionId) {
351
+ const session = this.deliverySessions.get(subscriptionId);
352
+ if (!session) return false;
353
+ this.deliverySessions.delete(subscriptionId);
354
+ try {
355
+ if (typeof session.close === 'function') await session.close();
356
+ } catch {
357
+ // Closing stale delivery sessions is best effort.
358
+ }
359
+ return true;
360
+ }
361
+
266
362
  async detachConnection(connectionId) {
267
- const timer = this.intervals.get(connectionId);
268
- if (timer) clearInterval(timer);
269
- this.intervals.delete(connectionId);
363
+ const timer = this.timers.get(connectionId);
364
+ if (timer) clearTimeout(timer);
365
+ this.timers.delete(connectionId);
270
366
  this.descriptors.delete(connectionId);
271
367
  this.lastErrors.delete(connectionId);
272
368
  this.pollInFlight.delete(connectionId);
273
369
 
370
+ for (const [subscriptionId, session] of this.deliverySessions) {
371
+ if (session.connectionId === connectionId) {
372
+ await this.closeSession(subscriptionId);
373
+ }
374
+ }
375
+
274
376
  const connection = this.connections.get(connectionId);
275
377
  const context = connection ? await this.scopeContext(connection) : null;
276
378
  if (connection) {
277
- this.scopeContexts.delete(`${connection.connectionId}::${connection.scopeId}`);
379
+ this.scopeContexts.delete(
380
+ `${connection.connectionId}::${connection.scopeId}`,
381
+ );
278
382
  }
279
383
  const removed = this.connections.delete(connectionId);
280
384
  if (removed && context) {
281
- const sources = context.store.listEventSources({ connectionIds: [connectionId] });
385
+ const sources = context.store.listEventSources({
386
+ connectionIds: [connectionId],
387
+ });
282
388
  for (const source of sources) {
283
389
  if (source.enabled === false) continue;
284
390
  const register = context.triggerControl?.registerEventSource
285
- ? (input, actor) => context.triggerControl.registerEventSource(input, actor)
286
- : async (input) => this.registerEventSource(input);
287
- await register({
288
- ...source,
289
- enabled: false,
290
- }, {
291
- type: 'system',
292
- principal_id: 'event-intelligence:host-mcp-client',
293
- });
391
+ ? (sourceInput, actor) =>
392
+ context.triggerControl.registerEventSource(sourceInput, actor)
393
+ : async (sourceInput) => this.registerEventSource(sourceInput);
394
+ await register(
395
+ {
396
+ ...source,
397
+ enabled: false,
398
+ },
399
+ {
400
+ type: 'system',
401
+ principal_id: 'event-intelligence:host-mcp-client',
402
+ },
403
+ );
294
404
  }
295
405
  }
296
406
  return removed;
@@ -309,87 +419,110 @@ export class McpEventsClientManager {
309
419
  async discoverConnection(connectionId) {
310
420
  const connection = this.connections.get(connectionId);
311
421
  if (!connection) {
312
- throw new Error(`Unknown host-managed MCP Events connection: ${connectionId}`);
422
+ throw new Error(
423
+ `Unknown host-managed MCP Events connection: ${connectionId}`,
424
+ );
313
425
  }
314
426
 
315
427
  const context = await this.scopeContext(connection);
316
- const discover = await this.rpc(connection, 'server/discover');
317
- const capabilities = discover?.capabilities ?? {};
318
- const experimental = capabilities?.experimental?.[EVENTS_CAPABILITY];
319
- const extension = capabilities?.extensions?.[EVENTS_CAPABILITY];
320
- const standardish = capabilities?.events;
321
- if (!experimental && !extension && !standardish) {
428
+ const capabilities = (await connection.getCapabilities()) ?? {};
429
+ const extensions =
430
+ capabilities.extensions &&
431
+ !Array.isArray(capabilities.extensions) &&
432
+ typeof capabilities.extensions === 'object'
433
+ ? capabilities.extensions
434
+ : {};
435
+ if (
436
+ !Object.prototype.hasOwnProperty.call(
437
+ extensions,
438
+ MCP_EVENTS_EXTENSION_ID,
439
+ )
440
+ ) {
322
441
  const error = new Error(
323
- `MCP server ${connectionId} does not advertise Events capability`,
442
+ `MCP server ${connectionId} does not advertise ${MCP_EVENTS_EXTENSION_ID}`,
324
443
  );
325
444
  error.code = 'MCP_EVENTS_CAPABILITY_UNAVAILABLE';
326
445
  throw error;
327
446
  }
328
447
 
329
- const listed = await this.rpc(connection, 'events/list', {});
330
- if (!Array.isArray(listed?.events)) {
331
- throw new Error(`MCP server ${connectionId} returned invalid events/list`);
332
- }
333
-
334
448
  const descriptors = [];
335
- for (const raw of listed.events) {
336
- const descriptor = assertObject(raw, 'Invalid MCP event descriptor');
337
- const name = String(descriptor.name || '');
338
- if (!name) throw new Error('MCP event descriptor requires name');
339
- const delivery = Array.isArray(descriptor.delivery)
340
- ? descriptor.delivery.map(String)
341
- : ['poll'];
342
- const normalized = {
343
- name,
344
- description: String(descriptor.description || ''),
345
- delivery,
346
- inputSchema:
347
- descriptor.inputSchema && typeof descriptor.inputSchema === 'object'
348
- ? descriptor.inputSchema
349
- : {},
350
- payloadSchema:
351
- descriptor.payloadSchema && typeof descriptor.payloadSchema === 'object'
352
- ? descriptor.payloadSchema
353
- : {},
354
- };
355
- descriptors.push(normalized);
449
+ const names = new Set();
450
+ let cursor = null;
451
+ do {
452
+ const listed = await this.rpc(
453
+ connection,
454
+ 'events/list',
455
+ cursor ? { cursor } : {},
456
+ );
457
+ if (!Array.isArray(listed?.events)) {
458
+ throw new Error(
459
+ `MCP server ${connectionId} returned invalid events/list`,
460
+ );
461
+ }
462
+ for (const raw of listed.events) {
463
+ const descriptor = sourceDescriptor(raw);
464
+ if (names.has(descriptor.name)) {
465
+ throw new Error(
466
+ `MCP server ${connectionId} returned duplicate event descriptor ${descriptor.name}`,
467
+ );
468
+ }
469
+ names.add(descriptor.name);
470
+ descriptors.push(descriptor);
471
+ }
472
+ cursor =
473
+ listed?.nextCursor === undefined || listed?.nextCursor === null
474
+ ? null
475
+ : String(listed.nextCursor);
476
+ } while (cursor);
356
477
 
357
- const sourceId = `mcp:${connection.connectionId}:${name}`;
358
- const existing = context.store.listEventSources()
478
+ for (const normalized of descriptors) {
479
+ const sourceId =
480
+ `mcp:${connection.connectionId}:${normalized.name}`;
481
+ const existing = context.store
482
+ .listEventSources()
359
483
  .find((source) => source.sourceId === sourceId);
360
484
  const same =
361
485
  existing &&
362
486
  existing.enabled !== false &&
363
487
  existing.connectionId === connection.connectionId &&
364
488
  existing.serverId === connection.serverId &&
365
- existing.eventName === name &&
366
- JSON.stringify(existing.delivery) === JSON.stringify(delivery) &&
367
- JSON.stringify(existing.inputSchema || {}) === JSON.stringify(normalized.inputSchema) &&
368
- JSON.stringify(existing.payloadSchema || {}) === JSON.stringify(normalized.payloadSchema);
489
+ existing.eventName === normalized.name &&
490
+ JSON.stringify(existing.delivery) ===
491
+ JSON.stringify(normalized.delivery) &&
492
+ JSON.stringify(existing.inputSchema || {}) ===
493
+ JSON.stringify(normalized.inputSchema) &&
494
+ JSON.stringify(existing.payloadSchema || {}) ===
495
+ JSON.stringify(normalized.payloadSchema);
369
496
 
370
497
  if (!same) {
371
498
  const register = context.triggerControl?.registerEventSource
372
- ? (input, actor) => context.triggerControl.registerEventSource(input, actor)
373
- : async (input) => this.registerEventSource(input);
374
- await register({
375
- sourceId,
376
- connectionId: connection.connectionId,
377
- serverId: connection.serverId,
378
- eventName: name,
379
- description: normalized.description,
380
- delivery,
381
- inputSchema: normalized.inputSchema,
382
- payloadSchema: normalized.payloadSchema,
383
- enabled: true,
384
- experimental: Boolean(experimental || extension),
385
- metadata: {
386
- transport: 'host-managed-mcp',
387
- hostManaged: true,
499
+ ? (sourceInput, actor) =>
500
+ context.triggerControl.registerEventSource(sourceInput, actor)
501
+ : async (sourceInput) => this.registerEventSource(sourceInput);
502
+ await register(
503
+ {
504
+ sourceId,
505
+ connectionId: connection.connectionId,
506
+ serverId: connection.serverId,
507
+ eventName: normalized.name,
508
+ description: normalized.description,
509
+ delivery: normalized.delivery,
510
+ inputSchema: normalized.inputSchema,
511
+ payloadSchema: normalized.payloadSchema,
512
+ enabled: true,
513
+ experimental: true,
514
+ metadata: {
515
+ transport: 'host-managed-mcp',
516
+ hostManaged: true,
517
+ extensionId: MCP_EVENTS_EXTENSION_ID,
518
+ ...(normalized._meta ? { descriptorMeta: normalized._meta } : {}),
519
+ },
388
520
  },
389
- }, {
390
- type: 'system',
391
- principal_id: 'event-intelligence:host-mcp-client',
392
- });
521
+ {
522
+ type: 'system',
523
+ principal_id: 'event-intelligence:host-mcp-client',
524
+ },
525
+ );
393
526
  }
394
527
  }
395
528
 
@@ -413,117 +546,401 @@ export class McpEventsClientManager {
413
546
  this.lastErrors.set(connection.connectionId, message);
414
547
  results.push({
415
548
  connectionId: connection.connectionId,
416
- status: 'error',
417
- error: message,
549
+ status:
550
+ error?.code === 'MCP_EVENTS_CAPABILITY_UNAVAILABLE'
551
+ ? 'ignored_no_events'
552
+ : 'error',
553
+ ...(error?.code === 'MCP_EVENTS_CAPABILITY_UNAVAILABLE'
554
+ ? {}
555
+ : { error: message }),
418
556
  });
419
557
  }
420
558
  }
421
559
  return results;
422
560
  }
423
561
 
424
- async pollSource(connection, descriptor, context) {
425
- if (!descriptor.delivery.includes('poll')) {
426
- return { eventName: descriptor.name, status: 'delivery_not_supported', accepted: 0 };
562
+ desiredSubscriptions(connection, context) {
563
+ const descriptors = this.descriptors.get(connection.connectionId) || [];
564
+ const byName = new Map(descriptors.map((item) => [item.name, item]));
565
+ const desired = new Map();
566
+ const triggers =
567
+ typeof context.triggerControl?.listTriggers === 'function'
568
+ ? context.triggerControl.listTriggers()
569
+ : typeof context.store?.listTriggers === 'function'
570
+ ? context.store.listTriggers().map((definition) => ({
571
+ definition,
572
+ state:
573
+ typeof context.store.getTriggerState === 'function'
574
+ ? context.store.getTriggerState(
575
+ definition.triggerId,
576
+ definition.version,
577
+ )
578
+ : null,
579
+ }))
580
+ : [];
581
+
582
+ for (const { definition, state } of triggers) {
583
+ if (state?.status !== 'active') continue;
584
+ for (const clause of definition.clauses ?? []) {
585
+ if (clause.serverId !== connection.serverId) continue;
586
+ const descriptor = byName.get(clause.event);
587
+ if (!descriptor) continue;
588
+ const args =
589
+ clause.arguments &&
590
+ !Array.isArray(clause.arguments) &&
591
+ typeof clause.arguments === 'object'
592
+ ? clause.arguments
593
+ : {};
594
+ const subscriptionId = eventSubscriptionId(
595
+ connection.connectionId,
596
+ clause.event,
597
+ args,
598
+ );
599
+ if (!desired.has(subscriptionId)) {
600
+ desired.set(subscriptionId, {
601
+ subscriptionId,
602
+ eventName: clause.event,
603
+ arguments: args,
604
+ descriptor,
605
+ });
606
+ }
607
+ }
608
+ }
609
+
610
+ return desired;
611
+ }
612
+
613
+ async ingestOccurrence(connection, context, subscription, raw) {
614
+ const descriptor = subscription.descriptor;
615
+ const occurrence = McpEventOccurrenceSchema.parse(raw);
616
+ if (occurrence.name !== descriptor.name) {
617
+ throw new Error(
618
+ `MCP event name mismatch: expected ${descriptor.name}, got ${occurrence.name}`,
619
+ );
427
620
  }
621
+ assertJsonSchemaValue(descriptor.payloadSchema, occurrence.data);
622
+
623
+ const receipt = await context.store.appendMcpOccurrence(
624
+ connection.serverId,
625
+ occurrence,
626
+ subscription.subscriptionId,
627
+ );
628
+ if (!receipt.accepted) return { accepted: false, occurrence };
629
+
630
+ await context.compositeEventConsumer.ingestMcpOccurrence({
631
+ event: occurrence,
632
+ serverId: connection.serverId,
633
+ provider: 'mcp',
634
+ traceId:
635
+ `mcp:${connection.serverId}:${subscription.subscriptionId}:${occurrence.eventId}`,
636
+ subscriptionArguments: subscription.arguments,
637
+ });
638
+ return { accepted: true, occurrence };
639
+ }
428
640
 
641
+ async saveSubscriptionState(
642
+ connection,
643
+ context,
644
+ subscription,
645
+ patch = {},
646
+ ) {
647
+ const previous = context.store.getMcpClientState(
648
+ connection.connectionId,
649
+ subscription.eventName,
650
+ subscription.arguments,
651
+ );
652
+ return context.store.putMcpClientState({
653
+ ...(previous || {}),
654
+ connectionId: connection.connectionId,
655
+ serverId: connection.serverId,
656
+ eventName: subscription.eventName,
657
+ arguments: subscription.arguments,
658
+ subscriptionId: subscription.subscriptionId,
659
+ ...patch,
660
+ });
661
+ }
662
+
663
+ async pollSubscription(
664
+ connection,
665
+ context,
666
+ subscription,
667
+ { respectSchedule = false } = {},
668
+ ) {
429
669
  const store = context.store;
430
- const consumer = context.compositeEventConsumer;
431
670
  const current = store.getMcpClientState(
432
671
  connection.connectionId,
433
- descriptor.name,
672
+ subscription.eventName,
673
+ subscription.arguments,
434
674
  );
675
+ if (
676
+ respectSchedule &&
677
+ current?.nextPollAt &&
678
+ Date.parse(current.nextPollAt) > this.now().getTime()
679
+ ) {
680
+ return {
681
+ subscriptionId: subscription.subscriptionId,
682
+ eventName: subscription.eventName,
683
+ delivery: 'poll',
684
+ status: 'scheduled',
685
+ accepted: 0,
686
+ cursor: current.cursor,
687
+ nextPollAt: current.nextPollAt,
688
+ };
689
+ }
435
690
 
436
691
  let cursor = current?.cursor ?? null;
437
692
  let lastEventAt = current?.lastEventAt;
438
693
  let accepted = 0;
439
694
  let batches = 0;
440
695
  let hasMore = false;
696
+ let truncated = false;
697
+ let nextPollMs = connection.pollIntervalMs;
441
698
 
442
- try {
443
- do {
444
- const result = await this.rpc(connection, 'events/poll', {
445
- name: descriptor.name,
446
- arguments: {},
447
- cursor,
448
- maxEvents: connection.maxEvents,
449
- });
450
-
451
- if (typeof result?.cursor !== 'string' || !Array.isArray(result.events)) {
452
- throw new Error(
453
- `MCP server ${connection.connectionId} returned invalid events/poll`,
454
- );
455
- }
456
-
457
- for (const raw of result.events) {
458
- const occurrence = McpEventOccurrenceSchema.parse(raw);
459
- if (occurrence.name !== descriptor.name) {
460
- throw new Error(
461
- `MCP event name mismatch: expected ${descriptor.name}, got ${occurrence.name}`,
462
- );
463
- }
464
- assertJsonSchemaValue(descriptor.payloadSchema, occurrence.data);
465
-
466
- const receipt = await store.appendMcpOccurrence(
467
- connection.serverId,
468
- occurrence,
469
- );
470
- if (!receipt.accepted) continue;
699
+ do {
700
+ const result = await this.rpc(connection, 'events/poll', {
701
+ name: subscription.eventName,
702
+ arguments: subscription.arguments,
703
+ cursor,
704
+ maxEvents: connection.maxEvents,
705
+ });
471
706
 
472
- accepted += 1;
473
- lastEventAt = occurrence.timestamp;
474
- await consumer.ingestMcpOccurrence({
475
- event: occurrence,
476
- serverId: connection.serverId,
477
- provider: 'mcp',
478
- traceId: `mcp:${connection.serverId}:${occurrence.eventId}`,
479
- });
480
- }
707
+ if (!Array.isArray(result?.events)) {
708
+ throw new Error(
709
+ `MCP server ${connection.connectionId} returned invalid events/poll`,
710
+ );
711
+ }
712
+ cursor = normalizeCursor(
713
+ result.cursor,
714
+ `MCP server ${connection.connectionId} events/poll`,
715
+ );
716
+ hasMore = result.hasMore === true;
717
+ truncated = truncated || result.truncated === true;
718
+ nextPollMs = normalizeNextPollMs(
719
+ result.nextPollMs,
720
+ connection.pollIntervalMs,
721
+ );
481
722
 
482
- cursor = result.cursor;
483
- hasMore = result.hasMore === true;
484
- batches += 1;
723
+ for (const raw of result.events) {
724
+ const receipt = await this.ingestOccurrence(
725
+ connection,
726
+ context,
727
+ subscription,
728
+ raw,
729
+ );
730
+ if (!receipt.accepted) continue;
731
+ accepted += 1;
732
+ lastEventAt = receipt.occurrence.timestamp;
733
+ }
485
734
 
486
- // Persist the cursor after every page so a crash cannot force the host
487
- // to replay the entire drained batch. Occurrence idempotency remains
488
- // the second line of defense.
489
- await store.putMcpClientState({
490
- connectionId: connection.connectionId,
491
- serverId: connection.serverId,
492
- eventName: descriptor.name,
735
+ batches += 1;
736
+ const nextPollAt = new Date(
737
+ this.now().getTime() + (hasMore ? 0 : nextPollMs),
738
+ ).toISOString();
739
+ await this.saveSubscriptionState(
740
+ connection,
741
+ context,
742
+ subscription,
743
+ {
744
+ deliveryMode: 'poll',
493
745
  cursor,
746
+ truncated,
747
+ nextPollAt,
494
748
  ...(lastEventAt ? { lastEventAt } : {}),
495
- });
496
- } while (hasMore && batches < connection.maxPollBatches);
749
+ },
750
+ );
751
+ } while (hasMore && batches < connection.maxPollBatches);
752
+
753
+ this.lastErrors.delete(connection.connectionId);
754
+ return {
755
+ subscriptionId: subscription.subscriptionId,
756
+ eventName: subscription.eventName,
757
+ delivery: 'poll',
758
+ status: 'ok',
759
+ accepted,
760
+ cursor,
761
+ truncated,
762
+ hasMore,
763
+ batches,
764
+ nextPollMs,
765
+ batchLimitReached: hasMore && batches >= connection.maxPollBatches,
766
+ };
767
+ }
497
768
 
498
- this.lastErrors.delete(connection.connectionId);
769
+ async ensureDeliverySession(
770
+ connection,
771
+ context,
772
+ subscription,
773
+ delivery,
774
+ ) {
775
+ const existing = this.deliverySessions.get(subscription.subscriptionId);
776
+ if (existing?.delivery === delivery) {
499
777
  return {
500
- eventName: descriptor.name,
501
- status: 'ok',
502
- accepted,
503
- cursor,
504
- hasMore,
505
- batches,
506
- batchLimitReached: hasMore && batches >= connection.maxPollBatches,
778
+ subscriptionId: subscription.subscriptionId,
779
+ eventName: subscription.eventName,
780
+ delivery,
781
+ status: 'active',
782
+ accepted: existing.accepted,
507
783
  };
508
- } catch (error) {
509
- const message = error instanceof Error ? error.message : String(error);
510
- this.lastErrors.set(connection.connectionId, message);
511
- await store.putMcpClientState({
512
- connectionId: connection.connectionId,
513
- serverId: connection.serverId,
514
- eventName: descriptor.name,
515
- cursor,
516
- lastError: message,
784
+ }
785
+ if (existing) await this.closeSession(subscription.subscriptionId);
786
+
787
+ const current = context.store.getMcpClientState(
788
+ connection.connectionId,
789
+ subscription.eventName,
790
+ subscription.arguments,
791
+ );
792
+ let accepted = 0;
793
+
794
+ const onEvent = async (raw) => {
795
+ const receipt = await this.ingestOccurrence(
796
+ connection,
797
+ context,
798
+ subscription,
799
+ raw,
800
+ );
801
+ if (receipt.accepted) accepted += 1;
802
+ if (Object.prototype.hasOwnProperty.call(raw ?? {}, 'cursor')) {
803
+ await this.saveSubscriptionState(
804
+ connection,
805
+ context,
806
+ subscription,
807
+ {
808
+ deliveryMode: delivery,
809
+ cursor: normalizeCursor(
810
+ raw.cursor,
811
+ `${delivery} event ${subscription.eventName}`,
812
+ ),
813
+ ...(receipt.accepted
814
+ ? { lastEventAt: receipt.occurrence.timestamp }
815
+ : {}),
816
+ },
817
+ );
818
+ }
819
+ };
820
+
821
+ const onActive = async (update = {}) => {
822
+ const patch = {
823
+ deliveryMode: delivery,
824
+ ...(Object.prototype.hasOwnProperty.call(update, 'cursor')
825
+ ? {
826
+ cursor: normalizeCursor(
827
+ update.cursor,
828
+ `${delivery} active ${subscription.eventName}`,
829
+ ),
830
+ }
831
+ : {}),
832
+ ...(typeof update.truncated === 'boolean'
833
+ ? { truncated: update.truncated }
834
+ : {}),
835
+ };
836
+ await this.saveSubscriptionState(
837
+ connection,
838
+ context,
839
+ subscription,
840
+ patch,
841
+ );
842
+ };
843
+
844
+ const onGap = async (update = {}) => {
845
+ await onActive({ ...update, truncated: true });
846
+ };
847
+
848
+ const onError = (error) => {
849
+ this.lastErrors.set(
850
+ connection.connectionId,
851
+ error instanceof Error ? error.message : String(error),
852
+ );
853
+ };
854
+
855
+ const onTerminated = async (detail) => {
856
+ this.deliverySessions.delete(subscription.subscriptionId);
857
+ if (detail) {
858
+ this.lastErrors.set(
859
+ connection.connectionId,
860
+ typeof detail === 'string' ? detail : JSON.stringify(detail),
861
+ );
862
+ }
863
+ };
864
+
865
+ const open =
866
+ delivery === 'push'
867
+ ? connection.openEventStream
868
+ : connection.createWebhookSubscription;
869
+ const handle = await open({
870
+ subscriptionId: subscription.subscriptionId,
871
+ name: subscription.eventName,
872
+ arguments: subscription.arguments,
873
+ cursor: current?.cursor ?? null,
874
+ onEvent,
875
+ onActive,
876
+ onGap,
877
+ onError,
878
+ onTerminated,
879
+ });
880
+
881
+ const close =
882
+ typeof handle === 'function'
883
+ ? handle
884
+ : typeof handle?.close === 'function'
885
+ ? () => handle.close()
886
+ : typeof handle?.cancel === 'function'
887
+ ? () => handle.cancel()
888
+ : typeof handle?.unsubscribe === 'function'
889
+ ? () => handle.unsubscribe()
890
+ : async () => {};
891
+
892
+ if (handle && Object.prototype.hasOwnProperty.call(handle, 'cursor')) {
893
+ await onActive({
894
+ cursor: handle.cursor,
895
+ truncated: handle.truncated === true,
517
896
  });
518
- throw error;
897
+ } else {
898
+ await this.saveSubscriptionState(
899
+ connection,
900
+ context,
901
+ subscription,
902
+ {
903
+ deliveryMode: delivery,
904
+ cursor: current?.cursor ?? null,
905
+ },
906
+ );
519
907
  }
908
+
909
+ this.deliverySessions.set(subscription.subscriptionId, {
910
+ connectionId: connection.connectionId,
911
+ delivery,
912
+ close,
913
+ get accepted() {
914
+ return accepted;
915
+ },
916
+ });
917
+ this.lastErrors.delete(connection.connectionId);
918
+
919
+ return {
920
+ subscriptionId: subscription.subscriptionId,
921
+ eventName: subscription.eventName,
922
+ delivery,
923
+ status: 'active',
924
+ accepted,
925
+ };
520
926
  }
521
927
 
522
- async pollConnection(connectionId) {
928
+ async reconcileDeliverySessions(connection, desiredIds) {
929
+ for (const [subscriptionId, session] of this.deliverySessions) {
930
+ if (
931
+ session.connectionId === connection.connectionId &&
932
+ !desiredIds.has(subscriptionId)
933
+ ) {
934
+ await this.closeSession(subscriptionId);
935
+ }
936
+ }
937
+ }
938
+
939
+ async pollConnection(connectionId, options = {}) {
523
940
  const existing = this.pollInFlight.get(connectionId);
524
941
  if (existing) return existing;
525
942
 
526
- const task = this.pollConnectionOnce(connectionId);
943
+ const task = this.pollConnectionOnce(connectionId, options);
527
944
  this.pollInFlight.set(connectionId, task);
528
945
  try {
529
946
  return await task;
@@ -534,10 +951,15 @@ export class McpEventsClientManager {
534
951
  }
535
952
  }
536
953
 
537
- async pollConnectionOnce(connectionId) {
954
+ async pollConnectionOnce(
955
+ connectionId,
956
+ { respectSchedule = false } = {},
957
+ ) {
538
958
  const connection = this.connections.get(connectionId);
539
959
  if (!connection) {
540
- throw new Error(`Unknown host-managed MCP Events connection: ${connectionId}`);
960
+ throw new Error(
961
+ `Unknown host-managed MCP Events connection: ${connectionId}`,
962
+ );
541
963
  }
542
964
 
543
965
  const context = await this.scopeContext(connection);
@@ -546,57 +968,133 @@ export class McpEventsClientManager {
546
968
  descriptors = await this.discoverConnection(connectionId);
547
969
  }
548
970
 
971
+ const desired = this.desiredSubscriptions(connection, context);
972
+ const desiredIds = new Set(desired.keys());
549
973
  const results = [];
550
- for (const descriptor of descriptors) {
551
- results.push(await this.pollSource(connection, descriptor, context));
974
+
975
+ for (const subscription of desired.values()) {
976
+ const delivery = chooseDelivery(connection, subscription.descriptor);
977
+ if (!delivery) {
978
+ results.push({
979
+ subscriptionId: subscription.subscriptionId,
980
+ eventName: subscription.eventName,
981
+ status: 'delivery_not_supported',
982
+ delivery: null,
983
+ accepted: 0,
984
+ });
985
+ continue;
986
+ }
987
+
988
+ if (delivery === 'poll') {
989
+ await this.closeSession(subscription.subscriptionId);
990
+ results.push(
991
+ await this.pollSubscription(
992
+ connection,
993
+ context,
994
+ subscription,
995
+ { respectSchedule },
996
+ ),
997
+ );
998
+ } else {
999
+ results.push(
1000
+ await this.ensureDeliverySession(
1001
+ connection,
1002
+ context,
1003
+ subscription,
1004
+ delivery,
1005
+ ),
1006
+ );
1007
+ }
552
1008
  }
1009
+
1010
+ await this.reconcileDeliverySessions(connection, desiredIds);
553
1011
  return results;
554
1012
  }
555
1013
 
556
- async pollAll() {
1014
+ async pollAll(options = {}) {
557
1015
  const results = [];
558
1016
  for (const connection of this.connections.values()) {
559
1017
  try {
560
1018
  results.push({
561
1019
  connectionId: connection.connectionId,
562
- results: await this.pollConnection(connection.connectionId),
1020
+ results: await this.pollConnection(
1021
+ connection.connectionId,
1022
+ options,
1023
+ ),
563
1024
  });
564
1025
  } catch (error) {
1026
+ const message = error instanceof Error ? error.message : String(error);
1027
+ this.lastErrors.set(connection.connectionId, message);
565
1028
  results.push({
566
1029
  connectionId: connection.connectionId,
567
- error: error instanceof Error ? error.message : String(error),
1030
+ error: message,
568
1031
  });
569
1032
  }
570
1033
  }
571
1034
  return results;
572
1035
  }
573
1036
 
574
- scheduleConnection(connection) {
575
- if (this.intervals.has(connection.connectionId)) return;
576
- const timer = setInterval(() => {
577
- this.pollConnection(connection.connectionId).catch((error) => {
578
- console.error(JSON.stringify({
579
- message: 'host_mcp_events_poll_error',
580
- connectionId: connection.connectionId,
581
- error: error instanceof Error ? error.message : String(error),
582
- }));
583
- });
584
- }, connection.pollIntervalMs);
1037
+ nextDelay(connection) {
1038
+ const context = this.scopeContexts.get(
1039
+ `${connection.connectionId}::${connection.scopeId}`,
1040
+ );
1041
+ if (!context) return connection.pollIntervalMs;
1042
+ const states = context.store.listMcpClientStates(
1043
+ connection.connectionId,
1044
+ );
1045
+ const now = this.now().getTime();
1046
+ const due = states
1047
+ .map((state) => Date.parse(state.nextPollAt || ''))
1048
+ .filter(Number.isFinite)
1049
+ .map((time) => Math.max(250, time - now));
1050
+ return due.length
1051
+ ? Math.min(connection.pollIntervalMs, ...due)
1052
+ : connection.pollIntervalMs;
1053
+ }
1054
+
1055
+ scheduleConnection(connection, delay = connection.pollIntervalMs) {
1056
+ const existing = this.timers.get(connection.connectionId);
1057
+ if (existing) clearTimeout(existing);
1058
+ if (!this.started) return;
1059
+
1060
+ const timer = setTimeout(async () => {
1061
+ try {
1062
+ await this.pollConnection(
1063
+ connection.connectionId,
1064
+ { respectSchedule: true },
1065
+ );
1066
+ } catch (error) {
1067
+ this.lastErrors.set(
1068
+ connection.connectionId,
1069
+ error instanceof Error ? error.message : String(error),
1070
+ );
1071
+ } finally {
1072
+ if (
1073
+ this.started &&
1074
+ this.connections.get(connection.connectionId) === connection
1075
+ ) {
1076
+ this.scheduleConnection(connection, this.nextDelay(connection));
1077
+ }
1078
+ }
1079
+ }, Math.max(0, delay));
585
1080
  timer.unref?.();
586
- this.intervals.set(connection.connectionId, timer);
1081
+ this.timers.set(connection.connectionId, timer);
587
1082
  }
588
1083
 
589
1084
  start() {
590
1085
  this.started = true;
591
1086
  for (const connection of this.connections.values()) {
592
- this.scheduleConnection(connection);
1087
+ this.scheduleConnection(connection, 0);
593
1088
  }
594
1089
  }
595
1090
 
596
- stop() {
1091
+ async stop() {
597
1092
  this.started = false;
598
- for (const timer of this.intervals.values()) clearInterval(timer);
599
- this.intervals.clear();
1093
+ for (const timer of this.timers.values()) clearTimeout(timer);
1094
+ this.timers.clear();
1095
+ for (const subscriptionId of [...this.deliverySessions.keys()]) {
1096
+ await this.closeSession(subscriptionId);
1097
+ }
600
1098
  }
601
1099
 
602
1100
  async drain() {
@@ -606,7 +1104,7 @@ export class McpEventsClientManager {
606
1104
  }
607
1105
 
608
1106
  async close() {
609
- this.stop();
1107
+ await this.stop();
610
1108
  await this.drain();
611
1109
  }
612
1110
 
@@ -616,20 +1114,29 @@ export class McpEventsClientManager {
616
1114
  `${connection.connectionId}::${connection.scopeId}`,
617
1115
  );
618
1116
  const store = context?.store ?? this.store;
1117
+ const states = store.listMcpClientStates(connection.connectionId);
619
1118
  return {
620
1119
  connectionId: connection.connectionId,
621
1120
  scopeId: connection.scopeId,
622
1121
  serverId: connection.serverId,
623
1122
  hostManaged: true,
624
- events: (this.descriptors.get(connection.connectionId) || [])
625
- .map((event) => ({
1123
+ extensionId: MCP_EVENTS_EXTENSION_ID,
1124
+ events: (this.descriptors.get(connection.connectionId) || []).map(
1125
+ (event) => ({
626
1126
  name: event.name,
627
1127
  delivery: event.delivery,
628
- cursor: store.getMcpClientState(
629
- connection.connectionId,
630
- event.name,
631
- )?.cursor ?? null,
632
- })),
1128
+ subscriptions: states
1129
+ .filter((state) => state.eventName === event.name)
1130
+ .map((state) => ({
1131
+ subscriptionId: state.subscriptionId,
1132
+ arguments: state.arguments ?? {},
1133
+ delivery: state.deliveryMode,
1134
+ cursor: state.cursor,
1135
+ nextPollAt: state.nextPollAt ?? null,
1136
+ truncated: state.truncated ?? false,
1137
+ })),
1138
+ }),
1139
+ ),
633
1140
  error: this.lastErrors.get(connection.connectionId) ?? null,
634
1141
  };
635
1142
  });