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 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
  [![CI](https://github.com/sarooo17/event-intelligence/actions/workflows/ci.yml/badge.svg)](https://github.com/sarooo17/event-intelligence/actions/workflows/ci.yml)
6
6
  [![npm](https://img.shields.io/npm/v/mcp-event-intelligence.svg)](https://www.npmjs.com/package/mcp-event-intelligence)
7
- [![MCP Registry](https://img.shields.io/badge/MCP%20Registry-v0.1.1-5b5bd6)](https://registry.modelcontextprotocol.io/?q=io.github.sarooo17%2Fevent-intelligence)
7
+ [![MCP Registry](https://img.shields.io/badge/MCP%20Registry-v0.2.0-5b5bd6)](https://registry.modelcontextprotocol.io/?q=io.github.sarooo17%2Fevent-intelligence)
8
8
  [![License](https://img.shields.io/badge/license-Apache--2.0-blue.svg)](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, persistence, deduplication, runtime receipts and replay handling to provide effectively-once runtime activation in the validated reference scenarios.
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. The v0.1 reference store is JSONL and single-writer.
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 storage/HA, broader host/provider evidence, scale benchmarks and upstream feedback.
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
 
@@ -4,7 +4,7 @@ const args = process.argv.slice(2);
4
4
  const flags = new Set(args);
5
5
 
6
6
  if (flags.has('--version') || flags.has('-v')) {
7
- console.log('0.1.1');
7
+ console.log('0.2.0');
8
8
  process.exit(0);
9
9
  }
10
10
 
@@ -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
- export interface CompositeTriggerStore {
4
- putTrigger(definition: CompositeTriggerDefinition): Promise<void>;
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
  }