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