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.
- package/CHANGELOG.md +72 -0
- package/README.md +79 -4
- package/bin/mcp-event-intelligence.mjs +1 -1
- package/dist/src/composite/derivedGraph.d.ts +4 -0
- package/dist/src/composite/derivedGraph.js +46 -0
- package/dist/src/composite/engine.d.ts +3 -64
- package/dist/src/composite/engine.js +26 -343
- package/dist/src/composite/matching.d.ts +8 -0
- package/dist/src/composite/matching.js +109 -0
- package/dist/src/composite/runtimeSupport.d.ts +24 -0
- package/dist/src/composite/runtimeSupport.js +176 -0
- package/dist/src/composite/store.d.ts +59 -0
- package/dist/src/composite/store.js +1 -0
- package/dist/src/intelligenceProtocol/schemas.d.ts +9 -7
- package/dist/src/intelligenceProtocol/schemas.js +1 -0
- package/dist/src/intelligenceProtocol/triggerSchemas.d.ts +6 -6
- package/dist/src/mcpEvents/erpnextProvider.d.ts +20 -0
- package/dist/src/mcpEvents/erpnextProvider.js +80 -0
- package/package.json +5 -1
- package/scripts/host-integration.d.mts +34 -4
- package/scripts/host-integration.mjs +44 -1
- package/scripts/lib/composite-wake-coordinator.mjs +233 -9
- package/scripts/lib/event-processor.mjs +235 -20
- package/scripts/lib/local-event-intelligence-runtime.mjs +238 -83
- package/scripts/lib/mcp-events-client.mjs +184 -66
- package/scripts/lib/persistent-event-store.mjs +320 -1
- package/scripts/lib/temporal-deadline-scheduler.mjs +12 -2
- package/scripts/lib/wake-retry-scheduler.mjs +104 -0
- package/scripts/mcp-stdio-server.mjs +1 -1
- package/scripts/service.mjs +20 -0
- 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 =
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
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
|
-
|
|
376
|
-
|
|
377
|
-
|
|
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
|
-
|
|
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
|
|
453
|
+
`MCP server ${connection.connectionId} returned invalid events/poll`,
|
|
388
454
|
);
|
|
389
455
|
}
|
|
390
|
-
assertJsonSchemaValue(descriptor.payloadSchema, occurrence.data);
|
|
391
456
|
|
|
392
|
-
const
|
|
393
|
-
|
|
394
|
-
occurrence
|
|
395
|
-
|
|
396
|
-
|
|
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
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
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
|
-
|
|
404
|
-
|
|
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
|
|
422
|
-
hasMore
|
|
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
|
|
511
|
+
await store.putMcpClientState({
|
|
428
512
|
connectionId: connection.connectionId,
|
|
429
513
|
serverId: connection.serverId,
|
|
430
514
|
eventName: descriptor.name,
|
|
431
|
-
cursor
|
|
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
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
)
|
|
515
|
-
|
|
516
|
-
|
|
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
|
}
|