mcp-event-intelligence 0.3.1 → 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.
- package/CHANGELOG.md +22 -0
- package/README.md +25 -13
- package/bin/mcp-event-intelligence.mjs +2 -2
- package/dist/src/composite/matching.d.ts +1 -1
- package/dist/src/composite/matching.js +21 -1
- package/dist/src/intelligenceProtocol/schemas.d.ts +2 -0
- package/dist/src/intelligenceProtocol/schemas.js +1 -0
- package/dist/src/intelligenceProtocol/triggerPlanSchemas.d.ts +2 -0
- package/dist/src/intelligenceProtocol/triggerPlanSchemas.js +1 -0
- package/dist/src/intelligenceProtocol/triggerSchemas.d.ts +6 -0
- package/dist/src/intelligenceProtocol/triggerSchemas.js +3 -0
- package/dist/src/mcpEvents/consumer.d.ts +1 -0
- package/dist/src/mcpEvents/consumer.js +1 -0
- package/dist/src/mcpEvents/erpnextProvider.d.ts +0 -2
- package/dist/src/mcpEvents/erpnextProvider.js +0 -3
- package/dist/src/mcpEvents/provider.d.ts +24 -10
- package/dist/src/mcpEvents/provider.js +124 -55
- package/dist/src/mcpEvents/server.d.ts +3 -1
- package/dist/src/mcpEvents/server.js +20 -19
- package/dist/src/protocol/types.d.ts +1 -0
- package/package.json +1 -1
- package/scripts/host-integration.d.mts +40 -0
- package/scripts/lib/composite-event-consumer.mjs +2 -0
- package/scripts/lib/json-schema.mjs +134 -0
- package/scripts/lib/local-event-intelligence-runtime.mjs +1 -0
- package/scripts/lib/mcp-events-client.mjs +760 -253
- package/scripts/lib/persistent-event-store.mjs +68 -9
- package/scripts/lib/trigger-control-plane.mjs +26 -0
- package/scripts/lib/trigger-planner.mjs +23 -0
- package/scripts/mcp-stdio-server.mjs +1 -0
- package/server.json +2 -2
|
@@ -1,15 +1,37 @@
|
|
|
1
1
|
import { McpEventOccurrenceSchema, } from '../intelligenceProtocol/schemas.js';
|
|
2
|
-
export const
|
|
2
|
+
export const MCP_EVENTS_EXTENSION_ID = 'io.modelcontextprotocol/events';
|
|
3
|
+
export const MCP_EVENTS_CAPABILITY_KEY = MCP_EVENTS_EXTENSION_ID;
|
|
3
4
|
export const MCP_EVENTS_CAPABILITY = Object.freeze({
|
|
4
|
-
status: 'draft',
|
|
5
|
-
designDate: '2026-02-19',
|
|
6
|
-
methods: ['events/list', 'events/poll'],
|
|
7
5
|
listChanged: false,
|
|
8
6
|
});
|
|
7
|
+
export const MCP_EVENTS_ERROR = Object.freeze({
|
|
8
|
+
INVALID_PARAMS: -32602,
|
|
9
|
+
METHOD_NOT_FOUND: -32601,
|
|
10
|
+
INTERNAL_ERROR: -32603,
|
|
11
|
+
NOT_FOUND: -32011,
|
|
12
|
+
FORBIDDEN: -32012,
|
|
13
|
+
RESOURCE_EXHAUSTED: -32013,
|
|
14
|
+
UNSUPPORTED: -32014,
|
|
15
|
+
CALLBACK_ENDPOINT_ERROR: -32015,
|
|
16
|
+
});
|
|
17
|
+
export class McpEventsProtocolError extends Error {
|
|
18
|
+
code;
|
|
19
|
+
data;
|
|
20
|
+
constructor(code, message, data) {
|
|
21
|
+
super(message);
|
|
22
|
+
this.name = 'McpEventsProtocolError';
|
|
23
|
+
this.code = code;
|
|
24
|
+
this.data = data;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
9
27
|
const ALLOWED_DELIVERY = new Set(['poll', 'push', 'webhook']);
|
|
28
|
+
const LIST_CURSOR_PREFIX = 'mcp-events-list-v1:';
|
|
29
|
+
function invalidParams(message, data) {
|
|
30
|
+
throw new McpEventsProtocolError(MCP_EVENTS_ERROR.INVALID_PARAMS, message, data);
|
|
31
|
+
}
|
|
10
32
|
function assertRecord(value, message) {
|
|
11
33
|
if (!value || Array.isArray(value) || typeof value !== 'object') {
|
|
12
|
-
|
|
34
|
+
invalidParams(message);
|
|
13
35
|
}
|
|
14
36
|
return value;
|
|
15
37
|
}
|
|
@@ -17,7 +39,7 @@ function optionalFiniteNumber(value, name) {
|
|
|
17
39
|
if (value === undefined)
|
|
18
40
|
return undefined;
|
|
19
41
|
if (typeof value !== 'number' || !Number.isFinite(value) || value < 0) {
|
|
20
|
-
|
|
42
|
+
invalidParams(`${name} must be a non-negative finite number`);
|
|
21
43
|
}
|
|
22
44
|
return value;
|
|
23
45
|
}
|
|
@@ -25,15 +47,18 @@ function normalizeDescriptor(input) {
|
|
|
25
47
|
const raw = assertRecord(input, 'MCP event descriptor must be an object');
|
|
26
48
|
const name = String(raw.name ?? '').trim();
|
|
27
49
|
if (!name)
|
|
28
|
-
|
|
50
|
+
invalidParams('MCP event descriptor requires name');
|
|
29
51
|
const deliveryRaw = Array.isArray(raw.delivery) ? raw.delivery : ['poll'];
|
|
30
52
|
const delivery = deliveryRaw.map((entry) => String(entry));
|
|
31
53
|
if (delivery.length === 0 ||
|
|
32
54
|
delivery.some((entry) => !ALLOWED_DELIVERY.has(entry))) {
|
|
33
|
-
|
|
55
|
+
invalidParams(`MCP event descriptor ${name} has invalid delivery modes`);
|
|
34
56
|
}
|
|
57
|
+
// createMcpEventsProvider is the poll-backed provider adapter. Hosts may
|
|
58
|
+
// consume push/webhook sources through the host delivery hooks, but a server
|
|
59
|
+
// using this adapter must expose poll for the check-since-cursor callback.
|
|
35
60
|
if (!delivery.includes('poll')) {
|
|
36
|
-
|
|
61
|
+
invalidParams(`MCP provider event ${name} must advertise poll delivery`);
|
|
37
62
|
}
|
|
38
63
|
const inputSchema = raw.inputSchema === undefined
|
|
39
64
|
? {}
|
|
@@ -41,12 +66,16 @@ function normalizeDescriptor(input) {
|
|
|
41
66
|
const payloadSchema = raw.payloadSchema === undefined
|
|
42
67
|
? {}
|
|
43
68
|
: assertRecord(raw.payloadSchema, `MCP event descriptor ${name} payloadSchema must be an object`);
|
|
69
|
+
const meta = raw._meta === undefined
|
|
70
|
+
? undefined
|
|
71
|
+
: assertRecord(raw._meta, `MCP event descriptor ${name} _meta must be an object`);
|
|
44
72
|
return {
|
|
45
73
|
name,
|
|
46
74
|
description: String(raw.description ?? ''),
|
|
47
75
|
delivery: delivery,
|
|
48
76
|
inputSchema,
|
|
49
77
|
payloadSchema,
|
|
78
|
+
...(meta ? { _meta: meta } : {}),
|
|
50
79
|
};
|
|
51
80
|
}
|
|
52
81
|
function schemaTypes(schema) {
|
|
@@ -59,11 +88,11 @@ export function assertJsonSchemaValue(schemaInput, value, path = '$') {
|
|
|
59
88
|
const schema = assertRecord(schemaInput, `Invalid JSON Schema at ${path}`);
|
|
60
89
|
if (Object.prototype.hasOwnProperty.call(schema, 'const') &&
|
|
61
90
|
!Object.is(schema.const, value)) {
|
|
62
|
-
throw new Error(`MCP event
|
|
91
|
+
throw new Error(`MCP event value violates schema at ${path}: const mismatch`);
|
|
63
92
|
}
|
|
64
93
|
if (Array.isArray(schema.enum) &&
|
|
65
94
|
!schema.enum.some((entry) => Object.is(entry, value))) {
|
|
66
|
-
throw new Error(`MCP event
|
|
95
|
+
throw new Error(`MCP event value violates schema at ${path}: value not in enum`);
|
|
67
96
|
}
|
|
68
97
|
const types = schemaTypes(schema);
|
|
69
98
|
if (types.length > 0) {
|
|
@@ -87,23 +116,23 @@ export function assertJsonSchemaValue(schemaInput, value, path = '$') {
|
|
|
87
116
|
return false;
|
|
88
117
|
});
|
|
89
118
|
if (!matches) {
|
|
90
|
-
throw new Error(`MCP event
|
|
119
|
+
throw new Error(`MCP event value violates schema at ${path}: expected ${types.join('|')}`);
|
|
91
120
|
}
|
|
92
121
|
}
|
|
93
122
|
if (typeof value === 'string') {
|
|
94
123
|
if (typeof schema.minLength === 'number' && value.length < schema.minLength) {
|
|
95
|
-
throw new Error(`MCP event
|
|
124
|
+
throw new Error(`MCP event value violates schema at ${path}: string too short`);
|
|
96
125
|
}
|
|
97
126
|
if (typeof schema.maxLength === 'number' && value.length > schema.maxLength) {
|
|
98
|
-
throw new Error(`MCP event
|
|
127
|
+
throw new Error(`MCP event value violates schema at ${path}: string too long`);
|
|
99
128
|
}
|
|
100
129
|
}
|
|
101
130
|
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
102
131
|
if (typeof schema.minimum === 'number' && value < schema.minimum) {
|
|
103
|
-
throw new Error(`MCP event
|
|
132
|
+
throw new Error(`MCP event value violates schema at ${path}: below minimum`);
|
|
104
133
|
}
|
|
105
134
|
if (typeof schema.maximum === 'number' && value > schema.maximum) {
|
|
106
|
-
throw new Error(`MCP event
|
|
135
|
+
throw new Error(`MCP event value violates schema at ${path}: above maximum`);
|
|
107
136
|
}
|
|
108
137
|
}
|
|
109
138
|
if (value !== null && !Array.isArray(value) && typeof value === 'object') {
|
|
@@ -113,7 +142,7 @@ export function assertJsonSchemaValue(schemaInput, value, path = '$') {
|
|
|
113
142
|
: [];
|
|
114
143
|
for (const key of required) {
|
|
115
144
|
if (!Object.prototype.hasOwnProperty.call(record, key)) {
|
|
116
|
-
throw new Error(`MCP event
|
|
145
|
+
throw new Error(`MCP event value violates schema at ${path}: missing required property ${key}`);
|
|
117
146
|
}
|
|
118
147
|
}
|
|
119
148
|
const properties = schema.properties &&
|
|
@@ -132,7 +161,7 @@ export function assertJsonSchemaValue(schemaInput, value, path = '$') {
|
|
|
132
161
|
if (schema.additionalProperties === false) {
|
|
133
162
|
for (const key of Object.keys(record)) {
|
|
134
163
|
if (!Object.prototype.hasOwnProperty.call(properties, key)) {
|
|
135
|
-
throw new Error(`MCP event
|
|
164
|
+
throw new Error(`MCP event value violates schema at ${path}: unexpected property ${key}`);
|
|
136
165
|
}
|
|
137
166
|
}
|
|
138
167
|
}
|
|
@@ -148,7 +177,7 @@ function normalizePollParams(paramsInput) {
|
|
|
148
177
|
const params = assertRecord(paramsInput ?? {}, 'events/poll params must be an object');
|
|
149
178
|
const name = String(params.name ?? '').trim();
|
|
150
179
|
if (!name)
|
|
151
|
-
|
|
180
|
+
invalidParams('events/poll requires name');
|
|
152
181
|
const cursor = params.cursor === undefined || params.cursor === null
|
|
153
182
|
? null
|
|
154
183
|
: String(params.cursor);
|
|
@@ -161,7 +190,7 @@ function normalizePollParams(paramsInput) {
|
|
|
161
190
|
!Number.isInteger(maxEventsRaw) ||
|
|
162
191
|
maxEventsRaw < 1 ||
|
|
163
192
|
maxEventsRaw > 500) {
|
|
164
|
-
|
|
193
|
+
invalidParams('maxEvents must be an integer between 1 and 500');
|
|
165
194
|
}
|
|
166
195
|
return {
|
|
167
196
|
name,
|
|
@@ -171,10 +200,22 @@ function normalizePollParams(paramsInput) {
|
|
|
171
200
|
maxEvents: maxEventsRaw,
|
|
172
201
|
};
|
|
173
202
|
}
|
|
203
|
+
function encodeListCursor(offset) {
|
|
204
|
+
return `${LIST_CURSOR_PREFIX}${offset}`;
|
|
205
|
+
}
|
|
206
|
+
function decodeListCursor(value) {
|
|
207
|
+
if (typeof value !== 'string' || !value.startsWith(LIST_CURSOR_PREFIX)) {
|
|
208
|
+
invalidParams('events/list cursor is invalid');
|
|
209
|
+
}
|
|
210
|
+
const offset = Number(value.slice(LIST_CURSOR_PREFIX.length));
|
|
211
|
+
if (!Number.isInteger(offset) || offset < 0) {
|
|
212
|
+
invalidParams('events/list cursor is invalid');
|
|
213
|
+
}
|
|
214
|
+
return offset;
|
|
215
|
+
}
|
|
174
216
|
export class McpEventsProvider {
|
|
175
217
|
events = new Map();
|
|
176
|
-
|
|
177
|
-
instructions;
|
|
218
|
+
listPageSize;
|
|
178
219
|
constructor(options) {
|
|
179
220
|
if (!options || !Array.isArray(options.events)) {
|
|
180
221
|
throw new Error('MCP Events provider requires events[]');
|
|
@@ -192,59 +233,79 @@ export class McpEventsProvider {
|
|
|
192
233
|
poll: input.poll,
|
|
193
234
|
});
|
|
194
235
|
}
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
236
|
+
const pageSize = options.listPageSize ?? 100;
|
|
237
|
+
if (!Number.isInteger(pageSize) || pageSize < 1 || pageSize > 1000) {
|
|
238
|
+
throw new Error('MCP Events listPageSize must be between 1 and 1000');
|
|
239
|
+
}
|
|
240
|
+
this.listPageSize = pageSize;
|
|
200
241
|
}
|
|
201
242
|
canHandle(method) {
|
|
202
|
-
return
|
|
203
|
-
method === 'events/list' ||
|
|
204
|
-
method === 'events/poll');
|
|
243
|
+
return method === 'events/list' || method === 'events/poll';
|
|
205
244
|
}
|
|
206
|
-
listEvents() {
|
|
207
|
-
|
|
245
|
+
listEvents(paramsInput) {
|
|
246
|
+
const params = paramsInput === undefined
|
|
247
|
+
? {}
|
|
248
|
+
: assertRecord(paramsInput, 'events/list params must be an object');
|
|
249
|
+
const offset = params.cursor === undefined || params.cursor === null
|
|
250
|
+
? 0
|
|
251
|
+
: decodeListCursor(params.cursor);
|
|
252
|
+
const all = [...this.events.values()].map(({ descriptor }) => descriptor);
|
|
253
|
+
if (offset > all.length)
|
|
254
|
+
invalidParams('events/list cursor is out of range');
|
|
255
|
+
const events = all.slice(offset, offset + this.listPageSize);
|
|
256
|
+
const nextOffset = offset + events.length;
|
|
257
|
+
return {
|
|
258
|
+
events,
|
|
259
|
+
...(nextOffset < all.length
|
|
260
|
+
? { nextCursor: encodeListCursor(nextOffset) }
|
|
261
|
+
: {}),
|
|
262
|
+
};
|
|
208
263
|
}
|
|
209
264
|
async handleRequest(request, context) {
|
|
210
265
|
try {
|
|
211
266
|
switch (request.method) {
|
|
212
|
-
case 'server/discover':
|
|
213
|
-
return this.ok(request.id, {
|
|
214
|
-
supportedVersions: this.supportedVersions,
|
|
215
|
-
capabilities: {
|
|
216
|
-
experimental: {
|
|
217
|
-
[MCP_EVENTS_CAPABILITY_KEY]: MCP_EVENTS_CAPABILITY,
|
|
218
|
-
},
|
|
219
|
-
},
|
|
220
|
-
instructions: this.instructions,
|
|
221
|
-
});
|
|
222
267
|
case 'events/list':
|
|
223
|
-
return this.ok(request.id,
|
|
224
|
-
events: this.listEvents(),
|
|
225
|
-
});
|
|
268
|
+
return this.ok(request.id, this.listEvents(request.params));
|
|
226
269
|
case 'events/poll':
|
|
227
270
|
return this.ok(request.id, await this.poll(request.params, context));
|
|
228
271
|
default:
|
|
229
|
-
return this.error(request.id,
|
|
272
|
+
return this.error(request.id, MCP_EVENTS_ERROR.METHOD_NOT_FOUND, `Method not found: ${request.method}`);
|
|
230
273
|
}
|
|
231
274
|
}
|
|
232
275
|
catch (error) {
|
|
233
|
-
|
|
276
|
+
if (error instanceof McpEventsProtocolError) {
|
|
277
|
+
return this.error(request.id, error.code, error.message, error.data);
|
|
278
|
+
}
|
|
279
|
+
return this.error(request.id, MCP_EVENTS_ERROR.INTERNAL_ERROR, error instanceof Error ? error.message : 'Internal error');
|
|
234
280
|
}
|
|
235
281
|
}
|
|
236
282
|
async poll(paramsInput, context) {
|
|
237
283
|
const params = normalizePollParams(paramsInput);
|
|
238
284
|
const registered = this.events.get(params.name);
|
|
239
285
|
if (!registered) {
|
|
240
|
-
throw new
|
|
286
|
+
throw new McpEventsProtocolError(MCP_EVENTS_ERROR.NOT_FOUND, `Unknown event: ${params.name}`, { kind: 'event' });
|
|
287
|
+
}
|
|
288
|
+
try {
|
|
289
|
+
assertJsonSchemaValue(registered.descriptor.inputSchema, params.arguments);
|
|
290
|
+
}
|
|
291
|
+
catch (error) {
|
|
292
|
+
throw new McpEventsProtocolError(MCP_EVENTS_ERROR.INVALID_PARAMS, error instanceof Error ? error.message : 'Invalid event arguments');
|
|
241
293
|
}
|
|
242
|
-
const
|
|
294
|
+
const rawResult = await registered.poll({
|
|
243
295
|
...params,
|
|
244
296
|
context,
|
|
245
|
-
})
|
|
246
|
-
if (
|
|
247
|
-
throw new Error(`MCP provider ${params.name} returned poll result
|
|
297
|
+
});
|
|
298
|
+
if (!rawResult || Array.isArray(rawResult) || typeof rawResult !== 'object') {
|
|
299
|
+
throw new Error(`MCP provider ${params.name} returned invalid poll result`);
|
|
300
|
+
}
|
|
301
|
+
const result = rawResult;
|
|
302
|
+
const cursor = result.cursor === null
|
|
303
|
+
? null
|
|
304
|
+
: typeof result.cursor === 'string'
|
|
305
|
+
? result.cursor
|
|
306
|
+
: undefined;
|
|
307
|
+
if (cursor === undefined) {
|
|
308
|
+
throw new Error(`MCP provider ${params.name} returned poll result without string|null cursor`);
|
|
248
309
|
}
|
|
249
310
|
if (!Array.isArray(result.events)) {
|
|
250
311
|
throw new Error(`MCP provider ${params.name} returned poll result without events[]`);
|
|
@@ -277,7 +338,7 @@ export class McpEventsProvider {
|
|
|
277
338
|
}
|
|
278
339
|
return {
|
|
279
340
|
events,
|
|
280
|
-
cursor
|
|
341
|
+
cursor,
|
|
281
342
|
truncated,
|
|
282
343
|
hasMore,
|
|
283
344
|
nextPollMs,
|
|
@@ -286,8 +347,16 @@ export class McpEventsProvider {
|
|
|
286
347
|
ok(id, result) {
|
|
287
348
|
return { jsonrpc: '2.0', id, result };
|
|
288
349
|
}
|
|
289
|
-
error(id, code, message) {
|
|
290
|
-
return {
|
|
350
|
+
error(id, code, message, data) {
|
|
351
|
+
return {
|
|
352
|
+
jsonrpc: '2.0',
|
|
353
|
+
id,
|
|
354
|
+
error: {
|
|
355
|
+
code,
|
|
356
|
+
message,
|
|
357
|
+
...(data === undefined ? {} : { data }),
|
|
358
|
+
},
|
|
359
|
+
};
|
|
291
360
|
}
|
|
292
361
|
}
|
|
293
362
|
export function createMcpEventsProvider(options) {
|
|
@@ -2,6 +2,8 @@ import type { EventDescriptor, EventOccurrence, JsonRpcRequest, JsonRpcResponse
|
|
|
2
2
|
export interface PersistedMcpOccurrence {
|
|
3
3
|
sequence: number;
|
|
4
4
|
serverId: string;
|
|
5
|
+
/** Client-side durable subscription identity when this log is used by EI. */
|
|
6
|
+
subscriptionId?: string | null;
|
|
5
7
|
event: EventOccurrence;
|
|
6
8
|
}
|
|
7
9
|
export interface McpEventOccurrenceStore {
|
|
@@ -21,7 +23,7 @@ export interface PollEventsParams {
|
|
|
21
23
|
}
|
|
22
24
|
export interface PollEventsResult {
|
|
23
25
|
events: EventOccurrence[];
|
|
24
|
-
cursor: string;
|
|
26
|
+
cursor: string | null;
|
|
25
27
|
truncated: boolean;
|
|
26
28
|
hasMore: boolean;
|
|
27
29
|
nextPollMs: number;
|
|
@@ -26,27 +26,20 @@ export class ExperimentalMcpEventsServer {
|
|
|
26
26
|
async handleRequest(request) {
|
|
27
27
|
try {
|
|
28
28
|
switch (request.method) {
|
|
29
|
-
case 'server/discover':
|
|
30
|
-
return this.ok(request.id, {
|
|
31
|
-
supportedVersions: ['2026-07-28'],
|
|
32
|
-
capabilities: {
|
|
33
|
-
experimental: {
|
|
34
|
-
'io.modelcontextprotocol.experimental/events': {
|
|
35
|
-
status: 'draft',
|
|
36
|
-
designDate: '2026-02-19',
|
|
37
|
-
methods: ['events/list', 'events/poll'],
|
|
38
|
-
listChanged: false,
|
|
39
|
-
},
|
|
40
|
-
},
|
|
41
|
-
},
|
|
42
|
-
instructions: 'Experimental MCP Events provider. Events follow the Triggers & Events WG design sketch; this is not finalized MCP conformance.',
|
|
43
|
-
});
|
|
44
29
|
case 'events/list':
|
|
45
30
|
return this.ok(request.id, {
|
|
46
31
|
events: [...this.events.values()].map((entry) => entry.descriptor),
|
|
47
32
|
});
|
|
48
|
-
case 'events/poll':
|
|
49
|
-
|
|
33
|
+
case 'events/poll': {
|
|
34
|
+
const params = (request.params ?? {});
|
|
35
|
+
if (!params.name || typeof params.name !== 'string') {
|
|
36
|
+
return this.error(request.id, -32602, 'events/poll requires name');
|
|
37
|
+
}
|
|
38
|
+
if (!this.events.has(params.name)) {
|
|
39
|
+
return this.error(request.id, -32011, `Unknown event: ${params.name}`, { kind: 'event' });
|
|
40
|
+
}
|
|
41
|
+
return this.ok(request.id, this.poll(params));
|
|
42
|
+
}
|
|
50
43
|
default:
|
|
51
44
|
return this.error(request.id, -32601, `Method not found: ${request.method}`);
|
|
52
45
|
}
|
|
@@ -116,7 +109,15 @@ export class ExperimentalMcpEventsServer {
|
|
|
116
109
|
ok(id, result) {
|
|
117
110
|
return { jsonrpc: '2.0', id, result };
|
|
118
111
|
}
|
|
119
|
-
error(id, code, message) {
|
|
120
|
-
return {
|
|
112
|
+
error(id, code, message, data) {
|
|
113
|
+
return {
|
|
114
|
+
jsonrpc: '2.0',
|
|
115
|
+
id,
|
|
116
|
+
error: {
|
|
117
|
+
code,
|
|
118
|
+
message,
|
|
119
|
+
...(data === undefined ? {} : { data }),
|
|
120
|
+
},
|
|
121
|
+
};
|
|
121
122
|
}
|
|
122
123
|
}
|
|
@@ -26,6 +26,7 @@ export interface EventDescriptor {
|
|
|
26
26
|
delivery: Array<'poll' | 'push' | 'webhook'>;
|
|
27
27
|
inputSchema: Record<string, unknown>;
|
|
28
28
|
payloadSchema: Record<string, unknown>;
|
|
29
|
+
_meta?: Record<string, unknown>;
|
|
29
30
|
}
|
|
30
31
|
export interface EventOccurrence<TData extends Record<string, unknown> = Record<string, unknown>> {
|
|
31
32
|
eventId: string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mcp-event-intelligence",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Durable temporal event intelligence for agent hosts: host-owned MCP event discovery, composite triggers, derived events, and targeted wakeups.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -11,6 +11,33 @@ export interface HostMcpClientLike {
|
|
|
11
11
|
getServerCapabilities?(): unknown;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
export interface HostMcpEventDeliveryCallbacks {
|
|
15
|
+
subscriptionId: string;
|
|
16
|
+
name: string;
|
|
17
|
+
arguments: Record<string, unknown>;
|
|
18
|
+
cursor: string | null;
|
|
19
|
+
onEvent(event: Record<string, unknown>): Promise<void>;
|
|
20
|
+
onActive(state?: Record<string, unknown>): Promise<void>;
|
|
21
|
+
onGap(state?: Record<string, unknown>): Promise<void>;
|
|
22
|
+
onError(error: unknown): void;
|
|
23
|
+
onTerminated(detail?: unknown): Promise<void>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type HostMcpEventDeliveryHandle =
|
|
27
|
+
| (() => void | Promise<void>)
|
|
28
|
+
| {
|
|
29
|
+
cursor?: string | null;
|
|
30
|
+
truncated?: boolean;
|
|
31
|
+
close?(): void | Promise<void>;
|
|
32
|
+
cancel?(): void | Promise<void>;
|
|
33
|
+
unsubscribe?(): void | Promise<void>;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export type HostMcpEventDeliveryOpener = (
|
|
37
|
+
input: HostMcpEventDeliveryCallbacks,
|
|
38
|
+
) => HostMcpEventDeliveryHandle | Promise<HostMcpEventDeliveryHandle>;
|
|
39
|
+
|
|
40
|
+
|
|
14
41
|
export interface HostMcpConnectionOptions {
|
|
15
42
|
connectionId?: string;
|
|
16
43
|
id?: string;
|
|
@@ -18,6 +45,8 @@ export interface HostMcpConnectionOptions {
|
|
|
18
45
|
serverId?: string;
|
|
19
46
|
client?: HostMcpClientLike;
|
|
20
47
|
request?: (method: string, params?: unknown) => Promise<unknown>;
|
|
48
|
+
getCapabilities?: () => unknown | Promise<unknown>;
|
|
49
|
+
capabilities?: Record<string, unknown>;
|
|
21
50
|
enabled?: boolean;
|
|
22
51
|
/** Isolation partition used by the shared EI host. Defaults to \"default\". */
|
|
23
52
|
scopeId?: string;
|
|
@@ -25,6 +54,12 @@ export interface HostMcpConnectionOptions {
|
|
|
25
54
|
maxEvents?: number;
|
|
26
55
|
/** Maximum number of immediately drained pages when the provider reports hasMore. */
|
|
27
56
|
maxPollBatches?: number;
|
|
57
|
+
/** Prefer one advertised delivery mode when the host can provide it. */
|
|
58
|
+
preferredDelivery?: 'poll' | 'push' | 'webhook';
|
|
59
|
+
/** Host-owned adapter for events/stream delivery. */
|
|
60
|
+
openEventStream?: HostMcpEventDeliveryOpener;
|
|
61
|
+
/** Host-owned adapter for webhook subscription/receiver delivery. */
|
|
62
|
+
createWebhookSubscription?: HostMcpEventDeliveryOpener;
|
|
28
63
|
}
|
|
29
64
|
|
|
30
65
|
export interface HostMcpRegistry {
|
|
@@ -113,9 +148,14 @@ export function createHostMcpEventsConnection(
|
|
|
113
148
|
connectionId: string;
|
|
114
149
|
serverId: string;
|
|
115
150
|
request(method: string, params?: unknown): Promise<unknown>;
|
|
151
|
+
getCapabilities(): unknown | Promise<unknown>;
|
|
116
152
|
enabled: boolean;
|
|
117
153
|
pollIntervalMs: number;
|
|
118
154
|
maxEvents: number;
|
|
155
|
+
maxPollBatches: number;
|
|
156
|
+
preferredDelivery: 'poll' | 'push' | 'webhook' | null;
|
|
157
|
+
openEventStream: HostMcpEventDeliveryOpener | null;
|
|
158
|
+
createWebhookSubscription: HostMcpEventDeliveryOpener | null;
|
|
119
159
|
};
|
|
120
160
|
|
|
121
161
|
export function createEventIntelligenceHost(
|
|
@@ -34,6 +34,7 @@ export class CompositeEventConsumer {
|
|
|
34
34
|
serverId,
|
|
35
35
|
provider,
|
|
36
36
|
traceId,
|
|
37
|
+
subscriptionArguments,
|
|
37
38
|
}) {
|
|
38
39
|
const correlatable = await mcpOccurrenceToCorrelatableEvent(
|
|
39
40
|
event,
|
|
@@ -41,6 +42,7 @@ export class CompositeEventConsumer {
|
|
|
41
42
|
traceId,
|
|
42
43
|
...(provider ? { provider } : {}),
|
|
43
44
|
...(serverId ? { serverId } : {}),
|
|
45
|
+
subscriptionArguments: subscriptionArguments ?? {},
|
|
44
46
|
},
|
|
45
47
|
);
|
|
46
48
|
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
function assertRecord(value, message) {
|
|
2
|
+
if (!value || Array.isArray(value) || typeof value !== 'object') {
|
|
3
|
+
throw new Error(message);
|
|
4
|
+
}
|
|
5
|
+
return value;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function schemaTypes(schema) {
|
|
9
|
+
if (Array.isArray(schema?.type)) return schema.type.map(String);
|
|
10
|
+
return schema?.type === undefined ? [] : [String(schema.type)];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function assertJsonSchemaValue(schemaInput, value, path = '$') {
|
|
14
|
+
if (!schemaInput || Array.isArray(schemaInput) || typeof schemaInput !== 'object') {
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
const schema = schemaInput;
|
|
18
|
+
|
|
19
|
+
if (
|
|
20
|
+
Object.prototype.hasOwnProperty.call(schema, 'const') &&
|
|
21
|
+
!Object.is(schema.const, value)
|
|
22
|
+
) {
|
|
23
|
+
throw new Error(`JSON Schema violation at ${path}: const mismatch`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (
|
|
27
|
+
Array.isArray(schema.enum) &&
|
|
28
|
+
!schema.enum.some((entry) => Object.is(entry, value))
|
|
29
|
+
) {
|
|
30
|
+
throw new Error(`JSON Schema violation at ${path}: value not in enum`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const types = schemaTypes(schema);
|
|
34
|
+
if (types.length) {
|
|
35
|
+
const matches = types.some((type) => {
|
|
36
|
+
if (type === 'null') return value === null;
|
|
37
|
+
if (type === 'array') return Array.isArray(value);
|
|
38
|
+
if (type === 'object') {
|
|
39
|
+
return value !== null && !Array.isArray(value) && typeof value === 'object';
|
|
40
|
+
}
|
|
41
|
+
if (type === 'integer') return Number.isInteger(value);
|
|
42
|
+
if (type === 'number') return typeof value === 'number' && Number.isFinite(value);
|
|
43
|
+
if (type === 'string') return typeof value === 'string';
|
|
44
|
+
if (type === 'boolean') return typeof value === 'boolean';
|
|
45
|
+
return false;
|
|
46
|
+
});
|
|
47
|
+
if (!matches) {
|
|
48
|
+
throw new Error(
|
|
49
|
+
`JSON Schema violation at ${path}: expected ${types.join('|')}`,
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (typeof value === 'string') {
|
|
55
|
+
if (typeof schema.minLength === 'number' && value.length < schema.minLength) {
|
|
56
|
+
throw new Error(`JSON Schema violation at ${path}: string too short`);
|
|
57
|
+
}
|
|
58
|
+
if (typeof schema.maxLength === 'number' && value.length > schema.maxLength) {
|
|
59
|
+
throw new Error(`JSON Schema violation at ${path}: string too long`);
|
|
60
|
+
}
|
|
61
|
+
if (typeof schema.pattern === 'string') {
|
|
62
|
+
const re = new RegExp(schema.pattern);
|
|
63
|
+
if (!re.test(value)) {
|
|
64
|
+
throw new Error(`JSON Schema violation at ${path}: pattern mismatch`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
70
|
+
if (typeof schema.minimum === 'number' && value < schema.minimum) {
|
|
71
|
+
throw new Error(`JSON Schema violation at ${path}: below minimum`);
|
|
72
|
+
}
|
|
73
|
+
if (typeof schema.maximum === 'number' && value > schema.maximum) {
|
|
74
|
+
throw new Error(`JSON Schema violation at ${path}: above maximum`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (value !== null && !Array.isArray(value) && typeof value === 'object') {
|
|
79
|
+
const record = value;
|
|
80
|
+
const required = Array.isArray(schema.required)
|
|
81
|
+
? schema.required.map(String)
|
|
82
|
+
: [];
|
|
83
|
+
for (const key of required) {
|
|
84
|
+
if (!Object.prototype.hasOwnProperty.call(record, key)) {
|
|
85
|
+
throw new Error(
|
|
86
|
+
`JSON Schema violation at ${path}: missing required property ${key}`,
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const properties =
|
|
92
|
+
schema.properties &&
|
|
93
|
+
!Array.isArray(schema.properties) &&
|
|
94
|
+
typeof schema.properties === 'object'
|
|
95
|
+
? schema.properties
|
|
96
|
+
: {};
|
|
97
|
+
|
|
98
|
+
for (const [key, childSchema] of Object.entries(properties)) {
|
|
99
|
+
if (
|
|
100
|
+
Object.prototype.hasOwnProperty.call(record, key) &&
|
|
101
|
+
childSchema &&
|
|
102
|
+
!Array.isArray(childSchema) &&
|
|
103
|
+
typeof childSchema === 'object'
|
|
104
|
+
) {
|
|
105
|
+
assertJsonSchemaValue(childSchema, record[key], `${path}.${key}`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (schema.additionalProperties === false) {
|
|
110
|
+
for (const key of Object.keys(record)) {
|
|
111
|
+
if (!Object.prototype.hasOwnProperty.call(properties, key)) {
|
|
112
|
+
throw new Error(
|
|
113
|
+
`JSON Schema violation at ${path}: unexpected property ${key}`,
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (
|
|
121
|
+
Array.isArray(value) &&
|
|
122
|
+
schema.items &&
|
|
123
|
+
!Array.isArray(schema.items) &&
|
|
124
|
+
typeof schema.items === 'object'
|
|
125
|
+
) {
|
|
126
|
+
value.forEach((entry, index) =>
|
|
127
|
+
assertJsonSchemaValue(schema.items, entry, `${path}[${index}]`),
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function assertJsonObject(value, message) {
|
|
133
|
+
return assertRecord(value, message);
|
|
134
|
+
}
|