mcp-event-intelligence 0.4.0 → 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 (40) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/README.md +102 -14
  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.js +45 -0
  8. package/dist/src/intelligenceProtocol/activationSchemas.d.ts +2 -0
  9. package/dist/src/intelligenceProtocol/activationSchemas.js +1 -0
  10. package/dist/src/intelligenceProtocol/index.d.ts +9 -2
  11. package/dist/src/intelligenceProtocol/index.js +5 -1
  12. package/dist/src/intelligenceProtocol/operatorRegistry.d.ts +567 -0
  13. package/dist/src/intelligenceProtocol/operatorRegistry.js +477 -0
  14. package/dist/src/intelligenceProtocol/patternV2Schemas.d.ts +188 -0
  15. package/dist/src/intelligenceProtocol/patternV2Schemas.js +801 -0
  16. package/dist/src/intelligenceProtocol/schemas.d.ts +4 -0
  17. package/dist/src/intelligenceProtocol/schemas.js +2 -0
  18. package/dist/src/intelligenceProtocol/triggerPlanSchemas.d.ts +177 -4
  19. package/dist/src/intelligenceProtocol/triggerPlanSchemas.js +37 -4
  20. package/dist/src/intelligenceProtocol/triggerSchemas.d.ts +256 -0
  21. package/dist/src/intelligenceProtocol/triggerSchemas.js +161 -2
  22. package/dist/src/mcpEvents/consumer.d.ts +1 -0
  23. package/dist/src/mcpEvents/consumer.js +1 -0
  24. package/dist/src/patternV2/evaluator.d.ts +43 -0
  25. package/dist/src/patternV2/evaluator.js +927 -0
  26. package/dist/src/patternV2/legacyCompiler.d.ts +3 -0
  27. package/dist/src/patternV2/legacyCompiler.js +184 -0
  28. package/package.json +5 -2
  29. package/scripts/host-integration.d.mts +7 -0
  30. package/scripts/host-integration.mjs +9 -0
  31. package/scripts/lib/activation-hydrator.mjs +1 -0
  32. package/scripts/lib/composite-event-consumer.mjs +2 -0
  33. package/scripts/lib/derived-event-coordinator.mjs +75 -1
  34. package/scripts/lib/mcp-events-client.mjs +62 -24
  35. package/scripts/lib/mcp-events-compatibility.mjs +54 -0
  36. package/scripts/lib/trigger-control-plane.mjs +154 -0
  37. package/scripts/lib/trigger-inspector.mjs +15 -0
  38. package/scripts/lib/trigger-planner.mjs +190 -12
  39. package/scripts/mcp-stdio-server.mjs +38 -53
  40. package/server.json +4 -4
@@ -0,0 +1,3 @@
1
+ import type { CompositeTriggerDefinition } from '../intelligenceProtocol/triggerSchemas.js';
2
+ import { type PatternAstV2Definition } from '../intelligenceProtocol/patternV2Schemas.js';
3
+ export declare function compileLegacyTriggerToPatternV2(definition: CompositeTriggerDefinition): PatternAstV2Definition;
@@ -0,0 +1,184 @@
1
+ import { PatternAstV2DefinitionSchema, } from '../intelligenceProtocol/patternV2Schemas.js';
2
+ function eventNode(ref) {
3
+ return { kind: 'event', ref };
4
+ }
5
+ function expressionNode(definition) {
6
+ const expression = definition.expression;
7
+ if (expression.kind === 'allOf') {
8
+ return {
9
+ kind: 'allOf',
10
+ children: expression.refs.map(eventNode),
11
+ };
12
+ }
13
+ if (expression.kind === 'anyOf') {
14
+ return {
15
+ kind: 'anyOf',
16
+ children: expression.refs.map(eventNode),
17
+ };
18
+ }
19
+ if (expression.kind === 'sequence') {
20
+ return {
21
+ kind: 'sequence',
22
+ children: expression.refs.map(eventNode),
23
+ contiguity: 'followedBy',
24
+ };
25
+ }
26
+ return {
27
+ kind: 'repeat',
28
+ child: eventNode(expression.ref),
29
+ min: expression.atLeast,
30
+ mode: 'greedy',
31
+ };
32
+ }
33
+ function wrapTemporal(root, condition) {
34
+ if (condition.kind === 'calendar') {
35
+ return {
36
+ kind: 'calendar',
37
+ child: root,
38
+ ref: condition.ref,
39
+ timezone: condition.timezone,
40
+ ...(condition.before ? { before: condition.before } : {}),
41
+ ...(condition.after ? { after: condition.after } : {}),
42
+ ...(condition.weekdays ? { weekdays: condition.weekdays } : {}),
43
+ ...(condition.dates ? { dates: condition.dates } : {}),
44
+ ...(condition.dateRange ? { dateRange: condition.dateRange } : {}),
45
+ ...(condition.dayOfMonth ? { dayOfMonth: condition.dayOfMonth } : {}),
46
+ };
47
+ }
48
+ if (condition.kind === 'absence') {
49
+ return {
50
+ kind: 'absence',
51
+ id: condition.id,
52
+ child: root,
53
+ ref: condition.ref,
54
+ afterRef: condition.afterRef,
55
+ ...(condition.forMs ? { forMs: condition.forMs } : {}),
56
+ ...(condition.untilLocalTime
57
+ ? { untilLocalTime: condition.untilLocalTime }
58
+ : {}),
59
+ ...(condition.timezone ? { timezone: condition.timezone } : {}),
60
+ };
61
+ }
62
+ if (condition.kind === 'not' || condition.kind === 'unless') {
63
+ return {
64
+ kind: 'notPresent',
65
+ child: root,
66
+ ref: condition.ref,
67
+ };
68
+ }
69
+ if (condition.kind === 'after') {
70
+ return {
71
+ kind: 'after',
72
+ child: root,
73
+ ref: condition.ref,
74
+ afterRef: condition.afterRef,
75
+ };
76
+ }
77
+ if (condition.kind === 'until') {
78
+ return {
79
+ kind: 'until',
80
+ child: root,
81
+ ref: condition.ref,
82
+ beforeRef: condition.beforeRef,
83
+ };
84
+ }
85
+ if (condition.kind === 'debounce') {
86
+ return {
87
+ kind: 'debounce',
88
+ id: condition.id,
89
+ child: root,
90
+ ref: condition.ref,
91
+ forMs: condition.forMs,
92
+ };
93
+ }
94
+ if (condition.kind === 'threshold') {
95
+ return {
96
+ kind: 'threshold',
97
+ child: root,
98
+ ref: condition.ref,
99
+ atLeast: condition.atLeast,
100
+ };
101
+ }
102
+ if (condition.kind === 'rate') {
103
+ return {
104
+ kind: 'rate',
105
+ child: root,
106
+ ref: condition.ref,
107
+ atLeast: condition.atLeast,
108
+ perMs: condition.perMs,
109
+ };
110
+ }
111
+ return {
112
+ kind: 'distinct',
113
+ child: root,
114
+ ref: condition.ref,
115
+ path: condition.path,
116
+ atLeast: condition.atLeast,
117
+ };
118
+ }
119
+ export function compileLegacyTriggerToPatternV2(definition) {
120
+ let root = expressionNode(definition);
121
+ const deterministic = definition.correlation?.deterministic;
122
+ if (deterministic?.fields?.length) {
123
+ const [anchorField, ...otherFields] = deterministic.fields;
124
+ if (!anchorField) {
125
+ throw new Error('Deterministic correlation requires at least one field');
126
+ }
127
+ for (const field of otherFields) {
128
+ root = {
129
+ kind: 'compare',
130
+ child: root,
131
+ left: {
132
+ kind: 'field',
133
+ ref: anchorField.ref,
134
+ path: anchorField.path,
135
+ select: 'last',
136
+ },
137
+ op: 'eq',
138
+ right: {
139
+ kind: 'field',
140
+ ref: field.ref,
141
+ path: field.path,
142
+ select: 'last',
143
+ },
144
+ };
145
+ }
146
+ }
147
+ for (const condition of definition.temporal ?? []) {
148
+ root = wrapTemporal(root, condition);
149
+ }
150
+ if (definition.correlation?.semantic) {
151
+ const semantic = definition.correlation.semantic;
152
+ root = {
153
+ kind: 'semantic',
154
+ id: 'legacy-semantic-correlation',
155
+ child: root,
156
+ refs: [...new Set(semantic.input
157
+ .map((path) => path.split('.')[0])
158
+ .filter(Boolean))],
159
+ instruction: semantic.instruction,
160
+ input: semantic.input,
161
+ matchThreshold: semantic.matchThreshold,
162
+ rejectThreshold: semantic.rejectThreshold,
163
+ uncertain: semantic.uncertain,
164
+ execution: {
165
+ cache: true,
166
+ timeoutMs: 5000,
167
+ },
168
+ };
169
+ }
170
+ return PatternAstV2DefinitionSchema.parse({
171
+ version: '2',
172
+ root,
173
+ partitionBy: [],
174
+ selection: {
175
+ overlap: 'disallow',
176
+ afterMatch: 'skipPastLast',
177
+ maxMatchesPerEvent: 10,
178
+ },
179
+ execution: {
180
+ maxCandidates: 512,
181
+ maxSemanticEvaluations: 16,
182
+ },
183
+ });
184
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "mcp-event-intelligence",
3
- "version": "0.4.0",
4
- "description": "Durable temporal event intelligence for agent hosts: host-owned MCP event discovery, composite triggers, derived events, and targeted wakeups.",
3
+ "version": "0.5.0",
4
+ "description": "MCP-native complex event processing for durable agent workflows: event-time patterns, semantic predicates, derived events, and targeted wakeups.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
7
7
  "main": "./dist/src/intelligenceProtocol/index.js",
@@ -61,6 +61,9 @@
61
61
  "model-context-protocol",
62
62
  "ai-agents",
63
63
  "event-driven",
64
+ "complex-event-processing",
65
+ "cep",
66
+ "event-time",
64
67
  "temporal",
65
68
  "durable-execution",
66
69
  "triggers",
@@ -56,6 +56,8 @@ export interface HostMcpConnectionOptions {
56
56
  maxPollBatches?: number;
57
57
  /** Prefer one advertised delivery mode when the host can provide it. */
58
58
  preferredDelivery?: 'poll' | 'push' | 'webhook';
59
+ /** Versioned MCP Events wire profile. Defaults to auto/current experimental snapshot. */
60
+ compatibilityProfile?: 'auto' | 'experimental-extension-2026-09-25' | string;
59
61
  /** Host-owned adapter for events/stream delivery. */
60
62
  openEventStream?: HostMcpEventDeliveryOpener;
61
63
  /** Host-owned adapter for webhook subscription/receiver delivery. */
@@ -113,6 +115,10 @@ export interface EventIntelligenceScopedHost {
113
115
  readonly triggerPlanner: any;
114
116
  readonly activationHydrator: any;
115
117
  readonly eventSources: any[];
118
+ describeTriggerLanguage(input?: {
119
+ category?: 'predicates' | 'composition' | 'temporal' | 'pattern' | 'windows' | 'aggregates' | 'state' | 'selection' | 'semantic' | 'execution' | 'correlation' | 'timing' | 'lifecycle';
120
+ operator?: string;
121
+ }): Record<string, unknown>;
116
122
  planTrigger(input: TriggerPlanInput): Promise<{
117
123
  planVersion: '1';
118
124
  definition: any;
@@ -154,6 +160,7 @@ export function createHostMcpEventsConnection(
154
160
  maxEvents: number;
155
161
  maxPollBatches: number;
156
162
  preferredDelivery: 'poll' | 'push' | 'webhook' | null;
163
+ compatibilityProfile: string;
157
164
  openEventStream: HostMcpEventDeliveryOpener | null;
158
165
  createWebhookSubscription: HostMcpEventDeliveryOpener | null;
159
166
  };
@@ -1,3 +1,6 @@
1
+ import {
2
+ describeTriggerLanguage,
3
+ } from '../dist/src/intelligenceProtocol/index.js';
1
4
  import {
2
5
  createLocalEventIntelligenceRuntime,
3
6
  } from './lib/local-event-intelligence-runtime.mjs';
@@ -79,6 +82,9 @@ function scopedHostView(runtime, context) {
79
82
  planTrigger(input) {
80
83
  return context.triggerPlanner.plan(input);
81
84
  },
85
+ describeTriggerLanguage(input = {}) {
86
+ return describeTriggerLanguage(input);
87
+ },
82
88
  hydrateWake(wakeId) {
83
89
  return context.activationHydrator.hydrateWake(wakeId);
84
90
  },
@@ -240,6 +246,9 @@ export async function createEventIntelligenceHost({
240
246
  planTrigger(input) {
241
247
  return runtime.triggerPlanner.plan(input);
242
248
  },
249
+ describeTriggerLanguage(input = {}) {
250
+ return describeTriggerLanguage(input);
251
+ },
243
252
  hydrateWake(wakeId) {
244
253
  return runtime.activationHydrator.hydrateWake(wakeId);
245
254
  },
@@ -100,6 +100,7 @@ export class ActivationHydrator {
100
100
  eventName: event.eventName,
101
101
  traceId: event.traceId,
102
102
  occurredAt: event.occurredAt,
103
+ ...(event.receivedAt ? { receivedAt: event.receivedAt } : {}),
103
104
  payloadHash: event.payloadHash ?? null,
104
105
  ...(includeData ? { data: event.data } : {}),
105
106
  }));
@@ -35,6 +35,7 @@ export class CompositeEventConsumer {
35
35
  provider,
36
36
  traceId,
37
37
  subscriptionArguments,
38
+ receivedAt,
38
39
  }) {
39
40
  const correlatable = await mcpOccurrenceToCorrelatableEvent(
40
41
  event,
@@ -43,6 +44,7 @@ export class CompositeEventConsumer {
43
44
  ...(provider ? { provider } : {}),
44
45
  ...(serverId ? { serverId } : {}),
45
46
  subscriptionArguments: subscriptionArguments ?? {},
47
+ ...(receivedAt ? { receivedAt } : {}),
46
48
  },
47
49
  );
48
50
 
@@ -2,6 +2,12 @@ import {
2
2
  parseDerivedEventRecord,
3
3
  sha256Hex,
4
4
  } from '../../dist/src/intelligenceProtocol/index.js';
5
+ import {
6
+ evaluateTemporalConditions,
7
+ } from '../../dist/src/composite/temporal.js';
8
+ import {
9
+ evaluatePatternMeasure,
10
+ } from '../../dist/src/patternV2/evaluator.js';
5
11
 
6
12
  export const DERIVED_EVENT_SERVER_ID = 'event-intelligence:derived';
7
13
  export const DERIVED_EVENT_CONNECTION_ID = 'event-intelligence:derived';
@@ -26,6 +32,7 @@ function evidenceRef(source) {
26
32
  sourceEventId: source.sourceEventId,
27
33
  traceId: source.traceId,
28
34
  occurredAt: source.occurredAt,
35
+ ...(source.receivedAt ? { receivedAt: source.receivedAt } : {}),
29
36
  payloadHash: source.payloadHash ?? null,
30
37
  contractVersion:
31
38
  source.data?._derived?.contractVersion ?? null,
@@ -60,6 +67,51 @@ function latestForClause(match, clauseId) {
60
67
  )[0] ?? null;
61
68
  }
62
69
 
70
+ function patternCandidateForMatch(match) {
71
+ const bindings = {};
72
+ for (const event of match.sourceEvents) {
73
+ bindings[event.clauseId] ??= [];
74
+ bindings[event.clauseId].push(event);
75
+ }
76
+ for (const events of Object.values(bindings)) {
77
+ events.sort((a, b) =>
78
+ Date.parse(a.occurredAt) - Date.parse(b.occurredAt) ||
79
+ a.sourceEventId.localeCompare(b.sourceEventId)
80
+ );
81
+ }
82
+ const events = [...match.sourceEvents].sort((a, b) =>
83
+ Date.parse(a.occurredAt) - Date.parse(b.occurredAt) ||
84
+ a.sourceEventId.localeCompare(b.sourceEventId)
85
+ );
86
+ return {
87
+ bindings,
88
+ events,
89
+ startIndex: events.length ? 0 : -1,
90
+ endIndex: events.length ? events.length - 1 : -1,
91
+ semanticDecisions: [],
92
+ };
93
+ }
94
+
95
+ function derivedOccurredAt(match, definition) {
96
+ const candidates = match.sourceEvents
97
+ .map((event) => Date.parse(event.occurredAt))
98
+ .filter(Number.isFinite);
99
+
100
+ const temporal = evaluateTemporalConditions(
101
+ definition,
102
+ match,
103
+ new Date(match.updatedAt),
104
+ );
105
+ for (const state of temporal.conditionStates) {
106
+ if (state.status !== 'satisfied' || !state.dueAt) continue;
107
+ const dueAt = Date.parse(state.dueAt);
108
+ if (Number.isFinite(dueAt)) candidates.push(dueAt);
109
+ }
110
+
111
+ if (!candidates.length) return match.updatedAt;
112
+ return new Date(Math.max(...candidates)).toISOString();
113
+ }
114
+
63
115
  export class DerivedEventCoordinator {
64
116
  constructor({
65
117
  store,
@@ -147,6 +199,26 @@ export class DerivedEventCoordinator {
147
199
  }
148
200
  }
149
201
 
202
+ const measured = {};
203
+ if (spec.measures?.length) {
204
+ const candidate = patternCandidateForMatch(match);
205
+ for (const measure of spec.measures) {
206
+ const value = evaluatePatternMeasure(candidate, measure);
207
+ if (
208
+ value === undefined ||
209
+ (
210
+ value !== null &&
211
+ !['string', 'number', 'boolean'].includes(typeof value)
212
+ )
213
+ ) {
214
+ throw new Error(
215
+ `Derived event measure must resolve to scalar or null: ${measure.key}`,
216
+ );
217
+ }
218
+ measured[measure.key] = value;
219
+ }
220
+ }
221
+
150
222
  const contract = this.store.getDerivedContract(
151
223
  spec.name,
152
224
  spec.contractVersion,
@@ -162,6 +234,7 @@ export class DerivedEventCoordinator {
162
234
  const data = {
163
235
  ...(spec.constants ?? {}),
164
236
  ...projected,
237
+ ...measured,
165
238
  _derived: {
166
239
  triggerId: definition.triggerId,
167
240
  triggerVersion: definition.version,
@@ -176,7 +249,8 @@ export class DerivedEventCoordinator {
176
249
  traceId: `derived:${eventId}`,
177
250
  sourceEventId: eventId,
178
251
  name: spec.name,
179
- occurredAt: match.updatedAt,
252
+ occurredAt: derivedOccurredAt(match, definition),
253
+ receivedAt: this.now().toISOString(),
180
254
  provider: 'event-intelligence',
181
255
  serverId: DERIVED_EVENT_SERVER_ID,
182
256
  payloadHash,
@@ -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
 
@@ -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}`,
@@ -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
  }
@@ -596,12 +608,18 @@ export class McpEventsClientManager {
596
608
  clause.event,
597
609
  args,
598
610
  );
599
- if (!desired.has(subscriptionId)) {
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 {
600
617
  desired.set(subscriptionId, {
601
618
  subscriptionId,
602
619
  eventName: clause.event,
603
620
  arguments: args,
604
621
  descriptor,
622
+ consumerRefs: [consumerRef],
605
623
  });
606
624
  }
607
625
  }
@@ -620,6 +638,7 @@ export class McpEventsClientManager {
620
638
  }
621
639
  assertJsonSchemaValue(descriptor.payloadSchema, occurrence.data);
622
640
 
641
+ const receivedAt = this.now().toISOString();
623
642
  const receipt = await context.store.appendMcpOccurrence(
624
643
  connection.serverId,
625
644
  occurrence,
@@ -634,6 +653,7 @@ export class McpEventsClientManager {
634
653
  traceId:
635
654
  `mcp:${connection.serverId}:${subscription.subscriptionId}:${occurrence.eventId}`,
636
655
  subscriptionArguments: subscription.arguments,
656
+ receivedAt,
637
657
  });
638
658
  return { accepted: true, occurrence };
639
659
  }
@@ -683,6 +703,7 @@ export class McpEventsClientManager {
683
703
  delivery: 'poll',
684
704
  status: 'scheduled',
685
705
  accepted: 0,
706
+ consumerCount: subscription.consumerRefs.length,
686
707
  cursor: current.cursor,
687
708
  nextPollAt: current.nextPollAt,
688
709
  };
@@ -697,7 +718,10 @@ export class McpEventsClientManager {
697
718
  let nextPollMs = connection.pollIntervalMs;
698
719
 
699
720
  do {
700
- const result = await this.rpc(connection, 'events/poll', {
721
+ const profile =
722
+ this.profiles.get(connection.connectionId) ??
723
+ getMcpEventsCompatibilityProfile(connection.compatibilityProfile);
724
+ const result = await this.rpc(connection, profile.pollMethod, {
701
725
  name: subscription.eventName,
702
726
  arguments: subscription.arguments,
703
727
  cursor,
@@ -757,6 +781,7 @@ export class McpEventsClientManager {
757
781
  delivery: 'poll',
758
782
  status: 'ok',
759
783
  accepted,
784
+ consumerCount: subscription.consumerRefs.length,
760
785
  cursor,
761
786
  truncated,
762
787
  hasMore,
@@ -780,6 +805,7 @@ export class McpEventsClientManager {
780
805
  delivery,
781
806
  status: 'active',
782
807
  accepted: existing.accepted,
808
+ consumerCount: subscription.consumerRefs.length,
783
809
  };
784
810
  }
785
811
  if (existing) await this.closeSession(subscription.subscriptionId);
@@ -922,6 +948,7 @@ export class McpEventsClientManager {
922
948
  delivery,
923
949
  status: 'active',
924
950
  accepted,
951
+ consumerCount: subscription.consumerRefs.length,
925
952
  };
926
953
  }
927
954
 
@@ -981,6 +1008,7 @@ export class McpEventsClientManager {
981
1008
  status: 'delivery_not_supported',
982
1009
  delivery: null,
983
1010
  accepted: 0,
1011
+ consumerCount: subscription.consumerRefs.length,
984
1012
  });
985
1013
  continue;
986
1014
  }
@@ -1115,12 +1143,20 @@ export class McpEventsClientManager {
1115
1143
  );
1116
1144
  const store = context?.store ?? this.store;
1117
1145
  const states = store.listMcpClientStates(connection.connectionId);
1146
+ const desired = context
1147
+ ? this.desiredSubscriptions(connection, context)
1148
+ : new Map();
1118
1149
  return {
1119
1150
  connectionId: connection.connectionId,
1120
1151
  scopeId: connection.scopeId,
1121
1152
  serverId: connection.serverId,
1122
1153
  hostManaged: true,
1123
- extensionId: MCP_EVENTS_EXTENSION_ID,
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,
1124
1160
  events: (this.descriptors.get(connection.connectionId) || []).map(
1125
1161
  (event) => ({
1126
1162
  name: event.name,
@@ -1130,6 +1166,8 @@ export class McpEventsClientManager {
1130
1166
  .map((state) => ({
1131
1167
  subscriptionId: state.subscriptionId,
1132
1168
  arguments: state.arguments ?? {},
1169
+ consumerCount:
1170
+ desired.get(state.subscriptionId)?.consumerRefs.length ?? 0,
1133
1171
  delivery: state.deliveryMode,
1134
1172
  cursor: state.cursor,
1135
1173
  nextPollAt: state.nextPollAt ?? null,