mcp-event-intelligence 0.4.0 → 0.6.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 (64) hide show
  1. package/CHANGELOG.md +100 -0
  2. package/README.md +107 -26
  3. package/SECURITY.md +4 -4
  4. package/bin/mcp-event-intelligence.mjs +3 -3
  5. package/dist/src/composite/engine.d.ts +14 -4
  6. package/dist/src/composite/engine.js +533 -208
  7. package/dist/src/composite/matching.d.ts +1 -5
  8. package/dist/src/composite/matching.js +45 -54
  9. package/dist/src/composite/runtimeSupport.js +10 -6
  10. package/dist/src/composite/store.d.ts +38 -4
  11. package/dist/src/composite/temporal.d.ts +0 -17
  12. package/dist/src/composite/temporal.js +0 -266
  13. package/dist/src/intelligenceProtocol/activationSchemas.d.ts +38 -3
  14. package/dist/src/intelligenceProtocol/activationSchemas.js +5 -3
  15. package/dist/src/intelligenceProtocol/index.d.ts +12 -9
  16. package/dist/src/intelligenceProtocol/index.js +6 -5
  17. package/dist/src/intelligenceProtocol/lifecycle.js +2 -10
  18. package/dist/src/intelligenceProtocol/operatorRegistry.d.ts +300 -0
  19. package/dist/src/intelligenceProtocol/operatorRegistry.js +338 -0
  20. package/dist/src/intelligenceProtocol/patternV2Schemas.d.ts +188 -0
  21. package/dist/src/intelligenceProtocol/patternV2Schemas.js +801 -0
  22. package/dist/src/intelligenceProtocol/schemas.d.ts +4 -189
  23. package/dist/src/intelligenceProtocol/schemas.js +3 -97
  24. package/dist/src/intelligenceProtocol/triggerPlanSchemas.d.ts +114 -38
  25. package/dist/src/intelligenceProtocol/triggerPlanSchemas.js +27 -18
  26. package/dist/src/intelligenceProtocol/triggerSchemas.d.ts +268 -213
  27. package/dist/src/intelligenceProtocol/triggerSchemas.js +155 -169
  28. package/dist/src/intelligenceProtocol/version.d.ts +2 -2
  29. package/dist/src/intelligenceProtocol/version.js +2 -2
  30. package/dist/src/mcpEvents/consumer.d.ts +1 -0
  31. package/dist/src/mcpEvents/consumer.js +1 -0
  32. package/dist/src/patternV2/evaluator.d.ts +51 -0
  33. package/dist/src/patternV2/evaluator.js +986 -0
  34. package/dist/src/semantic/types.d.ts +6 -0
  35. package/dist/src/semantic/typesafeJevEvaluator.d.ts +1 -0
  36. package/dist/src/semantic/typesafeJevEvaluator.js +6 -0
  37. package/package.json +9 -3
  38. package/scripts/host-integration.d.mts +54 -4
  39. package/scripts/host-integration.mjs +25 -4
  40. package/scripts/lib/activation-hydrator.mjs +13 -12
  41. package/scripts/lib/composite-event-consumer.mjs +10 -28
  42. package/scripts/lib/composite-wake-coordinator.mjs +19 -19
  43. package/scripts/lib/derived-contract-registry.mjs +4 -4
  44. package/scripts/lib/derived-event-coordinator.mjs +69 -6
  45. package/scripts/lib/local-event-intelligence-runtime.mjs +35 -3
  46. package/scripts/lib/mcp-events-client.mjs +97 -55
  47. package/scripts/lib/mcp-events-compatibility.mjs +54 -0
  48. package/scripts/lib/persistent-event-store.mjs +228 -61
  49. package/scripts/lib/store-capabilities.mjs +88 -0
  50. package/scripts/lib/store-conformance.mjs +127 -0
  51. package/scripts/lib/temporal-deadline-scheduler.mjs +3 -3
  52. package/scripts/lib/trigger-control-plane.mjs +202 -53
  53. package/scripts/lib/trigger-inspector.mjs +50 -147
  54. package/scripts/lib/trigger-planner.mjs +141 -52
  55. package/scripts/lib/wake-retry-scheduler.mjs +2 -19
  56. package/scripts/mcp-stdio-server.mjs +46 -61
  57. package/scripts/service.mjs +19 -138
  58. package/server.json +4 -4
  59. package/dist/src/intelligenceProtocol/invariants.d.ts +0 -3
  60. package/dist/src/intelligenceProtocol/invariants.js +0 -36
  61. package/dist/src/intelligenceProtocol/mapper.d.ts +0 -14
  62. package/dist/src/intelligenceProtocol/mapper.js +0 -28
  63. package/scripts/lib/event-processor.mjs +0 -624
  64. package/scripts/lib/http-wake-deliverer.mjs +0 -31
@@ -2,6 +2,9 @@ import {
2
2
  parseDerivedEventRecord,
3
3
  sha256Hex,
4
4
  } from '../../dist/src/intelligenceProtocol/index.js';
5
+ import {
6
+ evaluatePatternMeasure,
7
+ } from '../../dist/src/patternV2/evaluator.js';
5
8
 
6
9
  export const DERIVED_EVENT_SERVER_ID = 'event-intelligence:derived';
7
10
  export const DERIVED_EVENT_CONNECTION_ID = 'event-intelligence:derived';
@@ -26,6 +29,7 @@ function evidenceRef(source) {
26
29
  sourceEventId: source.sourceEventId,
27
30
  traceId: source.traceId,
28
31
  occurredAt: source.occurredAt,
32
+ ...(source.receivedAt ? { receivedAt: source.receivedAt } : {}),
29
33
  payloadHash: source.payloadHash ?? null,
30
34
  contractVersion:
31
35
  source.data?._derived?.contractVersion ?? null,
@@ -60,6 +64,43 @@ function latestForClause(match, clauseId) {
60
64
  )[0] ?? null;
61
65
  }
62
66
 
67
+ function patternCandidateForMatch(match) {
68
+ const bindings = {};
69
+ for (const event of match.sourceEvents) {
70
+ bindings[event.clauseId] ??= [];
71
+ bindings[event.clauseId].push(event);
72
+ }
73
+ for (const events of Object.values(bindings)) {
74
+ events.sort((a, b) =>
75
+ Date.parse(a.occurredAt) - Date.parse(b.occurredAt) ||
76
+ a.sourceEventId.localeCompare(b.sourceEventId)
77
+ );
78
+ }
79
+ const events = [...match.sourceEvents].sort((a, b) =>
80
+ Date.parse(a.occurredAt) - Date.parse(b.occurredAt) ||
81
+ a.sourceEventId.localeCompare(b.sourceEventId)
82
+ );
83
+ return {
84
+ bindings,
85
+ events,
86
+ startIndex: events.length ? 0 : -1,
87
+ endIndex: events.length ? events.length - 1 : -1,
88
+ semanticDecisions: [],
89
+ };
90
+ }
91
+
92
+ function derivedOccurredAt(match) {
93
+ if (match.patternState?.completedAt) {
94
+ return match.patternState.completedAt;
95
+ }
96
+
97
+ const candidates = match.sourceEvents
98
+ .map((event) => Date.parse(event.occurredAt))
99
+ .filter(Number.isFinite);
100
+ if (!candidates.length) return match.updatedAt;
101
+ return new Date(Math.max(...candidates)).toISOString();
102
+ }
103
+
63
104
  export class DerivedEventCoordinator {
64
105
  constructor({
65
106
  store,
@@ -89,7 +130,7 @@ export class DerivedEventCoordinator {
89
130
  })
90
131
  ).slice(0, 24)}`;
91
132
 
92
- const existing = this.store.getDerivedEvent(
133
+ const existing = await this.store.getDerivedEvent(
93
134
  eventId,
94
135
  DERIVED_EVENT_SERVER_ID,
95
136
  );
@@ -110,7 +151,7 @@ export class DerivedEventCoordinator {
110
151
  const rootEvidence = [];
111
152
  for (const source of match.sourceEvents) {
112
153
  if (source.serverId === DERIVED_EVENT_SERVER_ID) {
113
- const parent = this.store.getDerivedEvent(
154
+ const parent = await this.store.getDerivedEvent(
114
155
  source.sourceEventId,
115
156
  DERIVED_EVENT_SERVER_ID,
116
157
  );
@@ -147,7 +188,27 @@ export class DerivedEventCoordinator {
147
188
  }
148
189
  }
149
190
 
150
- const contract = this.store.getDerivedContract(
191
+ const measured = {};
192
+ if (spec.measures?.length) {
193
+ const candidate = patternCandidateForMatch(match);
194
+ for (const measure of spec.measures) {
195
+ const value = evaluatePatternMeasure(candidate, measure);
196
+ if (
197
+ value === undefined ||
198
+ (
199
+ value !== null &&
200
+ !['string', 'number', 'boolean'].includes(typeof value)
201
+ )
202
+ ) {
203
+ throw new Error(
204
+ `Derived event measure must resolve to scalar or null: ${measure.key}`,
205
+ );
206
+ }
207
+ measured[measure.key] = value;
208
+ }
209
+ }
210
+
211
+ const contract = await this.store.getDerivedContract(
151
212
  spec.name,
152
213
  spec.contractVersion,
153
214
  );
@@ -162,11 +223,12 @@ export class DerivedEventCoordinator {
162
223
  const data = {
163
224
  ...(spec.constants ?? {}),
164
225
  ...projected,
226
+ ...measured,
165
227
  _derived: {
166
228
  triggerId: definition.triggerId,
167
229
  triggerVersion: definition.version,
168
230
  matchId: match.matchId,
169
- correlationKey: match.correlationKey,
231
+ partitionKey: match.partitionKey,
170
232
  contractVersion: spec.contractVersion,
171
233
  schemaFingerprint: contract.schemaFingerprint,
172
234
  },
@@ -176,7 +238,8 @@ export class DerivedEventCoordinator {
176
238
  traceId: `derived:${eventId}`,
177
239
  sourceEventId: eventId,
178
240
  name: spec.name,
179
- occurredAt: match.updatedAt,
241
+ occurredAt: derivedOccurredAt(match),
242
+ receivedAt: this.now().toISOString(),
180
243
  provider: 'event-intelligence',
181
244
  serverId: DERIVED_EVENT_SERVER_ID,
182
245
  payloadHash,
@@ -222,7 +285,7 @@ export class DerivedEventCoordinator {
222
285
  await sha256Hex({
223
286
  kind,
224
287
  sourceEventId: record.event.sourceEventId,
225
- sequence: this.store.auditLength(),
288
+ sequence: await this.store.auditLength(),
226
289
  })
227
290
  ).slice(0, 24)}`,
228
291
  traceId: record.event.traceId,
@@ -26,6 +26,10 @@ import {
26
26
  PersistentEventStore,
27
27
  normalizeEventScopeId,
28
28
  } from './persistent-event-store.mjs';
29
+ import {
30
+ assertSharedStoreCapabilities,
31
+ describeStoreCapabilities,
32
+ } from './store-capabilities.mjs';
29
33
  import {
30
34
  TriggerInspector,
31
35
  } from './trigger-inspector.mjs';
@@ -114,6 +118,23 @@ export async function createLocalEventIntelligenceRuntime({
114
118
  const dataDir = env.DATA_DIR ?? './data';
115
119
  const rootStore = providedStore ?? new PersistentEventStore(dataDir);
116
120
  const restored = await initializeStore(rootStore);
121
+ const storeCapabilities = describeStoreCapabilities(rootStore);
122
+ const requireSharedStore =
123
+ env.EVENT_INTELLIGENCE_REQUIRE_SHARED_STORE === 'true';
124
+ if (requireSharedStore) {
125
+ assertSharedStoreCapabilities(rootStore);
126
+ if (!String(env.EVENT_INTELLIGENCE_WORKER_ID || '').trim()) {
127
+ const error = new Error(
128
+ 'EVENT_INTELLIGENCE_WORKER_ID is required in shared-store mode',
129
+ );
130
+ error.code = 'EVENT_INTELLIGENCE_WORKER_ID_REQUIRED';
131
+ throw error;
132
+ }
133
+ }
134
+ const rootWorkerId = String(
135
+ env.EVENT_INTELLIGENCE_WORKER_ID ||
136
+ `ei-${process.pid}`,
137
+ ).trim();
117
138
 
118
139
  const evaluator =
119
140
  semanticEvaluator !== undefined
@@ -161,7 +182,17 @@ export async function createLocalEventIntelligenceRuntime({
161
182
  assertStoreContract(store);
162
183
  }
163
184
 
164
- const triggerEngine = new CompositeTriggerEngine(store, evaluator);
185
+ const triggerEngine = new CompositeTriggerEngine(
186
+ store,
187
+ evaluator,
188
+ undefined,
189
+ {
190
+ workerId: `${rootWorkerId}:${scopeId}`,
191
+ partitionLeaseMs: Number(
192
+ env.EVENT_INTELLIGENCE_PARTITION_LEASE_MS ?? 300000,
193
+ ),
194
+ },
195
+ );
165
196
  const triggerPlanner = new TriggerPlanner({ store });
166
197
  const activationHydrator = new ActivationHydrator({ store });
167
198
  const wakeCoordinators = new Map();
@@ -195,7 +226,7 @@ export async function createLocalEventIntelligenceRuntime({
195
226
  packetBuilder,
196
227
  ...deliveryOptions,
197
228
  deliverer: async (packet) => {
198
- const activation = activationHydrator.hydrateWake(packet.wake_id);
229
+ const activation = await activationHydrator.hydrateWake(packet.wake_id);
199
230
  const result = await handler(packet, activation);
200
231
  if (typeof result === 'string' && result) {
201
232
  return { runtimeReceiptId: result };
@@ -224,7 +255,7 @@ export async function createLocalEventIntelligenceRuntime({
224
255
  packetBuilder,
225
256
  ...deliveryOptions,
226
257
  deliverer: async (packet) => {
227
- const activation = activationHydrator.hydrateWake(packet.wake_id);
258
+ const activation = await activationHydrator.hydrateWake(packet.wake_id);
228
259
  const result = await wake(packet, activation);
229
260
  if (typeof result === 'string' && result) {
230
261
  return { runtimeReceiptId: result };
@@ -330,6 +361,7 @@ export async function createLocalEventIntelligenceRuntime({
330
361
 
331
362
  return {
332
363
  restored,
364
+ storeCapabilities,
333
365
  store: defaultContext.store,
334
366
  evaluator,
335
367
  triggerEngine: defaultContext.triggerEngine,
@@ -8,10 +8,14 @@ import {
8
8
  normalizeEventScopeId,
9
9
  } from './persistent-event-store.mjs';
10
10
  import { assertJsonSchemaValue } from './json-schema.mjs';
11
+ import {
12
+ MCP_EVENTS_EXTENSION_ID,
13
+ getMcpEventsCompatibilityProfile,
14
+ resolveMcpEventsCompatibilityProfile,
15
+ } from './mcp-events-compatibility.mjs';
11
16
 
12
17
  export { assertJsonSchemaValue } from './json-schema.mjs';
13
-
14
- export const MCP_EVENTS_EXTENSION_ID = 'io.modelcontextprotocol/events';
18
+ export { MCP_EVENTS_EXTENSION_ID } from './mcp-events-compatibility.mjs';
15
19
  const UnknownResultSchema = z.unknown();
16
20
 
17
21
  function assertObject(value, message) {
@@ -116,6 +120,10 @@ function normalizeConnection(connection) {
116
120
  'maxPollBatches',
117
121
  ),
118
122
  preferredDelivery,
123
+ compatibilityProfile:
124
+ connection.compatibilityProfile === undefined
125
+ ? 'auto'
126
+ : String(connection.compatibilityProfile),
119
127
  openEventStream:
120
128
  typeof connection.openEventStream === 'function'
121
129
  ? connection.openEventStream
@@ -148,6 +156,7 @@ export function createHostMcpEventsConnection({
148
156
  maxPollBatches = 4,
149
157
  scopeId = DEFAULT_EVENT_SCOPE_ID,
150
158
  preferredDelivery,
159
+ compatibilityProfile = 'auto',
151
160
  openEventStream,
152
161
  createWebhookSubscription,
153
162
  } = {}) {
@@ -185,12 +194,13 @@ export function createHostMcpEventsConnection({
185
194
  maxPollBatches,
186
195
  scopeId,
187
196
  preferredDelivery,
197
+ compatibilityProfile,
188
198
  openEventStream,
189
199
  createWebhookSubscription,
190
200
  });
191
201
  }
192
202
 
193
- function sourceDescriptor(raw) {
203
+ function sourceDescriptor(raw, profile) {
194
204
  const descriptor = assertObject(raw, 'Invalid MCP event descriptor');
195
205
  const name = String(descriptor.name || '').trim();
196
206
  if (!name) throw new Error('MCP event descriptor requires name');
@@ -199,7 +209,7 @@ function sourceDescriptor(raw) {
199
209
  : [];
200
210
  if (
201
211
  delivery.length === 0 ||
202
- delivery.some((mode) => !['poll', 'push', 'webhook'].includes(mode))
212
+ delivery.some((mode) => !profile.deliveryModes.includes(mode))
203
213
  ) {
204
214
  throw new Error(`MCP event descriptor ${name} has invalid delivery modes`);
205
215
  }
@@ -284,6 +294,7 @@ export class McpEventsClientManager {
284
294
  this.connections = new Map();
285
295
  this.now = now;
286
296
  this.descriptors = new Map();
297
+ this.profiles = new Map();
287
298
  this.timers = new Map();
288
299
  this.lastErrors = new Map();
289
300
  this.scopeContexts = new Map();
@@ -364,6 +375,7 @@ export class McpEventsClientManager {
364
375
  if (timer) clearTimeout(timer);
365
376
  this.timers.delete(connectionId);
366
377
  this.descriptors.delete(connectionId);
378
+ this.profiles.delete(connectionId);
367
379
  this.lastErrors.delete(connectionId);
368
380
  this.pollInFlight.delete(connectionId);
369
381
 
@@ -382,7 +394,7 @@ export class McpEventsClientManager {
382
394
  }
383
395
  const removed = this.connections.delete(connectionId);
384
396
  if (removed && context) {
385
- const sources = context.store.listEventSources({
397
+ const sources = await context.store.listEventSources({
386
398
  connectionIds: [connectionId],
387
399
  });
388
400
  for (const source of sources) {
@@ -426,20 +438,16 @@ export class McpEventsClientManager {
426
438
 
427
439
  const context = await this.scopeContext(connection);
428
440
  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
- ) {
441
+ const profile = resolveMcpEventsCompatibilityProfile({
442
+ capabilities,
443
+ requested: connection.compatibilityProfile,
444
+ });
445
+ if (!profile) {
446
+ const expected = getMcpEventsCompatibilityProfile(
447
+ connection.compatibilityProfile,
448
+ );
441
449
  const error = new Error(
442
- `MCP server ${connectionId} does not advertise ${MCP_EVENTS_EXTENSION_ID}`,
450
+ `MCP server ${connectionId} does not advertise the ${expected.id} Events profile`,
443
451
  );
444
452
  error.code = 'MCP_EVENTS_CAPABILITY_UNAVAILABLE';
445
453
  throw error;
@@ -451,7 +459,7 @@ export class McpEventsClientManager {
451
459
  do {
452
460
  const listed = await this.rpc(
453
461
  connection,
454
- 'events/list',
462
+ profile.listMethod,
455
463
  cursor ? { cursor } : {},
456
464
  );
457
465
  if (!Array.isArray(listed?.events)) {
@@ -460,7 +468,7 @@ export class McpEventsClientManager {
460
468
  );
461
469
  }
462
470
  for (const raw of listed.events) {
463
- const descriptor = sourceDescriptor(raw);
471
+ const descriptor = sourceDescriptor(raw, profile);
464
472
  if (names.has(descriptor.name)) {
465
473
  throw new Error(
466
474
  `MCP server ${connectionId} returned duplicate event descriptor ${descriptor.name}`,
@@ -478,9 +486,9 @@ export class McpEventsClientManager {
478
486
  for (const normalized of descriptors) {
479
487
  const sourceId =
480
488
  `mcp:${connection.connectionId}:${normalized.name}`;
481
- const existing = context.store
482
- .listEventSources()
483
- .find((source) => source.sourceId === sourceId);
489
+ const existing = (
490
+ await context.store.listEventSources()
491
+ ).find((source) => source.sourceId === sourceId);
484
492
  const same =
485
493
  existing &&
486
494
  existing.enabled !== false &&
@@ -492,7 +500,9 @@ export class McpEventsClientManager {
492
500
  JSON.stringify(existing.inputSchema || {}) ===
493
501
  JSON.stringify(normalized.inputSchema) &&
494
502
  JSON.stringify(existing.payloadSchema || {}) ===
495
- JSON.stringify(normalized.payloadSchema);
503
+ JSON.stringify(normalized.payloadSchema) &&
504
+ existing.metadata?.compatibilityProfile === profile.id &&
505
+ existing.metadata?.extensionId === profile.extensionId;
496
506
 
497
507
  if (!same) {
498
508
  const register = context.triggerControl?.registerEventSource
@@ -514,7 +524,8 @@ export class McpEventsClientManager {
514
524
  metadata: {
515
525
  transport: 'host-managed-mcp',
516
526
  hostManaged: true,
517
- extensionId: MCP_EVENTS_EXTENSION_ID,
527
+ extensionId: profile.extensionId,
528
+ compatibilityProfile: profile.id,
518
529
  ...(normalized._meta ? { descriptorMeta: normalized._meta } : {}),
519
530
  },
520
531
  },
@@ -527,6 +538,7 @@ export class McpEventsClientManager {
527
538
  }
528
539
 
529
540
  this.descriptors.set(connection.connectionId, descriptors);
541
+ this.profiles.set(connection.connectionId, profile);
530
542
  this.lastErrors.delete(connection.connectionId);
531
543
  return descriptors;
532
544
  }
@@ -559,25 +571,26 @@ export class McpEventsClientManager {
559
571
  return results;
560
572
  }
561
573
 
562
- desiredSubscriptions(connection, context) {
574
+ async desiredSubscriptions(connection, context) {
563
575
  const descriptors = this.descriptors.get(connection.connectionId) || [];
564
576
  const byName = new Map(descriptors.map((item) => [item.name, item]));
565
577
  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
- : [];
578
+ let triggers = [];
579
+ if (typeof context.triggerControl?.listTriggers === 'function') {
580
+ triggers = await context.triggerControl.listTriggers();
581
+ } else if (typeof context.store?.listTriggers === 'function') {
582
+ const definitions = await context.store.listTriggers();
583
+ triggers = await Promise.all(definitions.map(async (definition) => ({
584
+ definition,
585
+ state:
586
+ typeof context.store.getTriggerState === 'function'
587
+ ? await context.store.getTriggerState(
588
+ definition.triggerId,
589
+ definition.version,
590
+ )
591
+ : null,
592
+ })));
593
+ }
581
594
 
582
595
  for (const { definition, state } of triggers) {
583
596
  if (state?.status !== 'active') continue;
@@ -596,12 +609,18 @@ export class McpEventsClientManager {
596
609
  clause.event,
597
610
  args,
598
611
  );
599
- if (!desired.has(subscriptionId)) {
612
+ const consumerRef =
613
+ `${definition.triggerId}@${definition.version}:${clause.id}`;
614
+ const existing = desired.get(subscriptionId);
615
+ if (existing) {
616
+ existing.consumerRefs.push(consumerRef);
617
+ } else {
600
618
  desired.set(subscriptionId, {
601
619
  subscriptionId,
602
620
  eventName: clause.event,
603
621
  arguments: args,
604
622
  descriptor,
623
+ consumerRefs: [consumerRef],
605
624
  });
606
625
  }
607
626
  }
@@ -620,6 +639,7 @@ export class McpEventsClientManager {
620
639
  }
621
640
  assertJsonSchemaValue(descriptor.payloadSchema, occurrence.data);
622
641
 
642
+ const receivedAt = this.now().toISOString();
623
643
  const receipt = await context.store.appendMcpOccurrence(
624
644
  connection.serverId,
625
645
  occurrence,
@@ -634,6 +654,7 @@ export class McpEventsClientManager {
634
654
  traceId:
635
655
  `mcp:${connection.serverId}:${subscription.subscriptionId}:${occurrence.eventId}`,
636
656
  subscriptionArguments: subscription.arguments,
657
+ receivedAt,
637
658
  });
638
659
  return { accepted: true, occurrence };
639
660
  }
@@ -644,7 +665,7 @@ export class McpEventsClientManager {
644
665
  subscription,
645
666
  patch = {},
646
667
  ) {
647
- const previous = context.store.getMcpClientState(
668
+ const previous = await context.store.getMcpClientState(
648
669
  connection.connectionId,
649
670
  subscription.eventName,
650
671
  subscription.arguments,
@@ -667,7 +688,7 @@ export class McpEventsClientManager {
667
688
  { respectSchedule = false } = {},
668
689
  ) {
669
690
  const store = context.store;
670
- const current = store.getMcpClientState(
691
+ const current = await store.getMcpClientState(
671
692
  connection.connectionId,
672
693
  subscription.eventName,
673
694
  subscription.arguments,
@@ -683,6 +704,7 @@ export class McpEventsClientManager {
683
704
  delivery: 'poll',
684
705
  status: 'scheduled',
685
706
  accepted: 0,
707
+ consumerCount: subscription.consumerRefs.length,
686
708
  cursor: current.cursor,
687
709
  nextPollAt: current.nextPollAt,
688
710
  };
@@ -697,7 +719,10 @@ export class McpEventsClientManager {
697
719
  let nextPollMs = connection.pollIntervalMs;
698
720
 
699
721
  do {
700
- const result = await this.rpc(connection, 'events/poll', {
722
+ const profile =
723
+ this.profiles.get(connection.connectionId) ??
724
+ getMcpEventsCompatibilityProfile(connection.compatibilityProfile);
725
+ const result = await this.rpc(connection, profile.pollMethod, {
701
726
  name: subscription.eventName,
702
727
  arguments: subscription.arguments,
703
728
  cursor,
@@ -757,6 +782,7 @@ export class McpEventsClientManager {
757
782
  delivery: 'poll',
758
783
  status: 'ok',
759
784
  accepted,
785
+ consumerCount: subscription.consumerRefs.length,
760
786
  cursor,
761
787
  truncated,
762
788
  hasMore,
@@ -780,11 +806,12 @@ export class McpEventsClientManager {
780
806
  delivery,
781
807
  status: 'active',
782
808
  accepted: existing.accepted,
809
+ consumerCount: subscription.consumerRefs.length,
783
810
  };
784
811
  }
785
812
  if (existing) await this.closeSession(subscription.subscriptionId);
786
813
 
787
- const current = context.store.getMcpClientState(
814
+ const current = await context.store.getMcpClientState(
788
815
  connection.connectionId,
789
816
  subscription.eventName,
790
817
  subscription.arguments,
@@ -922,6 +949,7 @@ export class McpEventsClientManager {
922
949
  delivery,
923
950
  status: 'active',
924
951
  accepted,
952
+ consumerCount: subscription.consumerRefs.length,
925
953
  };
926
954
  }
927
955
 
@@ -968,7 +996,7 @@ export class McpEventsClientManager {
968
996
  descriptors = await this.discoverConnection(connectionId);
969
997
  }
970
998
 
971
- const desired = this.desiredSubscriptions(connection, context);
999
+ const desired = await this.desiredSubscriptions(connection, context);
972
1000
  const desiredIds = new Set(desired.keys());
973
1001
  const results = [];
974
1002
 
@@ -981,6 +1009,7 @@ export class McpEventsClientManager {
981
1009
  status: 'delivery_not_supported',
982
1010
  delivery: null,
983
1011
  accepted: 0,
1012
+ consumerCount: subscription.consumerRefs.length,
984
1013
  });
985
1014
  continue;
986
1015
  }
@@ -1034,12 +1063,12 @@ export class McpEventsClientManager {
1034
1063
  return results;
1035
1064
  }
1036
1065
 
1037
- nextDelay(connection) {
1066
+ async nextDelay(connection) {
1038
1067
  const context = this.scopeContexts.get(
1039
1068
  `${connection.connectionId}::${connection.scopeId}`,
1040
1069
  );
1041
1070
  if (!context) return connection.pollIntervalMs;
1042
- const states = context.store.listMcpClientStates(
1071
+ const states = await context.store.listMcpClientStates(
1043
1072
  connection.connectionId,
1044
1073
  );
1045
1074
  const now = this.now().getTime();
@@ -1073,7 +1102,10 @@ export class McpEventsClientManager {
1073
1102
  this.started &&
1074
1103
  this.connections.get(connection.connectionId) === connection
1075
1104
  ) {
1076
- this.scheduleConnection(connection, this.nextDelay(connection));
1105
+ this.scheduleConnection(
1106
+ connection,
1107
+ await this.nextDelay(connection),
1108
+ );
1077
1109
  }
1078
1110
  }
1079
1111
  }, Math.max(0, delay));
@@ -1108,19 +1140,27 @@ export class McpEventsClientManager {
1108
1140
  await this.drain();
1109
1141
  }
1110
1142
 
1111
- status() {
1112
- return [...this.connections.values()].map((connection) => {
1143
+ async status() {
1144
+ return Promise.all([...this.connections.values()].map(async (connection) => {
1113
1145
  const context = this.scopeContexts.get(
1114
1146
  `${connection.connectionId}::${connection.scopeId}`,
1115
1147
  );
1116
1148
  const store = context?.store ?? this.store;
1117
- const states = store.listMcpClientStates(connection.connectionId);
1149
+ const states = await store.listMcpClientStates(connection.connectionId);
1150
+ const desired = context
1151
+ ? await this.desiredSubscriptions(connection, context)
1152
+ : new Map();
1118
1153
  return {
1119
1154
  connectionId: connection.connectionId,
1120
1155
  scopeId: connection.scopeId,
1121
1156
  serverId: connection.serverId,
1122
1157
  hostManaged: true,
1123
- extensionId: MCP_EVENTS_EXTENSION_ID,
1158
+ extensionId:
1159
+ this.profiles.get(connection.connectionId)?.extensionId ??
1160
+ MCP_EVENTS_EXTENSION_ID,
1161
+ compatibilityProfile:
1162
+ this.profiles.get(connection.connectionId)?.id ??
1163
+ connection.compatibilityProfile,
1124
1164
  events: (this.descriptors.get(connection.connectionId) || []).map(
1125
1165
  (event) => ({
1126
1166
  name: event.name,
@@ -1130,6 +1170,8 @@ export class McpEventsClientManager {
1130
1170
  .map((state) => ({
1131
1171
  subscriptionId: state.subscriptionId,
1132
1172
  arguments: state.arguments ?? {},
1173
+ consumerCount:
1174
+ desired.get(state.subscriptionId)?.consumerRefs.length ?? 0,
1133
1175
  delivery: state.deliveryMode,
1134
1176
  cursor: state.cursor,
1135
1177
  nextPollAt: state.nextPollAt ?? null,
@@ -1139,6 +1181,6 @@ export class McpEventsClientManager {
1139
1181
  ),
1140
1182
  error: this.lastErrors.get(connection.connectionId) ?? null,
1141
1183
  };
1142
- });
1184
+ }));
1143
1185
  }
1144
1186
  }
@@ -0,0 +1,54 @@
1
+ export const MCP_EVENTS_EXTENSION_ID = 'io.modelcontextprotocol/events';
2
+
3
+ export const MCP_EVENTS_COMPATIBILITY_PROFILES = Object.freeze({
4
+ 'experimental-extension-2026-09-25': Object.freeze({
5
+ id: 'experimental-extension-2026-09-25',
6
+ capabilityStyle: 'extension',
7
+ extensionId: MCP_EVENTS_EXTENSION_ID,
8
+ listMethod: 'events/list',
9
+ pollMethod: 'events/poll',
10
+ deliveryModes: Object.freeze(['poll', 'push', 'webhook']),
11
+ experimental: true,
12
+ }),
13
+ });
14
+
15
+ export const DEFAULT_MCP_EVENTS_COMPATIBILITY_PROFILE =
16
+ 'experimental-extension-2026-09-25';
17
+
18
+ export function getMcpEventsCompatibilityProfile(profileId) {
19
+ const id =
20
+ !profileId || profileId === 'auto'
21
+ ? DEFAULT_MCP_EVENTS_COMPATIBILITY_PROFILE
22
+ : String(profileId);
23
+ const profile = MCP_EVENTS_COMPATIBILITY_PROFILES[id];
24
+ if (!profile) {
25
+ const error = new Error(`Unknown MCP Events compatibility profile: ${id}`);
26
+ error.code = 'MCP_EVENTS_PROFILE_UNKNOWN';
27
+ throw error;
28
+ }
29
+ return profile;
30
+ }
31
+
32
+ export function profileAdvertisedByCapabilities(profile, capabilities = {}) {
33
+ if (profile.capabilityStyle !== 'extension') return false;
34
+ const extensions =
35
+ capabilities?.extensions &&
36
+ !Array.isArray(capabilities.extensions) &&
37
+ typeof capabilities.extensions === 'object'
38
+ ? capabilities.extensions
39
+ : {};
40
+ return Object.prototype.hasOwnProperty.call(
41
+ extensions,
42
+ profile.extensionId,
43
+ );
44
+ }
45
+
46
+ export function resolveMcpEventsCompatibilityProfile({
47
+ capabilities,
48
+ requested = 'auto',
49
+ } = {}) {
50
+ const profile = getMcpEventsCompatibilityProfile(requested);
51
+ return profileAdvertisedByCapabilities(profile, capabilities)
52
+ ? profile
53
+ : null;
54
+ }