@decocms/runtime 1.0.3 → 1.1.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/package.json +2 -3
- package/src/bindings/binder.ts +5 -4
- package/src/bindings.ts +2 -2
- package/src/events.ts +198 -72
- package/src/index.ts +2 -2
- package/src/state.ts +1 -1
- package/src/tools.ts +60 -30
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@decocms/runtime",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"check": "tsc --noEmit",
|
|
@@ -13,8 +13,7 @@
|
|
|
13
13
|
"@ai-sdk/provider": "^2.0.0",
|
|
14
14
|
"hono": "^4.10.7",
|
|
15
15
|
"jose": "^6.0.11",
|
|
16
|
-
"zod": "^
|
|
17
|
-
"zod-to-json-schema": "3.25.0"
|
|
16
|
+
"zod": "^4.0.0"
|
|
18
17
|
},
|
|
19
18
|
"exports": {
|
|
20
19
|
".": "./src/index.ts",
|
package/src/bindings/binder.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/* oxlint-disable no-explicit-any */
|
|
2
2
|
import { z } from "zod";
|
|
3
|
+
import type { ZodType, ZodTypeAny } from "zod";
|
|
3
4
|
import type { MCPConnection } from "../connection.ts";
|
|
4
5
|
import {
|
|
5
6
|
createMCPFetchStub,
|
|
@@ -17,8 +18,8 @@ export interface ToolLike<
|
|
|
17
18
|
> {
|
|
18
19
|
name: TName;
|
|
19
20
|
description: string;
|
|
20
|
-
inputSchema:
|
|
21
|
-
outputSchema?:
|
|
21
|
+
inputSchema: ZodType<TInput>;
|
|
22
|
+
outputSchema?: ZodType<TReturn>;
|
|
22
23
|
handler: (props: TInput) => Promise<TReturn> | TReturn;
|
|
23
24
|
}
|
|
24
25
|
|
|
@@ -36,7 +37,7 @@ export type BinderImplementation<
|
|
|
36
37
|
TDefinition[K]["name"],
|
|
37
38
|
z.infer<TDefinition[K]["inputSchema"]>,
|
|
38
39
|
TDefinition[K] extends { outputSchema: infer Schema }
|
|
39
|
-
? Schema extends
|
|
40
|
+
? Schema extends ZodTypeAny
|
|
40
41
|
? z.infer<Schema>
|
|
41
42
|
: never
|
|
42
43
|
: never
|
|
@@ -51,7 +52,7 @@ export type BinderImplementation<
|
|
|
51
52
|
TDefinition[K]["name"],
|
|
52
53
|
z.infer<TDefinition[K]["inputSchema"]>,
|
|
53
54
|
TDefinition[K] extends { outputSchema: infer Schema }
|
|
54
|
-
? Schema extends
|
|
55
|
+
? Schema extends ZodTypeAny
|
|
55
56
|
? z.infer<Schema>
|
|
56
57
|
: never
|
|
57
58
|
: never
|
package/src/bindings.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { CollectionBinding } from "
|
|
1
|
+
import type { CollectionBinding } from "@decocms/bindings/collections";
|
|
2
2
|
import type { MCPConnection } from "./connection.ts";
|
|
3
3
|
import type { RequestContext } from "./index.ts";
|
|
4
4
|
import { type MCPClientFetchStub, MCPClient, type ToolBinder } from "./mcp.ts";
|
|
@@ -38,7 +38,7 @@ export const BindingOf = <
|
|
|
38
38
|
name: TName,
|
|
39
39
|
) => {
|
|
40
40
|
return z.object({
|
|
41
|
-
__type: z.literal
|
|
41
|
+
__type: z.literal(name as string).default(name as string),
|
|
42
42
|
value: z.string(),
|
|
43
43
|
});
|
|
44
44
|
};
|
package/src/events.ts
CHANGED
|
@@ -3,9 +3,20 @@ import type {
|
|
|
3
3
|
EventResult,
|
|
4
4
|
OnEventsOutput,
|
|
5
5
|
} from "@decocms/bindings";
|
|
6
|
-
import z from "zod";
|
|
6
|
+
import { z } from "zod";
|
|
7
7
|
import { isBinding } from "./bindings.ts";
|
|
8
8
|
|
|
9
|
+
// ============================================================================
|
|
10
|
+
// Constants
|
|
11
|
+
// ============================================================================
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* SELF is a well-known property key for event handlers that represents
|
|
15
|
+
* the current connection. When used, subscriptions are created with the
|
|
16
|
+
* current connection's ID as the publisher.
|
|
17
|
+
*/
|
|
18
|
+
export const SELF = "SELF" as const;
|
|
19
|
+
|
|
9
20
|
// ============================================================================
|
|
10
21
|
// Types
|
|
11
22
|
// ============================================================================
|
|
@@ -34,12 +45,27 @@ export type BatchHandlerFn<TEnv> = (
|
|
|
34
45
|
) => OnEventsOutput | Promise<OnEventsOutput>;
|
|
35
46
|
|
|
36
47
|
/**
|
|
37
|
-
* Batch handler with explicit event types for subscription
|
|
48
|
+
* Batch handler with explicit event types for subscription.
|
|
49
|
+
*
|
|
50
|
+
* When used as a global handler, events must be prefixed with binding name:
|
|
51
|
+
* - "SELF::order.created" - subscribe to order.created from current connection
|
|
52
|
+
* - "DATABASE::record.updated" - subscribe to record.updated from DATABASE binding
|
|
53
|
+
*
|
|
54
|
+
* @example
|
|
55
|
+
* ```ts
|
|
56
|
+
* {
|
|
57
|
+
* handler: async ({ events }, env) => ({ success: true }),
|
|
58
|
+
* events: ["SELF::order.created", "DATABASE::record.updated"]
|
|
59
|
+
* }
|
|
60
|
+
* ```
|
|
38
61
|
*/
|
|
39
62
|
export interface BatchHandler<TEnv> {
|
|
40
63
|
/** Handler function */
|
|
41
64
|
handler: BatchHandlerFn<TEnv>;
|
|
42
|
-
/**
|
|
65
|
+
/**
|
|
66
|
+
* Event types to subscribe to.
|
|
67
|
+
* Format: "BINDING::EVENT_TYPE" (e.g., "SELF::order.created")
|
|
68
|
+
*/
|
|
43
69
|
events: string[];
|
|
44
70
|
}
|
|
45
71
|
|
|
@@ -56,24 +82,19 @@ export interface BatchHandler<TEnv> {
|
|
|
56
82
|
* { handler: fn, events: ["order.created", "order.updated"] }
|
|
57
83
|
* ```
|
|
58
84
|
*/
|
|
59
|
-
export type BindingHandlers<TEnv
|
|
85
|
+
export type BindingHandlers<TEnv> =
|
|
60
86
|
| BatchHandler<TEnv>
|
|
61
|
-
|
|
|
87
|
+
| Record<string, PerEventHandler<TEnv>>;
|
|
62
88
|
|
|
63
|
-
export type CronHandlers<Binding, Env = unknown> = Binding extends {
|
|
64
|
-
__type: "@deco/event-bus";
|
|
65
|
-
value: string;
|
|
66
|
-
}
|
|
67
|
-
? {
|
|
68
|
-
[key in `cron/${string}`]: (env: Env) => Promise<void>;
|
|
69
|
-
}
|
|
70
|
-
: {};
|
|
71
89
|
/**
|
|
72
|
-
* EventHandlers type supports
|
|
90
|
+
* EventHandlers type supports four handler formats:
|
|
73
91
|
*
|
|
74
|
-
* @example Global handler with
|
|
92
|
+
* @example Global handler with prefixed events (BINDING::EVENT_TYPE)
|
|
75
93
|
* ```ts
|
|
76
|
-
* {
|
|
94
|
+
* {
|
|
95
|
+
* handler: (ctx, env) => result,
|
|
96
|
+
* events: ["SELF::order.created", "DATABASE::record.updated"]
|
|
97
|
+
* }
|
|
77
98
|
* ```
|
|
78
99
|
*
|
|
79
100
|
* @example Per-binding batch handler
|
|
@@ -85,22 +106,31 @@ export type CronHandlers<Binding, Env = unknown> = Binding extends {
|
|
|
85
106
|
* ```ts
|
|
86
107
|
* { DATABASE: { "order.created": (ctx, env) => result } }
|
|
87
108
|
* ```
|
|
109
|
+
*
|
|
110
|
+
* @example SELF handlers for self-subscription (events from current connection)
|
|
111
|
+
* ```ts
|
|
112
|
+
* { SELF: { "order.created": (ctx, env) => result } }
|
|
113
|
+
* ```
|
|
88
114
|
*/
|
|
89
115
|
export type EventHandlers<
|
|
90
116
|
Env = unknown,
|
|
91
117
|
TSchema extends z.ZodTypeAny = never,
|
|
92
118
|
> = [TSchema] extends [never]
|
|
93
|
-
?
|
|
119
|
+
? // When no schema, only SELF is available
|
|
120
|
+
BatchHandler<Env> | { SELF?: BindingHandlers<Env> }
|
|
94
121
|
:
|
|
95
122
|
| BatchHandler<Env> // Global handler with events
|
|
96
|
-
| {
|
|
123
|
+
| ({
|
|
97
124
|
[K in keyof z.infer<TSchema> as z.infer<TSchema>[K] extends {
|
|
98
125
|
__type: string;
|
|
99
126
|
value: string;
|
|
100
127
|
}
|
|
101
128
|
? K
|
|
102
|
-
: never]?: BindingHandlers<Env
|
|
103
|
-
}
|
|
129
|
+
: never]?: BindingHandlers<Env>;
|
|
130
|
+
} & {
|
|
131
|
+
/** SELF: Subscribe to events from the current connection */
|
|
132
|
+
SELF?: BindingHandlers<Env>;
|
|
133
|
+
});
|
|
104
134
|
|
|
105
135
|
/**
|
|
106
136
|
* Extract only the keys from T where the value is a Binding shape.
|
|
@@ -150,6 +180,49 @@ const isBatchHandler = <TEnv>(
|
|
|
150
180
|
// Helper Functions
|
|
151
181
|
// ============================================================================
|
|
152
182
|
|
|
183
|
+
/**
|
|
184
|
+
* Event subscription separator - used in global handlers to specify binding
|
|
185
|
+
* Format: BINDING::EVENT_TYPE (e.g., "SELF::order.created", "DATABASE::record.updated")
|
|
186
|
+
*/
|
|
187
|
+
const EVENT_SEPARATOR = "::" as const;
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Parse a prefixed event type into binding and event type
|
|
191
|
+
* @param prefixedEvent - Event in format "BINDING::EVENT_TYPE"
|
|
192
|
+
* @returns Tuple of [binding, eventType] or null if not prefixed
|
|
193
|
+
*/
|
|
194
|
+
const parseEventPrefix = (
|
|
195
|
+
prefixedEvent: string,
|
|
196
|
+
): [binding: string, eventType: string] | null => {
|
|
197
|
+
const separatorIndex = prefixedEvent.indexOf(EVENT_SEPARATOR);
|
|
198
|
+
if (separatorIndex === -1) {
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
const binding = prefixedEvent.substring(0, separatorIndex);
|
|
202
|
+
const eventType = prefixedEvent.substring(
|
|
203
|
+
separatorIndex + EVENT_SEPARATOR.length,
|
|
204
|
+
);
|
|
205
|
+
return [binding, eventType];
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Parse a cron event type into name and expression
|
|
210
|
+
* Format: cron/NAME/EXPRESSION (e.g., "cron/daily-cleanup/0 9 * * 1")
|
|
211
|
+
* @param eventType - Event type that may be a cron event
|
|
212
|
+
* @returns Tuple of [name, cronExpression] or null if not a cron event
|
|
213
|
+
*/
|
|
214
|
+
const parseCronEvent = (
|
|
215
|
+
eventType: string,
|
|
216
|
+
): [name: string, cron: string] | null => {
|
|
217
|
+
if (!eventType.startsWith("cron/")) return null;
|
|
218
|
+
const parts = eventType.substring(5); // Remove "cron/"
|
|
219
|
+
const slashIndex = parts.indexOf("/");
|
|
220
|
+
if (slashIndex === -1) return null;
|
|
221
|
+
const name = parts.substring(0, slashIndex);
|
|
222
|
+
const cron = parts.substring(slashIndex + 1);
|
|
223
|
+
return [name, cron];
|
|
224
|
+
};
|
|
225
|
+
|
|
153
226
|
/**
|
|
154
227
|
* Get binding keys from event handlers object
|
|
155
228
|
*/
|
|
@@ -184,40 +257,84 @@ const getEventTypesForBinding = <TEnv, TSchema extends z.ZodTypeAny>(
|
|
|
184
257
|
return Object.keys(bindingHandler);
|
|
185
258
|
};
|
|
186
259
|
|
|
260
|
+
/**
|
|
261
|
+
* Resolve a binding name to its publisher (connection ID)
|
|
262
|
+
* Handles SELF specially by using the current connectionId
|
|
263
|
+
*/
|
|
264
|
+
const resolvePublisher = (
|
|
265
|
+
binding: string,
|
|
266
|
+
state: Record<string, unknown>,
|
|
267
|
+
connectionId?: string,
|
|
268
|
+
): string | null => {
|
|
269
|
+
if (binding === SELF) {
|
|
270
|
+
if (!connectionId) {
|
|
271
|
+
console.warn("[Event] SELF binding used but no connectionId available");
|
|
272
|
+
return null;
|
|
273
|
+
}
|
|
274
|
+
return connectionId;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const bindingValue = state[binding];
|
|
278
|
+
if (!isBinding(bindingValue)) {
|
|
279
|
+
console.warn(`[Event] Binding "${binding}" not found in state`);
|
|
280
|
+
return null;
|
|
281
|
+
}
|
|
282
|
+
return bindingValue.value;
|
|
283
|
+
};
|
|
284
|
+
|
|
187
285
|
/**
|
|
188
286
|
* Get subscriptions from event handlers and state
|
|
189
287
|
* Returns flat array of { eventType, publisher } for EVENT_SYNC_SUBSCRIPTIONS
|
|
288
|
+
*
|
|
289
|
+
* For global handlers, events must be prefixed with binding name:
|
|
290
|
+
* - "SELF::order.created" - subscribe to order.created from current connection
|
|
291
|
+
* - "DATABASE::record.updated" - subscribe to record.updated from DATABASE binding
|
|
292
|
+
*
|
|
293
|
+
* @param handlers - Event handlers configuration
|
|
294
|
+
* @param state - Resolved bindings state (can be unknown when only SELF is used)
|
|
295
|
+
* @param connectionId - Current connection ID (used for SELF subscriptions)
|
|
190
296
|
*/
|
|
191
297
|
const eventsSubscriptions = <TEnv, TSchema extends z.ZodTypeAny = never>(
|
|
192
298
|
handlers: EventHandlers<TEnv, TSchema>,
|
|
193
|
-
state: z.infer<TSchema>,
|
|
299
|
+
state: z.infer<TSchema> | Record<string, unknown>,
|
|
300
|
+
connectionId?: string,
|
|
194
301
|
): EventSubscription[] => {
|
|
302
|
+
const stateRecord = state as Record<string, unknown>;
|
|
303
|
+
|
|
195
304
|
if (isGlobalHandler<TEnv>(handlers)) {
|
|
196
|
-
// Global handler -
|
|
305
|
+
// Global handler - events must be prefixed with BINDING::EVENT_TYPE
|
|
197
306
|
const subscriptions: EventSubscription[] = [];
|
|
198
|
-
for (const
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
}
|
|
307
|
+
for (const prefixedEvent of handlers.events) {
|
|
308
|
+
const parsed = parseEventPrefix(prefixedEvent);
|
|
309
|
+
if (!parsed) {
|
|
310
|
+
console.warn(
|
|
311
|
+
`[Event] Global handler event "${prefixedEvent}" must be prefixed with BINDING:: (e.g., "SELF::${prefixedEvent}" or "DATABASE::${prefixedEvent}")`,
|
|
312
|
+
);
|
|
313
|
+
continue;
|
|
206
314
|
}
|
|
315
|
+
|
|
316
|
+
const [binding, eventType] = parsed;
|
|
317
|
+
const publisher = resolvePublisher(binding, stateRecord, connectionId);
|
|
318
|
+
if (!publisher) continue;
|
|
319
|
+
|
|
320
|
+
subscriptions.push({
|
|
321
|
+
eventType,
|
|
322
|
+
publisher,
|
|
323
|
+
});
|
|
207
324
|
}
|
|
208
325
|
return subscriptions;
|
|
209
326
|
}
|
|
210
327
|
|
|
211
328
|
const subscriptions: EventSubscription[] = [];
|
|
212
329
|
for (const binding of getBindingKeys(handlers)) {
|
|
213
|
-
const
|
|
214
|
-
if (!
|
|
330
|
+
const publisher = resolvePublisher(binding, stateRecord, connectionId);
|
|
331
|
+
if (!publisher) continue;
|
|
215
332
|
|
|
216
333
|
const eventTypes = getEventTypesForBinding(handlers, binding);
|
|
217
334
|
for (const eventType of eventTypes) {
|
|
218
335
|
subscriptions.push({
|
|
219
336
|
eventType,
|
|
220
|
-
publisher
|
|
337
|
+
publisher,
|
|
221
338
|
});
|
|
222
339
|
}
|
|
223
340
|
}
|
|
@@ -310,21 +427,55 @@ const mergeResults = (results: OnEventsOutput[]): OnEventsOutput => {
|
|
|
310
427
|
/**
|
|
311
428
|
* Execute event handlers and return merged result
|
|
312
429
|
*
|
|
313
|
-
* Supports
|
|
314
|
-
* 1. Global: `
|
|
315
|
-
* 2. Per-binding: `{ BINDING:
|
|
316
|
-
* 3. Per-event: `{ BINDING: { "event.type":
|
|
430
|
+
* Supports four handler formats:
|
|
431
|
+
* 1. Global: `{ handler: fn, events: ["SELF::order.created", "DB::record.updated"] }` - prefixed events
|
|
432
|
+
* 2. Per-binding batch: `{ BINDING: { handler: fn, events: [...] } }` - handles all events from binding
|
|
433
|
+
* 3. Per-event: `{ BINDING: { "event.type": handler } }` - handles specific events
|
|
434
|
+
* 4. SELF: `{ SELF: { "event.type": handler } }` - handles events from current connection
|
|
435
|
+
*
|
|
436
|
+
* @param handlers - Event handlers configuration
|
|
437
|
+
* @param events - CloudEvents to process
|
|
438
|
+
* @param env - Environment
|
|
439
|
+
* @param state - Resolved bindings state (can be unknown when only SELF is used)
|
|
440
|
+
* @param connectionId - Current connection ID (used for SELF handlers)
|
|
317
441
|
*/
|
|
318
442
|
const executeEventHandlers = async <TEnv, TSchema extends z.ZodTypeAny>(
|
|
319
443
|
handlers: EventHandlers<TEnv, TSchema>,
|
|
320
444
|
events: CloudEvent[],
|
|
321
|
-
env:
|
|
322
|
-
state: z.infer<TSchema>,
|
|
445
|
+
env: TEnv,
|
|
446
|
+
state: z.infer<TSchema> | Record<string, unknown>,
|
|
447
|
+
connectionId?: string,
|
|
323
448
|
): Promise<OnEventsOutput> => {
|
|
324
|
-
|
|
449
|
+
const stateRecord = state as Record<string, unknown>;
|
|
450
|
+
|
|
451
|
+
// Case 1: Global handler with prefixed events
|
|
325
452
|
if (isGlobalHandler<TEnv>(handlers)) {
|
|
453
|
+
// Build a set of valid (publisher, eventType) pairs from prefixed events
|
|
454
|
+
const validSubscriptions = new Set<string>();
|
|
455
|
+
for (const prefixedEvent of handlers.events) {
|
|
456
|
+
const parsed = parseEventPrefix(prefixedEvent);
|
|
457
|
+
if (!parsed) continue;
|
|
458
|
+
|
|
459
|
+
const [binding, eventType] = parsed;
|
|
460
|
+
const publisher = resolvePublisher(binding, stateRecord, connectionId);
|
|
461
|
+
if (!publisher) continue;
|
|
462
|
+
|
|
463
|
+
// Create a key for quick lookup: "publisher:eventType"
|
|
464
|
+
validSubscriptions.add(`${publisher}:${eventType}`);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// Filter events to only those that match our subscriptions
|
|
468
|
+
const matchingEvents = events.filter((event) => {
|
|
469
|
+
const key = `${event.source}:${event.type}`;
|
|
470
|
+
return validSubscriptions.has(key);
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
if (matchingEvents.length === 0) {
|
|
474
|
+
return { success: true };
|
|
475
|
+
}
|
|
476
|
+
|
|
326
477
|
try {
|
|
327
|
-
return await handlers.handler({ events }, env);
|
|
478
|
+
return await handlers.handler({ events: matchingEvents }, env);
|
|
328
479
|
} catch (error) {
|
|
329
480
|
return {
|
|
330
481
|
success: false,
|
|
@@ -336,9 +487,9 @@ const executeEventHandlers = async <TEnv, TSchema extends z.ZodTypeAny>(
|
|
|
336
487
|
// Build a map from connectionId -> binding key
|
|
337
488
|
const connectionToBinding = new Map<string, string>();
|
|
338
489
|
for (const binding of getBindingKeys(handlers)) {
|
|
339
|
-
const
|
|
340
|
-
if (
|
|
341
|
-
connectionToBinding.set(
|
|
490
|
+
const publisher = resolvePublisher(binding, stateRecord, connectionId);
|
|
491
|
+
if (publisher) {
|
|
492
|
+
connectionToBinding.set(publisher, binding);
|
|
342
493
|
}
|
|
343
494
|
}
|
|
344
495
|
|
|
@@ -383,7 +534,7 @@ const executeEventHandlers = async <TEnv, TSchema extends z.ZodTypeAny>(
|
|
|
383
534
|
// Case 3: Per-event handlers
|
|
384
535
|
const perEventHandlers = bindingHandler as Record<
|
|
385
536
|
string,
|
|
386
|
-
PerEventHandler<
|
|
537
|
+
PerEventHandler<TEnv>
|
|
387
538
|
>;
|
|
388
539
|
const eventsByType = groupEventsByType(sourceEvents);
|
|
389
540
|
|
|
@@ -394,33 +545,7 @@ const executeEventHandlers = async <TEnv, TSchema extends z.ZodTypeAny>(
|
|
|
394
545
|
continue;
|
|
395
546
|
}
|
|
396
547
|
|
|
397
|
-
// Case
|
|
398
|
-
// - Handler signature: (env) => Promise<void>
|
|
399
|
-
// - Fire and forget (don't await)
|
|
400
|
-
// - Always return success immediately
|
|
401
|
-
if (eventType.startsWith("cron/")) {
|
|
402
|
-
const cronHandler = eventHandler as unknown as (
|
|
403
|
-
env: z.infer<TSchema>,
|
|
404
|
-
) => Promise<void>;
|
|
405
|
-
|
|
406
|
-
// Fire and forget - don't await, just log errors
|
|
407
|
-
cronHandler(env).catch((error) => {
|
|
408
|
-
console.error(
|
|
409
|
-
`[Event] Cron handler error for ${eventType}:`,
|
|
410
|
-
error instanceof Error ? error.message : String(error),
|
|
411
|
-
);
|
|
412
|
-
});
|
|
413
|
-
|
|
414
|
-
// Immediately return success for all cron events
|
|
415
|
-
const results: Record<string, EventResult> = {};
|
|
416
|
-
for (const event of typedEvents) {
|
|
417
|
-
results[event.id] = { success: true };
|
|
418
|
-
}
|
|
419
|
-
promises.push(Promise.resolve({ results }));
|
|
420
|
-
continue;
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
// Case 3b: Regular per-event handlers
|
|
548
|
+
// Case 3: Per-event handlers
|
|
424
549
|
// Call handler for each event type (handler receives all events of that type)
|
|
425
550
|
promises.push(
|
|
426
551
|
(async () => {
|
|
@@ -469,4 +594,5 @@ const executeEventHandlers = async <TEnv, TSchema extends z.ZodTypeAny>(
|
|
|
469
594
|
export const Event = {
|
|
470
595
|
subscriptions: eventsSubscriptions,
|
|
471
596
|
execute: executeEventHandlers,
|
|
597
|
+
parseCron: parseCronEvent,
|
|
472
598
|
};
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/* oxlint-disable no-explicit-any */
|
|
2
2
|
import { decodeJwt } from "jose";
|
|
3
|
-
import
|
|
3
|
+
import { z } from "zod";
|
|
4
4
|
import {
|
|
5
5
|
BindingRegistry,
|
|
6
6
|
initializeBindings,
|
|
@@ -164,7 +164,7 @@ export const withBindings = <TEnv>({
|
|
|
164
164
|
authToken,
|
|
165
165
|
}: {
|
|
166
166
|
env: TEnv;
|
|
167
|
-
server: MCPServer<TEnv, any>;
|
|
167
|
+
server: MCPServer<TEnv, any, any>;
|
|
168
168
|
// token is x-mesh-token
|
|
169
169
|
tokenOrContext?: string | RequestContext;
|
|
170
170
|
// authToken is the authorization header
|
package/src/state.ts
CHANGED
|
@@ -8,7 +8,7 @@ export const State = {
|
|
|
8
8
|
getStore: () => {
|
|
9
9
|
return asyncLocalStorage.getStore();
|
|
10
10
|
},
|
|
11
|
-
run: <TEnv extends DefaultEnv, R, TArgs extends unknown[]>(
|
|
11
|
+
run: <TEnv extends DefaultEnv<any, any>, R, TArgs extends unknown[]>(
|
|
12
12
|
ctx: AppContext<TEnv>,
|
|
13
13
|
f: (...args: TArgs) => R,
|
|
14
14
|
...args: TArgs
|
package/src/tools.ts
CHANGED
|
@@ -9,13 +9,14 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
9
9
|
import { WebStandardStreamableHTTPServerTransport as HttpServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
|
|
10
10
|
import type { GetPromptResult } from "@modelcontextprotocol/sdk/types.js";
|
|
11
11
|
import { z } from "zod";
|
|
12
|
-
import {
|
|
12
|
+
import type { ZodRawShape, ZodSchema, ZodTypeAny } from "zod";
|
|
13
13
|
import { BindingRegistry } from "./bindings.ts";
|
|
14
14
|
import { Event, type EventHandlers } from "./events.ts";
|
|
15
15
|
import type { DefaultEnv } from "./index.ts";
|
|
16
16
|
import { State } from "./state.ts";
|
|
17
17
|
|
|
18
|
-
// Re-export EventHandlers type for external use
|
|
18
|
+
// Re-export EventHandlers type and SELF constant for external use
|
|
19
|
+
export { SELF } from "./events.ts";
|
|
19
20
|
export type { EventHandlers } from "./events.ts";
|
|
20
21
|
|
|
21
22
|
export const createRuntimeContext = (prev?: AppContext) => {
|
|
@@ -30,7 +31,7 @@ export const createRuntimeContext = (prev?: AppContext) => {
|
|
|
30
31
|
};
|
|
31
32
|
|
|
32
33
|
export interface ToolExecutionContext<
|
|
33
|
-
TSchemaIn extends
|
|
34
|
+
TSchemaIn extends ZodTypeAny = ZodTypeAny,
|
|
34
35
|
> {
|
|
35
36
|
context: z.infer<TSchemaIn>;
|
|
36
37
|
runtimeContext: AppContext;
|
|
@@ -40,8 +41,8 @@ export interface ToolExecutionContext<
|
|
|
40
41
|
* Tool interface with generic schema types for type-safe tool creation.
|
|
41
42
|
*/
|
|
42
43
|
export interface Tool<
|
|
43
|
-
TSchemaIn extends
|
|
44
|
-
TSchemaOut extends
|
|
44
|
+
TSchemaIn extends ZodTypeAny = ZodTypeAny,
|
|
45
|
+
TSchemaOut extends ZodTypeAny | undefined = undefined,
|
|
45
46
|
> {
|
|
46
47
|
id: string;
|
|
47
48
|
description?: string;
|
|
@@ -49,7 +50,7 @@ export interface Tool<
|
|
|
49
50
|
outputSchema?: TSchemaOut;
|
|
50
51
|
execute(
|
|
51
52
|
context: ToolExecutionContext<TSchemaIn>,
|
|
52
|
-
): TSchemaOut extends
|
|
53
|
+
): TSchemaOut extends ZodSchema
|
|
53
54
|
? Promise<z.infer<TSchemaOut>>
|
|
54
55
|
: Promise<unknown>;
|
|
55
56
|
}
|
|
@@ -57,7 +58,7 @@ export interface Tool<
|
|
|
57
58
|
/**
|
|
58
59
|
* Streamable tool interface for tools that return Response streams.
|
|
59
60
|
*/
|
|
60
|
-
export interface StreamableTool<TSchemaIn extends
|
|
61
|
+
export interface StreamableTool<TSchemaIn extends ZodSchema = ZodSchema> {
|
|
61
62
|
id: string;
|
|
62
63
|
inputSchema: TSchemaIn;
|
|
63
64
|
streamable?: true;
|
|
@@ -72,8 +73,8 @@ export interface StreamableTool<TSchemaIn extends z.ZodSchema = z.ZodSchema> {
|
|
|
72
73
|
export type CreatedTool = {
|
|
73
74
|
id: string;
|
|
74
75
|
description?: string;
|
|
75
|
-
inputSchema:
|
|
76
|
-
outputSchema?:
|
|
76
|
+
inputSchema: ZodTypeAny;
|
|
77
|
+
outputSchema?: ZodTypeAny;
|
|
77
78
|
streamable?: true;
|
|
78
79
|
// Use a permissive execute signature - accepts any context shape
|
|
79
80
|
execute(context: {
|
|
@@ -90,18 +91,16 @@ export type { GetPromptResult } from "@modelcontextprotocol/sdk/types.js";
|
|
|
90
91
|
* Unlike tool arguments, prompt arguments are always strings.
|
|
91
92
|
*/
|
|
92
93
|
export type PromptArgsRawShape = {
|
|
93
|
-
[k: string]:
|
|
94
|
-
| z.ZodType<string, z.ZodTypeDef, string>
|
|
95
|
-
| z.ZodOptional<z.ZodType<string, z.ZodTypeDef, string>>;
|
|
94
|
+
[k: string]: z.ZodType<string> | z.ZodOptional<z.ZodType<string>>;
|
|
96
95
|
};
|
|
97
96
|
|
|
98
97
|
/**
|
|
99
98
|
* Context passed to prompt execute functions.
|
|
100
99
|
*/
|
|
101
100
|
export interface PromptExecutionContext<
|
|
102
|
-
|
|
101
|
+
_TArgs extends PromptArgsRawShape = PromptArgsRawShape,
|
|
103
102
|
> {
|
|
104
|
-
args:
|
|
103
|
+
args: Record<string, string | undefined>;
|
|
105
104
|
runtimeContext: AppContext;
|
|
106
105
|
}
|
|
107
106
|
|
|
@@ -138,8 +137,8 @@ export type CreatedPrompt = {
|
|
|
138
137
|
* creates a private tool that always ensure for athentication before being executed
|
|
139
138
|
*/
|
|
140
139
|
export function createPrivateTool<
|
|
141
|
-
TSchemaIn extends
|
|
142
|
-
TSchemaOut extends
|
|
140
|
+
TSchemaIn extends ZodSchema = ZodSchema,
|
|
141
|
+
TSchemaOut extends ZodSchema | undefined = undefined,
|
|
143
142
|
>(opts: Tool<TSchemaIn, TSchemaOut>): Tool<TSchemaIn, TSchemaOut> {
|
|
144
143
|
const execute = opts.execute;
|
|
145
144
|
if (typeof execute === "function") {
|
|
@@ -154,9 +153,9 @@ export function createPrivateTool<
|
|
|
154
153
|
return createTool(opts);
|
|
155
154
|
}
|
|
156
155
|
|
|
157
|
-
export function createStreamableTool<
|
|
158
|
-
TSchemaIn
|
|
159
|
-
|
|
156
|
+
export function createStreamableTool<TSchemaIn extends ZodSchema = ZodSchema>(
|
|
157
|
+
streamableTool: StreamableTool<TSchemaIn>,
|
|
158
|
+
): StreamableTool<TSchemaIn> {
|
|
160
159
|
return {
|
|
161
160
|
...streamableTool,
|
|
162
161
|
execute: (input: ToolExecutionContext<TSchemaIn>) => {
|
|
@@ -173,8 +172,8 @@ export function createStreamableTool<
|
|
|
173
172
|
}
|
|
174
173
|
|
|
175
174
|
export function createTool<
|
|
176
|
-
TSchemaIn extends
|
|
177
|
-
TSchemaOut extends
|
|
175
|
+
TSchemaIn extends ZodSchema = ZodSchema,
|
|
176
|
+
TSchemaOut extends ZodSchema | undefined = undefined,
|
|
178
177
|
>(opts: Tool<TSchemaIn, TSchemaOut>): Tool<TSchemaIn, TSchemaOut> {
|
|
179
178
|
return {
|
|
180
179
|
...opts,
|
|
@@ -321,7 +320,7 @@ type PickByType<T, Value> = {
|
|
|
321
320
|
|
|
322
321
|
export interface CreateMCPServerOptions<
|
|
323
322
|
Env = unknown,
|
|
324
|
-
TSchema extends
|
|
323
|
+
TSchema extends ZodTypeAny = never,
|
|
325
324
|
TBindings extends BindingRegistry = BindingRegistry,
|
|
326
325
|
TEnv extends Env & DefaultEnv<TSchema, TBindings> = Env &
|
|
327
326
|
DefaultEnv<TSchema, TBindings>,
|
|
@@ -385,12 +384,12 @@ const getEventBus = (
|
|
|
385
384
|
: env?.MESH_REQUEST_CONTEXT.state[prop];
|
|
386
385
|
};
|
|
387
386
|
|
|
388
|
-
const toolsFor = <TSchema extends
|
|
387
|
+
const toolsFor = <TSchema extends ZodTypeAny = never>({
|
|
389
388
|
events,
|
|
390
389
|
configuration: { state: schema, scopes, onChange } = {},
|
|
391
390
|
}: CreateMCPServerOptions<any, TSchema> = {}): CreatedTool[] => {
|
|
392
391
|
const jsonSchema = schema
|
|
393
|
-
?
|
|
392
|
+
? z.toJSONSchema(schema)
|
|
394
393
|
: { type: "object", properties: {} };
|
|
395
394
|
const busProp = String(events?.bus ?? "EVENT_BUS");
|
|
396
395
|
return [
|
|
@@ -409,19 +408,47 @@ const toolsFor = <TSchema extends z.ZodTypeAny = never>({
|
|
|
409
408
|
}),
|
|
410
409
|
outputSchema: z.object({}),
|
|
411
410
|
execute: async (input) => {
|
|
412
|
-
const state = input.context
|
|
411
|
+
const state = (input.context as { state: unknown })
|
|
412
|
+
.state as z.infer<TSchema>;
|
|
413
413
|
await onChange?.(input.runtimeContext.env, {
|
|
414
414
|
state,
|
|
415
|
-
scopes: input.context.scopes,
|
|
415
|
+
scopes: (input.context as { scopes: string[] }).scopes,
|
|
416
416
|
});
|
|
417
417
|
const bus = getEventBus(busProp, input.runtimeContext.env);
|
|
418
418
|
if (events && state && bus) {
|
|
419
|
+
// Get connectionId for SELF subscriptions
|
|
420
|
+
const connectionId =
|
|
421
|
+
input.runtimeContext.env.MESH_REQUEST_CONTEXT?.connectionId;
|
|
419
422
|
// Sync subscriptions - always call to handle deletions too
|
|
420
423
|
const subscriptions = Event.subscriptions(
|
|
421
424
|
events?.handlers ?? ({} as Record<string, never>),
|
|
422
425
|
state,
|
|
426
|
+
connectionId,
|
|
423
427
|
);
|
|
424
428
|
await bus.EVENT_SYNC_SUBSCRIPTIONS({ subscriptions });
|
|
429
|
+
|
|
430
|
+
// Publish cron events for SELF cron subscriptions
|
|
431
|
+
// Publishing is idempotent - if cron event already exists, it returns existing
|
|
432
|
+
if (connectionId) {
|
|
433
|
+
const cronSubscriptions = subscriptions.filter(
|
|
434
|
+
(sub) =>
|
|
435
|
+
sub.eventType.startsWith("cron/") &&
|
|
436
|
+
sub.publisher === connectionId,
|
|
437
|
+
);
|
|
438
|
+
|
|
439
|
+
await Promise.all(
|
|
440
|
+
cronSubscriptions.map(async (sub) => {
|
|
441
|
+
const parsed = Event.parseCron(sub.eventType);
|
|
442
|
+
if (parsed) {
|
|
443
|
+
const [, cronExpression] = parsed;
|
|
444
|
+
await bus.EVENT_PUBLISH({
|
|
445
|
+
type: sub.eventType,
|
|
446
|
+
cron: cronExpression,
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
}),
|
|
450
|
+
);
|
|
451
|
+
}
|
|
425
452
|
}
|
|
426
453
|
return Promise.resolve({});
|
|
427
454
|
},
|
|
@@ -441,11 +468,14 @@ const toolsFor = <TSchema extends z.ZodTypeAny = never>({
|
|
|
441
468
|
const env = input.runtimeContext.env;
|
|
442
469
|
// Get state from MESH_REQUEST_CONTEXT - this has the binding values
|
|
443
470
|
const state = env.MESH_REQUEST_CONTEXT?.state as z.infer<TSchema>;
|
|
471
|
+
// Get connectionId for SELF handlers
|
|
472
|
+
const connectionId = env.MESH_REQUEST_CONTEXT?.connectionId;
|
|
444
473
|
return Event.execute(
|
|
445
474
|
events.handlers!,
|
|
446
475
|
input.context.events,
|
|
447
476
|
env,
|
|
448
477
|
state,
|
|
478
|
+
connectionId,
|
|
449
479
|
);
|
|
450
480
|
},
|
|
451
481
|
}),
|
|
@@ -479,7 +509,7 @@ type CallTool = (opts: {
|
|
|
479
509
|
|
|
480
510
|
export type MCPServer<
|
|
481
511
|
TEnv = unknown,
|
|
482
|
-
TSchema extends
|
|
512
|
+
TSchema extends ZodTypeAny = never,
|
|
483
513
|
TBindings extends BindingRegistry = BindingRegistry,
|
|
484
514
|
> = {
|
|
485
515
|
fetch: Fetch<TEnv & DefaultEnv<TSchema, TBindings>>;
|
|
@@ -488,7 +518,7 @@ export type MCPServer<
|
|
|
488
518
|
|
|
489
519
|
export const createMCPServer = <
|
|
490
520
|
Env = unknown,
|
|
491
|
-
TSchema extends
|
|
521
|
+
TSchema extends ZodTypeAny = never,
|
|
492
522
|
TBindings extends BindingRegistry = BindingRegistry,
|
|
493
523
|
TEnv extends Env & DefaultEnv<TSchema, TBindings> = Env &
|
|
494
524
|
DefaultEnv<TSchema, TBindings>,
|
|
@@ -535,14 +565,14 @@ export const createMCPServer = <
|
|
|
535
565
|
description: tool.description,
|
|
536
566
|
inputSchema:
|
|
537
567
|
tool.inputSchema && "shape" in tool.inputSchema
|
|
538
|
-
? (tool.inputSchema.shape as
|
|
568
|
+
? (tool.inputSchema.shape as ZodRawShape)
|
|
539
569
|
: z.object({}).shape,
|
|
540
570
|
outputSchema: isStreamableTool(tool)
|
|
541
571
|
? z.object({ bytes: z.record(z.string(), z.number()) }).shape
|
|
542
572
|
: tool.outputSchema &&
|
|
543
573
|
typeof tool.outputSchema === "object" &&
|
|
544
574
|
"shape" in tool.outputSchema
|
|
545
|
-
? (tool.outputSchema.shape as
|
|
575
|
+
? (tool.outputSchema.shape as ZodRawShape)
|
|
546
576
|
: z.object({}).shape,
|
|
547
577
|
},
|
|
548
578
|
async (args) => {
|