mcp-event-intelligence 0.1.1 → 0.2.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.
- package/CHANGELOG.md +57 -0
- package/README.md +67 -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 +227 -82
- package/scripts/lib/mcp-events-client.mjs +173 -66
- package/scripts/lib/persistent-event-store.mjs +308 -1
- package/scripts/lib/wake-retry-scheduler.mjs +94 -0
- package/scripts/mcp-stdio-server.mjs +1 -1
- package/scripts/service.mjs +20 -0
- package/server.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,63 @@
|
|
|
2
2
|
|
|
3
3
|
All notable public changes are documented here.
|
|
4
4
|
|
|
5
|
+
## [0.2.0] - 2026-09-20
|
|
6
|
+
|
|
7
|
+
Production-hardening release for shared Event Intelligence hosts.
|
|
8
|
+
|
|
9
|
+
### Shared-host isolation
|
|
10
|
+
|
|
11
|
+
- explicit `scopeId` partitions for tenant/workspace isolation;
|
|
12
|
+
- scoped host views via `host.scope(scopeId)`;
|
|
13
|
+
- isolated triggers, matches, event sources, cursors, deadlines, derived events and wake delivery state;
|
|
14
|
+
- persistent scope restoration after restart;
|
|
15
|
+
- root host documented as a trusted operator/control-plane capability.
|
|
16
|
+
|
|
17
|
+
### Pluggable persistence
|
|
18
|
+
|
|
19
|
+
- `createEventIntelligenceHost({ store })` now accepts an external storage backend;
|
|
20
|
+
- `PersistentEventStore` remains the zero-dependency default;
|
|
21
|
+
- scoped custom backends can implement `forScope(scopeId)`;
|
|
22
|
+
- storage contract validates the operations required by Event Intelligence at startup.
|
|
23
|
+
|
|
24
|
+
### Durable wake delivery
|
|
25
|
+
|
|
26
|
+
- persisted wake-delivery state with `pending`, `claimed`, `retry_pending`, `delivered` and `dead_letter`;
|
|
27
|
+
- atomic claim/lease API for concurrent workers;
|
|
28
|
+
- bounded exponential retry and retry scheduler;
|
|
29
|
+
- restart recovery without a new provider event;
|
|
30
|
+
- reconciliation of a persisted runtime receipt after a crash without redelivering the wake;
|
|
31
|
+
- the direct `EventProcessor` path now uses the same durable retry semantics.
|
|
32
|
+
|
|
33
|
+
### Polling and load control
|
|
34
|
+
|
|
35
|
+
- single-flight polling per MCP connection;
|
|
36
|
+
- bounded `hasMore` page draining;
|
|
37
|
+
- cursor persistence after every drained page;
|
|
38
|
+
- explicit batch-limit reporting.
|
|
39
|
+
|
|
40
|
+
### Internal modularity
|
|
41
|
+
|
|
42
|
+
- composite store contracts extracted from `engine.ts`;
|
|
43
|
+
- matching/correlation helpers extracted into a dedicated module;
|
|
44
|
+
- derived-event cycle validation extracted into a graph module;
|
|
45
|
+
- lifecycle, deadline and audit support extracted from the engine.
|
|
46
|
+
|
|
47
|
+
### Provider coverage
|
|
48
|
+
|
|
49
|
+
- new `mcp-event-intelligence/provider/erpnext` export;
|
|
50
|
+
- provider-native ERPNext adapter for `erpnext.sales_invoice.submitted` and `erpnext.sales_order.created`;
|
|
51
|
+
- schema-validation and provider-generalization regression coverage.
|
|
52
|
+
|
|
53
|
+
### Verification
|
|
54
|
+
|
|
55
|
+
- tenant-isolation tests with identical trigger IDs in separate scopes;
|
|
56
|
+
- custom-store injection and restart persistence tests;
|
|
57
|
+
- polling single-flight and bounded-batching tests;
|
|
58
|
+
- retry, dead-letter, lease-contention and post-receipt crash-reconciliation tests;
|
|
59
|
+
- ERPNext provider adapter tests;
|
|
60
|
+
- CI, Security Scan, MCP Launch Check, Clean-room Smoke and Public Release Check green before release.
|
|
61
|
+
|
|
5
62
|
## [0.1.1] - 2026-09-20
|
|
6
63
|
|
|
7
64
|
Provider integration patch.
|
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
[](https://github.com/sarooo17/event-intelligence/actions/workflows/ci.yml)
|
|
6
6
|
[](https://www.npmjs.com/package/mcp-event-intelligence)
|
|
7
|
-
[](https://registry.modelcontextprotocol.io/?q=io.github.sarooo17%2Fevent-intelligence)
|
|
8
8
|
[](LICENSE)
|
|
9
9
|
|
|
10
10
|
MCP Event Intelligence is an experimental event runtime for agents that need to react to **future conditions over multiple event sources** without keeping an LLM or agent loop alive.
|
|
@@ -55,6 +55,38 @@ const ei = await createEventIntelligenceHost({
|
|
|
55
55
|
Event Intelligence enumerates the host registry automatically. GitHub, Gmail, private/company MCPs and future connections do not need to be configured again inside EI. Tools-only MCPs remain available to the agent and are ignored by the Events layer; Events-capable MCPs are attached automatically.
|
|
56
56
|
|
|
57
57
|
|
|
58
|
+
### Shared hosts, tenant isolation and storage
|
|
59
|
+
|
|
60
|
+
One EI host can serve many tenants/workspaces without sharing trigger state. Give each host-owned MCP connection a `scopeId` and give tenant-facing code only the corresponding scoped view:
|
|
61
|
+
|
|
62
|
+
```js
|
|
63
|
+
const tenant = await ei.scope('tenant-acme');
|
|
64
|
+
|
|
65
|
+
await tenant.triggerControl.createTrigger({
|
|
66
|
+
definition,
|
|
67
|
+
connectionIds: ['acme-erp'],
|
|
68
|
+
actor,
|
|
69
|
+
owner,
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const acmeConnections = tenant.mcpStatus();
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
The default reference store physically namespaces non-default scopes under separate persistent store partitions. Trigger IDs, match IDs, cursors, event sources, deadlines, derived events and wake delivery state are therefore resolved inside a scope rather than filtered out of a global result after the fact. The root host object is the trusted operator/control-plane capability; tenant code should receive a scoped view.
|
|
76
|
+
|
|
77
|
+
Storage is injectable:
|
|
78
|
+
|
|
79
|
+
```js
|
|
80
|
+
const ei = await createEventIntelligenceHost({
|
|
81
|
+
store: myEventIntelligenceStore,
|
|
82
|
+
mcpRegistry,
|
|
83
|
+
wake,
|
|
84
|
+
});
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
`PersistentEventStore` remains the zero-dependency default. A custom backend can implement `forScope(scopeId)` to return an isolated tenant view. For horizontally scaled workers, its wake-delivery claim/lease operations must be atomic across processes; the bundled JSONL store provides serialized atomicity inside one process and is a reference backend, not a distributed database.
|
|
88
|
+
|
|
89
|
+
|
|
58
90
|
## Add Events to an MCP provider
|
|
59
91
|
|
|
60
92
|
Providers can expose the experimental Events boundary without reimplementing the generic JSON-RPC glue:
|
|
@@ -92,6 +124,24 @@ The package owns capability advertisement, `server/discover`, `events/list`, `ev
|
|
|
92
124
|
|
|
93
125
|
This adapter remains experimental compatibility work around MCP Events; it is not a claim of finalized MCP Events conformance.
|
|
94
126
|
|
|
127
|
+
|
|
128
|
+
A concrete ERP-shaped adapter is also exported, so the provider abstraction is exercised against a source structurally different from GitHub:
|
|
129
|
+
|
|
130
|
+
```js
|
|
131
|
+
import {
|
|
132
|
+
createErpNextEventsProvider,
|
|
133
|
+
} from 'mcp-event-intelligence/provider/erpnext';
|
|
134
|
+
|
|
135
|
+
const events = createErpNextEventsProvider({
|
|
136
|
+
pollSalesInvoices: ({ cursor, maxEvents }) =>
|
|
137
|
+
erp.pollSubmittedInvoices({ cursor, maxEvents }),
|
|
138
|
+
pollSalesOrders: ({ cursor, maxEvents }) =>
|
|
139
|
+
erp.pollCreatedSalesOrders({ cursor, maxEvents }),
|
|
140
|
+
});
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
It exposes `erpnext.sales_invoice.submitted` and `erpnext.sales_order.created` while leaving ERP authentication/query ownership with the provider.
|
|
144
|
+
|
|
95
145
|
## What v0.1 implements
|
|
96
146
|
|
|
97
147
|
### Host-owned event sources
|
|
@@ -101,6 +151,8 @@ This adapter remains experimental compatibility work around MCP Events; it is no
|
|
|
101
151
|
- experimental MCP Events capability discovery;
|
|
102
152
|
- `events/list` and `events/poll`;
|
|
103
153
|
- persistent opaque cursors;
|
|
154
|
+
- per-scope source/cursor isolation for shared hosts;
|
|
155
|
+
- single-flight polling per connection plus bounded `hasMore` batch draining;
|
|
104
156
|
- automatic event-source registration;
|
|
105
157
|
- dynamic attach/detach of host MCP clients;
|
|
106
158
|
- provider-native compatibility adapters such as GitHub webhooks.
|
|
@@ -160,6 +212,8 @@ Derived event names are versioned contracts such as `release.ready@1`.
|
|
|
160
212
|
|
|
161
213
|
An embedded harness can provide one in-process wake dispatcher that routes by `target.runtime`, `target.kind` and `target.id`; runtime-specific handlers remain available as a lower-level option. A standalone deployment can instead use signed HMAC callbacks. Both paths return a stable `runtimeReceiptId`.
|
|
162
214
|
|
|
215
|
+
Runtime delivery is durable: a stable wake ID gets a persisted delivery record, a worker claims it with a lease, transient failures are retried with bounded exponential backoff, expired claims can be recovered after restart, and only an exhausted retry budget enters dead-letter. This prevents two workers sharing an atomic store from intentionally owning the same delivery at the same time. The runtime should still treat the stable wake ID as an idempotency key because no system can make an arbitrary external side effect transactionally exactly-once without cooperation from the receiver.
|
|
216
|
+
|
|
163
217
|
## Why this exists
|
|
164
218
|
|
|
165
219
|
MCP Events is concerned with the event transport/subscription boundary. Event Intelligence explores the layer **above transport**:
|
|
@@ -195,6 +249,10 @@ The v0.1 acceptance suite verifies:
|
|
|
195
249
|
- process shutdown while a temporal deadline is pending;
|
|
196
250
|
- restart on the same datastore after the deadline;
|
|
197
251
|
- wake recovery **without a new provider event**;
|
|
252
|
+
- retry-state recovery after process restart and lease-based concurrent-worker exclusion;
|
|
253
|
+
- shared-host tenant isolation with identical trigger IDs in different scopes;
|
|
254
|
+
- bounded/single-flight provider polling;
|
|
255
|
+
- provider-generalization coverage with ERPNext-shaped invoice/order events;
|
|
198
256
|
- hash-linked audit verification.
|
|
199
257
|
|
|
200
258
|
A separate live regression also verified real GitHub webhook ingress into the MCP EventOccurrence / composite fan-in path.
|
|
@@ -263,6 +321,11 @@ Standalone/core environment variables:
|
|
|
263
321
|
| `RUNTIME_WAKE_TARGETS_JSON` | no | signed standalone runtime callbacks |
|
|
264
322
|
| `GITHUB_WEBHOOK_SECRET` | no | verify GitHub webhook ingress |
|
|
265
323
|
| `TYPESAFE_API_KEY` | no | bundled TypeSafe Jev semantic evaluator |
|
|
324
|
+
| `WAKE_DELIVERY_MAX_ATTEMPTS` | no | maximum wake delivery attempts, default `5` |
|
|
325
|
+
| `WAKE_DELIVERY_LEASE_MS` | no | claim lease duration, default `30000` |
|
|
326
|
+
| `WAKE_RETRY_BASE_DELAY_MS` | no | first retry delay, default `1000` |
|
|
327
|
+
| `WAKE_RETRY_MAX_DELAY_MS` | no | retry backoff cap, default `60000` |
|
|
328
|
+
| `WAKE_RETRY_TICK_MS` | no | retry scheduler tick, default `1000` |
|
|
266
329
|
|
|
267
330
|
Embedded hosts pass one MCP registry adapter. Event Intelligence discovers already-connected clients from that registry; provider MCP connection settings are not duplicated inside EI.
|
|
268
331
|
|
|
@@ -286,7 +349,7 @@ Absence/deadline progression uses Event Intelligence processing time. This separ
|
|
|
286
349
|
|
|
287
350
|
The implementation does not claim theoretical distributed exactly-once delivery.
|
|
288
351
|
|
|
289
|
-
It uses stable wake IDs,
|
|
352
|
+
It uses stable wake IDs, persisted delivery state, atomic claim leases, bounded retries, runtime receipts and replay handling to provide effectively-once runtime activation in the validated reference scenarios.
|
|
290
353
|
|
|
291
354
|
### Derived event vs current state
|
|
292
355
|
|
|
@@ -298,7 +361,7 @@ v0.1 intentionally does not implement a mutable current-state/facts database.
|
|
|
298
361
|
|
|
299
362
|
Read [SECURITY.md](SECURITY.md) and [docs/SECURITY-MODEL.md](docs/SECURITY-MODEL.md).
|
|
300
363
|
|
|
301
|
-
In embedded mode, the host retains MCP authorization and credentials; Event Intelligence discovers only the client objects exposed through the host-provided MCP registry.
|
|
364
|
+
In embedded mode, the host retains MCP authorization and credentials; Event Intelligence discovers only the client objects exposed through the host-provided MCP registry. Tenant-facing callers should receive only `host.scope(scopeId)`. The reference JSONL store is scope-partitioned and single-process; distributed custom stores must preserve scope isolation and atomic wake claims.
|
|
302
365
|
|
|
303
366
|
## MCP compatibility status
|
|
304
367
|
|
|
@@ -319,7 +382,7 @@ io.github.sarooo17/event-intelligence
|
|
|
319
382
|
|
|
320
383
|
**v0.1 reference implementation / experimental.**
|
|
321
384
|
|
|
322
|
-
The architecture is implemented and exercised end-to-end. Remaining work is primarily production
|
|
385
|
+
The architecture is implemented and exercised end-to-end. Storage is now injectable and scoped, while the bundled JSONL backend remains a single-process reference implementation. Remaining work is primarily production database adapters/HA validation, scale benchmarks and upstream feedback.
|
|
323
386
|
|
|
324
387
|
## Example
|
|
325
388
|
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { CompositeTriggerDefinition } from '../intelligenceProtocol/triggerSchemas.js';
|
|
2
|
+
export declare function assertNoDerivedEventCycle(existing: CompositeTriggerDefinition[], candidate: CompositeTriggerDefinition, getState?: (triggerId: string, version: string) => {
|
|
3
|
+
status: string;
|
|
4
|
+
} | null): void;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
const DERIVED_SERVER_ID = 'event-intelligence:derived';
|
|
2
|
+
function eventNode(eventName, serverId) {
|
|
3
|
+
return `${serverId || DERIVED_SERVER_ID}::${eventName}`;
|
|
4
|
+
}
|
|
5
|
+
export function assertNoDerivedEventCycle(existing, candidate, getState) {
|
|
6
|
+
const definitions = [
|
|
7
|
+
...existing.filter((definition) => {
|
|
8
|
+
const state = getState?.(definition.triggerId, definition.version);
|
|
9
|
+
return !state || state.status === 'active' || state.status === 'paused';
|
|
10
|
+
}),
|
|
11
|
+
candidate,
|
|
12
|
+
].filter((definition) => definition.derivedEvent);
|
|
13
|
+
const graph = new Map();
|
|
14
|
+
for (const definition of definitions) {
|
|
15
|
+
const output = eventNode(definition.derivedEvent.name, DERIVED_SERVER_ID);
|
|
16
|
+
for (const clause of definition.clauses) {
|
|
17
|
+
const input = eventNode(clause.event, clause.serverId);
|
|
18
|
+
const edges = graph.get(input) ?? new Set();
|
|
19
|
+
edges.add(output);
|
|
20
|
+
graph.set(input, edges);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
const visiting = new Set();
|
|
24
|
+
const visited = new Set();
|
|
25
|
+
const dfs = (node) => {
|
|
26
|
+
if (visiting.has(node))
|
|
27
|
+
return true;
|
|
28
|
+
if (visited.has(node))
|
|
29
|
+
return false;
|
|
30
|
+
visiting.add(node);
|
|
31
|
+
for (const next of graph.get(node) ?? []) {
|
|
32
|
+
if (dfs(next))
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
visiting.delete(node);
|
|
36
|
+
visited.add(node);
|
|
37
|
+
return false;
|
|
38
|
+
};
|
|
39
|
+
for (const node of graph.keys()) {
|
|
40
|
+
if (dfs(node)) {
|
|
41
|
+
const error = new Error(`Derived event graph contains a cycle involving ${node}`);
|
|
42
|
+
error.code = 'TRIGGER_DERIVED_EVENT_CYCLE';
|
|
43
|
+
throw error;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -1,63 +1,7 @@
|
|
|
1
1
|
import type { SemanticEvaluator } from '../semantic/types.js';
|
|
2
2
|
import { type CompositeTriggerDefinition, type TriggerMatchRecord } from '../intelligenceProtocol/triggerSchemas.js';
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
listTriggers(): CompositeTriggerDefinition[];
|
|
6
|
-
getTriggerState?(triggerId: string, version: string): {
|
|
7
|
-
status: 'active' | 'paused' | 'completed' | 'expired' | 'deleted';
|
|
8
|
-
owner?: unknown;
|
|
9
|
-
connectionIds?: string[];
|
|
10
|
-
fireCount?: number;
|
|
11
|
-
lastFiredAt?: string | null;
|
|
12
|
-
revision?: number;
|
|
13
|
-
} | null;
|
|
14
|
-
setTriggerState?(triggerId: string, version: string, status: 'active' | 'paused' | 'completed' | 'expired' | 'deleted', actor?: unknown, metadata?: Record<string, unknown>): Promise<unknown>;
|
|
15
|
-
appendTriggerMatch(record: TriggerMatchRecord): Promise<void>;
|
|
16
|
-
listTriggerMatches(triggerId?: string): TriggerMatchRecord[];
|
|
17
|
-
getTemporalDeadline?(deadlineId: string): {
|
|
18
|
-
deadlineId: string;
|
|
19
|
-
triggerId: string;
|
|
20
|
-
triggerVersion: string;
|
|
21
|
-
matchId: string;
|
|
22
|
-
conditionId: string;
|
|
23
|
-
dueAt: string;
|
|
24
|
-
status: 'pending' | 'cancelled' | 'fired';
|
|
25
|
-
} | null;
|
|
26
|
-
listTemporalDeadlines?(filter?: {
|
|
27
|
-
triggerId?: string;
|
|
28
|
-
matchId?: string;
|
|
29
|
-
status?: string;
|
|
30
|
-
}): Array<{
|
|
31
|
-
deadlineId: string;
|
|
32
|
-
triggerId: string;
|
|
33
|
-
triggerVersion: string;
|
|
34
|
-
matchId: string;
|
|
35
|
-
conditionId: string;
|
|
36
|
-
dueAt: string;
|
|
37
|
-
status: string;
|
|
38
|
-
}>;
|
|
39
|
-
putTemporalDeadline?(input: {
|
|
40
|
-
deadlineId: string;
|
|
41
|
-
triggerId: string;
|
|
42
|
-
triggerVersion: string;
|
|
43
|
-
matchId: string;
|
|
44
|
-
conditionId: string;
|
|
45
|
-
dueAt: string;
|
|
46
|
-
status: 'pending' | 'cancelled' | 'fired';
|
|
47
|
-
}): Promise<unknown>;
|
|
48
|
-
setTemporalDeadlineStatus?(deadlineId: string, status: 'pending' | 'cancelled' | 'fired'): Promise<unknown>;
|
|
49
|
-
}
|
|
50
|
-
export interface CompositeTriggerAudit {
|
|
51
|
-
appendAudit(input: {
|
|
52
|
-
auditId: string;
|
|
53
|
-
traceId: string;
|
|
54
|
-
timestamp: string;
|
|
55
|
-
kind: string;
|
|
56
|
-
entityType: 'trigger_match' | 'trigger';
|
|
57
|
-
entityId: string;
|
|
58
|
-
details?: Record<string, unknown>;
|
|
59
|
-
}): Promise<unknown>;
|
|
60
|
-
}
|
|
3
|
+
import type { CompositeTriggerAudit, CompositeTriggerStore } from './store.js';
|
|
4
|
+
export type { CompositeTriggerAudit, CompositeTriggerStore, } from './store.js';
|
|
61
5
|
export interface CompositeIngestResult {
|
|
62
6
|
triggerId: string;
|
|
63
7
|
match: TriggerMatchRecord | null;
|
|
@@ -68,6 +12,7 @@ export declare class CompositeTriggerEngine {
|
|
|
68
12
|
private readonly store;
|
|
69
13
|
private readonly evaluator;
|
|
70
14
|
private readonly now;
|
|
15
|
+
private readonly support;
|
|
71
16
|
constructor(store: CompositeTriggerStore & CompositeTriggerAudit, evaluator?: SemanticEvaluator | null, now?: () => Date);
|
|
72
17
|
register(definitionInput: unknown): Promise<CompositeTriggerDefinition>;
|
|
73
18
|
ingest(eventInput: unknown): Promise<CompositeIngestResult[]>;
|
|
@@ -76,12 +21,6 @@ export declare class CompositeTriggerEngine {
|
|
|
76
21
|
private ingestTimerEvent;
|
|
77
22
|
private applyEvent;
|
|
78
23
|
private finalizeEligible;
|
|
79
|
-
private scheduleDeadlines;
|
|
80
|
-
private cancelDeadlinesForMatch;
|
|
81
|
-
private advanceLifecycleAfterEffect;
|
|
82
|
-
private lifecycleAllows;
|
|
83
|
-
private auditTriggerLifecycle;
|
|
84
24
|
private newMatch;
|
|
85
25
|
private latestMatch;
|
|
86
|
-
private audit;
|
|
87
26
|
}
|