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
@@ -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
+ }
@@ -32,6 +32,22 @@ function scopeIdFromDirectoryName(name) {
32
32
  }
33
33
  }
34
34
 
35
+ function canonicalStateValue(value) {
36
+ if (Array.isArray(value)) return value.map(canonicalStateValue);
37
+ if (value && typeof value === 'object') {
38
+ return Object.fromEntries(
39
+ Object.keys(value)
40
+ .sort()
41
+ .map((key) => [key, canonicalStateValue(value[key])]),
42
+ );
43
+ }
44
+ return value;
45
+ }
46
+
47
+ function mcpClientStateKey(connectionId, eventName, args = {}) {
48
+ return `${String(connectionId)}::${String(eventName)}::${JSON.stringify(canonicalStateValue(args ?? {}))}`;
49
+ }
50
+
35
51
  async function readJsonLines(filePath) {
36
52
  try {
37
53
  const raw = await readFile(filePath, 'utf8');
@@ -263,6 +279,7 @@ export class PersistentEventStore {
263
279
  const record = {
264
280
  sequence: Number(raw.sequence),
265
281
  serverId: String(raw.serverId),
282
+ subscriptionId: raw.subscriptionId ? String(raw.subscriptionId) : null,
266
283
  event: McpEventOccurrenceSchema.parse(raw.event),
267
284
  };
268
285
  if (!Number.isInteger(record.sequence) || record.sequence < 1) {
@@ -270,7 +287,7 @@ export class PersistentEventStore {
270
287
  }
271
288
  this.#mcpOccurrences.push(record);
272
289
  this.#mcpOccurrenceKeys.add(
273
- `${record.serverId}:${record.event.eventId}`,
290
+ `${record.serverId}:${record.subscriptionId ?? ''}:${record.event.eventId}`,
274
291
  );
275
292
  }
276
293
  this.#mcpOccurrences.sort((a, b) => a.sequence - b.sequence);
@@ -282,15 +299,27 @@ export class PersistentEventStore {
282
299
  connectionId: String(raw.connectionId),
283
300
  serverId: String(raw.serverId || raw.connectionId),
284
301
  eventName: String(raw.eventName),
302
+ arguments:
303
+ raw.arguments && !Array.isArray(raw.arguments) && typeof raw.arguments === 'object'
304
+ ? raw.arguments
305
+ : {},
306
+ subscriptionId: raw.subscriptionId ? String(raw.subscriptionId) : null,
307
+ deliveryMode: raw.deliveryMode ? String(raw.deliveryMode) : 'poll',
285
308
  cursor: raw.cursor === null || raw.cursor === undefined
286
309
  ? null
287
310
  : String(raw.cursor),
288
311
  updatedAt: String(raw.updatedAt || new Date(0).toISOString()),
312
+ ...(raw.nextPollAt ? { nextPollAt: String(raw.nextPollAt) } : {}),
289
313
  ...(raw.lastEventAt ? { lastEventAt: String(raw.lastEventAt) } : {}),
290
314
  ...(raw.lastError ? { lastError: String(raw.lastError) } : {}),
315
+ ...(typeof raw.truncated === 'boolean' ? { truncated: raw.truncated } : {}),
291
316
  };
292
317
  this.#mcpClientStates.set(
293
- `${state.connectionId}::${state.eventName}`,
318
+ mcpClientStateKey(
319
+ state.connectionId,
320
+ state.eventName,
321
+ state.arguments,
322
+ ),
294
323
  state,
295
324
  );
296
325
  }
@@ -776,13 +805,19 @@ export class PersistentEventStore {
776
805
  .filter((record) => !matchId || record.matchId === matchId);
777
806
  }
778
807
 
779
- async appendMcpOccurrence(serverId, eventInput) {
808
+ async appendMcpOccurrence(serverId, eventInput, subscriptionIdInput = null) {
780
809
  return this.#serialized(async () => {
781
810
  const event = McpEventOccurrenceSchema.parse(eventInput);
782
- const key = `${serverId}:${event.eventId}`;
811
+ const subscriptionId =
812
+ subscriptionIdInput === null || subscriptionIdInput === undefined
813
+ ? null
814
+ : String(subscriptionIdInput);
815
+ const key =
816
+ `${serverId}:${subscriptionId ?? ''}:${event.eventId}`;
783
817
  const existing = this.#mcpOccurrences.find(
784
818
  (record) =>
785
819
  record.serverId === serverId &&
820
+ (record.subscriptionId ?? null) === subscriptionId &&
786
821
  record.event.eventId === event.eventId,
787
822
  );
788
823
  if (this.#mcpOccurrenceKeys.has(key)) {
@@ -794,7 +829,12 @@ export class PersistentEventStore {
794
829
  }
795
830
 
796
831
  const sequence = this.latestMcpEventSequence() + 1;
797
- const record = { sequence, serverId, event };
832
+ const record = {
833
+ sequence,
834
+ serverId,
835
+ subscriptionId,
836
+ event,
837
+ };
798
838
  this.#mcpOccurrenceKeys.add(key);
799
839
  this.#mcpOccurrences.push(record);
800
840
  await appendFile(
@@ -816,6 +856,7 @@ export class PersistentEventStore {
816
856
  .map((record) => ({
817
857
  sequence: record.sequence,
818
858
  serverId: record.serverId,
859
+ subscriptionId: record.subscriptionId ?? null,
819
860
  event: {
820
861
  ...record.event,
821
862
  data: { ...record.event.data },
@@ -823,9 +864,9 @@ export class PersistentEventStore {
823
864
  }));
824
865
  }
825
866
 
826
- getMcpClientState(connectionId, eventName) {
867
+ getMcpClientState(connectionId, eventName, args = {}) {
827
868
  return this.#mcpClientStates.get(
828
- `${String(connectionId)}::${String(eventName)}`,
869
+ mcpClientStateKey(connectionId, eventName, args),
829
870
  ) ?? null;
830
871
  }
831
872
 
@@ -843,24 +884,42 @@ export class PersistentEventStore {
843
884
  if (!connectionId || !eventName) {
844
885
  throw new Error('MCP client state requires connectionId and eventName');
845
886
  }
846
- const previous = this.getMcpClientState(connectionId, eventName);
887
+ const args =
888
+ input.arguments && !Array.isArray(input.arguments) && typeof input.arguments === 'object'
889
+ ? input.arguments
890
+ : {};
891
+ const previous = this.getMcpClientState(connectionId, eventName, args);
847
892
  const state = {
848
893
  connectionId,
849
894
  serverId: String(input.serverId || previous?.serverId || connectionId),
850
895
  eventName,
896
+ arguments: args,
897
+ subscriptionId:
898
+ input.subscriptionId
899
+ ? String(input.subscriptionId)
900
+ : previous?.subscriptionId ?? null,
901
+ deliveryMode: String(input.deliveryMode || previous?.deliveryMode || 'poll'),
851
902
  cursor: input.cursor === null || input.cursor === undefined
852
903
  ? null
853
904
  : String(input.cursor),
854
905
  updatedAt: new Date().toISOString(),
906
+ ...(input.nextPollAt || previous?.nextPollAt
907
+ ? { nextPollAt: String(input.nextPollAt || previous.nextPollAt) }
908
+ : {}),
855
909
  ...(input.lastEventAt || previous?.lastEventAt
856
910
  ? { lastEventAt: String(input.lastEventAt || previous.lastEventAt) }
857
911
  : {}),
858
912
  ...(input.lastError
859
913
  ? { lastError: String(input.lastError) }
860
914
  : {}),
915
+ ...(typeof input.truncated === 'boolean'
916
+ ? { truncated: input.truncated }
917
+ : typeof previous?.truncated === 'boolean'
918
+ ? { truncated: previous.truncated }
919
+ : {}),
861
920
  };
862
921
  this.#mcpClientStates.set(
863
- `${connectionId}::${eventName}`,
922
+ mcpClientStateKey(connectionId, eventName, args),
864
923
  state,
865
924
  );
866
925
  await appendFile(
@@ -5,6 +5,7 @@ import {
5
5
  import {
6
6
  DerivedContractRegistry,
7
7
  } from './derived-contract-registry.mjs';
8
+ import { assertJsonSchemaValue } from './json-schema.mjs';
8
9
 
9
10
  function actorIdentity(actor) {
10
11
  if (!actor || typeof actor !== 'object') {
@@ -66,6 +67,67 @@ function scalarSchemaForConstant(value) {
66
67
  return {};
67
68
  }
68
69
 
70
+ function walkPatternValue(value, visitor) {
71
+ if (!value || typeof value !== 'object') return;
72
+ if (value.kind === 'field') {
73
+ visitor(value.ref, value.path, 'pattern value');
74
+ return;
75
+ }
76
+ if (value.kind === 'arithmetic') {
77
+ for (const arg of value.args ?? []) walkPatternValue(arg, visitor);
78
+ }
79
+ }
80
+
81
+ function walkPatternNode(node, visitor) {
82
+ if (!node || typeof node !== 'object') return;
83
+
84
+ if (node.kind === 'compare') {
85
+ walkPatternValue(node.left, visitor);
86
+ if (node.right) walkPatternValue(node.right, visitor);
87
+ }
88
+ if (
89
+ node.kind === 'aggregate' ||
90
+ node.kind === 'state' ||
91
+ node.kind === 'distinct'
92
+ ) {
93
+ if (node.path) visitor(node.ref, node.path, `pattern ${node.kind}`);
94
+ }
95
+ if (node.kind === 'semantic') {
96
+ for (const input of node.input ?? []) {
97
+ const [ref, ...parts] = String(input).split('.');
98
+ if (ref && parts.length) {
99
+ visitor(ref, parts.join('.'), 'pattern semantic input');
100
+ }
101
+ }
102
+ }
103
+
104
+ if (Array.isArray(node.children)) {
105
+ for (const child of node.children) walkPatternNode(child, visitor);
106
+ }
107
+ if (node.child) walkPatternNode(node.child, visitor);
108
+ if (node.forbidden) walkPatternNode(node.forbidden, visitor);
109
+ }
110
+
111
+ function assertSubscriptionArguments(source, args, purpose) {
112
+ const inputSchema = source?.inputSchema;
113
+ if (
114
+ !inputSchema ||
115
+ typeof inputSchema !== 'object' ||
116
+ Object.keys(inputSchema).length === 0
117
+ ) {
118
+ return;
119
+ }
120
+ try {
121
+ assertJsonSchemaValue(inputSchema, args ?? {});
122
+ } catch (error) {
123
+ const wrapped = new Error(
124
+ `${purpose} arguments are invalid for ${source.serverId}/${source.eventName}: ${error instanceof Error ? error.message : String(error)}`,
125
+ );
126
+ wrapped.code = 'TRIGGER_SOURCE_ARGUMENTS_INVALID';
127
+ throw wrapped;
128
+ }
129
+ }
130
+
69
131
  function assertAdvertisedPath(source, path, purpose) {
70
132
  const payloadSchema = source?.payloadSchema;
71
133
  if (
@@ -695,6 +757,11 @@ export class TriggerControlPlane {
695
757
 
696
758
  for (const clause of definition.clauses) {
697
759
  const source = resolveSource(clause);
760
+ assertSubscriptionArguments(
761
+ source,
762
+ clause.arguments ?? {},
763
+ `Trigger clause ${clause.id}`,
764
+ );
698
765
  for (const predicate of clause.where ?? []) {
699
766
  assertAdvertisedPath(
700
767
  source,
@@ -716,6 +783,59 @@ export class TriggerControlPlane {
716
783
  );
717
784
  }
718
785
 
786
+ if (definition.patternV2) {
787
+ const clauseForRef = (ref) =>
788
+ definition.clauses.find((candidate) => candidate.id === ref);
789
+ const sourceForRef = (ref) => {
790
+ const clause = clauseForRef(ref);
791
+ return clause ? resolveSource(clause) : null;
792
+ };
793
+
794
+ for (const dimension of definition.patternV2.partitionBy ?? []) {
795
+ for (const field of dimension.fields ?? []) {
796
+ assertAdvertisedPath(
797
+ sourceForRef(field.ref),
798
+ field.path,
799
+ `Pattern partition ${dimension.key}`,
800
+ );
801
+ }
802
+ }
803
+
804
+ walkPatternNode(definition.patternV2.root, (ref, fieldPath, purpose) => {
805
+ assertAdvertisedPath(
806
+ sourceForRef(ref),
807
+ fieldPath,
808
+ purpose,
809
+ );
810
+ });
811
+ }
812
+
813
+ for (const measure of definition.derivedEvent?.measures ?? []) {
814
+ const expression = measure.expression;
815
+ const clauseForRef = (ref) =>
816
+ definition.clauses.find((candidate) => candidate.id === ref);
817
+ const sourceForRef = (ref) => {
818
+ const clause = clauseForRef(ref);
819
+ return clause ? resolveSource(clause) : null;
820
+ };
821
+
822
+ if (expression.kind === 'aggregate' && expression.path) {
823
+ assertAdvertisedPath(
824
+ sourceForRef(expression.ref),
825
+ expression.path,
826
+ `Derived measure ${measure.key}`,
827
+ );
828
+ } else {
829
+ walkPatternValue(expression, (ref, fieldPath) => {
830
+ assertAdvertisedPath(
831
+ sourceForRef(ref),
832
+ fieldPath,
833
+ `Derived measure ${measure.key}`,
834
+ );
835
+ });
836
+ }
837
+ }
838
+
719
839
  for (const projection of definition.derivedEvent?.projections ?? []) {
720
840
  const clause = definition.clauses.find(
721
841
  (candidate) => candidate.id === projection.ref,
@@ -780,6 +900,65 @@ export class TriggerControlPlane {
780
900
  }
781
901
 
782
902
  const allSources = this.store.listEventSources();
903
+ const sourceSchemaForRefPath = (ref, fieldPath) => {
904
+ const clause = definition.clauses.find(
905
+ (candidate) => candidate.id === ref,
906
+ );
907
+ const source = clause
908
+ ? allSources.find(
909
+ (candidate) =>
910
+ candidate.eventName === clause.event &&
911
+ candidate.serverId === clause.serverId &&
912
+ (
913
+ !clause.contractVersion ||
914
+ String(candidate.metadata?.contractVersion || '') ===
915
+ String(clause.contractVersion)
916
+ ),
917
+ )
918
+ : null;
919
+ return getSchemaNode(source?.payloadSchema, fieldPath) ?? {};
920
+ };
921
+
922
+ const schemaForMeasureExpression = (expression) => {
923
+ if (!expression || typeof expression !== 'object') return {};
924
+ if (expression.kind === 'literal') {
925
+ if (expression.value === null) return { type: 'null' };
926
+ return scalarSchemaForConstant(expression.value);
927
+ }
928
+ if (expression.kind === 'field') {
929
+ return sourceSchemaForRefPath(expression.ref, expression.path);
930
+ }
931
+ if (
932
+ expression.kind === 'occurredAt' ||
933
+ expression.kind === 'arithmetic'
934
+ ) {
935
+ return { type: 'number' };
936
+ }
937
+ if (expression.kind === 'aggregate') {
938
+ if (
939
+ ['count', 'countDistinct'].includes(expression.function)
940
+ ) {
941
+ return { type: 'integer' };
942
+ }
943
+ if (
944
+ ['sum', 'avg', 'min', 'max', 'stddev', 'percentile']
945
+ .includes(expression.function)
946
+ ) {
947
+ return { type: 'number' };
948
+ }
949
+ return sourceSchemaForRefPath(
950
+ expression.ref,
951
+ expression.path,
952
+ );
953
+ }
954
+ return {};
955
+ };
956
+
957
+ for (const measure of definition.derivedEvent.measures ?? []) {
958
+ properties[measure.key] =
959
+ schemaForMeasureExpression(measure.expression);
960
+ }
961
+
783
962
  for (const projection of definition.derivedEvent.projections ?? []) {
784
963
  const clause = definition.clauses.find(
785
964
  (candidate) => candidate.id === projection.ref,
@@ -806,6 +985,7 @@ export class TriggerControlPlane {
806
985
  required: [
807
986
  '_derived',
808
987
  ...Object.keys(definition.derivedEvent.constants ?? {}),
988
+ ...(definition.derivedEvent.measures ?? []).map((measure) => measure.key),
809
989
  ],
810
990
  };
811
991
  }
@@ -166,6 +166,18 @@ function explain({
166
166
  };
167
167
  }
168
168
 
169
+ if (definition.patternV2) {
170
+ return {
171
+ code: 'pattern_v2_waiting',
172
+ summary:
173
+ 'Pattern AST v2 has buffered evidence but no new complete match is currently eligible.',
174
+ patternVersion: '2',
175
+ bufferRole: match.patternState?.role ?? null,
176
+ semanticDecisions:
177
+ match.patternState?.semanticDecisions ?? [],
178
+ };
179
+ }
180
+
169
181
  const missing = clauses
170
182
  .filter((clause) => clause.status !== 'satisfied')
171
183
  .map((clause) => clause.clauseId);
@@ -312,6 +324,7 @@ export class TriggerInspector {
312
324
  derivedEvent: definition.derivedEvent ?? null,
313
325
  expression: definition.expression,
314
326
  temporal: definition.temporal ?? [],
327
+ patternV2: definition.patternV2 ?? null,
315
328
  lifecycle: definition.lifecycle ?? {},
316
329
  },
317
330
  lifecycle: state,
@@ -323,6 +336,7 @@ export class TriggerInspector {
323
336
  openedAt: match.openedAt,
324
337
  expiresAt: match.expiresAt,
325
338
  updatedAt: match.updatedAt,
339
+ patternState: match.patternState ?? null,
326
340
  }
327
341
  : null,
328
342
  clauses,
@@ -360,6 +374,7 @@ export class TriggerInspector {
360
374
  rootEvidence: record.rootEvidence,
361
375
  })),
362
376
  matchHistory,
377
+ patternState: match?.patternState ?? null,
363
378
  },
364
379
  why: explain({
365
380
  definition,