mcp-event-intelligence 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/README.md +26 -14
  3. package/SECURITY.md +11 -7
  4. package/bin/mcp-event-intelligence.mjs +2 -2
  5. package/dist/src/composite/matching.d.ts +1 -1
  6. package/dist/src/composite/matching.js +21 -1
  7. package/dist/src/intelligenceProtocol/schemas.d.ts +2 -0
  8. package/dist/src/intelligenceProtocol/schemas.js +1 -0
  9. package/dist/src/intelligenceProtocol/triggerPlanSchemas.d.ts +2 -0
  10. package/dist/src/intelligenceProtocol/triggerPlanSchemas.js +1 -0
  11. package/dist/src/intelligenceProtocol/triggerSchemas.d.ts +6 -0
  12. package/dist/src/intelligenceProtocol/triggerSchemas.js +3 -0
  13. package/dist/src/mcpEvents/consumer.d.ts +1 -0
  14. package/dist/src/mcpEvents/consumer.js +1 -0
  15. package/dist/src/mcpEvents/erpnextProvider.d.ts +0 -2
  16. package/dist/src/mcpEvents/erpnextProvider.js +0 -3
  17. package/dist/src/mcpEvents/provider.d.ts +24 -10
  18. package/dist/src/mcpEvents/provider.js +124 -55
  19. package/dist/src/mcpEvents/server.d.ts +3 -1
  20. package/dist/src/mcpEvents/server.js +20 -19
  21. package/dist/src/protocol/types.d.ts +1 -0
  22. package/package.json +1 -1
  23. package/scripts/host-integration.d.mts +40 -0
  24. package/scripts/lib/composite-event-consumer.mjs +2 -0
  25. package/scripts/lib/json-schema.mjs +134 -0
  26. package/scripts/lib/local-event-intelligence-runtime.mjs +1 -0
  27. package/scripts/lib/mcp-events-client.mjs +760 -253
  28. package/scripts/lib/persistent-event-store.mjs +68 -9
  29. package/scripts/lib/trigger-control-plane.mjs +26 -0
  30. package/scripts/lib/trigger-planner.mjs +23 -0
  31. package/scripts/mcp-stdio-server.mjs +2 -1
  32. package/server.json +2 -2
@@ -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,26 @@ function scalarSchemaForConstant(value) {
66
67
  return {};
67
68
  }
68
69
 
70
+ function assertSubscriptionArguments(source, args, purpose) {
71
+ const inputSchema = source?.inputSchema;
72
+ if (
73
+ !inputSchema ||
74
+ typeof inputSchema !== 'object' ||
75
+ Object.keys(inputSchema).length === 0
76
+ ) {
77
+ return;
78
+ }
79
+ try {
80
+ assertJsonSchemaValue(inputSchema, args ?? {});
81
+ } catch (error) {
82
+ const wrapped = new Error(
83
+ `${purpose} arguments are invalid for ${source.serverId}/${source.eventName}: ${error instanceof Error ? error.message : String(error)}`,
84
+ );
85
+ wrapped.code = 'TRIGGER_SOURCE_ARGUMENTS_INVALID';
86
+ throw wrapped;
87
+ }
88
+ }
89
+
69
90
  function assertAdvertisedPath(source, path, purpose) {
70
91
  const payloadSchema = source?.payloadSchema;
71
92
  if (
@@ -695,6 +716,11 @@ export class TriggerControlPlane {
695
716
 
696
717
  for (const clause of definition.clauses) {
697
718
  const source = resolveSource(clause);
719
+ assertSubscriptionArguments(
720
+ source,
721
+ clause.arguments ?? {},
722
+ `Trigger clause ${clause.id}`,
723
+ );
698
724
  for (const predicate of clause.where ?? []) {
699
725
  assertAdvertisedPath(
700
726
  source,
@@ -4,6 +4,7 @@ import {
4
4
  parseTriggerPlanInput,
5
5
  sha256Hex,
6
6
  } from '../../dist/src/intelligenceProtocol/index.js';
7
+ import { assertJsonSchemaValue } from './json-schema.mjs';
7
8
 
8
9
  function schemaNodeAtPath(schema, path) {
9
10
  if (!schema || typeof schema !== 'object') return null;
@@ -25,9 +26,28 @@ function sourceSummary(source) {
25
26
  serverId: source.serverId,
26
27
  eventName: source.eventName,
27
28
  description: source.description ?? null,
29
+ delivery: source.delivery ?? [],
30
+ inputSchema: source.inputSchema ?? {},
31
+ payloadSchema: source.payloadSchema ?? {},
28
32
  };
29
33
  }
30
34
 
35
+ function validateArgumentsAgainstSource(source, args) {
36
+ const schema = source.inputSchema;
37
+ if (!schema || typeof schema !== 'object' || Object.keys(schema).length === 0) {
38
+ return;
39
+ }
40
+ try {
41
+ assertJsonSchemaValue(schema, args ?? {});
42
+ } catch (error) {
43
+ throw sourceError(
44
+ 'TRIGGER_PLAN_ARGUMENTS_INVALID',
45
+ `Subscription arguments do not match ${source.serverId}/${source.eventName}: ${error instanceof Error ? error.message : String(error)}`,
46
+ [source],
47
+ );
48
+ }
49
+ }
50
+
31
51
  function sourceError(code, message, sources = []) {
32
52
  const error = new Error(message);
33
53
  error.code = code;
@@ -149,6 +169,7 @@ export class TriggerPlanner {
149
169
  }
150
170
 
151
171
  const source = matches[0];
172
+ validateArgumentsAgainstSource(source, event.arguments);
152
173
  for (const predicate of event.where) {
153
174
  validatePredicateAgainstSource(source, predicate, warnings);
154
175
  }
@@ -158,6 +179,7 @@ export class TriggerPlanner {
158
179
  id,
159
180
  event: event.event,
160
181
  serverId: source.serverId,
182
+ arguments: event.arguments,
161
183
  where: event.where,
162
184
  };
163
185
  });
@@ -230,6 +252,7 @@ export class TriggerPlanner {
230
252
  id: clause.id,
231
253
  event: clause.event,
232
254
  serverId: clause.serverId,
255
+ arguments: clause.arguments,
233
256
  where: clause.where,
234
257
  })),
235
258
  withinMs: definition.withinMs,
@@ -71,6 +71,7 @@ const triggerPlanInputSchema = z.object({
71
71
  id: z.string().min(1).optional(),
72
72
  event: z.string().min(1),
73
73
  serverId: z.string().min(1).optional(),
74
+ arguments: z.record(z.string(), z.unknown()).optional(),
74
75
  where: z.array(predicateInputSchema).optional(),
75
76
  })).min(1),
76
77
  match: z.union([
@@ -451,7 +452,7 @@ export async function buildEventIntelligenceMcpServer({
451
452
  const server = new McpServer(
452
453
  {
453
454
  name: 'mcp-event-intelligence',
454
- version: '0.3.0',
455
+ version: '0.3.1',
455
456
  },
456
457
  {
457
458
  instructions:
package/server.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "name": "io.github.sarooo17/event-intelligence",
4
4
  "title": "MCP Event Intelligence",
5
5
  "description": "Durable temporal event intelligence: composite triggers, derived events, and runtime wakeups.",
6
- "version": "0.3.0",
6
+ "version": "0.4.0",
7
7
  "repository": {
8
8
  "url": "https://github.com/sarooo17/event-intelligence",
9
9
  "source": "github"
@@ -13,7 +13,7 @@
13
13
  "registryType": "npm",
14
14
  "registryBaseUrl": "https://registry.npmjs.org",
15
15
  "identifier": "mcp-event-intelligence",
16
- "version": "0.3.0",
16
+ "version": "0.4.0",
17
17
  "transport": {
18
18
  "type": "stdio"
19
19
  },