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,3 +1,4 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
1
2
|
import * as z from 'zod/v4';
|
|
2
3
|
import {
|
|
3
4
|
McpEventOccurrenceSchema,
|
|
@@ -6,8 +7,11 @@ import {
|
|
|
6
7
|
DEFAULT_EVENT_SCOPE_ID,
|
|
7
8
|
normalizeEventScopeId,
|
|
8
9
|
} from './persistent-event-store.mjs';
|
|
10
|
+
import { assertJsonSchemaValue } from './json-schema.mjs';
|
|
9
11
|
|
|
10
|
-
|
|
12
|
+
export { assertJsonSchemaValue } from './json-schema.mjs';
|
|
13
|
+
|
|
14
|
+
export const MCP_EVENTS_EXTENSION_ID = 'io.modelcontextprotocol/events';
|
|
11
15
|
const UnknownResultSchema = z.unknown();
|
|
12
16
|
|
|
13
17
|
function assertObject(value, message) {
|
|
@@ -17,6 +21,32 @@ function assertObject(value, message) {
|
|
|
17
21
|
return value;
|
|
18
22
|
}
|
|
19
23
|
|
|
24
|
+
function stableValue(value) {
|
|
25
|
+
if (Array.isArray(value)) return value.map(stableValue);
|
|
26
|
+
if (value && typeof value === 'object') {
|
|
27
|
+
return Object.fromEntries(
|
|
28
|
+
Object.keys(value)
|
|
29
|
+
.sort()
|
|
30
|
+
.map((key) => [key, stableValue(value[key])]),
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
return value;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function stableJson(value) {
|
|
37
|
+
return JSON.stringify(stableValue(value ?? {}));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function eventSubscriptionId(connectionId, eventName, args = {}) {
|
|
41
|
+
const digest = createHash('sha256')
|
|
42
|
+
.update(
|
|
43
|
+
`${String(connectionId)}\u0000${String(eventName)}\u0000${stableJson(args)}`,
|
|
44
|
+
)
|
|
45
|
+
.digest('hex')
|
|
46
|
+
.slice(0, 24);
|
|
47
|
+
return `mcp_sub_${digest}`;
|
|
48
|
+
}
|
|
49
|
+
|
|
20
50
|
function normalizePositiveInteger(value, fallback, min, max, name) {
|
|
21
51
|
const normalized = Number(value ?? fallback);
|
|
22
52
|
if (!Number.isInteger(normalized) || normalized < min || normalized > max) {
|
|
@@ -36,18 +66,38 @@ function normalizeConnection(connection) {
|
|
|
36
66
|
`Host-managed MCP Events connection ${connectionId} requires request(method, params)`,
|
|
37
67
|
);
|
|
38
68
|
}
|
|
69
|
+
if (typeof connection.getCapabilities !== 'function') {
|
|
70
|
+
throw new Error(
|
|
71
|
+
`Host-managed MCP Events connection ${connectionId} requires getCapabilities()`,
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const preferredDelivery = connection.preferredDelivery
|
|
76
|
+
? String(connection.preferredDelivery)
|
|
77
|
+
: null;
|
|
78
|
+
if (
|
|
79
|
+
preferredDelivery &&
|
|
80
|
+
!['poll', 'push', 'webhook'].includes(preferredDelivery)
|
|
81
|
+
) {
|
|
82
|
+
throw new Error(
|
|
83
|
+
`Host-managed MCP Events connection ${connectionId} has invalid preferredDelivery`,
|
|
84
|
+
);
|
|
85
|
+
}
|
|
39
86
|
|
|
40
87
|
return {
|
|
41
88
|
connectionId,
|
|
42
|
-
scopeId: normalizeEventScopeId(
|
|
89
|
+
scopeId: normalizeEventScopeId(
|
|
90
|
+
connection.scopeId ?? DEFAULT_EVENT_SCOPE_ID,
|
|
91
|
+
),
|
|
43
92
|
serverId: String(connection.serverId || `host-mcp:${connectionId}`),
|
|
44
93
|
request: connection.request,
|
|
94
|
+
getCapabilities: connection.getCapabilities,
|
|
45
95
|
identity: connection.identity ?? connection.request,
|
|
46
96
|
enabled: connection.enabled !== false,
|
|
47
97
|
pollIntervalMs: normalizePositiveInteger(
|
|
48
98
|
connection.pollIntervalMs,
|
|
49
99
|
5000,
|
|
50
|
-
|
|
100
|
+
250,
|
|
51
101
|
300000,
|
|
52
102
|
'pollIntervalMs',
|
|
53
103
|
),
|
|
@@ -65,120 +115,157 @@ function normalizeConnection(connection) {
|
|
|
65
115
|
32,
|
|
66
116
|
'maxPollBatches',
|
|
67
117
|
),
|
|
118
|
+
preferredDelivery,
|
|
119
|
+
openEventStream:
|
|
120
|
+
typeof connection.openEventStream === 'function'
|
|
121
|
+
? connection.openEventStream
|
|
122
|
+
: null,
|
|
123
|
+
createWebhookSubscription:
|
|
124
|
+
typeof connection.createWebhookSubscription === 'function'
|
|
125
|
+
? connection.createWebhookSubscription
|
|
126
|
+
: null,
|
|
68
127
|
};
|
|
69
128
|
}
|
|
70
129
|
|
|
71
130
|
/**
|
|
72
|
-
* Adapt an MCP client
|
|
131
|
+
* Adapt an MCP client already connected and authorized by the host.
|
|
73
132
|
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
133
|
+
* Poll works with the ordinary MCP request surface. Push and webhook remain
|
|
134
|
+
* host-owned transport concerns: a host can provide openEventStream() and/or
|
|
135
|
+
* createWebhookSubscription() adapters without giving Event Intelligence
|
|
136
|
+
* transport credentials or callback infrastructure.
|
|
76
137
|
*/
|
|
77
138
|
export function createHostMcpEventsConnection({
|
|
78
139
|
connectionId,
|
|
79
140
|
serverId,
|
|
80
141
|
client,
|
|
81
142
|
request,
|
|
143
|
+
getCapabilities,
|
|
144
|
+
capabilities,
|
|
82
145
|
enabled = true,
|
|
83
146
|
pollIntervalMs = 5000,
|
|
84
147
|
maxEvents = 100,
|
|
85
148
|
maxPollBatches = 4,
|
|
86
149
|
scopeId = DEFAULT_EVENT_SCOPE_ID,
|
|
150
|
+
preferredDelivery,
|
|
151
|
+
openEventStream,
|
|
152
|
+
createWebhookSubscription,
|
|
87
153
|
} = {}) {
|
|
88
154
|
const directRequest =
|
|
89
155
|
typeof request === 'function'
|
|
90
156
|
? request
|
|
91
157
|
: client && typeof client.request === 'function'
|
|
92
158
|
? async (method, params) => {
|
|
93
|
-
if (
|
|
94
|
-
method === 'server/discover' &&
|
|
95
|
-
typeof client.getServerCapabilities === 'function'
|
|
96
|
-
) {
|
|
97
|
-
const capabilities = client.getServerCapabilities();
|
|
98
|
-
if (capabilities) return { capabilities };
|
|
99
|
-
}
|
|
100
|
-
|
|
101
159
|
const message = {
|
|
102
160
|
method,
|
|
103
161
|
...(params === undefined ? {} : { params }),
|
|
104
162
|
};
|
|
105
|
-
|
|
106
|
-
// Events is currently an experimental/custom surface, so the
|
|
107
|
-
// official SDK requires an explicit result schema for those calls.
|
|
108
|
-
if (method === 'server/discover') {
|
|
109
|
-
return client.request(message);
|
|
110
|
-
}
|
|
111
163
|
return client.request(message, UnknownResultSchema);
|
|
112
164
|
}
|
|
113
165
|
: null;
|
|
114
166
|
|
|
167
|
+
const directCapabilities =
|
|
168
|
+
typeof getCapabilities === 'function'
|
|
169
|
+
? getCapabilities
|
|
170
|
+
: client && typeof client.getServerCapabilities === 'function'
|
|
171
|
+
? async () => client.getServerCapabilities()
|
|
172
|
+
: capabilities && typeof capabilities === 'object'
|
|
173
|
+
? async () => capabilities
|
|
174
|
+
: null;
|
|
175
|
+
|
|
115
176
|
return normalizeConnection({
|
|
116
177
|
connectionId,
|
|
117
178
|
serverId,
|
|
118
179
|
request: directRequest,
|
|
180
|
+
getCapabilities: directCapabilities,
|
|
119
181
|
identity: client ?? request ?? directRequest,
|
|
120
182
|
enabled,
|
|
121
183
|
pollIntervalMs,
|
|
122
184
|
maxEvents,
|
|
123
185
|
maxPollBatches,
|
|
124
186
|
scopeId,
|
|
187
|
+
preferredDelivery,
|
|
188
|
+
openEventStream,
|
|
189
|
+
createWebhookSubscription,
|
|
125
190
|
});
|
|
126
191
|
}
|
|
127
192
|
|
|
128
|
-
function
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
193
|
+
function sourceDescriptor(raw) {
|
|
194
|
+
const descriptor = assertObject(raw, 'Invalid MCP event descriptor');
|
|
195
|
+
const name = String(descriptor.name || '').trim();
|
|
196
|
+
if (!name) throw new Error('MCP event descriptor requires name');
|
|
197
|
+
const delivery = Array.isArray(descriptor.delivery)
|
|
198
|
+
? descriptor.delivery.map(String)
|
|
199
|
+
: [];
|
|
200
|
+
if (
|
|
201
|
+
delivery.length === 0 ||
|
|
202
|
+
delivery.some((mode) => !['poll', 'push', 'webhook'].includes(mode))
|
|
203
|
+
) {
|
|
204
|
+
throw new Error(`MCP event descriptor ${name} has invalid delivery modes`);
|
|
205
|
+
}
|
|
206
|
+
return {
|
|
207
|
+
name,
|
|
208
|
+
description: String(descriptor.description || ''),
|
|
209
|
+
delivery,
|
|
210
|
+
inputSchema:
|
|
211
|
+
descriptor.inputSchema &&
|
|
212
|
+
!Array.isArray(descriptor.inputSchema) &&
|
|
213
|
+
typeof descriptor.inputSchema === 'object'
|
|
214
|
+
? descriptor.inputSchema
|
|
215
|
+
: {},
|
|
216
|
+
payloadSchema:
|
|
217
|
+
descriptor.payloadSchema &&
|
|
218
|
+
!Array.isArray(descriptor.payloadSchema) &&
|
|
219
|
+
typeof descriptor.payloadSchema === 'object'
|
|
220
|
+
? descriptor.payloadSchema
|
|
221
|
+
: {},
|
|
222
|
+
...(descriptor._meta &&
|
|
223
|
+
!Array.isArray(descriptor._meta) &&
|
|
224
|
+
typeof descriptor._meta === 'object'
|
|
225
|
+
? { _meta: descriptor._meta }
|
|
226
|
+
: {}),
|
|
227
|
+
};
|
|
132
228
|
}
|
|
133
229
|
|
|
134
|
-
|
|
135
|
-
if (
|
|
230
|
+
function normalizeCursor(cursor, label) {
|
|
231
|
+
if (cursor === null) return null;
|
|
232
|
+
if (typeof cursor === 'string') return cursor;
|
|
233
|
+
throw new Error(`${label} returned invalid cursor; expected string|null`);
|
|
234
|
+
}
|
|
136
235
|
|
|
137
|
-
|
|
138
|
-
|
|
236
|
+
function normalizeNextPollMs(value, fallback) {
|
|
237
|
+
const result = Number(value ?? fallback);
|
|
238
|
+
if (!Number.isInteger(result) || result < 0 || result > 300000) {
|
|
239
|
+
throw new Error('events/poll returned invalid nextPollMs');
|
|
139
240
|
}
|
|
241
|
+
return result;
|
|
242
|
+
}
|
|
140
243
|
|
|
141
|
-
|
|
142
|
-
if (
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
if (type === 'object') return value !== null && !Array.isArray(value) && typeof value === 'object';
|
|
147
|
-
if (type === 'integer') return Number.isInteger(value);
|
|
148
|
-
if (type === 'number') return typeof value === 'number' && Number.isFinite(value);
|
|
149
|
-
return typeof value === type;
|
|
150
|
-
});
|
|
151
|
-
if (!matches) {
|
|
152
|
-
throw new Error(
|
|
153
|
-
`MCP event payload violates schema at ${path}: expected ${types.join('|')}`,
|
|
154
|
-
);
|
|
155
|
-
}
|
|
244
|
+
function supportsMode(connection, mode) {
|
|
245
|
+
if (mode === 'poll') return true;
|
|
246
|
+
if (mode === 'push') return typeof connection.openEventStream === 'function';
|
|
247
|
+
if (mode === 'webhook') {
|
|
248
|
+
return typeof connection.createWebhookSubscription === 'function';
|
|
156
249
|
}
|
|
250
|
+
return false;
|
|
251
|
+
}
|
|
157
252
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
}
|
|
167
|
-
const properties = schema.properties && typeof schema.properties === 'object'
|
|
168
|
-
? schema.properties
|
|
169
|
-
: {};
|
|
170
|
-
for (const [key, childSchema] of Object.entries(properties)) {
|
|
171
|
-
if (key in value) {
|
|
172
|
-
assertJsonSchemaValue(childSchema, value[key], `${path}.${key}`);
|
|
173
|
-
}
|
|
174
|
-
}
|
|
253
|
+
function chooseDelivery(connection, descriptor) {
|
|
254
|
+
const advertised = descriptor.delivery;
|
|
255
|
+
if (
|
|
256
|
+
connection.preferredDelivery &&
|
|
257
|
+
advertised.includes(connection.preferredDelivery) &&
|
|
258
|
+
supportsMode(connection, connection.preferredDelivery)
|
|
259
|
+
) {
|
|
260
|
+
return connection.preferredDelivery;
|
|
175
261
|
}
|
|
176
262
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
263
|
+
for (const mode of ['push', 'webhook', 'poll']) {
|
|
264
|
+
if (advertised.includes(mode) && supportsMode(connection, mode)) {
|
|
265
|
+
return mode;
|
|
266
|
+
}
|
|
181
267
|
}
|
|
268
|
+
return null;
|
|
182
269
|
}
|
|
183
270
|
|
|
184
271
|
export class McpEventsClientManager {
|
|
@@ -197,10 +284,11 @@ export class McpEventsClientManager {
|
|
|
197
284
|
this.connections = new Map();
|
|
198
285
|
this.now = now;
|
|
199
286
|
this.descriptors = new Map();
|
|
200
|
-
this.
|
|
287
|
+
this.timers = new Map();
|
|
201
288
|
this.lastErrors = new Map();
|
|
202
289
|
this.scopeContexts = new Map();
|
|
203
290
|
this.pollInFlight = new Map();
|
|
291
|
+
this.deliverySessions = new Map();
|
|
204
292
|
this.started = false;
|
|
205
293
|
|
|
206
294
|
for (const connection of connections) {
|
|
@@ -222,11 +310,7 @@ export class McpEventsClientManager {
|
|
|
222
310
|
triggerControl: null,
|
|
223
311
|
};
|
|
224
312
|
|
|
225
|
-
if (
|
|
226
|
-
!context ||
|
|
227
|
-
!context.store ||
|
|
228
|
-
!context.compositeEventConsumer
|
|
229
|
-
) {
|
|
313
|
+
if (!context || !context.store || !context.compositeEventConsumer) {
|
|
230
314
|
throw new Error(
|
|
231
315
|
`Invalid Event Intelligence scope context for ${connection.scopeId}`,
|
|
232
316
|
);
|
|
@@ -255,7 +339,7 @@ export class McpEventsClientManager {
|
|
|
255
339
|
events = await this.discoverConnection(connection.connectionId);
|
|
256
340
|
}
|
|
257
341
|
if (connection.enabled !== false && this.started) {
|
|
258
|
-
this.scheduleConnection(connection);
|
|
342
|
+
this.scheduleConnection(connection, 0);
|
|
259
343
|
}
|
|
260
344
|
return {
|
|
261
345
|
connectionId: connection.connectionId,
|
|
@@ -263,34 +347,60 @@ export class McpEventsClientManager {
|
|
|
263
347
|
};
|
|
264
348
|
}
|
|
265
349
|
|
|
350
|
+
async closeSession(subscriptionId) {
|
|
351
|
+
const session = this.deliverySessions.get(subscriptionId);
|
|
352
|
+
if (!session) return false;
|
|
353
|
+
this.deliverySessions.delete(subscriptionId);
|
|
354
|
+
try {
|
|
355
|
+
if (typeof session.close === 'function') await session.close();
|
|
356
|
+
} catch {
|
|
357
|
+
// Closing stale delivery sessions is best effort.
|
|
358
|
+
}
|
|
359
|
+
return true;
|
|
360
|
+
}
|
|
361
|
+
|
|
266
362
|
async detachConnection(connectionId) {
|
|
267
|
-
const timer = this.
|
|
268
|
-
if (timer)
|
|
269
|
-
this.
|
|
363
|
+
const timer = this.timers.get(connectionId);
|
|
364
|
+
if (timer) clearTimeout(timer);
|
|
365
|
+
this.timers.delete(connectionId);
|
|
270
366
|
this.descriptors.delete(connectionId);
|
|
271
367
|
this.lastErrors.delete(connectionId);
|
|
272
368
|
this.pollInFlight.delete(connectionId);
|
|
273
369
|
|
|
370
|
+
for (const [subscriptionId, session] of this.deliverySessions) {
|
|
371
|
+
if (session.connectionId === connectionId) {
|
|
372
|
+
await this.closeSession(subscriptionId);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
274
376
|
const connection = this.connections.get(connectionId);
|
|
275
377
|
const context = connection ? await this.scopeContext(connection) : null;
|
|
276
378
|
if (connection) {
|
|
277
|
-
this.scopeContexts.delete(
|
|
379
|
+
this.scopeContexts.delete(
|
|
380
|
+
`${connection.connectionId}::${connection.scopeId}`,
|
|
381
|
+
);
|
|
278
382
|
}
|
|
279
383
|
const removed = this.connections.delete(connectionId);
|
|
280
384
|
if (removed && context) {
|
|
281
|
-
const sources = context.store.listEventSources({
|
|
385
|
+
const sources = context.store.listEventSources({
|
|
386
|
+
connectionIds: [connectionId],
|
|
387
|
+
});
|
|
282
388
|
for (const source of sources) {
|
|
283
389
|
if (source.enabled === false) continue;
|
|
284
390
|
const register = context.triggerControl?.registerEventSource
|
|
285
|
-
? (
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
391
|
+
? (sourceInput, actor) =>
|
|
392
|
+
context.triggerControl.registerEventSource(sourceInput, actor)
|
|
393
|
+
: async (sourceInput) => this.registerEventSource(sourceInput);
|
|
394
|
+
await register(
|
|
395
|
+
{
|
|
396
|
+
...source,
|
|
397
|
+
enabled: false,
|
|
398
|
+
},
|
|
399
|
+
{
|
|
400
|
+
type: 'system',
|
|
401
|
+
principal_id: 'event-intelligence:host-mcp-client',
|
|
402
|
+
},
|
|
403
|
+
);
|
|
294
404
|
}
|
|
295
405
|
}
|
|
296
406
|
return removed;
|
|
@@ -309,87 +419,110 @@ export class McpEventsClientManager {
|
|
|
309
419
|
async discoverConnection(connectionId) {
|
|
310
420
|
const connection = this.connections.get(connectionId);
|
|
311
421
|
if (!connection) {
|
|
312
|
-
throw new Error(
|
|
422
|
+
throw new Error(
|
|
423
|
+
`Unknown host-managed MCP Events connection: ${connectionId}`,
|
|
424
|
+
);
|
|
313
425
|
}
|
|
314
426
|
|
|
315
427
|
const context = await this.scopeContext(connection);
|
|
316
|
-
const
|
|
317
|
-
const
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
428
|
+
const capabilities = (await connection.getCapabilities()) ?? {};
|
|
429
|
+
const extensions =
|
|
430
|
+
capabilities.extensions &&
|
|
431
|
+
!Array.isArray(capabilities.extensions) &&
|
|
432
|
+
typeof capabilities.extensions === 'object'
|
|
433
|
+
? capabilities.extensions
|
|
434
|
+
: {};
|
|
435
|
+
if (
|
|
436
|
+
!Object.prototype.hasOwnProperty.call(
|
|
437
|
+
extensions,
|
|
438
|
+
MCP_EVENTS_EXTENSION_ID,
|
|
439
|
+
)
|
|
440
|
+
) {
|
|
322
441
|
const error = new Error(
|
|
323
|
-
`MCP server ${connectionId} does not advertise
|
|
442
|
+
`MCP server ${connectionId} does not advertise ${MCP_EVENTS_EXTENSION_ID}`,
|
|
324
443
|
);
|
|
325
444
|
error.code = 'MCP_EVENTS_CAPABILITY_UNAVAILABLE';
|
|
326
445
|
throw error;
|
|
327
446
|
}
|
|
328
447
|
|
|
329
|
-
const listed = await this.rpc(connection, 'events/list', {});
|
|
330
|
-
if (!Array.isArray(listed?.events)) {
|
|
331
|
-
throw new Error(`MCP server ${connectionId} returned invalid events/list`);
|
|
332
|
-
}
|
|
333
|
-
|
|
334
448
|
const descriptors = [];
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
:
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
449
|
+
const names = new Set();
|
|
450
|
+
let cursor = null;
|
|
451
|
+
do {
|
|
452
|
+
const listed = await this.rpc(
|
|
453
|
+
connection,
|
|
454
|
+
'events/list',
|
|
455
|
+
cursor ? { cursor } : {},
|
|
456
|
+
);
|
|
457
|
+
if (!Array.isArray(listed?.events)) {
|
|
458
|
+
throw new Error(
|
|
459
|
+
`MCP server ${connectionId} returned invalid events/list`,
|
|
460
|
+
);
|
|
461
|
+
}
|
|
462
|
+
for (const raw of listed.events) {
|
|
463
|
+
const descriptor = sourceDescriptor(raw);
|
|
464
|
+
if (names.has(descriptor.name)) {
|
|
465
|
+
throw new Error(
|
|
466
|
+
`MCP server ${connectionId} returned duplicate event descriptor ${descriptor.name}`,
|
|
467
|
+
);
|
|
468
|
+
}
|
|
469
|
+
names.add(descriptor.name);
|
|
470
|
+
descriptors.push(descriptor);
|
|
471
|
+
}
|
|
472
|
+
cursor =
|
|
473
|
+
listed?.nextCursor === undefined || listed?.nextCursor === null
|
|
474
|
+
? null
|
|
475
|
+
: String(listed.nextCursor);
|
|
476
|
+
} while (cursor);
|
|
356
477
|
|
|
357
|
-
|
|
358
|
-
const
|
|
478
|
+
for (const normalized of descriptors) {
|
|
479
|
+
const sourceId =
|
|
480
|
+
`mcp:${connection.connectionId}:${normalized.name}`;
|
|
481
|
+
const existing = context.store
|
|
482
|
+
.listEventSources()
|
|
359
483
|
.find((source) => source.sourceId === sourceId);
|
|
360
484
|
const same =
|
|
361
485
|
existing &&
|
|
362
486
|
existing.enabled !== false &&
|
|
363
487
|
existing.connectionId === connection.connectionId &&
|
|
364
488
|
existing.serverId === connection.serverId &&
|
|
365
|
-
existing.eventName === name &&
|
|
366
|
-
JSON.stringify(existing.delivery) ===
|
|
367
|
-
|
|
368
|
-
JSON.stringify(existing.
|
|
489
|
+
existing.eventName === normalized.name &&
|
|
490
|
+
JSON.stringify(existing.delivery) ===
|
|
491
|
+
JSON.stringify(normalized.delivery) &&
|
|
492
|
+
JSON.stringify(existing.inputSchema || {}) ===
|
|
493
|
+
JSON.stringify(normalized.inputSchema) &&
|
|
494
|
+
JSON.stringify(existing.payloadSchema || {}) ===
|
|
495
|
+
JSON.stringify(normalized.payloadSchema);
|
|
369
496
|
|
|
370
497
|
if (!same) {
|
|
371
498
|
const register = context.triggerControl?.registerEventSource
|
|
372
|
-
? (
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
499
|
+
? (sourceInput, actor) =>
|
|
500
|
+
context.triggerControl.registerEventSource(sourceInput, actor)
|
|
501
|
+
: async (sourceInput) => this.registerEventSource(sourceInput);
|
|
502
|
+
await register(
|
|
503
|
+
{
|
|
504
|
+
sourceId,
|
|
505
|
+
connectionId: connection.connectionId,
|
|
506
|
+
serverId: connection.serverId,
|
|
507
|
+
eventName: normalized.name,
|
|
508
|
+
description: normalized.description,
|
|
509
|
+
delivery: normalized.delivery,
|
|
510
|
+
inputSchema: normalized.inputSchema,
|
|
511
|
+
payloadSchema: normalized.payloadSchema,
|
|
512
|
+
enabled: true,
|
|
513
|
+
experimental: true,
|
|
514
|
+
metadata: {
|
|
515
|
+
transport: 'host-managed-mcp',
|
|
516
|
+
hostManaged: true,
|
|
517
|
+
extensionId: MCP_EVENTS_EXTENSION_ID,
|
|
518
|
+
...(normalized._meta ? { descriptorMeta: normalized._meta } : {}),
|
|
519
|
+
},
|
|
388
520
|
},
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
521
|
+
{
|
|
522
|
+
type: 'system',
|
|
523
|
+
principal_id: 'event-intelligence:host-mcp-client',
|
|
524
|
+
},
|
|
525
|
+
);
|
|
393
526
|
}
|
|
394
527
|
}
|
|
395
528
|
|
|
@@ -413,117 +546,401 @@ export class McpEventsClientManager {
|
|
|
413
546
|
this.lastErrors.set(connection.connectionId, message);
|
|
414
547
|
results.push({
|
|
415
548
|
connectionId: connection.connectionId,
|
|
416
|
-
status:
|
|
417
|
-
|
|
549
|
+
status:
|
|
550
|
+
error?.code === 'MCP_EVENTS_CAPABILITY_UNAVAILABLE'
|
|
551
|
+
? 'ignored_no_events'
|
|
552
|
+
: 'error',
|
|
553
|
+
...(error?.code === 'MCP_EVENTS_CAPABILITY_UNAVAILABLE'
|
|
554
|
+
? {}
|
|
555
|
+
: { error: message }),
|
|
418
556
|
});
|
|
419
557
|
}
|
|
420
558
|
}
|
|
421
559
|
return results;
|
|
422
560
|
}
|
|
423
561
|
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
562
|
+
desiredSubscriptions(connection, context) {
|
|
563
|
+
const descriptors = this.descriptors.get(connection.connectionId) || [];
|
|
564
|
+
const byName = new Map(descriptors.map((item) => [item.name, item]));
|
|
565
|
+
const desired = new Map();
|
|
566
|
+
const triggers =
|
|
567
|
+
typeof context.triggerControl?.listTriggers === 'function'
|
|
568
|
+
? context.triggerControl.listTriggers()
|
|
569
|
+
: typeof context.store?.listTriggers === 'function'
|
|
570
|
+
? context.store.listTriggers().map((definition) => ({
|
|
571
|
+
definition,
|
|
572
|
+
state:
|
|
573
|
+
typeof context.store.getTriggerState === 'function'
|
|
574
|
+
? context.store.getTriggerState(
|
|
575
|
+
definition.triggerId,
|
|
576
|
+
definition.version,
|
|
577
|
+
)
|
|
578
|
+
: null,
|
|
579
|
+
}))
|
|
580
|
+
: [];
|
|
581
|
+
|
|
582
|
+
for (const { definition, state } of triggers) {
|
|
583
|
+
if (state?.status !== 'active') continue;
|
|
584
|
+
for (const clause of definition.clauses ?? []) {
|
|
585
|
+
if (clause.serverId !== connection.serverId) continue;
|
|
586
|
+
const descriptor = byName.get(clause.event);
|
|
587
|
+
if (!descriptor) continue;
|
|
588
|
+
const args =
|
|
589
|
+
clause.arguments &&
|
|
590
|
+
!Array.isArray(clause.arguments) &&
|
|
591
|
+
typeof clause.arguments === 'object'
|
|
592
|
+
? clause.arguments
|
|
593
|
+
: {};
|
|
594
|
+
const subscriptionId = eventSubscriptionId(
|
|
595
|
+
connection.connectionId,
|
|
596
|
+
clause.event,
|
|
597
|
+
args,
|
|
598
|
+
);
|
|
599
|
+
if (!desired.has(subscriptionId)) {
|
|
600
|
+
desired.set(subscriptionId, {
|
|
601
|
+
subscriptionId,
|
|
602
|
+
eventName: clause.event,
|
|
603
|
+
arguments: args,
|
|
604
|
+
descriptor,
|
|
605
|
+
});
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
return desired;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
async ingestOccurrence(connection, context, subscription, raw) {
|
|
614
|
+
const descriptor = subscription.descriptor;
|
|
615
|
+
const occurrence = McpEventOccurrenceSchema.parse(raw);
|
|
616
|
+
if (occurrence.name !== descriptor.name) {
|
|
617
|
+
throw new Error(
|
|
618
|
+
`MCP event name mismatch: expected ${descriptor.name}, got ${occurrence.name}`,
|
|
619
|
+
);
|
|
427
620
|
}
|
|
621
|
+
assertJsonSchemaValue(descriptor.payloadSchema, occurrence.data);
|
|
622
|
+
|
|
623
|
+
const receipt = await context.store.appendMcpOccurrence(
|
|
624
|
+
connection.serverId,
|
|
625
|
+
occurrence,
|
|
626
|
+
subscription.subscriptionId,
|
|
627
|
+
);
|
|
628
|
+
if (!receipt.accepted) return { accepted: false, occurrence };
|
|
629
|
+
|
|
630
|
+
await context.compositeEventConsumer.ingestMcpOccurrence({
|
|
631
|
+
event: occurrence,
|
|
632
|
+
serverId: connection.serverId,
|
|
633
|
+
provider: 'mcp',
|
|
634
|
+
traceId:
|
|
635
|
+
`mcp:${connection.serverId}:${subscription.subscriptionId}:${occurrence.eventId}`,
|
|
636
|
+
subscriptionArguments: subscription.arguments,
|
|
637
|
+
});
|
|
638
|
+
return { accepted: true, occurrence };
|
|
639
|
+
}
|
|
428
640
|
|
|
641
|
+
async saveSubscriptionState(
|
|
642
|
+
connection,
|
|
643
|
+
context,
|
|
644
|
+
subscription,
|
|
645
|
+
patch = {},
|
|
646
|
+
) {
|
|
647
|
+
const previous = context.store.getMcpClientState(
|
|
648
|
+
connection.connectionId,
|
|
649
|
+
subscription.eventName,
|
|
650
|
+
subscription.arguments,
|
|
651
|
+
);
|
|
652
|
+
return context.store.putMcpClientState({
|
|
653
|
+
...(previous || {}),
|
|
654
|
+
connectionId: connection.connectionId,
|
|
655
|
+
serverId: connection.serverId,
|
|
656
|
+
eventName: subscription.eventName,
|
|
657
|
+
arguments: subscription.arguments,
|
|
658
|
+
subscriptionId: subscription.subscriptionId,
|
|
659
|
+
...patch,
|
|
660
|
+
});
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
async pollSubscription(
|
|
664
|
+
connection,
|
|
665
|
+
context,
|
|
666
|
+
subscription,
|
|
667
|
+
{ respectSchedule = false } = {},
|
|
668
|
+
) {
|
|
429
669
|
const store = context.store;
|
|
430
|
-
const consumer = context.compositeEventConsumer;
|
|
431
670
|
const current = store.getMcpClientState(
|
|
432
671
|
connection.connectionId,
|
|
433
|
-
|
|
672
|
+
subscription.eventName,
|
|
673
|
+
subscription.arguments,
|
|
434
674
|
);
|
|
675
|
+
if (
|
|
676
|
+
respectSchedule &&
|
|
677
|
+
current?.nextPollAt &&
|
|
678
|
+
Date.parse(current.nextPollAt) > this.now().getTime()
|
|
679
|
+
) {
|
|
680
|
+
return {
|
|
681
|
+
subscriptionId: subscription.subscriptionId,
|
|
682
|
+
eventName: subscription.eventName,
|
|
683
|
+
delivery: 'poll',
|
|
684
|
+
status: 'scheduled',
|
|
685
|
+
accepted: 0,
|
|
686
|
+
cursor: current.cursor,
|
|
687
|
+
nextPollAt: current.nextPollAt,
|
|
688
|
+
};
|
|
689
|
+
}
|
|
435
690
|
|
|
436
691
|
let cursor = current?.cursor ?? null;
|
|
437
692
|
let lastEventAt = current?.lastEventAt;
|
|
438
693
|
let accepted = 0;
|
|
439
694
|
let batches = 0;
|
|
440
695
|
let hasMore = false;
|
|
696
|
+
let truncated = false;
|
|
697
|
+
let nextPollMs = connection.pollIntervalMs;
|
|
441
698
|
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
});
|
|
450
|
-
|
|
451
|
-
if (typeof result?.cursor !== 'string' || !Array.isArray(result.events)) {
|
|
452
|
-
throw new Error(
|
|
453
|
-
`MCP server ${connection.connectionId} returned invalid events/poll`,
|
|
454
|
-
);
|
|
455
|
-
}
|
|
456
|
-
|
|
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;
|
|
699
|
+
do {
|
|
700
|
+
const result = await this.rpc(connection, 'events/poll', {
|
|
701
|
+
name: subscription.eventName,
|
|
702
|
+
arguments: subscription.arguments,
|
|
703
|
+
cursor,
|
|
704
|
+
maxEvents: connection.maxEvents,
|
|
705
|
+
});
|
|
471
706
|
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
707
|
+
if (!Array.isArray(result?.events)) {
|
|
708
|
+
throw new Error(
|
|
709
|
+
`MCP server ${connection.connectionId} returned invalid events/poll`,
|
|
710
|
+
);
|
|
711
|
+
}
|
|
712
|
+
cursor = normalizeCursor(
|
|
713
|
+
result.cursor,
|
|
714
|
+
`MCP server ${connection.connectionId} events/poll`,
|
|
715
|
+
);
|
|
716
|
+
hasMore = result.hasMore === true;
|
|
717
|
+
truncated = truncated || result.truncated === true;
|
|
718
|
+
nextPollMs = normalizeNextPollMs(
|
|
719
|
+
result.nextPollMs,
|
|
720
|
+
connection.pollIntervalMs,
|
|
721
|
+
);
|
|
481
722
|
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
723
|
+
for (const raw of result.events) {
|
|
724
|
+
const receipt = await this.ingestOccurrence(
|
|
725
|
+
connection,
|
|
726
|
+
context,
|
|
727
|
+
subscription,
|
|
728
|
+
raw,
|
|
729
|
+
);
|
|
730
|
+
if (!receipt.accepted) continue;
|
|
731
|
+
accepted += 1;
|
|
732
|
+
lastEventAt = receipt.occurrence.timestamp;
|
|
733
|
+
}
|
|
485
734
|
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
735
|
+
batches += 1;
|
|
736
|
+
const nextPollAt = new Date(
|
|
737
|
+
this.now().getTime() + (hasMore ? 0 : nextPollMs),
|
|
738
|
+
).toISOString();
|
|
739
|
+
await this.saveSubscriptionState(
|
|
740
|
+
connection,
|
|
741
|
+
context,
|
|
742
|
+
subscription,
|
|
743
|
+
{
|
|
744
|
+
deliveryMode: 'poll',
|
|
493
745
|
cursor,
|
|
746
|
+
truncated,
|
|
747
|
+
nextPollAt,
|
|
494
748
|
...(lastEventAt ? { lastEventAt } : {}),
|
|
495
|
-
}
|
|
496
|
-
|
|
749
|
+
},
|
|
750
|
+
);
|
|
751
|
+
} while (hasMore && batches < connection.maxPollBatches);
|
|
752
|
+
|
|
753
|
+
this.lastErrors.delete(connection.connectionId);
|
|
754
|
+
return {
|
|
755
|
+
subscriptionId: subscription.subscriptionId,
|
|
756
|
+
eventName: subscription.eventName,
|
|
757
|
+
delivery: 'poll',
|
|
758
|
+
status: 'ok',
|
|
759
|
+
accepted,
|
|
760
|
+
cursor,
|
|
761
|
+
truncated,
|
|
762
|
+
hasMore,
|
|
763
|
+
batches,
|
|
764
|
+
nextPollMs,
|
|
765
|
+
batchLimitReached: hasMore && batches >= connection.maxPollBatches,
|
|
766
|
+
};
|
|
767
|
+
}
|
|
497
768
|
|
|
498
|
-
|
|
769
|
+
async ensureDeliverySession(
|
|
770
|
+
connection,
|
|
771
|
+
context,
|
|
772
|
+
subscription,
|
|
773
|
+
delivery,
|
|
774
|
+
) {
|
|
775
|
+
const existing = this.deliverySessions.get(subscription.subscriptionId);
|
|
776
|
+
if (existing?.delivery === delivery) {
|
|
499
777
|
return {
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
batches,
|
|
506
|
-
batchLimitReached: hasMore && batches >= connection.maxPollBatches,
|
|
778
|
+
subscriptionId: subscription.subscriptionId,
|
|
779
|
+
eventName: subscription.eventName,
|
|
780
|
+
delivery,
|
|
781
|
+
status: 'active',
|
|
782
|
+
accepted: existing.accepted,
|
|
507
783
|
};
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
784
|
+
}
|
|
785
|
+
if (existing) await this.closeSession(subscription.subscriptionId);
|
|
786
|
+
|
|
787
|
+
const current = context.store.getMcpClientState(
|
|
788
|
+
connection.connectionId,
|
|
789
|
+
subscription.eventName,
|
|
790
|
+
subscription.arguments,
|
|
791
|
+
);
|
|
792
|
+
let accepted = 0;
|
|
793
|
+
|
|
794
|
+
const onEvent = async (raw) => {
|
|
795
|
+
const receipt = await this.ingestOccurrence(
|
|
796
|
+
connection,
|
|
797
|
+
context,
|
|
798
|
+
subscription,
|
|
799
|
+
raw,
|
|
800
|
+
);
|
|
801
|
+
if (receipt.accepted) accepted += 1;
|
|
802
|
+
if (Object.prototype.hasOwnProperty.call(raw ?? {}, 'cursor')) {
|
|
803
|
+
await this.saveSubscriptionState(
|
|
804
|
+
connection,
|
|
805
|
+
context,
|
|
806
|
+
subscription,
|
|
807
|
+
{
|
|
808
|
+
deliveryMode: delivery,
|
|
809
|
+
cursor: normalizeCursor(
|
|
810
|
+
raw.cursor,
|
|
811
|
+
`${delivery} event ${subscription.eventName}`,
|
|
812
|
+
),
|
|
813
|
+
...(receipt.accepted
|
|
814
|
+
? { lastEventAt: receipt.occurrence.timestamp }
|
|
815
|
+
: {}),
|
|
816
|
+
},
|
|
817
|
+
);
|
|
818
|
+
}
|
|
819
|
+
};
|
|
820
|
+
|
|
821
|
+
const onActive = async (update = {}) => {
|
|
822
|
+
const patch = {
|
|
823
|
+
deliveryMode: delivery,
|
|
824
|
+
...(Object.prototype.hasOwnProperty.call(update, 'cursor')
|
|
825
|
+
? {
|
|
826
|
+
cursor: normalizeCursor(
|
|
827
|
+
update.cursor,
|
|
828
|
+
`${delivery} active ${subscription.eventName}`,
|
|
829
|
+
),
|
|
830
|
+
}
|
|
831
|
+
: {}),
|
|
832
|
+
...(typeof update.truncated === 'boolean'
|
|
833
|
+
? { truncated: update.truncated }
|
|
834
|
+
: {}),
|
|
835
|
+
};
|
|
836
|
+
await this.saveSubscriptionState(
|
|
837
|
+
connection,
|
|
838
|
+
context,
|
|
839
|
+
subscription,
|
|
840
|
+
patch,
|
|
841
|
+
);
|
|
842
|
+
};
|
|
843
|
+
|
|
844
|
+
const onGap = async (update = {}) => {
|
|
845
|
+
await onActive({ ...update, truncated: true });
|
|
846
|
+
};
|
|
847
|
+
|
|
848
|
+
const onError = (error) => {
|
|
849
|
+
this.lastErrors.set(
|
|
850
|
+
connection.connectionId,
|
|
851
|
+
error instanceof Error ? error.message : String(error),
|
|
852
|
+
);
|
|
853
|
+
};
|
|
854
|
+
|
|
855
|
+
const onTerminated = async (detail) => {
|
|
856
|
+
this.deliverySessions.delete(subscription.subscriptionId);
|
|
857
|
+
if (detail) {
|
|
858
|
+
this.lastErrors.set(
|
|
859
|
+
connection.connectionId,
|
|
860
|
+
typeof detail === 'string' ? detail : JSON.stringify(detail),
|
|
861
|
+
);
|
|
862
|
+
}
|
|
863
|
+
};
|
|
864
|
+
|
|
865
|
+
const open =
|
|
866
|
+
delivery === 'push'
|
|
867
|
+
? connection.openEventStream
|
|
868
|
+
: connection.createWebhookSubscription;
|
|
869
|
+
const handle = await open({
|
|
870
|
+
subscriptionId: subscription.subscriptionId,
|
|
871
|
+
name: subscription.eventName,
|
|
872
|
+
arguments: subscription.arguments,
|
|
873
|
+
cursor: current?.cursor ?? null,
|
|
874
|
+
onEvent,
|
|
875
|
+
onActive,
|
|
876
|
+
onGap,
|
|
877
|
+
onError,
|
|
878
|
+
onTerminated,
|
|
879
|
+
});
|
|
880
|
+
|
|
881
|
+
const close =
|
|
882
|
+
typeof handle === 'function'
|
|
883
|
+
? handle
|
|
884
|
+
: typeof handle?.close === 'function'
|
|
885
|
+
? () => handle.close()
|
|
886
|
+
: typeof handle?.cancel === 'function'
|
|
887
|
+
? () => handle.cancel()
|
|
888
|
+
: typeof handle?.unsubscribe === 'function'
|
|
889
|
+
? () => handle.unsubscribe()
|
|
890
|
+
: async () => {};
|
|
891
|
+
|
|
892
|
+
if (handle && Object.prototype.hasOwnProperty.call(handle, 'cursor')) {
|
|
893
|
+
await onActive({
|
|
894
|
+
cursor: handle.cursor,
|
|
895
|
+
truncated: handle.truncated === true,
|
|
517
896
|
});
|
|
518
|
-
|
|
897
|
+
} else {
|
|
898
|
+
await this.saveSubscriptionState(
|
|
899
|
+
connection,
|
|
900
|
+
context,
|
|
901
|
+
subscription,
|
|
902
|
+
{
|
|
903
|
+
deliveryMode: delivery,
|
|
904
|
+
cursor: current?.cursor ?? null,
|
|
905
|
+
},
|
|
906
|
+
);
|
|
519
907
|
}
|
|
908
|
+
|
|
909
|
+
this.deliverySessions.set(subscription.subscriptionId, {
|
|
910
|
+
connectionId: connection.connectionId,
|
|
911
|
+
delivery,
|
|
912
|
+
close,
|
|
913
|
+
get accepted() {
|
|
914
|
+
return accepted;
|
|
915
|
+
},
|
|
916
|
+
});
|
|
917
|
+
this.lastErrors.delete(connection.connectionId);
|
|
918
|
+
|
|
919
|
+
return {
|
|
920
|
+
subscriptionId: subscription.subscriptionId,
|
|
921
|
+
eventName: subscription.eventName,
|
|
922
|
+
delivery,
|
|
923
|
+
status: 'active',
|
|
924
|
+
accepted,
|
|
925
|
+
};
|
|
520
926
|
}
|
|
521
927
|
|
|
522
|
-
async
|
|
928
|
+
async reconcileDeliverySessions(connection, desiredIds) {
|
|
929
|
+
for (const [subscriptionId, session] of this.deliverySessions) {
|
|
930
|
+
if (
|
|
931
|
+
session.connectionId === connection.connectionId &&
|
|
932
|
+
!desiredIds.has(subscriptionId)
|
|
933
|
+
) {
|
|
934
|
+
await this.closeSession(subscriptionId);
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
async pollConnection(connectionId, options = {}) {
|
|
523
940
|
const existing = this.pollInFlight.get(connectionId);
|
|
524
941
|
if (existing) return existing;
|
|
525
942
|
|
|
526
|
-
const task = this.pollConnectionOnce(connectionId);
|
|
943
|
+
const task = this.pollConnectionOnce(connectionId, options);
|
|
527
944
|
this.pollInFlight.set(connectionId, task);
|
|
528
945
|
try {
|
|
529
946
|
return await task;
|
|
@@ -534,10 +951,15 @@ export class McpEventsClientManager {
|
|
|
534
951
|
}
|
|
535
952
|
}
|
|
536
953
|
|
|
537
|
-
async pollConnectionOnce(
|
|
954
|
+
async pollConnectionOnce(
|
|
955
|
+
connectionId,
|
|
956
|
+
{ respectSchedule = false } = {},
|
|
957
|
+
) {
|
|
538
958
|
const connection = this.connections.get(connectionId);
|
|
539
959
|
if (!connection) {
|
|
540
|
-
throw new Error(
|
|
960
|
+
throw new Error(
|
|
961
|
+
`Unknown host-managed MCP Events connection: ${connectionId}`,
|
|
962
|
+
);
|
|
541
963
|
}
|
|
542
964
|
|
|
543
965
|
const context = await this.scopeContext(connection);
|
|
@@ -546,57 +968,133 @@ export class McpEventsClientManager {
|
|
|
546
968
|
descriptors = await this.discoverConnection(connectionId);
|
|
547
969
|
}
|
|
548
970
|
|
|
971
|
+
const desired = this.desiredSubscriptions(connection, context);
|
|
972
|
+
const desiredIds = new Set(desired.keys());
|
|
549
973
|
const results = [];
|
|
550
|
-
|
|
551
|
-
|
|
974
|
+
|
|
975
|
+
for (const subscription of desired.values()) {
|
|
976
|
+
const delivery = chooseDelivery(connection, subscription.descriptor);
|
|
977
|
+
if (!delivery) {
|
|
978
|
+
results.push({
|
|
979
|
+
subscriptionId: subscription.subscriptionId,
|
|
980
|
+
eventName: subscription.eventName,
|
|
981
|
+
status: 'delivery_not_supported',
|
|
982
|
+
delivery: null,
|
|
983
|
+
accepted: 0,
|
|
984
|
+
});
|
|
985
|
+
continue;
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
if (delivery === 'poll') {
|
|
989
|
+
await this.closeSession(subscription.subscriptionId);
|
|
990
|
+
results.push(
|
|
991
|
+
await this.pollSubscription(
|
|
992
|
+
connection,
|
|
993
|
+
context,
|
|
994
|
+
subscription,
|
|
995
|
+
{ respectSchedule },
|
|
996
|
+
),
|
|
997
|
+
);
|
|
998
|
+
} else {
|
|
999
|
+
results.push(
|
|
1000
|
+
await this.ensureDeliverySession(
|
|
1001
|
+
connection,
|
|
1002
|
+
context,
|
|
1003
|
+
subscription,
|
|
1004
|
+
delivery,
|
|
1005
|
+
),
|
|
1006
|
+
);
|
|
1007
|
+
}
|
|
552
1008
|
}
|
|
1009
|
+
|
|
1010
|
+
await this.reconcileDeliverySessions(connection, desiredIds);
|
|
553
1011
|
return results;
|
|
554
1012
|
}
|
|
555
1013
|
|
|
556
|
-
async pollAll() {
|
|
1014
|
+
async pollAll(options = {}) {
|
|
557
1015
|
const results = [];
|
|
558
1016
|
for (const connection of this.connections.values()) {
|
|
559
1017
|
try {
|
|
560
1018
|
results.push({
|
|
561
1019
|
connectionId: connection.connectionId,
|
|
562
|
-
results: await this.pollConnection(
|
|
1020
|
+
results: await this.pollConnection(
|
|
1021
|
+
connection.connectionId,
|
|
1022
|
+
options,
|
|
1023
|
+
),
|
|
563
1024
|
});
|
|
564
1025
|
} catch (error) {
|
|
1026
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1027
|
+
this.lastErrors.set(connection.connectionId, message);
|
|
565
1028
|
results.push({
|
|
566
1029
|
connectionId: connection.connectionId,
|
|
567
|
-
error:
|
|
1030
|
+
error: message,
|
|
568
1031
|
});
|
|
569
1032
|
}
|
|
570
1033
|
}
|
|
571
1034
|
return results;
|
|
572
1035
|
}
|
|
573
1036
|
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
1037
|
+
nextDelay(connection) {
|
|
1038
|
+
const context = this.scopeContexts.get(
|
|
1039
|
+
`${connection.connectionId}::${connection.scopeId}`,
|
|
1040
|
+
);
|
|
1041
|
+
if (!context) return connection.pollIntervalMs;
|
|
1042
|
+
const states = context.store.listMcpClientStates(
|
|
1043
|
+
connection.connectionId,
|
|
1044
|
+
);
|
|
1045
|
+
const now = this.now().getTime();
|
|
1046
|
+
const due = states
|
|
1047
|
+
.map((state) => Date.parse(state.nextPollAt || ''))
|
|
1048
|
+
.filter(Number.isFinite)
|
|
1049
|
+
.map((time) => Math.max(250, time - now));
|
|
1050
|
+
return due.length
|
|
1051
|
+
? Math.min(connection.pollIntervalMs, ...due)
|
|
1052
|
+
: connection.pollIntervalMs;
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
scheduleConnection(connection, delay = connection.pollIntervalMs) {
|
|
1056
|
+
const existing = this.timers.get(connection.connectionId);
|
|
1057
|
+
if (existing) clearTimeout(existing);
|
|
1058
|
+
if (!this.started) return;
|
|
1059
|
+
|
|
1060
|
+
const timer = setTimeout(async () => {
|
|
1061
|
+
try {
|
|
1062
|
+
await this.pollConnection(
|
|
1063
|
+
connection.connectionId,
|
|
1064
|
+
{ respectSchedule: true },
|
|
1065
|
+
);
|
|
1066
|
+
} catch (error) {
|
|
1067
|
+
this.lastErrors.set(
|
|
1068
|
+
connection.connectionId,
|
|
1069
|
+
error instanceof Error ? error.message : String(error),
|
|
1070
|
+
);
|
|
1071
|
+
} finally {
|
|
1072
|
+
if (
|
|
1073
|
+
this.started &&
|
|
1074
|
+
this.connections.get(connection.connectionId) === connection
|
|
1075
|
+
) {
|
|
1076
|
+
this.scheduleConnection(connection, this.nextDelay(connection));
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
}, Math.max(0, delay));
|
|
585
1080
|
timer.unref?.();
|
|
586
|
-
this.
|
|
1081
|
+
this.timers.set(connection.connectionId, timer);
|
|
587
1082
|
}
|
|
588
1083
|
|
|
589
1084
|
start() {
|
|
590
1085
|
this.started = true;
|
|
591
1086
|
for (const connection of this.connections.values()) {
|
|
592
|
-
this.scheduleConnection(connection);
|
|
1087
|
+
this.scheduleConnection(connection, 0);
|
|
593
1088
|
}
|
|
594
1089
|
}
|
|
595
1090
|
|
|
596
|
-
stop() {
|
|
1091
|
+
async stop() {
|
|
597
1092
|
this.started = false;
|
|
598
|
-
for (const timer of this.
|
|
599
|
-
this.
|
|
1093
|
+
for (const timer of this.timers.values()) clearTimeout(timer);
|
|
1094
|
+
this.timers.clear();
|
|
1095
|
+
for (const subscriptionId of [...this.deliverySessions.keys()]) {
|
|
1096
|
+
await this.closeSession(subscriptionId);
|
|
1097
|
+
}
|
|
600
1098
|
}
|
|
601
1099
|
|
|
602
1100
|
async drain() {
|
|
@@ -606,7 +1104,7 @@ export class McpEventsClientManager {
|
|
|
606
1104
|
}
|
|
607
1105
|
|
|
608
1106
|
async close() {
|
|
609
|
-
this.stop();
|
|
1107
|
+
await this.stop();
|
|
610
1108
|
await this.drain();
|
|
611
1109
|
}
|
|
612
1110
|
|
|
@@ -616,20 +1114,29 @@ export class McpEventsClientManager {
|
|
|
616
1114
|
`${connection.connectionId}::${connection.scopeId}`,
|
|
617
1115
|
);
|
|
618
1116
|
const store = context?.store ?? this.store;
|
|
1117
|
+
const states = store.listMcpClientStates(connection.connectionId);
|
|
619
1118
|
return {
|
|
620
1119
|
connectionId: connection.connectionId,
|
|
621
1120
|
scopeId: connection.scopeId,
|
|
622
1121
|
serverId: connection.serverId,
|
|
623
1122
|
hostManaged: true,
|
|
624
|
-
|
|
625
|
-
|
|
1123
|
+
extensionId: MCP_EVENTS_EXTENSION_ID,
|
|
1124
|
+
events: (this.descriptors.get(connection.connectionId) || []).map(
|
|
1125
|
+
(event) => ({
|
|
626
1126
|
name: event.name,
|
|
627
1127
|
delivery: event.delivery,
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
1128
|
+
subscriptions: states
|
|
1129
|
+
.filter((state) => state.eventName === event.name)
|
|
1130
|
+
.map((state) => ({
|
|
1131
|
+
subscriptionId: state.subscriptionId,
|
|
1132
|
+
arguments: state.arguments ?? {},
|
|
1133
|
+
delivery: state.deliveryMode,
|
|
1134
|
+
cursor: state.cursor,
|
|
1135
|
+
nextPollAt: state.nextPollAt ?? null,
|
|
1136
|
+
truncated: state.truncated ?? false,
|
|
1137
|
+
})),
|
|
1138
|
+
}),
|
|
1139
|
+
),
|
|
633
1140
|
error: this.lastErrors.get(connection.connectionId) ?? null,
|
|
634
1141
|
};
|
|
635
1142
|
});
|