mcp-event-intelligence 0.1.1 → 0.2.1

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 (31) hide show
  1. package/CHANGELOG.md +72 -0
  2. package/README.md +79 -4
  3. package/bin/mcp-event-intelligence.mjs +1 -1
  4. package/dist/src/composite/derivedGraph.d.ts +4 -0
  5. package/dist/src/composite/derivedGraph.js +46 -0
  6. package/dist/src/composite/engine.d.ts +3 -64
  7. package/dist/src/composite/engine.js +26 -343
  8. package/dist/src/composite/matching.d.ts +8 -0
  9. package/dist/src/composite/matching.js +109 -0
  10. package/dist/src/composite/runtimeSupport.d.ts +24 -0
  11. package/dist/src/composite/runtimeSupport.js +176 -0
  12. package/dist/src/composite/store.d.ts +59 -0
  13. package/dist/src/composite/store.js +1 -0
  14. package/dist/src/intelligenceProtocol/schemas.d.ts +9 -7
  15. package/dist/src/intelligenceProtocol/schemas.js +1 -0
  16. package/dist/src/intelligenceProtocol/triggerSchemas.d.ts +6 -6
  17. package/dist/src/mcpEvents/erpnextProvider.d.ts +20 -0
  18. package/dist/src/mcpEvents/erpnextProvider.js +80 -0
  19. package/package.json +5 -1
  20. package/scripts/host-integration.d.mts +34 -4
  21. package/scripts/host-integration.mjs +44 -1
  22. package/scripts/lib/composite-wake-coordinator.mjs +233 -9
  23. package/scripts/lib/event-processor.mjs +235 -20
  24. package/scripts/lib/local-event-intelligence-runtime.mjs +238 -83
  25. package/scripts/lib/mcp-events-client.mjs +184 -66
  26. package/scripts/lib/persistent-event-store.mjs +320 -1
  27. package/scripts/lib/temporal-deadline-scheduler.mjs +12 -2
  28. package/scripts/lib/wake-retry-scheduler.mjs +104 -0
  29. package/scripts/mcp-stdio-server.mjs +1 -1
  30. package/scripts/service.mjs +20 -0
  31. package/server.json +2 -2
@@ -2,6 +2,10 @@ import * as z from 'zod/v4';
2
2
  import {
3
3
  McpEventOccurrenceSchema,
4
4
  } from '../../dist/src/intelligenceProtocol/index.js';
5
+ import {
6
+ DEFAULT_EVENT_SCOPE_ID,
7
+ normalizeEventScopeId,
8
+ } from './persistent-event-store.mjs';
5
9
 
6
10
  const EVENTS_CAPABILITY = 'io.modelcontextprotocol.experimental/events';
7
11
  const UnknownResultSchema = z.unknown();
@@ -35,6 +39,7 @@ function normalizeConnection(connection) {
35
39
 
36
40
  return {
37
41
  connectionId,
42
+ scopeId: normalizeEventScopeId(connection.scopeId ?? DEFAULT_EVENT_SCOPE_ID),
38
43
  serverId: String(connection.serverId || `host-mcp:${connectionId}`),
39
44
  request: connection.request,
40
45
  identity: connection.identity ?? connection.request,
@@ -53,6 +58,13 @@ function normalizeConnection(connection) {
53
58
  500,
54
59
  'maxEvents',
55
60
  ),
61
+ maxPollBatches: normalizePositiveInteger(
62
+ connection.maxPollBatches,
63
+ 4,
64
+ 1,
65
+ 32,
66
+ 'maxPollBatches',
67
+ ),
56
68
  };
57
69
  }
58
70
 
@@ -70,6 +82,8 @@ export function createHostMcpEventsConnection({
70
82
  enabled = true,
71
83
  pollIntervalMs = 5000,
72
84
  maxEvents = 100,
85
+ maxPollBatches = 4,
86
+ scopeId = DEFAULT_EVENT_SCOPE_ID,
73
87
  } = {}) {
74
88
  const directRequest =
75
89
  typeof request === 'function'
@@ -106,6 +120,8 @@ export function createHostMcpEventsConnection({
106
120
  enabled,
107
121
  pollIntervalMs,
108
122
  maxEvents,
123
+ maxPollBatches,
124
+ scopeId,
109
125
  });
110
126
  }
111
127
 
@@ -170,17 +186,21 @@ export class McpEventsClientManager {
170
186
  store,
171
187
  compositeEventConsumer,
172
188
  registerEventSource,
189
+ resolveScope = null,
173
190
  connections = [],
174
191
  now = () => new Date(),
175
192
  }) {
176
193
  this.store = store;
177
194
  this.compositeEventConsumer = compositeEventConsumer;
178
195
  this.registerEventSource = registerEventSource;
196
+ this.resolveScope = resolveScope;
179
197
  this.connections = new Map();
180
198
  this.now = now;
181
199
  this.descriptors = new Map();
182
200
  this.intervals = new Map();
183
201
  this.lastErrors = new Map();
202
+ this.scopeContexts = new Map();
203
+ this.pollInFlight = new Map();
184
204
  this.started = false;
185
205
 
186
206
  for (const connection of connections) {
@@ -188,6 +208,34 @@ export class McpEventsClientManager {
188
208
  }
189
209
  }
190
210
 
211
+ async scopeContext(connection) {
212
+ const cacheKey = `${connection.connectionId}::${connection.scopeId}`;
213
+ const cached = this.scopeContexts.get(cacheKey);
214
+ if (cached) return cached;
215
+
216
+ const context =
217
+ typeof this.resolveScope === 'function'
218
+ ? await this.resolveScope(connection.scopeId)
219
+ : {
220
+ store: this.store,
221
+ compositeEventConsumer: this.compositeEventConsumer,
222
+ triggerControl: null,
223
+ };
224
+
225
+ if (
226
+ !context ||
227
+ !context.store ||
228
+ !context.compositeEventConsumer
229
+ ) {
230
+ throw new Error(
231
+ `Invalid Event Intelligence scope context for ${connection.scopeId}`,
232
+ );
233
+ }
234
+
235
+ this.scopeContexts.set(cacheKey, context);
236
+ return context;
237
+ }
238
+
191
239
  addConnection(input) {
192
240
  const connection = normalizeConnection(input);
193
241
  if (connection.enabled === false) return connection;
@@ -221,15 +269,27 @@ export class McpEventsClientManager {
221
269
  this.intervals.delete(connectionId);
222
270
  this.descriptors.delete(connectionId);
223
271
  this.lastErrors.delete(connectionId);
272
+ this.pollInFlight.delete(connectionId);
224
273
 
274
+ const connection = this.connections.get(connectionId);
275
+ const context = connection ? await this.scopeContext(connection) : null;
276
+ if (connection) {
277
+ this.scopeContexts.delete(`${connection.connectionId}::${connection.scopeId}`);
278
+ }
225
279
  const removed = this.connections.delete(connectionId);
226
- if (removed) {
227
- const sources = this.store.listEventSources({ connectionIds: [connectionId] });
280
+ if (removed && context) {
281
+ const sources = context.store.listEventSources({ connectionIds: [connectionId] });
228
282
  for (const source of sources) {
229
283
  if (source.enabled === false) continue;
230
- await this.registerEventSource({
284
+ const register = context.triggerControl?.registerEventSource
285
+ ? (input, actor) => context.triggerControl.registerEventSource(input, actor)
286
+ : async (input) => this.registerEventSource(input);
287
+ await register({
231
288
  ...source,
232
289
  enabled: false,
290
+ }, {
291
+ type: 'system',
292
+ principal_id: 'event-intelligence:host-mcp-client',
233
293
  });
234
294
  }
235
295
  }
@@ -252,6 +312,7 @@ export class McpEventsClientManager {
252
312
  throw new Error(`Unknown host-managed MCP Events connection: ${connectionId}`);
253
313
  }
254
314
 
315
+ const context = await this.scopeContext(connection);
255
316
  const discover = await this.rpc(connection, 'server/discover');
256
317
  const capabilities = discover?.capabilities ?? {};
257
318
  const experimental = capabilities?.experimental?.[EVENTS_CAPABILITY];
@@ -294,7 +355,7 @@ export class McpEventsClientManager {
294
355
  descriptors.push(normalized);
295
356
 
296
357
  const sourceId = `mcp:${connection.connectionId}:${name}`;
297
- const existing = this.store.listEventSources()
358
+ const existing = context.store.listEventSources()
298
359
  .find((source) => source.sourceId === sourceId);
299
360
  const same =
300
361
  existing &&
@@ -307,7 +368,10 @@ export class McpEventsClientManager {
307
368
  JSON.stringify(existing.payloadSchema || {}) === JSON.stringify(normalized.payloadSchema);
308
369
 
309
370
  if (!same) {
310
- await this.registerEventSource({
371
+ const register = context.triggerControl?.registerEventSource
372
+ ? (input, actor) => context.triggerControl.registerEventSource(input, actor)
373
+ : async (input) => this.registerEventSource(input);
374
+ await register({
311
375
  sourceId,
312
376
  connectionId: connection.connectionId,
313
377
  serverId: connection.serverId,
@@ -322,6 +386,9 @@ export class McpEventsClientManager {
322
386
  transport: 'host-managed-mcp',
323
387
  hostManaged: true,
324
388
  },
389
+ }, {
390
+ type: 'system',
391
+ principal_id: 'event-intelligence:host-mcp-client',
325
392
  });
326
393
  }
327
394
  }
@@ -354,81 +421,98 @@ export class McpEventsClientManager {
354
421
  return results;
355
422
  }
356
423
 
357
- async pollSource(connection, descriptor) {
424
+ async pollSource(connection, descriptor, context) {
358
425
  if (!descriptor.delivery.includes('poll')) {
359
426
  return { eventName: descriptor.name, status: 'delivery_not_supported', accepted: 0 };
360
427
  }
361
428
 
362
- const current = this.store.getMcpClientState(
429
+ const store = context.store;
430
+ const consumer = context.compositeEventConsumer;
431
+ const current = store.getMcpClientState(
363
432
  connection.connectionId,
364
433
  descriptor.name,
365
434
  );
366
435
 
367
- try {
368
- const result = await this.rpc(connection, 'events/poll', {
369
- name: descriptor.name,
370
- arguments: {},
371
- cursor: current?.cursor ?? null,
372
- maxEvents: connection.maxEvents,
373
- });
436
+ let cursor = current?.cursor ?? null;
437
+ let lastEventAt = current?.lastEventAt;
438
+ let accepted = 0;
439
+ let batches = 0;
440
+ let hasMore = false;
374
441
 
375
- if (typeof result?.cursor !== 'string' || !Array.isArray(result.events)) {
376
- throw new Error(
377
- `MCP server ${connection.connectionId} returned invalid events/poll`,
378
- );
379
- }
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
+ });
380
450
 
381
- let accepted = 0;
382
- let lastEventAt = current?.lastEventAt;
383
- for (const raw of result.events) {
384
- const occurrence = McpEventOccurrenceSchema.parse(raw);
385
- if (occurrence.name !== descriptor.name) {
451
+ if (typeof result?.cursor !== 'string' || !Array.isArray(result.events)) {
386
452
  throw new Error(
387
- `MCP event name mismatch: expected ${descriptor.name}, got ${occurrence.name}`,
453
+ `MCP server ${connection.connectionId} returned invalid events/poll`,
388
454
  );
389
455
  }
390
- assertJsonSchemaValue(descriptor.payloadSchema, occurrence.data);
391
456
 
392
- const receipt = await this.store.appendMcpOccurrence(
393
- connection.serverId,
394
- occurrence,
395
- );
396
- if (!receipt.accepted) continue;
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;
471
+
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
+ }
481
+
482
+ cursor = result.cursor;
483
+ hasMore = result.hasMore === true;
484
+ batches += 1;
397
485
 
398
- accepted += 1;
399
- lastEventAt = occurrence.timestamp;
400
- await this.compositeEventConsumer.ingestMcpOccurrence({
401
- event: occurrence,
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,
402
491
  serverId: connection.serverId,
403
- provider: 'mcp',
404
- traceId: `mcp:${connection.serverId}:${occurrence.eventId}`,
492
+ eventName: descriptor.name,
493
+ cursor,
494
+ ...(lastEventAt ? { lastEventAt } : {}),
405
495
  });
406
- }
496
+ } while (hasMore && batches < connection.maxPollBatches);
407
497
 
408
- await this.store.putMcpClientState({
409
- connectionId: connection.connectionId,
410
- serverId: connection.serverId,
411
- eventName: descriptor.name,
412
- cursor: result.cursor,
413
- ...(lastEventAt ? { lastEventAt } : {}),
414
- });
415
498
  this.lastErrors.delete(connection.connectionId);
416
-
417
499
  return {
418
500
  eventName: descriptor.name,
419
501
  status: 'ok',
420
502
  accepted,
421
- cursor: result.cursor,
422
- hasMore: result.hasMore === true,
503
+ cursor,
504
+ hasMore,
505
+ batches,
506
+ batchLimitReached: hasMore && batches >= connection.maxPollBatches,
423
507
  };
424
508
  } catch (error) {
425
509
  const message = error instanceof Error ? error.message : String(error);
426
510
  this.lastErrors.set(connection.connectionId, message);
427
- await this.store.putMcpClientState({
511
+ await store.putMcpClientState({
428
512
  connectionId: connection.connectionId,
429
513
  serverId: connection.serverId,
430
514
  eventName: descriptor.name,
431
- cursor: current?.cursor ?? null,
515
+ cursor,
432
516
  lastError: message,
433
517
  });
434
518
  throw error;
@@ -436,11 +520,27 @@ export class McpEventsClientManager {
436
520
  }
437
521
 
438
522
  async pollConnection(connectionId) {
523
+ const existing = this.pollInFlight.get(connectionId);
524
+ if (existing) return existing;
525
+
526
+ const task = this.pollConnectionOnce(connectionId);
527
+ this.pollInFlight.set(connectionId, task);
528
+ try {
529
+ return await task;
530
+ } finally {
531
+ if (this.pollInFlight.get(connectionId) === task) {
532
+ this.pollInFlight.delete(connectionId);
533
+ }
534
+ }
535
+ }
536
+
537
+ async pollConnectionOnce(connectionId) {
439
538
  const connection = this.connections.get(connectionId);
440
539
  if (!connection) {
441
540
  throw new Error(`Unknown host-managed MCP Events connection: ${connectionId}`);
442
541
  }
443
542
 
543
+ const context = await this.scopeContext(connection);
444
544
  let descriptors = this.descriptors.get(connectionId);
445
545
  if (!descriptors) {
446
546
  descriptors = await this.discoverConnection(connectionId);
@@ -448,7 +548,7 @@ export class McpEventsClientManager {
448
548
 
449
549
  const results = [];
450
550
  for (const descriptor of descriptors) {
451
- results.push(await this.pollSource(connection, descriptor));
551
+ results.push(await this.pollSource(connection, descriptor, context));
452
552
  }
453
553
  return results;
454
554
  }
@@ -499,21 +599,39 @@ export class McpEventsClientManager {
499
599
  this.intervals.clear();
500
600
  }
501
601
 
602
+ async drain() {
603
+ while (this.pollInFlight.size > 0) {
604
+ await Promise.allSettled([...this.pollInFlight.values()]);
605
+ }
606
+ }
607
+
608
+ async close() {
609
+ this.stop();
610
+ await this.drain();
611
+ }
612
+
502
613
  status() {
503
- return [...this.connections.values()].map((connection) => ({
504
- connectionId: connection.connectionId,
505
- serverId: connection.serverId,
506
- hostManaged: true,
507
- events: (this.descriptors.get(connection.connectionId) || [])
508
- .map((event) => ({
509
- name: event.name,
510
- delivery: event.delivery,
511
- cursor: this.store.getMcpClientState(
512
- connection.connectionId,
513
- event.name,
514
- )?.cursor ?? null,
515
- })),
516
- error: this.lastErrors.get(connection.connectionId) ?? null,
517
- }));
614
+ return [...this.connections.values()].map((connection) => {
615
+ const context = this.scopeContexts.get(
616
+ `${connection.connectionId}::${connection.scopeId}`,
617
+ );
618
+ const store = context?.store ?? this.store;
619
+ return {
620
+ connectionId: connection.connectionId,
621
+ scopeId: connection.scopeId,
622
+ serverId: connection.serverId,
623
+ hostManaged: true,
624
+ events: (this.descriptors.get(connection.connectionId) || [])
625
+ .map((event) => ({
626
+ name: event.name,
627
+ delivery: event.delivery,
628
+ cursor: store.getMcpClientState(
629
+ connection.connectionId,
630
+ event.name,
631
+ )?.cursor ?? null,
632
+ })),
633
+ error: this.lastErrors.get(connection.connectionId) ?? null,
634
+ };
635
+ });
518
636
  }
519
637
  }