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