@decocms/runtime 1.0.0-alpha.31 → 1.0.0-alpha.33
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 +5 -2
- package/src/asset-server/index.ts +13 -1
- package/src/events.ts +469 -0
- package/src/tools.ts +85 -11
package/package.json
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@decocms/runtime",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.33",
|
|
4
4
|
"type": "module",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"check": "tsc --noEmit"
|
|
7
|
+
},
|
|
5
8
|
"dependencies": {
|
|
6
9
|
"@cloudflare/workers-types": "^4.20250617.0",
|
|
7
10
|
"@deco/mcp": "npm:@jsr/deco__mcp@0.7.8",
|
|
8
|
-
"@decocms/bindings": "1.0.1-alpha.
|
|
11
|
+
"@decocms/bindings": "1.0.1-alpha.19",
|
|
9
12
|
"@modelcontextprotocol/sdk": "1.20.2",
|
|
10
13
|
"@ai-sdk/provider": "^2.0.0",
|
|
11
14
|
"hono": "^4.10.7",
|
|
@@ -6,11 +6,21 @@ import { Handler } from "hono/types";
|
|
|
6
6
|
interface AssetServerConfig {
|
|
7
7
|
env: "development" | "production" | "test";
|
|
8
8
|
localDevProxyUrl?: string | URL;
|
|
9
|
+
/**
|
|
10
|
+
* The prefix to use for serving the assets.
|
|
11
|
+
* Default: "/assets/*"
|
|
12
|
+
*/
|
|
13
|
+
assetsMiddlewarePath?: string;
|
|
14
|
+
/**
|
|
15
|
+
* The directory to serve the assets from.
|
|
16
|
+
* Default: "./dist/client"
|
|
17
|
+
*/
|
|
9
18
|
assetsDirectory?: string;
|
|
10
19
|
}
|
|
11
20
|
|
|
12
21
|
const DEFAULT_LOCAL_DEV_PROXY_URL = "http://localhost:4000";
|
|
13
22
|
const DEFAULT_ASSETS_DIRECTORY = "./dist/client";
|
|
23
|
+
const DEFAULT_ASSETS_MIDDLEWARE_PATH = "/assets/*";
|
|
14
24
|
|
|
15
25
|
interface HonoApp {
|
|
16
26
|
use: (path: string, handler: Handler) => void;
|
|
@@ -25,11 +35,13 @@ export const applyAssetServerRoutes = (
|
|
|
25
35
|
const localDevProxyUrl =
|
|
26
36
|
config.localDevProxyUrl ?? DEFAULT_LOCAL_DEV_PROXY_URL;
|
|
27
37
|
const assetsDirectory = config.assetsDirectory ?? DEFAULT_ASSETS_DIRECTORY;
|
|
38
|
+
const assetsMiddlewarePath =
|
|
39
|
+
config.assetsMiddlewarePath ?? DEFAULT_ASSETS_MIDDLEWARE_PATH;
|
|
28
40
|
|
|
29
41
|
if (environment === "development") {
|
|
30
42
|
app.use("*", devServerProxy(localDevProxyUrl));
|
|
31
43
|
} else if (environment === "production") {
|
|
32
|
-
app.use(
|
|
44
|
+
app.use(assetsMiddlewarePath, serveStatic({ root: assetsDirectory }));
|
|
33
45
|
app.get("*", serveStatic({ path: `${assetsDirectory}/index.html` }));
|
|
34
46
|
}
|
|
35
47
|
};
|
package/src/events.ts
ADDED
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
CloudEvent,
|
|
3
|
+
EventResult,
|
|
4
|
+
OnEventsOutput,
|
|
5
|
+
} from "@decocms/bindings";
|
|
6
|
+
import z from "zod";
|
|
7
|
+
|
|
8
|
+
// ============================================================================
|
|
9
|
+
// Types
|
|
10
|
+
// ============================================================================
|
|
11
|
+
|
|
12
|
+
export interface EventSubscription {
|
|
13
|
+
connectionId: string;
|
|
14
|
+
events: string[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface Binding {
|
|
18
|
+
__type: string;
|
|
19
|
+
value: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Per-event handler - handles events of a specific type
|
|
24
|
+
* Returns result for each event individually
|
|
25
|
+
*/
|
|
26
|
+
export type PerEventHandler<TEnv> = (
|
|
27
|
+
context: { events: CloudEvent[] },
|
|
28
|
+
env: TEnv,
|
|
29
|
+
) => EventResult | Promise<EventResult>;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Batch handler function - handles multiple events at once
|
|
33
|
+
* Can return batch result or per-event results
|
|
34
|
+
*/
|
|
35
|
+
export type BatchHandlerFn<TEnv> = (
|
|
36
|
+
context: { events: CloudEvent[] },
|
|
37
|
+
env: TEnv,
|
|
38
|
+
) => OnEventsOutput | Promise<OnEventsOutput>;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Batch handler with explicit event types for subscription
|
|
42
|
+
*/
|
|
43
|
+
export interface BatchHandler<TEnv> {
|
|
44
|
+
/** Handler function */
|
|
45
|
+
handler: BatchHandlerFn<TEnv>;
|
|
46
|
+
/** Event types to subscribe to */
|
|
47
|
+
events: string[];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Binding-level handlers - either a batch handler with events or per-event handlers
|
|
52
|
+
*
|
|
53
|
+
* @example Per-event handlers (event types inferred from keys)
|
|
54
|
+
* ```ts
|
|
55
|
+
* { "order.created": handler, "order.updated": handler }
|
|
56
|
+
* ```
|
|
57
|
+
*
|
|
58
|
+
* @example Batch handler with explicit events
|
|
59
|
+
* ```ts
|
|
60
|
+
* { handler: fn, events: ["order.created", "order.updated"] }
|
|
61
|
+
* ```
|
|
62
|
+
*/
|
|
63
|
+
export type BindingHandlers<TEnv> =
|
|
64
|
+
| BatchHandler<TEnv>
|
|
65
|
+
| Record<string, PerEventHandler<TEnv>>;
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* EventHandlers type supports three granularity levels:
|
|
69
|
+
*
|
|
70
|
+
* @example Global handler with explicit events
|
|
71
|
+
* ```ts
|
|
72
|
+
* { handler: (ctx, env) => result, events: ["order.created"] }
|
|
73
|
+
* ```
|
|
74
|
+
*
|
|
75
|
+
* @example Per-binding batch handler
|
|
76
|
+
* ```ts
|
|
77
|
+
* { DATABASE: { handler: fn, events: ["order.created"] } }
|
|
78
|
+
* ```
|
|
79
|
+
*
|
|
80
|
+
* @example Per-event handlers (events inferred from keys)
|
|
81
|
+
* ```ts
|
|
82
|
+
* { DATABASE: { "order.created": (ctx, env) => result } }
|
|
83
|
+
* ```
|
|
84
|
+
*/
|
|
85
|
+
export type EventHandlers<TSchema extends z.ZodTypeAny = never> = [
|
|
86
|
+
TSchema,
|
|
87
|
+
] extends [never]
|
|
88
|
+
? Record<string, never>
|
|
89
|
+
:
|
|
90
|
+
| BatchHandler<z.infer<TSchema>> // Global handler with events
|
|
91
|
+
| {
|
|
92
|
+
[K in keyof z.infer<TSchema> as z.infer<TSchema>[K] extends {
|
|
93
|
+
__type: string;
|
|
94
|
+
value: string;
|
|
95
|
+
}
|
|
96
|
+
? K
|
|
97
|
+
: never]?: BindingHandlers<z.infer<TSchema>>;
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Extract only the keys from T where the value is a Binding shape.
|
|
102
|
+
* Filters out non-binding properties at the type level.
|
|
103
|
+
*/
|
|
104
|
+
export type BindingKeysOf<T> = {
|
|
105
|
+
[K in keyof T]: T[K] extends { __type: string; value: string } ? K : never;
|
|
106
|
+
}[keyof T];
|
|
107
|
+
|
|
108
|
+
// ============================================================================
|
|
109
|
+
// Type Guards
|
|
110
|
+
// ============================================================================
|
|
111
|
+
|
|
112
|
+
const isBinding = (v: unknown): v is Binding => {
|
|
113
|
+
return (
|
|
114
|
+
typeof v === "object" &&
|
|
115
|
+
v !== null &&
|
|
116
|
+
"__type" in v &&
|
|
117
|
+
typeof v.__type === "string" &&
|
|
118
|
+
"value" in v &&
|
|
119
|
+
typeof v.value === "string"
|
|
120
|
+
);
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Check if handlers is a global batch handler (has handler + events at top level)
|
|
125
|
+
*/
|
|
126
|
+
const isGlobalHandler = <TEnv>(
|
|
127
|
+
handlers: EventHandlers<z.ZodTypeAny>,
|
|
128
|
+
): handlers is BatchHandler<TEnv> => {
|
|
129
|
+
return (
|
|
130
|
+
typeof handlers === "object" &&
|
|
131
|
+
handlers !== null &&
|
|
132
|
+
"handler" in handlers &&
|
|
133
|
+
"events" in handlers &&
|
|
134
|
+
typeof handlers.handler === "function" &&
|
|
135
|
+
Array.isArray(handlers.events)
|
|
136
|
+
);
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Check if a binding handler is a batch handler (has handler + events) vs per-event handlers (object of functions)
|
|
141
|
+
*/
|
|
142
|
+
const isBatchHandler = <TEnv>(
|
|
143
|
+
handler: BindingHandlers<TEnv>,
|
|
144
|
+
): handler is BatchHandler<TEnv> => {
|
|
145
|
+
return (
|
|
146
|
+
typeof handler === "object" &&
|
|
147
|
+
handler !== null &&
|
|
148
|
+
"handler" in handler &&
|
|
149
|
+
"events" in handler &&
|
|
150
|
+
typeof handler.handler === "function" &&
|
|
151
|
+
Array.isArray(handler.events)
|
|
152
|
+
);
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
// ============================================================================
|
|
156
|
+
// Helper Functions
|
|
157
|
+
// ============================================================================
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Get binding keys from event handlers object
|
|
161
|
+
*/
|
|
162
|
+
const getBindingKeys = <TSchema extends z.ZodTypeAny>(
|
|
163
|
+
handlers: EventHandlers<TSchema>,
|
|
164
|
+
): string[] => {
|
|
165
|
+
if (isGlobalHandler(handlers)) {
|
|
166
|
+
return [];
|
|
167
|
+
}
|
|
168
|
+
return Object.keys(handlers);
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Get event types for a binding from handlers
|
|
173
|
+
*/
|
|
174
|
+
const getEventTypesForBinding = <TSchema extends z.ZodTypeAny>(
|
|
175
|
+
handlers: EventHandlers<TSchema>,
|
|
176
|
+
binding: string,
|
|
177
|
+
): string[] => {
|
|
178
|
+
if (isGlobalHandler(handlers)) {
|
|
179
|
+
return handlers.events;
|
|
180
|
+
}
|
|
181
|
+
const bindingHandler = handlers[binding as keyof typeof handlers];
|
|
182
|
+
if (!bindingHandler) {
|
|
183
|
+
return [];
|
|
184
|
+
}
|
|
185
|
+
if (isBatchHandler(bindingHandler)) {
|
|
186
|
+
// Batch handler - return explicit events array
|
|
187
|
+
return bindingHandler.events;
|
|
188
|
+
}
|
|
189
|
+
// Per-event handlers - event types are the keys
|
|
190
|
+
return Object.keys(bindingHandler);
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Get scopes from event handlers for subscription
|
|
195
|
+
*/
|
|
196
|
+
const scopesFromEvents = <TSchema extends z.ZodTypeAny = never>(
|
|
197
|
+
handlers: EventHandlers<TSchema>,
|
|
198
|
+
): string[] => {
|
|
199
|
+
if (isGlobalHandler(handlers)) {
|
|
200
|
+
// Global handler - scopes are based on explicit events array
|
|
201
|
+
// Note: "*" binding means all bindings
|
|
202
|
+
return handlers.events.map((event) => `*::event@${event}`);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const scopes: string[] = [];
|
|
206
|
+
for (const binding of getBindingKeys(handlers)) {
|
|
207
|
+
const eventTypes = getEventTypesForBinding(handlers, binding);
|
|
208
|
+
for (const eventType of eventTypes) {
|
|
209
|
+
scopes.push(`${binding}::event@${eventType}`);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return scopes;
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Get subscriptions from event handlers and state
|
|
217
|
+
*/
|
|
218
|
+
const eventsSubscriptions = <TSchema extends z.ZodTypeAny = never>(
|
|
219
|
+
handlers: EventHandlers<TSchema>,
|
|
220
|
+
state: z.infer<TSchema>,
|
|
221
|
+
): EventSubscription[] => {
|
|
222
|
+
if (isGlobalHandler(handlers)) {
|
|
223
|
+
// Global handler - subscribe to all bindings with the explicit events
|
|
224
|
+
const subscriptions: EventSubscription[] = [];
|
|
225
|
+
for (const [, value] of Object.entries(state)) {
|
|
226
|
+
if (isBinding(value)) {
|
|
227
|
+
subscriptions.push({
|
|
228
|
+
connectionId: value.value,
|
|
229
|
+
events: handlers.events,
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return subscriptions;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const subscriptions: EventSubscription[] = [];
|
|
237
|
+
for (const binding of getBindingKeys(handlers)) {
|
|
238
|
+
const bindingValue = state[binding as keyof typeof state];
|
|
239
|
+
if (!isBinding(bindingValue)) continue;
|
|
240
|
+
|
|
241
|
+
const eventTypes = getEventTypesForBinding(handlers, binding);
|
|
242
|
+
subscriptions.push({
|
|
243
|
+
connectionId: bindingValue.value,
|
|
244
|
+
events: eventTypes,
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
return subscriptions;
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
// ============================================================================
|
|
251
|
+
// Event Execution
|
|
252
|
+
// ============================================================================
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Group events by source (connection ID)
|
|
256
|
+
*/
|
|
257
|
+
const groupEventsBySource = (
|
|
258
|
+
events: CloudEvent[],
|
|
259
|
+
): Map<string, CloudEvent[]> => {
|
|
260
|
+
const grouped = new Map<string, CloudEvent[]>();
|
|
261
|
+
for (const event of events) {
|
|
262
|
+
const source = event.source;
|
|
263
|
+
const existing = grouped.get(source) || [];
|
|
264
|
+
existing.push(event);
|
|
265
|
+
grouped.set(source, existing);
|
|
266
|
+
}
|
|
267
|
+
return grouped;
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Group events by type
|
|
272
|
+
*/
|
|
273
|
+
const groupEventsByType = (events: CloudEvent[]): Map<string, CloudEvent[]> => {
|
|
274
|
+
const grouped = new Map<string, CloudEvent[]>();
|
|
275
|
+
for (const event of events) {
|
|
276
|
+
const type = event.type;
|
|
277
|
+
const existing = grouped.get(type) || [];
|
|
278
|
+
existing.push(event);
|
|
279
|
+
grouped.set(type, existing);
|
|
280
|
+
}
|
|
281
|
+
return grouped;
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Merge multiple OnEventsOutput results into a single result
|
|
286
|
+
*/
|
|
287
|
+
const mergeResults = (results: OnEventsOutput[]): OnEventsOutput => {
|
|
288
|
+
const merged: OnEventsOutput = {};
|
|
289
|
+
const allResults: Record<string, EventResult> = {};
|
|
290
|
+
|
|
291
|
+
let hasAnyFailure = false;
|
|
292
|
+
let totalProcessed = 0;
|
|
293
|
+
const errors: string[] = [];
|
|
294
|
+
|
|
295
|
+
for (const result of results) {
|
|
296
|
+
// Merge per-event results
|
|
297
|
+
if (result.results) {
|
|
298
|
+
Object.assign(allResults, result.results);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Track batch-level status
|
|
302
|
+
if (result.success === false) {
|
|
303
|
+
hasAnyFailure = true;
|
|
304
|
+
if (result.error) {
|
|
305
|
+
errors.push(result.error);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (result.processedCount !== undefined) {
|
|
310
|
+
totalProcessed += result.processedCount;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Build merged result
|
|
315
|
+
if (Object.keys(allResults).length > 0) {
|
|
316
|
+
merged.results = allResults;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Set batch-level success based on all results
|
|
320
|
+
merged.success = !hasAnyFailure;
|
|
321
|
+
|
|
322
|
+
if (errors.length > 0) {
|
|
323
|
+
merged.error = errors.join("; ");
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (totalProcessed > 0) {
|
|
327
|
+
merged.processedCount = totalProcessed;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
return merged;
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Execute event handlers and return merged result
|
|
335
|
+
*
|
|
336
|
+
* Supports three handler formats:
|
|
337
|
+
* 1. Global: `(context, env) => result` - handles all events
|
|
338
|
+
* 2. Per-binding: `{ BINDING: (context, env) => result }` - handles all events from binding
|
|
339
|
+
* 3. Per-event: `{ BINDING: { "event.type": (context, env) => result } }` - handles specific events
|
|
340
|
+
*/
|
|
341
|
+
const executeEventHandlers = async <TSchema extends z.ZodTypeAny>(
|
|
342
|
+
handlers: EventHandlers<TSchema>,
|
|
343
|
+
events: CloudEvent[],
|
|
344
|
+
env: z.infer<TSchema>,
|
|
345
|
+
state: z.infer<TSchema>,
|
|
346
|
+
): Promise<OnEventsOutput> => {
|
|
347
|
+
// Case 1: Global handler
|
|
348
|
+
if (isGlobalHandler(handlers)) {
|
|
349
|
+
try {
|
|
350
|
+
return await handlers.handler({ events }, env);
|
|
351
|
+
} catch (error) {
|
|
352
|
+
return {
|
|
353
|
+
success: false,
|
|
354
|
+
error: error instanceof Error ? error.message : String(error),
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// Build a map from connectionId -> binding key
|
|
360
|
+
const connectionToBinding = new Map<string, string>();
|
|
361
|
+
for (const binding of getBindingKeys(handlers)) {
|
|
362
|
+
const bindingValue = state[binding as keyof typeof state];
|
|
363
|
+
if (isBinding(bindingValue)) {
|
|
364
|
+
connectionToBinding.set(bindingValue.value, binding);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Group events by source
|
|
369
|
+
const eventsBySource = groupEventsBySource(events);
|
|
370
|
+
|
|
371
|
+
// Process each binding's events in parallel
|
|
372
|
+
const promises: Promise<OnEventsOutput>[] = [];
|
|
373
|
+
|
|
374
|
+
for (const [source, sourceEvents] of eventsBySource) {
|
|
375
|
+
const binding = connectionToBinding.get(source);
|
|
376
|
+
if (!binding) {
|
|
377
|
+
// No handler for this source - mark as success (ignore)
|
|
378
|
+
continue;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const bindingHandler = handlers[binding as keyof typeof handlers];
|
|
382
|
+
if (!bindingHandler) continue;
|
|
383
|
+
|
|
384
|
+
// Case 2: Per-binding batch handler
|
|
385
|
+
if (isBatchHandler(bindingHandler)) {
|
|
386
|
+
promises.push(
|
|
387
|
+
(async () => {
|
|
388
|
+
try {
|
|
389
|
+
return await bindingHandler.handler({ events: sourceEvents }, env);
|
|
390
|
+
} catch (error) {
|
|
391
|
+
// Mark all events from this binding as failed
|
|
392
|
+
const results: Record<string, EventResult> = {};
|
|
393
|
+
for (const event of sourceEvents) {
|
|
394
|
+
results[event.id] = {
|
|
395
|
+
success: false,
|
|
396
|
+
error: error instanceof Error ? error.message : String(error),
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
return { results };
|
|
400
|
+
}
|
|
401
|
+
})(),
|
|
402
|
+
);
|
|
403
|
+
continue;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// Case 3: Per-event handlers
|
|
407
|
+
const perEventHandlers = bindingHandler as Record<
|
|
408
|
+
string,
|
|
409
|
+
PerEventHandler<z.infer<TSchema>>
|
|
410
|
+
>;
|
|
411
|
+
const eventsByType = groupEventsByType(sourceEvents);
|
|
412
|
+
|
|
413
|
+
for (const [eventType, typedEvents] of eventsByType) {
|
|
414
|
+
const eventHandler = perEventHandlers[eventType];
|
|
415
|
+
if (!eventHandler) {
|
|
416
|
+
// No handler for this event type - mark as success (ignore)
|
|
417
|
+
continue;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// Call handler for each event type (handler receives all events of that type)
|
|
421
|
+
promises.push(
|
|
422
|
+
(async () => {
|
|
423
|
+
try {
|
|
424
|
+
const result = await eventHandler({ events: typedEvents }, env);
|
|
425
|
+
// Convert per-event result to output with results for each event
|
|
426
|
+
const results: Record<string, EventResult> = {};
|
|
427
|
+
for (const event of typedEvents) {
|
|
428
|
+
results[event.id] = result;
|
|
429
|
+
}
|
|
430
|
+
return { results };
|
|
431
|
+
} catch (error) {
|
|
432
|
+
const results: Record<string, EventResult> = {};
|
|
433
|
+
for (const event of typedEvents) {
|
|
434
|
+
results[event.id] = {
|
|
435
|
+
success: false,
|
|
436
|
+
error: error instanceof Error ? error.message : String(error),
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
return { results };
|
|
440
|
+
}
|
|
441
|
+
})(),
|
|
442
|
+
);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// Wait for all handlers to complete
|
|
447
|
+
const results = await Promise.all(promises);
|
|
448
|
+
|
|
449
|
+
// If no handlers were called, return success
|
|
450
|
+
if (results.length === 0) {
|
|
451
|
+
return { success: true };
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// Merge all results
|
|
455
|
+
return mergeResults(results);
|
|
456
|
+
};
|
|
457
|
+
|
|
458
|
+
// ============================================================================
|
|
459
|
+
// Exports
|
|
460
|
+
// ============================================================================
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* Event utilities for subscriptions and execution
|
|
464
|
+
*/
|
|
465
|
+
export const Event = {
|
|
466
|
+
subscriptions: eventsSubscriptions,
|
|
467
|
+
scopes: scopesFromEvents,
|
|
468
|
+
execute: executeEventHandlers,
|
|
469
|
+
};
|
package/src/tools.ts
CHANGED
|
@@ -1,13 +1,22 @@
|
|
|
1
1
|
/* oxlint-disable no-explicit-any */
|
|
2
2
|
/* oxlint-disable ban-types */
|
|
3
3
|
import { HttpServerTransport } from "@deco/mcp/http";
|
|
4
|
+
import {
|
|
5
|
+
OnEventsInputSchema,
|
|
6
|
+
OnEventsOutputSchema,
|
|
7
|
+
type EventBusBindingClient,
|
|
8
|
+
} from "@decocms/bindings";
|
|
4
9
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
5
10
|
import { z } from "zod";
|
|
6
11
|
import { zodToJsonSchema } from "zod-to-json-schema";
|
|
12
|
+
import { Event, type EventHandlers } from "./events.ts";
|
|
7
13
|
import type { DefaultEnv } from "./index.ts";
|
|
8
14
|
import { State } from "./state.ts";
|
|
9
15
|
import { Binding } from "./wrangler.ts";
|
|
10
16
|
|
|
17
|
+
// Re-export EventHandlers type for external use
|
|
18
|
+
export type { EventHandlers } from "./events.ts";
|
|
19
|
+
|
|
11
20
|
export const createRuntimeContext = (prev?: AppContext) => {
|
|
12
21
|
const store = State.getStore();
|
|
13
22
|
if (!store) {
|
|
@@ -213,12 +222,23 @@ export interface OAuthConfig {
|
|
|
213
222
|
};
|
|
214
223
|
}
|
|
215
224
|
|
|
225
|
+
/**
|
|
226
|
+
* Constructs a type by picking all properties from T that are assignable to Value.
|
|
227
|
+
*/
|
|
228
|
+
type PickByType<T, Value> = {
|
|
229
|
+
[P in keyof T as T[P] extends Value ? P : never]: T[P];
|
|
230
|
+
};
|
|
231
|
+
|
|
216
232
|
export interface CreateMCPServerOptions<
|
|
217
233
|
Env = unknown,
|
|
218
234
|
TSchema extends z.ZodTypeAny = never,
|
|
219
235
|
> {
|
|
220
236
|
before?: (env: Env & DefaultEnv<TSchema>) => Promise<void> | void;
|
|
221
237
|
oauth?: OAuthConfig;
|
|
238
|
+
events?: {
|
|
239
|
+
bus?: keyof PickByType<Env & DefaultEnv<TSchema>, EventBusBindingClient>;
|
|
240
|
+
handlers?: EventHandlers<TSchema>;
|
|
241
|
+
};
|
|
222
242
|
configuration?: {
|
|
223
243
|
onChange?: (
|
|
224
244
|
env: Env & DefaultEnv<TSchema>,
|
|
@@ -255,17 +275,22 @@ export interface AppContext<TEnv extends DefaultEnv = DefaultEnv> {
|
|
|
255
275
|
req?: Request;
|
|
256
276
|
}
|
|
257
277
|
|
|
258
|
-
const
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
278
|
+
const getEventBus = (
|
|
279
|
+
prop: string | number,
|
|
280
|
+
env: DefaultEnv,
|
|
281
|
+
): EventBusBindingClient | undefined => {
|
|
282
|
+
const bus = env as unknown as { [prop]: EventBusBindingClient };
|
|
283
|
+
return typeof bus[prop] !== "undefined" ? bus[prop] : undefined;
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
const toolsFor = <TSchema extends z.ZodTypeAny = never>({
|
|
287
|
+
events,
|
|
288
|
+
configuration: { state: schema, scopes, onChange } = {},
|
|
289
|
+
}: CreateMCPServerOptions<any, TSchema> = {}): CreatedTool[] => {
|
|
266
290
|
const jsonSchema = schema
|
|
267
291
|
? zodToJsonSchema(schema)
|
|
268
292
|
: { type: "object", properties: {} };
|
|
293
|
+
const busProp = String(events?.bus ?? "EVENT_BUS");
|
|
269
294
|
return [
|
|
270
295
|
...(onChange
|
|
271
296
|
? [
|
|
@@ -282,15 +307,60 @@ const configurationToolsFor = <TSchema extends z.ZodTypeAny = never>({
|
|
|
282
307
|
}),
|
|
283
308
|
outputSchema: z.object({}),
|
|
284
309
|
execute: async (input) => {
|
|
310
|
+
const state = input.context.state as z.infer<TSchema>;
|
|
285
311
|
await onChange(input.runtimeContext.env, {
|
|
286
|
-
state
|
|
312
|
+
state,
|
|
287
313
|
scopes: input.context.scopes,
|
|
288
314
|
});
|
|
315
|
+
const bus = getEventBus(busProp, input.runtimeContext.env);
|
|
316
|
+
if (events && state && bus) {
|
|
317
|
+
// subscribe to events
|
|
318
|
+
const subscriptions = Event.subscriptions(
|
|
319
|
+
events?.handlers ?? {},
|
|
320
|
+
state,
|
|
321
|
+
);
|
|
322
|
+
|
|
323
|
+
await Promise.all(
|
|
324
|
+
subscriptions.map(async (subscription) => {
|
|
325
|
+
return Promise.all(
|
|
326
|
+
subscription.events.map(async (event) => {
|
|
327
|
+
return bus.EVENT_SUBSCRIBE({
|
|
328
|
+
publisher: subscription.connectionId,
|
|
329
|
+
eventType: event,
|
|
330
|
+
});
|
|
331
|
+
}),
|
|
332
|
+
);
|
|
333
|
+
}),
|
|
334
|
+
);
|
|
335
|
+
}
|
|
289
336
|
return Promise.resolve({});
|
|
290
337
|
},
|
|
291
338
|
}),
|
|
292
339
|
]
|
|
293
340
|
: []),
|
|
341
|
+
|
|
342
|
+
...(events?.handlers
|
|
343
|
+
? [
|
|
344
|
+
createTool({
|
|
345
|
+
id: "ON_EVENTS",
|
|
346
|
+
description:
|
|
347
|
+
"Receive and process CloudEvents from the event bus. Returns per-event or batch results.",
|
|
348
|
+
inputSchema: OnEventsInputSchema,
|
|
349
|
+
outputSchema: OnEventsOutputSchema,
|
|
350
|
+
execute: async (input) => {
|
|
351
|
+
const env = input.runtimeContext.env;
|
|
352
|
+
// Get state from env - it should have the binding values
|
|
353
|
+
const state = env as z.infer<TSchema>;
|
|
354
|
+
return Event.execute(
|
|
355
|
+
events.handlers!,
|
|
356
|
+
input.context.events,
|
|
357
|
+
env,
|
|
358
|
+
state,
|
|
359
|
+
);
|
|
360
|
+
},
|
|
361
|
+
}),
|
|
362
|
+
]
|
|
363
|
+
: []),
|
|
294
364
|
createTool({
|
|
295
365
|
id: "MCP_CONFIGURATION",
|
|
296
366
|
description: "MCP Configuration",
|
|
@@ -302,7 +372,11 @@ const configurationToolsFor = <TSchema extends z.ZodTypeAny = never>({
|
|
|
302
372
|
execute: () => {
|
|
303
373
|
return Promise.resolve({
|
|
304
374
|
stateSchema: jsonSchema,
|
|
305
|
-
scopes
|
|
375
|
+
scopes: [
|
|
376
|
+
...(scopes ?? []),
|
|
377
|
+
...Event.scopes(events?.handlers ?? {}),
|
|
378
|
+
...(busProp ? [`${busProp}::EVENT_SUBSCRIBE`] : []),
|
|
379
|
+
],
|
|
306
380
|
});
|
|
307
381
|
},
|
|
308
382
|
}),
|
|
@@ -353,7 +427,7 @@ export const createMCPServer = <
|
|
|
353
427
|
};
|
|
354
428
|
const tools = await toolsFn(bindings);
|
|
355
429
|
|
|
356
|
-
tools.push(...
|
|
430
|
+
tools.push(...toolsFor<TSchema>(options));
|
|
357
431
|
|
|
358
432
|
for (const tool of tools) {
|
|
359
433
|
server.registerTool(
|