@decocms/runtime 1.0.3 → 1.1.1

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decocms/runtime",
3
- "version": "1.0.3",
3
+ "version": "1.1.1",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "check": "tsc --noEmit",
@@ -8,13 +8,12 @@
8
8
  },
9
9
  "dependencies": {
10
10
  "@cloudflare/workers-types": "^4.20250617.0",
11
- "@decocms/bindings": "1.0.3",
11
+ "@decocms/bindings": "1.0.7",
12
12
  "@modelcontextprotocol/sdk": "1.25.1",
13
- "@ai-sdk/provider": "^2.0.0",
13
+ "@ai-sdk/provider": "^3.0.0",
14
14
  "hono": "^4.10.7",
15
15
  "jose": "^6.0.11",
16
- "zod": "^3.25.76",
17
- "zod-to-json-schema": "3.25.0"
16
+ "zod": "^4.0.0"
18
17
  },
19
18
  "exports": {
20
19
  ".": "./src/index.ts",
@@ -35,4 +34,4 @@
35
34
  "publishConfig": {
36
35
  "access": "public"
37
36
  }
38
- }
37
+ }
@@ -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: z.ZodType<TInput>;
21
- outputSchema?: z.ZodType<TReturn>;
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 z.ZodType
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 z.ZodType
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 "packages/bindings/src/well-known/collections.ts";
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<TName>(name).default(name),
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
- /** Event types to subscribe to */
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, Binding = unknown> =
85
+ export type BindingHandlers<TEnv> =
60
86
  | BatchHandler<TEnv>
61
- | (Record<string, PerEventHandler<TEnv>> & CronHandlers<Binding, TEnv>);
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 three granularity levels:
90
+ * EventHandlers type supports four handler formats:
73
91
  *
74
- * @example Global handler with explicit events
92
+ * @example Global handler with prefixed events (BINDING::EVENT_TYPE)
75
93
  * ```ts
76
- * { handler: (ctx, env) => result, events: ["order.created"] }
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
- ? Record<string, never>
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, z.infer<TSchema>[K]>;
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 - subscribe to all bindings with the explicit events
305
+ // Global handler - events must be prefixed with BINDING::EVENT_TYPE
197
306
  const subscriptions: EventSubscription[] = [];
198
- for (const [, value] of Object.entries(state)) {
199
- if (isBinding(value)) {
200
- for (const eventType of handlers.events) {
201
- subscriptions.push({
202
- eventType,
203
- publisher: value.value,
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 bindingValue = state[binding as keyof typeof state];
214
- if (!isBinding(bindingValue)) continue;
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: bindingValue.value,
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 three handler formats:
314
- * 1. Global: `(context, env) => result` - handles all events
315
- * 2. Per-binding: `{ BINDING: (context, env) => result }` - handles all events from binding
316
- * 3. Per-event: `{ BINDING: { "event.type": (context, env) => result } }` - handles specific events
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: z.infer<TSchema>,
322
- state: z.infer<TSchema>,
445
+ env: TEnv,
446
+ state: z.infer<TSchema> | Record<string, unknown>,
447
+ connectionId?: string,
323
448
  ): Promise<OnEventsOutput> => {
324
- // Case 1: Global handler
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 bindingValue = state[binding as keyof typeof state];
340
- if (isBinding(bindingValue)) {
341
- connectionToBinding.set(bindingValue.value, binding);
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<z.infer<TSchema>>
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 3a: Cron handlers (event type starts with "cron/")
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 type { z } from "zod";
3
+ import { z } from "zod";
4
4
  import {
5
5
  BindingRegistry,
6
6
  initializeBindings,
@@ -105,6 +105,7 @@ export interface RequestContext<
105
105
  }) => User | undefined;
106
106
  callerApp?: string;
107
107
  connectionId?: string;
108
+ organizationId?: string;
108
109
  }
109
110
 
110
111
  const withDefaultBindings = ({
@@ -164,7 +165,7 @@ export const withBindings = <TEnv>({
164
165
  authToken,
165
166
  }: {
166
167
  env: TEnv;
167
- server: MCPServer<TEnv, any>;
168
+ server: MCPServer<TEnv, any, any>;
168
169
  // token is x-mesh-token
169
170
  tokenOrContext?: string | RequestContext;
170
171
  // authToken is the authorization header
@@ -183,6 +184,7 @@ export const withBindings = <TEnv>({
183
184
  state?: Record<string, unknown>;
184
185
  meshUrl?: string;
185
186
  connectionId?: string;
187
+ organizationId?: string;
186
188
  }) ?? {};
187
189
 
188
190
  context = {
@@ -191,6 +193,8 @@ export const withBindings = <TEnv>({
191
193
  token: tokenOrContext,
192
194
  meshUrl: (decoded.meshUrl as string) ?? metadata.meshUrl,
193
195
  connectionId: (decoded.connectionId as string) ?? metadata.connectionId,
196
+ organizationId:
197
+ (decoded.organizationId as string) ?? metadata.organizationId,
194
198
  ensureAuthenticated: AUTHENTICATED(decoded.user ?? decoded.sub),
195
199
  } as RequestContext<any>;
196
200
  } else if (typeof tokenOrContext === "object") {
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 { zodToJsonSchema } from "zod-to-json-schema";
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 z.ZodTypeAny = z.ZodTypeAny,
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 z.ZodTypeAny = z.ZodTypeAny,
44
- TSchemaOut extends z.ZodTypeAny | undefined = undefined,
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 z.ZodSchema
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 z.ZodSchema = z.ZodSchema> {
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: z.ZodTypeAny;
76
- outputSchema?: z.ZodTypeAny;
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
- TArgs extends PromptArgsRawShape = PromptArgsRawShape,
101
+ _TArgs extends PromptArgsRawShape = PromptArgsRawShape,
103
102
  > {
104
- args: z.objectOutputType<TArgs, z.ZodTypeAny>;
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 z.ZodSchema = z.ZodSchema,
142
- TSchemaOut extends z.ZodSchema | undefined = undefined,
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 extends z.ZodSchema = z.ZodSchema,
159
- >(streamableTool: StreamableTool<TSchemaIn>): StreamableTool<TSchemaIn> {
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 z.ZodSchema = z.ZodSchema,
177
- TSchemaOut extends z.ZodSchema | undefined = undefined,
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 z.ZodTypeAny = never,
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 z.ZodTypeAny = never>({
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
- ? zodToJsonSchema(schema)
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.state as z.infer<TSchema>;
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 z.ZodTypeAny = never,
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 z.ZodTypeAny = never,
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 z.ZodRawShape)
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 z.ZodRawShape)
575
+ ? (tool.outputSchema.shape as ZodRawShape)
546
576
  : z.object({}).shape,
547
577
  },
548
578
  async (args) => {