mcp-event-intelligence 0.2.1 → 0.3.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 +46 -0
- package/README.md +64 -11
- package/bin/mcp-event-intelligence.mjs +1 -1
- package/dist/src/composite/matching.js +8 -0
- package/dist/src/intelligenceProtocol/activationSchemas.d.ts +67 -0
- package/dist/src/intelligenceProtocol/activationSchemas.js +51 -0
- package/dist/src/intelligenceProtocol/index.d.ts +6 -2
- package/dist/src/intelligenceProtocol/index.js +3 -1
- package/dist/src/intelligenceProtocol/triggerPlanSchemas.d.ts +158 -0
- package/dist/src/intelligenceProtocol/triggerPlanSchemas.js +45 -0
- package/dist/src/intelligenceProtocol/triggerSchemas.d.ts +80 -0
- package/dist/src/intelligenceProtocol/triggerSchemas.js +24 -0
- package/package.json +1 -1
- package/scripts/host-integration.d.mts +17 -0
- package/scripts/host-integration.mjs +20 -0
- package/scripts/lib/activation-hydrator.mjs +140 -0
- package/scripts/lib/local-event-intelligence-runtime.mjs +16 -2
- package/scripts/lib/trigger-inspector.mjs +1 -0
- package/scripts/lib/trigger-planner.mjs +247 -0
- package/scripts/mcp-stdio-server.mjs +123 -13
- package/server.json +2 -2
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import {
|
|
2
|
+
canonicalJson,
|
|
3
|
+
parseCompositeTriggerDefinition,
|
|
4
|
+
parseTriggerPlanInput,
|
|
5
|
+
sha256Hex,
|
|
6
|
+
} from '../../dist/src/intelligenceProtocol/index.js';
|
|
7
|
+
|
|
8
|
+
function schemaNodeAtPath(schema, path) {
|
|
9
|
+
if (!schema || typeof schema !== 'object') return null;
|
|
10
|
+
let node = schema;
|
|
11
|
+
for (const part of String(path).split('.')) {
|
|
12
|
+
const properties = node?.properties;
|
|
13
|
+
if (!properties || typeof properties !== 'object' || !(part in properties)) {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
node = properties[part];
|
|
17
|
+
}
|
|
18
|
+
return node;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function sourceSummary(source) {
|
|
22
|
+
return {
|
|
23
|
+
sourceId: source.sourceId,
|
|
24
|
+
connectionId: source.connectionId,
|
|
25
|
+
serverId: source.serverId,
|
|
26
|
+
eventName: source.eventName,
|
|
27
|
+
description: source.description ?? null,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function sourceError(code, message, sources = []) {
|
|
32
|
+
const error = new Error(message);
|
|
33
|
+
error.code = code;
|
|
34
|
+
if (sources.length) error.sources = sources.map(sourceSummary);
|
|
35
|
+
return error;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function validatePredicateAgainstSource(source, predicate, warnings) {
|
|
39
|
+
const schema = source.payloadSchema;
|
|
40
|
+
if (!schema || typeof schema !== 'object' || Object.keys(schema).length === 0) {
|
|
41
|
+
warnings.push({
|
|
42
|
+
code: 'SOURCE_SCHEMA_UNAVAILABLE',
|
|
43
|
+
event: source.eventName,
|
|
44
|
+
serverId: source.serverId,
|
|
45
|
+
path: predicate.path,
|
|
46
|
+
message: 'Source does not advertise a payload schema; predicate path cannot be prevalidated.',
|
|
47
|
+
});
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const node = schemaNodeAtPath(schema, predicate.path);
|
|
52
|
+
if (!node) {
|
|
53
|
+
throw sourceError(
|
|
54
|
+
'TRIGGER_PLAN_FIELD_UNAVAILABLE',
|
|
55
|
+
`Field ${predicate.path} is not advertised by ${source.serverId}/${source.eventName}`,
|
|
56
|
+
[source],
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (
|
|
61
|
+
['gt', 'gte', 'lt', 'lte'].includes(predicate.op) &&
|
|
62
|
+
node?.type &&
|
|
63
|
+
!['number', 'integer'].includes(node.type)
|
|
64
|
+
) {
|
|
65
|
+
throw sourceError(
|
|
66
|
+
'TRIGGER_PLAN_NUMERIC_FIELD_REQUIRED',
|
|
67
|
+
`Predicate ${predicate.op} requires a numeric field, but ${predicate.path} is ${node.type}`,
|
|
68
|
+
[source],
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function expressionFor(match, refs) {
|
|
74
|
+
if (typeof match === 'object' && match?.kind === 'count') {
|
|
75
|
+
if (!refs.includes(match.eventId)) {
|
|
76
|
+
const error = new Error(`Count match references unknown event id: ${match.eventId}`);
|
|
77
|
+
error.code = 'TRIGGER_PLAN_EVENT_REF_UNKNOWN';
|
|
78
|
+
throw error;
|
|
79
|
+
}
|
|
80
|
+
return { kind: 'count', ref: match.eventId, atLeast: match.atLeast };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (match === 'sequence') {
|
|
84
|
+
if (refs.length < 2) {
|
|
85
|
+
const error = new Error('Sequence match requires at least two events');
|
|
86
|
+
error.code = 'TRIGGER_PLAN_SEQUENCE_REQUIRES_MULTIPLE_EVENTS';
|
|
87
|
+
throw error;
|
|
88
|
+
}
|
|
89
|
+
return { kind: 'sequence', refs };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (match === 'any' || refs.length === 1) {
|
|
93
|
+
return { kind: 'anyOf', refs };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return { kind: 'allOf', refs };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function lifecycleFor(input = {}) {
|
|
100
|
+
return {
|
|
101
|
+
oneShot: input.oneShot ?? false,
|
|
102
|
+
cooldownMs: input.cooldownMs ?? 0,
|
|
103
|
+
completeOnGoal: input.completeOnGoal ?? false,
|
|
104
|
+
...(input.maxFirings !== undefined ? { maxFirings: input.maxFirings } : {}),
|
|
105
|
+
...(input.expiresAt ? { expiresAt: input.expiresAt } : {}),
|
|
106
|
+
...(input.leaseUntil ? { leaseUntil: input.leaseUntil } : {}),
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export class TriggerPlanner {
|
|
111
|
+
constructor({ store }) {
|
|
112
|
+
this.store = store;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async plan(input) {
|
|
116
|
+
const plan = parseTriggerPlanInput(input);
|
|
117
|
+
const activeSources = this.store.listEventSources({ enabledOnly: true });
|
|
118
|
+
const warnings = [];
|
|
119
|
+
const resolvedSources = [];
|
|
120
|
+
const seenIds = new Set();
|
|
121
|
+
|
|
122
|
+
const clauses = plan.events.map((event, index) => {
|
|
123
|
+
const id = String(event.id || `event_${index + 1}`);
|
|
124
|
+
if (seenIds.has(id)) {
|
|
125
|
+
const error = new Error(`Duplicate plan event id: ${id}`);
|
|
126
|
+
error.code = 'TRIGGER_PLAN_DUPLICATE_EVENT_ID';
|
|
127
|
+
throw error;
|
|
128
|
+
}
|
|
129
|
+
seenIds.add(id);
|
|
130
|
+
|
|
131
|
+
const matches = activeSources.filter((source) =>
|
|
132
|
+
source.eventName === event.event &&
|
|
133
|
+
(!event.serverId || source.serverId === event.serverId)
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
if (matches.length === 0) {
|
|
137
|
+
throw sourceError(
|
|
138
|
+
'TRIGGER_PLAN_SOURCE_NOT_FOUND',
|
|
139
|
+
`No active source exposes ${event.serverId ? `${event.serverId}/` : ''}${event.event}`,
|
|
140
|
+
activeSources.filter((source) => source.eventName === event.event),
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
if (matches.length > 1 && !event.serverId) {
|
|
144
|
+
throw sourceError(
|
|
145
|
+
'TRIGGER_PLAN_SOURCE_AMBIGUOUS',
|
|
146
|
+
`Event ${event.event} is exposed by multiple sources; provide serverId`,
|
|
147
|
+
matches,
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const source = matches[0];
|
|
152
|
+
for (const predicate of event.where) {
|
|
153
|
+
validatePredicateAgainstSource(source, predicate, warnings);
|
|
154
|
+
}
|
|
155
|
+
resolvedSources.push(source);
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
id,
|
|
159
|
+
event: event.event,
|
|
160
|
+
serverId: source.serverId,
|
|
161
|
+
where: event.where,
|
|
162
|
+
};
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
const refs = clauses.map((clause) => clause.id);
|
|
166
|
+
const correlation = {};
|
|
167
|
+
if (plan.correlateBy) {
|
|
168
|
+
for (const field of plan.correlateBy) {
|
|
169
|
+
if (!refs.includes(field.eventId)) {
|
|
170
|
+
const error = new Error(
|
|
171
|
+
`Correlation references unknown event id: ${field.eventId}`,
|
|
172
|
+
);
|
|
173
|
+
error.code = 'TRIGGER_PLAN_EVENT_REF_UNKNOWN';
|
|
174
|
+
throw error;
|
|
175
|
+
}
|
|
176
|
+
const clause = clauses.find((candidate) => candidate.id === field.eventId);
|
|
177
|
+
const source = resolvedSources[clauses.indexOf(clause)];
|
|
178
|
+
validatePredicateAgainstSource(
|
|
179
|
+
source,
|
|
180
|
+
{ path: field.path, op: 'exists', value: true },
|
|
181
|
+
warnings,
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
correlation.deterministic = {
|
|
185
|
+
kind: 'same_value',
|
|
186
|
+
fields: plan.correlateBy.map((field) => ({
|
|
187
|
+
ref: field.eventId,
|
|
188
|
+
path: field.path,
|
|
189
|
+
})),
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
if (plan.semanticCorrelation) {
|
|
193
|
+
correlation.semantic = plan.semanticCorrelation;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const triggerId = plan.triggerId || `planned_${(
|
|
197
|
+
await sha256Hex(canonicalJson({
|
|
198
|
+
target: plan.target,
|
|
199
|
+
events: plan.events,
|
|
200
|
+
match: plan.match,
|
|
201
|
+
continuation: plan.continuation.instruction,
|
|
202
|
+
}))
|
|
203
|
+
).slice(0, 24)}`;
|
|
204
|
+
|
|
205
|
+
const definition = parseCompositeTriggerDefinition({
|
|
206
|
+
triggerId,
|
|
207
|
+
version: plan.version,
|
|
208
|
+
description:
|
|
209
|
+
plan.description ||
|
|
210
|
+
plan.continuation.instruction.slice(0, 500),
|
|
211
|
+
continuation: plan.continuation,
|
|
212
|
+
clauses,
|
|
213
|
+
expression: expressionFor(plan.match, refs),
|
|
214
|
+
withinMs: plan.withinMs,
|
|
215
|
+
lifecycle: lifecycleFor(plan.lifecycle),
|
|
216
|
+
...(Object.keys(correlation).length ? { correlation } : {}),
|
|
217
|
+
target: plan.target,
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
return {
|
|
221
|
+
planVersion: '1',
|
|
222
|
+
definition,
|
|
223
|
+
connectionIds: [...new Set(resolvedSources.map((source) => source.connectionId))],
|
|
224
|
+
resolvedSources: resolvedSources.map(sourceSummary),
|
|
225
|
+
warnings,
|
|
226
|
+
explanation: {
|
|
227
|
+
when: {
|
|
228
|
+
expression: definition.expression,
|
|
229
|
+
events: clauses.map((clause) => ({
|
|
230
|
+
id: clause.id,
|
|
231
|
+
event: clause.event,
|
|
232
|
+
serverId: clause.serverId,
|
|
233
|
+
where: clause.where,
|
|
234
|
+
})),
|
|
235
|
+
withinMs: definition.withinMs,
|
|
236
|
+
},
|
|
237
|
+
then: {
|
|
238
|
+
target: definition.target,
|
|
239
|
+
instruction: definition.continuation?.instruction ?? null,
|
|
240
|
+
evidence: definition.continuation?.contextPolicy?.evidence ?? 'matched_events',
|
|
241
|
+
},
|
|
242
|
+
},
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export default { TriggerPlanner };
|
|
@@ -57,6 +57,59 @@ function ownerFromEnv(env) {
|
|
|
57
57
|
};
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
const predicateInputSchema = z.object({
|
|
61
|
+
path: z.string().min(1),
|
|
62
|
+
op: z.enum(['eq', 'neq', 'contains', 'in', 'exists', 'gt', 'gte', 'lt', 'lte']),
|
|
63
|
+
value: z.unknown().optional(),
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const triggerPlanInputSchema = z.object({
|
|
67
|
+
triggerId: z.string().min(1).optional(),
|
|
68
|
+
version: z.string().min(1).optional(),
|
|
69
|
+
description: z.string().max(500).optional(),
|
|
70
|
+
events: z.array(z.object({
|
|
71
|
+
id: z.string().min(1).optional(),
|
|
72
|
+
event: z.string().min(1),
|
|
73
|
+
serverId: z.string().min(1).optional(),
|
|
74
|
+
where: z.array(predicateInputSchema).optional(),
|
|
75
|
+
})).min(1),
|
|
76
|
+
match: z.union([
|
|
77
|
+
z.enum(['all', 'any', 'sequence']),
|
|
78
|
+
z.object({
|
|
79
|
+
kind: z.literal('count'),
|
|
80
|
+
eventId: z.string().min(1),
|
|
81
|
+
atLeast: z.number().int().min(1),
|
|
82
|
+
}),
|
|
83
|
+
]).optional(),
|
|
84
|
+
withinMs: z.number().int().positive().optional(),
|
|
85
|
+
lifecycle: z.object({
|
|
86
|
+
oneShot: z.boolean().optional(),
|
|
87
|
+
maxFirings: z.number().int().min(1).optional(),
|
|
88
|
+
cooldownMs: z.number().int().nonnegative().optional(),
|
|
89
|
+
expiresAt: z.string().optional(),
|
|
90
|
+
leaseUntil: z.string().optional(),
|
|
91
|
+
completeOnGoal: z.boolean().optional(),
|
|
92
|
+
}).optional(),
|
|
93
|
+
target: z.object({
|
|
94
|
+
runtime: z.string().min(1),
|
|
95
|
+
kind: z.enum(['goal', 'session', 'conversation', 'task', 'spawn_template']),
|
|
96
|
+
id: z.string().min(1),
|
|
97
|
+
}),
|
|
98
|
+
continuation: z.object({
|
|
99
|
+
instruction: z.string().min(1).max(4000),
|
|
100
|
+
contextPolicy: z.object({
|
|
101
|
+
evidence: z.enum(['matched_events', 'refs_only']).optional(),
|
|
102
|
+
maxEvents: z.number().int().min(1).max(50).optional(),
|
|
103
|
+
includeData: z.boolean().optional(),
|
|
104
|
+
}).optional(),
|
|
105
|
+
}),
|
|
106
|
+
correlateBy: z.array(z.object({
|
|
107
|
+
eventId: z.string().min(1),
|
|
108
|
+
path: z.string().min(1),
|
|
109
|
+
})).min(2).optional(),
|
|
110
|
+
semanticCorrelation: z.record(z.string(), z.unknown()).optional(),
|
|
111
|
+
});
|
|
112
|
+
|
|
60
113
|
function registerReadTools(server, runtime) {
|
|
61
114
|
server.registerTool(
|
|
62
115
|
'event_sources_list',
|
|
@@ -80,6 +133,22 @@ function registerReadTools(server, runtime) {
|
|
|
80
133
|
},
|
|
81
134
|
);
|
|
82
135
|
|
|
136
|
+
server.registerTool(
|
|
137
|
+
'trigger_plan',
|
|
138
|
+
{
|
|
139
|
+
description:
|
|
140
|
+
'Compile an agent-friendly trigger plan into a validated durable trigger definition using the event sources currently available. This does not mutate state and does not call another model.',
|
|
141
|
+
inputSchema: triggerPlanInputSchema,
|
|
142
|
+
},
|
|
143
|
+
async (input) => {
|
|
144
|
+
try {
|
|
145
|
+
return jsonResult(await runtime.triggerPlanner.plan(input));
|
|
146
|
+
} catch (error) {
|
|
147
|
+
return errorResult(error);
|
|
148
|
+
}
|
|
149
|
+
},
|
|
150
|
+
);
|
|
151
|
+
|
|
83
152
|
server.registerTool(
|
|
84
153
|
'trigger_list',
|
|
85
154
|
{
|
|
@@ -163,6 +232,24 @@ function registerReadTools(server, runtime) {
|
|
|
163
232
|
},
|
|
164
233
|
);
|
|
165
234
|
|
|
235
|
+
server.registerTool(
|
|
236
|
+
'wake_hydrate',
|
|
237
|
+
{
|
|
238
|
+
description:
|
|
239
|
+
'Hydrate a composite wake into an Activation Envelope containing the configured continuation and matched event evidence. Evidence data is included only according to the trigger context policy.',
|
|
240
|
+
inputSchema: z.object({
|
|
241
|
+
wakeId: z.string().min(1),
|
|
242
|
+
}),
|
|
243
|
+
},
|
|
244
|
+
async ({ wakeId }) => {
|
|
245
|
+
try {
|
|
246
|
+
return jsonResult(runtime.activationHydrator.hydrateWake(wakeId));
|
|
247
|
+
} catch (error) {
|
|
248
|
+
return errorResult(error);
|
|
249
|
+
}
|
|
250
|
+
},
|
|
251
|
+
);
|
|
252
|
+
|
|
166
253
|
server.registerTool(
|
|
167
254
|
'runtime_status',
|
|
168
255
|
{
|
|
@@ -198,22 +285,45 @@ function registerWriteTools(server, runtime) {
|
|
|
198
285
|
description:
|
|
199
286
|
'Create a durable trigger. Persistent MCP mutations require explicit confirmationId and MCP_WRITE_ENABLED=true.',
|
|
200
287
|
inputSchema: z.object({
|
|
201
|
-
definition: z.record(z.string(), z.unknown()),
|
|
202
|
-
|
|
288
|
+
definition: z.record(z.string(), z.unknown()).optional(),
|
|
289
|
+
plan: triggerPlanInputSchema.optional(),
|
|
290
|
+
connectionIds: z.array(z.string()).min(1).optional(),
|
|
203
291
|
confirmationId: z.string().min(1),
|
|
204
292
|
}),
|
|
205
293
|
},
|
|
206
|
-
async ({ definition, connectionIds, confirmationId }) => {
|
|
294
|
+
async ({ definition, plan, connectionIds, confirmationId }) => {
|
|
207
295
|
try {
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
296
|
+
if ((definition ? 1 : 0) + (plan ? 1 : 0) !== 1) {
|
|
297
|
+
const error = new Error('trigger_create requires exactly one of definition or plan');
|
|
298
|
+
error.code = 'TRIGGER_CREATE_INPUT_INVALID';
|
|
299
|
+
throw error;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
let resolvedDefinition = definition;
|
|
303
|
+
let resolvedConnectionIds = connectionIds;
|
|
304
|
+
let planning = null;
|
|
305
|
+
if (plan) {
|
|
306
|
+
planning = await runtime.triggerPlanner.plan(plan);
|
|
307
|
+
resolvedDefinition = planning.definition;
|
|
308
|
+
resolvedConnectionIds = planning.connectionIds;
|
|
309
|
+
}
|
|
310
|
+
if (!Array.isArray(resolvedConnectionIds) || resolvedConnectionIds.length === 0) {
|
|
311
|
+
const error = new Error('connectionIds are required when trigger_create uses a raw definition');
|
|
312
|
+
error.code = 'TRIGGER_CONNECTION_REQUIRED';
|
|
313
|
+
throw error;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const receipt = await runtime.triggerControl.createTrigger({
|
|
317
|
+
definition: resolvedDefinition,
|
|
318
|
+
connectionIds: resolvedConnectionIds,
|
|
319
|
+
actor: actor(),
|
|
320
|
+
owner: owner(),
|
|
321
|
+
confirmationId,
|
|
322
|
+
});
|
|
323
|
+
return jsonResult({
|
|
324
|
+
...receipt,
|
|
325
|
+
...(planning ? { planning } : {}),
|
|
326
|
+
});
|
|
217
327
|
} catch (error) {
|
|
218
328
|
return errorResult(error);
|
|
219
329
|
}
|
|
@@ -341,7 +451,7 @@ export async function buildEventIntelligenceMcpServer({
|
|
|
341
451
|
const server = new McpServer(
|
|
342
452
|
{
|
|
343
453
|
name: 'mcp-event-intelligence',
|
|
344
|
-
version: '0.
|
|
454
|
+
version: '0.3.0',
|
|
345
455
|
},
|
|
346
456
|
{
|
|
347
457
|
instructions:
|
package/server.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"name": "io.github.sarooo17/event-intelligence",
|
|
4
4
|
"title": "MCP Event Intelligence",
|
|
5
5
|
"description": "Durable temporal event intelligence: composite triggers, derived events, and runtime wakeups.",
|
|
6
|
-
"version": "0.
|
|
6
|
+
"version": "0.3.0",
|
|
7
7
|
"repository": {
|
|
8
8
|
"url": "https://github.com/sarooo17/event-intelligence",
|
|
9
9
|
"source": "github"
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"registryType": "npm",
|
|
14
14
|
"registryBaseUrl": "https://registry.npmjs.org",
|
|
15
15
|
"identifier": "mcp-event-intelligence",
|
|
16
|
-
"version": "0.
|
|
16
|
+
"version": "0.3.0",
|
|
17
17
|
"transport": {
|
|
18
18
|
"type": "stdio"
|
|
19
19
|
},
|