@decocms/runtime 1.2.11 → 1.2.13

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/src/tools.ts CHANGED
@@ -13,14 +13,21 @@ import type {
13
13
  } from "@modelcontextprotocol/sdk/types.js";
14
14
  import { z } from "zod";
15
15
  import type { ZodRawShape, ZodSchema, ZodTypeAny } from "zod";
16
- import { BindingRegistry } from "./bindings.ts";
16
+ import { BindingRegistry, injectBindingSchemas } from "./bindings.ts";
17
17
  import { Event, type EventHandlers } from "./events.ts";
18
18
  import type { DefaultEnv } from "./index.ts";
19
19
  import { State } from "./state.ts";
20
+ import {
21
+ type WorkflowDefinition,
22
+ Workflow,
23
+ WORKFLOW_SCOPES,
24
+ workflowToolId,
25
+ } from "./workflows.ts";
20
26
 
21
27
  // Re-export EventHandlers type and SELF constant for external use
22
28
  export { SELF } from "./events.ts";
23
29
  export type { EventHandlers } from "./events.ts";
30
+ export type { WorkflowDefinition } from "./workflows.ts";
24
31
 
25
32
  export const createRuntimeContext = (prev?: AppContext) => {
26
33
  const store = State.getStore();
@@ -42,13 +49,17 @@ export interface ToolExecutionContext<
42
49
 
43
50
  /**
44
51
  * Tool interface with generic schema types for type-safe tool creation.
52
+ *
53
+ * TId preserves the literal string type of `id` so consumers (e.g. the
54
+ * workflow builder) can derive union types of tool names without codegen.
45
55
  */
46
56
  export interface Tool<
47
57
  TSchemaIn extends ZodTypeAny = ZodTypeAny,
48
58
  TSchemaOut extends ZodTypeAny | undefined = undefined,
59
+ TId extends string = string,
49
60
  > {
50
61
  _meta?: Record<string, unknown>;
51
- id: string;
62
+ id: TId;
52
63
  description?: string;
53
64
  annotations?: ToolAnnotations;
54
65
  inputSchema: TSchemaIn;
@@ -246,7 +257,8 @@ export function createStreamableTool<TSchemaIn extends ZodSchema = ZodSchema>(
246
257
  export function createTool<
247
258
  TSchemaIn extends ZodSchema = ZodSchema,
248
259
  TSchemaOut extends ZodSchema | undefined = undefined,
249
- >(opts: Tool<TSchemaIn, TSchemaOut>): Tool<TSchemaIn, TSchemaOut> {
260
+ TId extends string = string,
261
+ >(opts: Tool<TSchemaIn, TSchemaOut, TId>): Tool<TSchemaIn, TSchemaOut, TId> {
250
262
  return {
251
263
  ...opts,
252
264
  execute: (input: ToolExecutionContext<TSchemaIn>) => {
@@ -517,6 +529,9 @@ export interface CreateMCPServerOptions<
517
529
  | Promise<CreatedResource[]>
518
530
  >
519
531
  | ((env: TEnv) => CreatedResource[] | Promise<CreatedResource[]>);
532
+ workflows?:
533
+ | WorkflowDefinition[]
534
+ | ((env: TEnv) => WorkflowDefinition[] | Promise<WorkflowDefinition[]>);
520
535
  }
521
536
 
522
537
  export type Fetch<TEnv = unknown> = (
@@ -541,16 +556,34 @@ const getEventBus = (
541
556
  : env?.MESH_REQUEST_CONTEXT?.state?.[prop];
542
557
  };
543
558
 
559
+ // TEnv is erased here because toolsFor() only reads events/workflows/configuration
560
+ // and doesn't need the full env type. Replacing `any` with a proper generic
561
+ // would require threading TEnv through toolsFor, which is a larger refactor.
562
+ type ResolvedMCPServerOptions<TSchema extends ZodTypeAny = never> = Omit<
563
+ CreateMCPServerOptions<any, TSchema>, // eslint-disable-line @typescript-eslint/no-explicit-any
564
+ "workflows"
565
+ > & { workflows?: WorkflowDefinition[] };
566
+
567
+ const getMeshCtx = (input: { runtimeContext: AppContext }) => {
568
+ const ctx = input.runtimeContext.env.MESH_REQUEST_CONTEXT;
569
+ return {
570
+ connectionId: ctx?.connectionId,
571
+ meshUrl: ctx?.meshUrl,
572
+ token: ctx?.token,
573
+ };
574
+ };
575
+
544
576
  const toolsFor = <TSchema extends ZodTypeAny = never>({
545
577
  events,
578
+ workflows,
546
579
  configuration: { state: schema, scopes, onChange } = {},
547
- }: CreateMCPServerOptions<any, TSchema> = {}): CreatedTool[] => {
580
+ }: ResolvedMCPServerOptions<TSchema> = {}): CreatedTool[] => {
548
581
  const jsonSchema = schema
549
- ? z.toJSONSchema(schema)
582
+ ? injectBindingSchemas(z.toJSONSchema(schema) as Record<string, unknown>)
550
583
  : { type: "object", properties: {} };
551
584
  const busProp = String(events?.bus ?? "EVENT_BUS");
552
585
  return [
553
- ...(onChange || events
586
+ ...(onChange || events || workflows?.length
554
587
  ? [
555
588
  createTool({
556
589
  id: "ON_MCP_CONFIGURATION",
@@ -573,9 +606,7 @@ const toolsFor = <TSchema extends ZodTypeAny = never>({
573
606
  });
574
607
  const bus = getEventBus(busProp, input.runtimeContext.env);
575
608
  if (events && state && bus) {
576
- // Get connectionId for SELF subscriptions
577
- const connectionId =
578
- input.runtimeContext.env.MESH_REQUEST_CONTEXT?.connectionId;
609
+ const { connectionId } = getMeshCtx(input);
579
610
  // Sync subscriptions - always call to handle deletions too
580
611
  const subscriptions = Event.subscriptions(
581
612
  events?.handlers ?? ({} as Record<string, never>),
@@ -607,6 +638,23 @@ const toolsFor = <TSchema extends ZodTypeAny = never>({
607
638
  );
608
639
  }
609
640
  }
641
+
642
+ if (workflows?.length) {
643
+ const {
644
+ connectionId: wfConnectionId,
645
+ meshUrl,
646
+ token,
647
+ } = getMeshCtx(input);
648
+ if (wfConnectionId && meshUrl) {
649
+ await Workflow.sync(
650
+ workflows,
651
+ meshUrl,
652
+ wfConnectionId,
653
+ token,
654
+ );
655
+ }
656
+ }
657
+
610
658
  return Promise.resolve({});
611
659
  },
612
660
  }),
@@ -623,10 +671,8 @@ const toolsFor = <TSchema extends ZodTypeAny = never>({
623
671
  outputSchema: OnEventsOutputSchema,
624
672
  execute: async (input) => {
625
673
  const env = input.runtimeContext.env;
626
- // Get state from MESH_REQUEST_CONTEXT - this has the binding values
627
674
  const state = env.MESH_REQUEST_CONTEXT?.state as z.infer<TSchema>;
628
- // Get connectionId for SELF handlers
629
- const connectionId = env.MESH_REQUEST_CONTEXT?.connectionId;
675
+ const { connectionId } = getMeshCtx(input);
630
676
  return Event.execute(
631
677
  events.handlers!,
632
678
  input.context.events,
@@ -652,10 +698,90 @@ const toolsFor = <TSchema extends ZodTypeAny = never>({
652
698
  scopes: [
653
699
  ...((scopes as string[]) ?? []),
654
700
  ...(events ? [`${busProp}::EVENT_SYNC_SUBSCRIPTIONS`] : []),
701
+ ...(workflows?.length ? [...WORKFLOW_SCOPES] : []),
655
702
  ],
656
703
  });
657
704
  },
658
705
  }),
706
+
707
+ // Auto-generated trigger tool for each declared workflow.
708
+ // Calls COLLECTION_WORKFLOW_EXECUTION_CREATE on the mesh and returns the
709
+ // execution ID immediately (fire-and-forget; poll with
710
+ // COLLECTION_WORKFLOW_EXECUTION_GET to track progress).
711
+ ...(workflows?.length
712
+ ? workflows.map((wf) => {
713
+ const id = wf.toolId ?? workflowToolId(wf.title);
714
+ return createTool({
715
+ id,
716
+ description: [
717
+ wf.description
718
+ ? `Run workflow: ${wf.description}`
719
+ : `Start the "${wf.title}" workflow.`,
720
+ "Returns an execution_id immediately. Use COLLECTION_WORKFLOW_EXECUTION_GET to track progress.",
721
+ ].join(" "),
722
+ inputSchema: z.object({
723
+ input: z
724
+ .record(z.string(), z.unknown())
725
+ .optional()
726
+ .describe(
727
+ "Input data for the workflow. Steps reference these values via @input.field.",
728
+ ),
729
+ virtual_mcp_id: z
730
+ .string()
731
+ .optional()
732
+ .describe(
733
+ wf.virtual_mcp_id
734
+ ? `Virtual MCP ID to use for execution (defaults to "${wf.virtual_mcp_id}").`
735
+ : "Virtual MCP ID that will execute the workflow steps.",
736
+ ),
737
+ start_at_epoch_ms: z
738
+ .number()
739
+ .int()
740
+ .min(0)
741
+ .optional()
742
+ .describe(
743
+ "Unix timestamp (ms) for scheduled execution. Omit to start immediately.",
744
+ ),
745
+ }),
746
+ outputSchema: z.object({
747
+ execution_id: z
748
+ .string()
749
+ .describe("ID of the created workflow execution."),
750
+ }),
751
+ execute: async (input) => {
752
+ const { connectionId, meshUrl, token } = getMeshCtx(input);
753
+
754
+ if (!connectionId || !meshUrl) {
755
+ throw new Error(
756
+ `[${id}] Missing MESH_REQUEST_CONTEXT (connectionId or meshUrl).`,
757
+ );
758
+ }
759
+
760
+ const ctx = input.context as {
761
+ input?: Record<string, unknown>;
762
+ virtual_mcp_id?: string;
763
+ start_at_epoch_ms?: number;
764
+ };
765
+
766
+ const virtualMcpId = ctx.virtual_mcp_id ?? wf.virtual_mcp_id;
767
+
768
+ const collectionId = Workflow.workflowId(connectionId, wf.title);
769
+ const executionId = await Workflow.createExecution(
770
+ meshUrl,
771
+ token,
772
+ {
773
+ workflow_collection_id: collectionId,
774
+ virtual_mcp_id: virtualMcpId,
775
+ input: ctx.input,
776
+ start_at_epoch_ms: ctx.start_at_epoch_ms,
777
+ },
778
+ );
779
+
780
+ return { execution_id: executionId };
781
+ },
782
+ });
783
+ })
784
+ : []),
659
785
  ];
660
786
  };
661
787
 
@@ -710,7 +836,14 @@ export const createMCPServer = <
710
836
  };
711
837
  const tools = await toolsFn(bindings);
712
838
 
713
- tools.push(...toolsFor<TSchema>(options));
839
+ const resolvedWorkflows =
840
+ typeof options.workflows === "function"
841
+ ? await options.workflows(bindings)
842
+ : options.workflows;
843
+
844
+ tools.push(
845
+ ...toolsFor<TSchema>({ ...options, workflows: resolvedWorkflows }),
846
+ );
714
847
 
715
848
  for (const tool of tools) {
716
849
  server.registerTool(
@@ -858,6 +991,9 @@ export const createMCPServer = <
858
991
  }
859
992
 
860
993
  // MCP SDK expects either text or blob content, not both
994
+ const meta =
995
+ (result as { _meta?: Record<string, unknown> | null })._meta ??
996
+ undefined;
861
997
  if (result.text !== undefined) {
862
998
  return {
863
999
  contents: [
@@ -865,6 +1001,7 @@ export const createMCPServer = <
865
1001
  uri: result.uri,
866
1002
  mimeType: result.mimeType,
867
1003
  text: result.text,
1004
+ ...(meta !== undefined ? { _meta: meta } : {}),
868
1005
  },
869
1006
  ],
870
1007
  };
@@ -875,6 +1012,7 @@ export const createMCPServer = <
875
1012
  uri: result.uri,
876
1013
  mimeType: result.mimeType,
877
1014
  blob: result.blob,
1015
+ ...(meta !== undefined ? { _meta: meta } : {}),
878
1016
  },
879
1017
  ],
880
1018
  };