@decocms/runtime 1.3.0 → 1.4.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/README.md +0 -33
- package/package.json +1 -1
- package/src/bindings/binder.ts +10 -32
- package/src/bindings.ts +1 -1
- package/src/index.ts +1 -0
- package/src/tools.ts +311 -264
- package/src/workflows.ts +50 -11
package/README.md
CHANGED
|
@@ -152,37 +152,6 @@ const getUserDataTool = createPrivateTool({
|
|
|
152
152
|
});
|
|
153
153
|
```
|
|
154
154
|
|
|
155
|
-
### Streamable Tools
|
|
156
|
-
|
|
157
|
-
For tools that return streaming responses:
|
|
158
|
-
|
|
159
|
-
```typescript
|
|
160
|
-
import { createStreamableTool } from "@decocms/runtime";
|
|
161
|
-
|
|
162
|
-
const streamDataTool = createStreamableTool({
|
|
163
|
-
id: "streamData",
|
|
164
|
-
description: "Streams data as a response",
|
|
165
|
-
inputSchema: z.object({
|
|
166
|
-
query: z.string(),
|
|
167
|
-
}),
|
|
168
|
-
streamable: true,
|
|
169
|
-
execute: async ({ context }) => {
|
|
170
|
-
// Return a streaming Response
|
|
171
|
-
const stream = new ReadableStream({
|
|
172
|
-
async start(controller) {
|
|
173
|
-
controller.enqueue(new TextEncoder().encode("Chunk 1\n"));
|
|
174
|
-
controller.enqueue(new TextEncoder().encode("Chunk 2\n"));
|
|
175
|
-
controller.close();
|
|
176
|
-
},
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
return new Response(stream, {
|
|
180
|
-
headers: { "Content-Type": "text/plain" },
|
|
181
|
-
});
|
|
182
|
-
},
|
|
183
|
-
});
|
|
184
|
-
```
|
|
185
|
-
|
|
186
155
|
### Registering Tools
|
|
187
156
|
|
|
188
157
|
Tools can be registered in multiple ways:
|
|
@@ -701,7 +670,6 @@ export default withRuntime({
|
|
|
701
670
|
### Types
|
|
702
671
|
|
|
703
672
|
- `Tool<TSchemaIn, TSchemaOut>` - Tool definition with typed input/output
|
|
704
|
-
- `StreamableTool<TSchemaIn>` - Tool that returns streaming Response
|
|
705
673
|
- `Prompt<TArgs>` - Prompt definition with typed arguments
|
|
706
674
|
- `Resource` - Resource definition
|
|
707
675
|
- `OAuthConfig` - OAuth configuration
|
|
@@ -714,7 +682,6 @@ export default withRuntime({
|
|
|
714
682
|
- `withRuntime(options)` - Create an MCP server
|
|
715
683
|
- `createTool(opts)` - Create a public tool
|
|
716
684
|
- `createPrivateTool(opts)` - Create an authenticated tool
|
|
717
|
-
- `createStreamableTool(opts)` - Create a streaming tool
|
|
718
685
|
- `createPrompt(opts)` - Create an authenticated prompt
|
|
719
686
|
- `createPublicPrompt(opts)` - Create a public prompt
|
|
720
687
|
- `createResource(opts)` - Create an authenticated resource
|
package/package.json
CHANGED
package/src/bindings/binder.ts
CHANGED
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
type MCPClientFetchStub,
|
|
8
8
|
type ToolBinder,
|
|
9
9
|
} from "../mcp.ts";
|
|
10
|
-
import { createPrivateTool
|
|
10
|
+
import { createPrivateTool } from "../tools.ts";
|
|
11
11
|
import { CHANNEL_BINDING } from "./channels.ts";
|
|
12
12
|
|
|
13
13
|
// ToolLike is a simplified version of the Tool interface that matches what we need for bindings
|
|
@@ -77,13 +77,6 @@ export const bindingClient = <TDefinition extends readonly ToolBinder[]>(
|
|
|
77
77
|
): MCPClientFetchStub<TDefinition> => {
|
|
78
78
|
return createMCPFetchStub<TDefinition>({
|
|
79
79
|
connection: mcpConnection,
|
|
80
|
-
streamable: binder.reduce(
|
|
81
|
-
(acc, tool) => {
|
|
82
|
-
acc[tool.name] = tool.streamable === true;
|
|
83
|
-
return acc;
|
|
84
|
-
},
|
|
85
|
-
{} as Record<string, boolean>,
|
|
86
|
-
),
|
|
87
80
|
});
|
|
88
81
|
},
|
|
89
82
|
};
|
|
@@ -99,12 +92,8 @@ export const impl = <TBinder extends Binder>(
|
|
|
99
92
|
schema: TBinder,
|
|
100
93
|
implementation: BinderImplementation<TBinder>,
|
|
101
94
|
createToolFn = createPrivateTool,
|
|
102
|
-
createStreamableToolFn = createStreamableTool,
|
|
103
95
|
) => {
|
|
104
|
-
const impl:
|
|
105
|
-
| ReturnType<typeof createToolFn>
|
|
106
|
-
| ReturnType<typeof createStreamableToolFn>
|
|
107
|
-
)[] = [];
|
|
96
|
+
const impl: ReturnType<typeof createToolFn>[] = [];
|
|
108
97
|
for (const key in schema) {
|
|
109
98
|
const toolSchema = schema[key];
|
|
110
99
|
const toolImplementation = implementation[key];
|
|
@@ -117,28 +106,17 @@ export const impl = <TBinder extends Binder>(
|
|
|
117
106
|
throw new Error(`Implementation for ${key} is required`);
|
|
118
107
|
}
|
|
119
108
|
|
|
120
|
-
const { name, handler,
|
|
109
|
+
const { name, handler, ...toolLike } = {
|
|
121
110
|
...toolSchema,
|
|
122
111
|
...toolImplementation,
|
|
123
112
|
};
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
}),
|
|
132
|
-
);
|
|
133
|
-
} else {
|
|
134
|
-
impl.push(
|
|
135
|
-
createToolFn({
|
|
136
|
-
...toolLike,
|
|
137
|
-
id: name,
|
|
138
|
-
execute: ({ context }) => Promise.resolve(handler(context)),
|
|
139
|
-
}),
|
|
140
|
-
);
|
|
141
|
-
}
|
|
113
|
+
impl.push(
|
|
114
|
+
createToolFn({
|
|
115
|
+
...toolLike,
|
|
116
|
+
id: name,
|
|
117
|
+
execute: ({ context }) => Promise.resolve(handler(context)),
|
|
118
|
+
}),
|
|
119
|
+
);
|
|
142
120
|
}
|
|
143
121
|
return impl;
|
|
144
122
|
};
|
package/src/bindings.ts
CHANGED
|
@@ -161,7 +161,7 @@ export const AgentOf = () =>
|
|
|
161
161
|
thinking: AgentModelInfoSchema.optional(),
|
|
162
162
|
coding: AgentModelInfoSchema.optional(),
|
|
163
163
|
fast: AgentModelInfoSchema.optional(),
|
|
164
|
-
toolApprovalLevel: z.enum(["auto", "readonly", "plan"]).default("
|
|
164
|
+
toolApprovalLevel: z.enum(["auto", "readonly", "plan"]).default("auto"),
|
|
165
165
|
temperature: z.number().default(0.5),
|
|
166
166
|
});
|
|
167
167
|
|
package/src/index.ts
CHANGED
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 {
|
|
11
11
|
GetPromptResult,
|
|
12
|
+
Implementation,
|
|
12
13
|
ToolAnnotations,
|
|
13
14
|
} from "@modelcontextprotocol/sdk/types.js";
|
|
14
15
|
import { z } from "zod";
|
|
15
16
|
import type { ZodRawShape, ZodSchema, ZodTypeAny } from "zod";
|
|
16
17
|
import { BindingRegistry, injectBindingSchemas } from "./bindings.ts";
|
|
17
18
|
import { Event, type EventHandlers } from "./events.ts";
|
|
18
|
-
import type { DefaultEnv } from "./index.ts";
|
|
19
|
+
import type { DefaultEnv, User } from "./index.ts";
|
|
19
20
|
import { State } from "./state.ts";
|
|
20
21
|
import {
|
|
21
22
|
type WorkflowDefinition,
|
|
@@ -66,25 +67,14 @@ export interface Tool<
|
|
|
66
67
|
outputSchema?: TSchemaOut;
|
|
67
68
|
execute(
|
|
68
69
|
context: ToolExecutionContext<TSchemaIn>,
|
|
70
|
+
ctx?: AppContext,
|
|
69
71
|
): TSchemaOut extends ZodSchema
|
|
70
72
|
? Promise<z.infer<TSchemaOut>>
|
|
71
73
|
: Promise<unknown>;
|
|
72
74
|
}
|
|
73
75
|
|
|
74
76
|
/**
|
|
75
|
-
*
|
|
76
|
-
*/
|
|
77
|
-
export interface StreamableTool<TSchemaIn extends ZodSchema = ZodSchema> {
|
|
78
|
-
_meta?: Record<string, unknown>;
|
|
79
|
-
id: string;
|
|
80
|
-
inputSchema: TSchemaIn;
|
|
81
|
-
streamable?: true;
|
|
82
|
-
description?: string;
|
|
83
|
-
execute(input: ToolExecutionContext<TSchemaIn>): Promise<Response>;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* CreatedTool is a permissive type that any Tool or StreamableTool can be assigned to.
|
|
77
|
+
* CreatedTool is a permissive type that any Tool can be assigned to.
|
|
88
78
|
* Uses a structural type with relaxed execute signature to allow tools with any schema.
|
|
89
79
|
*/
|
|
90
80
|
export type CreatedTool = {
|
|
@@ -94,12 +84,14 @@ export type CreatedTool = {
|
|
|
94
84
|
annotations?: ToolAnnotations;
|
|
95
85
|
inputSchema: ZodTypeAny;
|
|
96
86
|
outputSchema?: ZodTypeAny;
|
|
97
|
-
streamable?: true;
|
|
98
87
|
// Use a permissive execute signature - accepts any context shape
|
|
99
|
-
execute(
|
|
100
|
-
context:
|
|
101
|
-
|
|
102
|
-
|
|
88
|
+
execute(
|
|
89
|
+
context: {
|
|
90
|
+
context: unknown;
|
|
91
|
+
runtimeContext: AppContext;
|
|
92
|
+
},
|
|
93
|
+
ctx?: AppContext,
|
|
94
|
+
): Promise<unknown>;
|
|
103
95
|
};
|
|
104
96
|
|
|
105
97
|
// Re-export types for external use
|
|
@@ -136,6 +128,7 @@ export interface Prompt<TArgs extends PromptArgsRawShape = PromptArgsRawShape> {
|
|
|
136
128
|
argsSchema?: TArgs;
|
|
137
129
|
execute(
|
|
138
130
|
context: PromptExecutionContext<TArgs>,
|
|
131
|
+
ctx?: AppContext,
|
|
139
132
|
): Promise<GetPromptResult> | GetPromptResult;
|
|
140
133
|
}
|
|
141
134
|
|
|
@@ -149,10 +142,13 @@ export type CreatedPrompt = {
|
|
|
149
142
|
description?: string;
|
|
150
143
|
argsSchema?: PromptArgsRawShape;
|
|
151
144
|
// Use a permissive execute signature - accepts any args shape
|
|
152
|
-
execute(
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
145
|
+
execute(
|
|
146
|
+
context: {
|
|
147
|
+
args: Record<string, string | undefined>;
|
|
148
|
+
runtimeContext: AppContext;
|
|
149
|
+
},
|
|
150
|
+
ctx?: AppContext,
|
|
151
|
+
): Promise<GetPromptResult> | GetPromptResult;
|
|
156
152
|
};
|
|
157
153
|
|
|
158
154
|
// ============================================================================
|
|
@@ -198,6 +194,7 @@ export interface Resource {
|
|
|
198
194
|
/** Handler function to read the resource content */
|
|
199
195
|
read(
|
|
200
196
|
context: ResourceExecutionContext,
|
|
197
|
+
ctx?: AppContext,
|
|
201
198
|
): Promise<ResourceContents> | ResourceContents;
|
|
202
199
|
}
|
|
203
200
|
|
|
@@ -210,48 +207,58 @@ export type CreatedResource = {
|
|
|
210
207
|
name: string;
|
|
211
208
|
description?: string;
|
|
212
209
|
mimeType?: string;
|
|
213
|
-
read(
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
210
|
+
read(
|
|
211
|
+
context: {
|
|
212
|
+
uri: URL;
|
|
213
|
+
runtimeContext: AppContext;
|
|
214
|
+
},
|
|
215
|
+
ctx?: AppContext,
|
|
216
|
+
): Promise<ResourceContents> | ResourceContents;
|
|
217
217
|
};
|
|
218
218
|
|
|
219
219
|
/**
|
|
220
|
-
*
|
|
220
|
+
* Ensure the current request is authenticated.
|
|
221
|
+
* Reads from the per-request AppContext (AsyncLocalStorage), not from a cached env.
|
|
222
|
+
*
|
|
223
|
+
* @param ctx - Per-request AppContext from the second arg of execute/read handlers
|
|
224
|
+
* @returns The authenticated User
|
|
225
|
+
* @throws Error if no request context or user is not authenticated
|
|
226
|
+
*/
|
|
227
|
+
export function ensureAuthenticated(ctx: AppContext): User {
|
|
228
|
+
const reqCtx = ctx?.env?.MESH_REQUEST_CONTEXT;
|
|
229
|
+
if (!reqCtx) {
|
|
230
|
+
throw new Error("Unauthorized: missing request context");
|
|
231
|
+
}
|
|
232
|
+
const user = reqCtx.ensureAuthenticated();
|
|
233
|
+
if (!user) {
|
|
234
|
+
throw new Error("Unauthorized");
|
|
235
|
+
}
|
|
236
|
+
return user;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
let _warnedPrivateTool = false;
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* @deprecated Use `createTool` with `ensureAuthenticated(ctx)` instead.
|
|
243
|
+
*
|
|
244
|
+
* Creates a private tool that ensures authentication before execution.
|
|
221
245
|
*/
|
|
222
246
|
export function createPrivateTool<
|
|
223
247
|
TSchemaIn extends ZodSchema = ZodSchema,
|
|
224
248
|
TSchemaOut extends ZodSchema | undefined = undefined,
|
|
225
249
|
>(opts: Tool<TSchemaIn, TSchemaOut>): Tool<TSchemaIn, TSchemaOut> {
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
env.MESH_REQUEST_CONTEXT?.ensureAuthenticated();
|
|
232
|
-
}
|
|
233
|
-
return execute(input);
|
|
234
|
-
};
|
|
250
|
+
if (!_warnedPrivateTool) {
|
|
251
|
+
console.warn(
|
|
252
|
+
"[runtime] createPrivateTool is deprecated. Use createTool with ensureAuthenticated(ctx) instead.",
|
|
253
|
+
);
|
|
254
|
+
_warnedPrivateTool = true;
|
|
235
255
|
}
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
streamableTool: StreamableTool<TSchemaIn>,
|
|
241
|
-
): StreamableTool<TSchemaIn> {
|
|
242
|
-
return {
|
|
243
|
-
...streamableTool,
|
|
244
|
-
execute: (input: ToolExecutionContext<TSchemaIn>) => {
|
|
245
|
-
const env = input.runtimeContext.env;
|
|
246
|
-
if (env) {
|
|
247
|
-
env.MESH_REQUEST_CONTEXT?.ensureAuthenticated();
|
|
248
|
-
}
|
|
249
|
-
return streamableTool.execute({
|
|
250
|
-
...input,
|
|
251
|
-
runtimeContext: createRuntimeContext(input.runtimeContext),
|
|
252
|
-
});
|
|
253
|
-
},
|
|
256
|
+
const execute = opts.execute;
|
|
257
|
+
opts.execute = (input: ToolExecutionContext<TSchemaIn>, ctx: AppContext) => {
|
|
258
|
+
ensureAuthenticated(ctx);
|
|
259
|
+
return execute(input, ctx);
|
|
254
260
|
};
|
|
261
|
+
return createTool(opts);
|
|
255
262
|
}
|
|
256
263
|
|
|
257
264
|
export function createTool<
|
|
@@ -262,10 +269,8 @@ export function createTool<
|
|
|
262
269
|
return {
|
|
263
270
|
...opts,
|
|
264
271
|
execute: (input: ToolExecutionContext<TSchemaIn>) => {
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
runtimeContext: createRuntimeContext(input.runtimeContext),
|
|
268
|
-
});
|
|
272
|
+
const ctx = createRuntimeContext(input.runtimeContext);
|
|
273
|
+
return opts.execute({ ...input, runtimeContext: ctx }, ctx);
|
|
269
274
|
},
|
|
270
275
|
};
|
|
271
276
|
}
|
|
@@ -279,10 +284,8 @@ export function createPublicPrompt<TArgs extends PromptArgsRawShape>(
|
|
|
279
284
|
return {
|
|
280
285
|
...opts,
|
|
281
286
|
execute: (input: PromptExecutionContext<TArgs>) => {
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
runtimeContext: createRuntimeContext(input.runtimeContext),
|
|
285
|
-
});
|
|
287
|
+
const ctx = createRuntimeContext(input.runtimeContext);
|
|
288
|
+
return opts.execute({ ...input, runtimeContext: ctx }, ctx);
|
|
286
289
|
},
|
|
287
290
|
};
|
|
288
291
|
}
|
|
@@ -297,12 +300,9 @@ export function createPrompt<TArgs extends PromptArgsRawShape>(
|
|
|
297
300
|
const execute = opts.execute;
|
|
298
301
|
return createPublicPrompt({
|
|
299
302
|
...opts,
|
|
300
|
-
execute: (input: PromptExecutionContext<TArgs
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
env.MESH_REQUEST_CONTEXT?.ensureAuthenticated();
|
|
304
|
-
}
|
|
305
|
-
return execute(input);
|
|
303
|
+
execute: (input: PromptExecutionContext<TArgs>, ctx: AppContext) => {
|
|
304
|
+
ensureAuthenticated(ctx);
|
|
305
|
+
return execute(input, ctx);
|
|
306
306
|
},
|
|
307
307
|
});
|
|
308
308
|
}
|
|
@@ -314,10 +314,8 @@ export function createPublicResource(opts: Resource): Resource {
|
|
|
314
314
|
return {
|
|
315
315
|
...opts,
|
|
316
316
|
read: (input: ResourceExecutionContext) => {
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
runtimeContext: createRuntimeContext(input.runtimeContext),
|
|
320
|
-
});
|
|
317
|
+
const ctx = createRuntimeContext(input.runtimeContext);
|
|
318
|
+
return opts.read({ ...input, runtimeContext: ctx }, ctx);
|
|
321
319
|
},
|
|
322
320
|
};
|
|
323
321
|
}
|
|
@@ -330,12 +328,9 @@ export function createResource(opts: Resource): Resource {
|
|
|
330
328
|
const read = opts.read;
|
|
331
329
|
return createPublicResource({
|
|
332
330
|
...opts,
|
|
333
|
-
read: (input: ResourceExecutionContext) => {
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
env.MESH_REQUEST_CONTEXT?.ensureAuthenticated();
|
|
337
|
-
}
|
|
338
|
-
return read(input);
|
|
331
|
+
read: (input: ResourceExecutionContext, ctx: AppContext) => {
|
|
332
|
+
ensureAuthenticated(ctx);
|
|
333
|
+
return read(input, ctx);
|
|
339
334
|
},
|
|
340
335
|
});
|
|
341
336
|
}
|
|
@@ -354,12 +349,6 @@ export interface Integration {
|
|
|
354
349
|
appId: string;
|
|
355
350
|
}
|
|
356
351
|
|
|
357
|
-
export function isStreamableTool(
|
|
358
|
-
tool: CreatedTool,
|
|
359
|
-
): tool is StreamableTool & CreatedTool {
|
|
360
|
-
return tool && "streamable" in tool && tool.streamable === true;
|
|
361
|
-
}
|
|
362
|
-
|
|
363
352
|
export interface OnChangeCallback<TState> {
|
|
364
353
|
state: TState;
|
|
365
354
|
scopes: string[];
|
|
@@ -485,6 +474,7 @@ export interface CreateMCPServerOptions<
|
|
|
485
474
|
State extends
|
|
486
475
|
TEnv["MESH_REQUEST_CONTEXT"]["state"] = TEnv["MESH_REQUEST_CONTEXT"]["state"],
|
|
487
476
|
> {
|
|
477
|
+
serverInfo?: Partial<Implementation> & { instructions?: string };
|
|
488
478
|
before?: (env: TEnv) => Promise<void> | void;
|
|
489
479
|
oauth?: OAuthConfig;
|
|
490
480
|
events?: {
|
|
@@ -498,35 +488,38 @@ export interface CreateMCPServerOptions<
|
|
|
498
488
|
};
|
|
499
489
|
tools?:
|
|
500
490
|
| Array<
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
491
|
+
| CreatedTool
|
|
492
|
+
| ((
|
|
493
|
+
env: TEnv,
|
|
494
|
+
) =>
|
|
495
|
+
| Promise<CreatedTool>
|
|
496
|
+
| CreatedTool
|
|
497
|
+
| CreatedTool[]
|
|
498
|
+
| Promise<CreatedTool[]>)
|
|
508
499
|
>
|
|
509
500
|
| ((env: TEnv) => CreatedTool[] | Promise<CreatedTool[]>);
|
|
510
501
|
prompts?:
|
|
511
502
|
| Array<
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
503
|
+
| CreatedPrompt
|
|
504
|
+
| ((
|
|
505
|
+
env: TEnv,
|
|
506
|
+
) =>
|
|
507
|
+
| Promise<CreatedPrompt>
|
|
508
|
+
| CreatedPrompt
|
|
509
|
+
| CreatedPrompt[]
|
|
510
|
+
| Promise<CreatedPrompt[]>)
|
|
519
511
|
>
|
|
520
512
|
| ((env: TEnv) => CreatedPrompt[] | Promise<CreatedPrompt[]>);
|
|
521
513
|
resources?:
|
|
522
514
|
| Array<
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
515
|
+
| CreatedResource
|
|
516
|
+
| ((
|
|
517
|
+
env: TEnv,
|
|
518
|
+
) =>
|
|
519
|
+
| Promise<CreatedResource>
|
|
520
|
+
| CreatedResource
|
|
521
|
+
| CreatedResource[]
|
|
522
|
+
| Promise<CreatedResource[]>)
|
|
530
523
|
>
|
|
531
524
|
| ((env: TEnv) => CreatedResource[] | Promise<CreatedResource[]>);
|
|
532
525
|
workflows?:
|
|
@@ -711,14 +704,21 @@ const toolsFor = <TSchema extends ZodTypeAny = never>({
|
|
|
711
704
|
...(workflows?.length
|
|
712
705
|
? workflows.map((wf) => {
|
|
713
706
|
const id = wf.toolId ?? workflowToolId(wf.title);
|
|
707
|
+
const baseDescription = [
|
|
708
|
+
wf.description
|
|
709
|
+
? `Run workflow: ${wf.description}`
|
|
710
|
+
: `Start the "${wf.title}" workflow.`,
|
|
711
|
+
"Returns an execution_id immediately. Use COLLECTION_WORKFLOW_EXECUTION_GET to track progress.",
|
|
712
|
+
].join(" ");
|
|
714
713
|
return createTool({
|
|
715
714
|
id,
|
|
716
|
-
description:
|
|
717
|
-
wf.
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
715
|
+
description: (() => {
|
|
716
|
+
if (!wf.inputSchema) return baseDescription;
|
|
717
|
+
const schemaStr = JSON.stringify(wf.inputSchema, null, 2);
|
|
718
|
+
return schemaStr.length <= 2048
|
|
719
|
+
? `${baseDescription}\n\nInput schema:\n${schemaStr}`
|
|
720
|
+
: `${baseDescription}\n\nThis workflow expects structured input. Use COLLECTION_WORKFLOW_GET to inspect the full input schema.`;
|
|
721
|
+
})(),
|
|
722
722
|
inputSchema: z.object({
|
|
723
723
|
input: z
|
|
724
724
|
.record(z.string(), z.unknown())
|
|
@@ -808,89 +808,155 @@ export const createMCPServer = <
|
|
|
808
808
|
>(
|
|
809
809
|
options: CreateMCPServerOptions<TEnv, TSchema, TBindings>,
|
|
810
810
|
): MCPServer<TEnv, TSchema, TBindings> => {
|
|
811
|
-
|
|
812
|
-
|
|
811
|
+
// Tool/prompt/resource definitions are resolved once on first request and
|
|
812
|
+
// cached for the lifetime of the process. Tool *execution* reads per-request
|
|
813
|
+
// context from State (AsyncLocalStorage) via the second `ctx` argument, so
|
|
814
|
+
// reusing definitions is safe.
|
|
815
|
+
type Registrations = {
|
|
816
|
+
tools: CreatedTool[];
|
|
817
|
+
prompts: CreatedPrompt[];
|
|
818
|
+
resources: CreatedResource[];
|
|
819
|
+
workflows?: WorkflowDefinition[];
|
|
820
|
+
};
|
|
813
821
|
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
{ capabilities: { tools: {}, prompts: {}, resources: {} } },
|
|
817
|
-
);
|
|
822
|
+
let cached: Registrations | null = null;
|
|
823
|
+
let inflightResolve: Promise<Registrations> | null = null;
|
|
818
824
|
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
const awaited = await toolResult;
|
|
830
|
-
if (Array.isArray(awaited)) {
|
|
831
|
-
return awaited;
|
|
832
|
-
}
|
|
833
|
-
return [awaited];
|
|
834
|
-
}) ?? [],
|
|
835
|
-
).then((t) => t.flat());
|
|
836
|
-
};
|
|
837
|
-
const tools = await toolsFn(bindings);
|
|
825
|
+
let _warnedFactoryDeprecation = false;
|
|
826
|
+
const warnFactoryDeprecation = () => {
|
|
827
|
+
if (!_warnedFactoryDeprecation) {
|
|
828
|
+
console.warn(
|
|
829
|
+
"[runtime] Passing factory functions to tools/prompts/resources is deprecated. " +
|
|
830
|
+
"Pass createTool()/createPrompt()/createResource() instances directly.",
|
|
831
|
+
);
|
|
832
|
+
_warnedFactoryDeprecation = true;
|
|
833
|
+
}
|
|
834
|
+
};
|
|
838
835
|
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
836
|
+
/**
|
|
837
|
+
* Check whether a value is an already-created instance (has an `id` or `name` property)
|
|
838
|
+
* rather than a factory function.
|
|
839
|
+
*/
|
|
840
|
+
const isInstance = (v: unknown): boolean =>
|
|
841
|
+
typeof v === "object" &&
|
|
842
|
+
v !== null &&
|
|
843
|
+
("id" in v || "name" in v || "uri" in v);
|
|
843
844
|
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
845
|
+
/**
|
|
846
|
+
* Resolve an array that may contain both direct instances and factory functions.
|
|
847
|
+
* Factories are called with `bindings` and trigger a deprecation warning.
|
|
848
|
+
*/
|
|
849
|
+
async function resolveArray<T>(
|
|
850
|
+
items: Array<unknown> | undefined,
|
|
851
|
+
bindings: TEnv,
|
|
852
|
+
): Promise<T[]> {
|
|
853
|
+
if (!items) return [];
|
|
854
|
+
return (
|
|
855
|
+
await Promise.all(
|
|
856
|
+
items.flatMap(async (item) => {
|
|
857
|
+
if (isInstance(item)) {
|
|
858
|
+
return [item as T];
|
|
859
|
+
}
|
|
860
|
+
// Factory function — deprecated path
|
|
861
|
+
warnFactoryDeprecation();
|
|
862
|
+
const result = await (item as (env: TEnv) => unknown)(bindings);
|
|
863
|
+
if (Array.isArray(result)) return result as T[];
|
|
864
|
+
return [result as T];
|
|
865
|
+
}),
|
|
866
|
+
)
|
|
867
|
+
).flat();
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
const resolveRegistrations = async (
|
|
871
|
+
bindings: TEnv,
|
|
872
|
+
): Promise<Registrations> => {
|
|
873
|
+
if (cached) return cached;
|
|
874
|
+
if (inflightResolve) return inflightResolve;
|
|
875
|
+
|
|
876
|
+
inflightResolve = (async (): Promise<Registrations> => {
|
|
877
|
+
try {
|
|
878
|
+
let tools: CreatedTool[];
|
|
879
|
+
if (typeof options.tools === "function") {
|
|
880
|
+
warnFactoryDeprecation();
|
|
881
|
+
tools = await options.tools(bindings);
|
|
882
|
+
} else {
|
|
883
|
+
tools = await resolveArray<CreatedTool>(options.tools, bindings);
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
const resolvedWorkflows =
|
|
887
|
+
typeof options.workflows === "function"
|
|
888
|
+
? await options.workflows(bindings)
|
|
889
|
+
: options.workflows;
|
|
890
|
+
|
|
891
|
+
tools.push(
|
|
892
|
+
...toolsFor<TSchema>({ ...options, workflows: resolvedWorkflows }),
|
|
893
|
+
);
|
|
894
|
+
|
|
895
|
+
let prompts: CreatedPrompt[];
|
|
896
|
+
if (typeof options.prompts === "function") {
|
|
897
|
+
warnFactoryDeprecation();
|
|
898
|
+
prompts = await options.prompts(bindings);
|
|
899
|
+
} else {
|
|
900
|
+
prompts = await resolveArray<CreatedPrompt>(
|
|
901
|
+
options.prompts,
|
|
902
|
+
bindings,
|
|
903
|
+
);
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
let resources: CreatedResource[];
|
|
907
|
+
if (typeof options.resources === "function") {
|
|
908
|
+
warnFactoryDeprecation();
|
|
909
|
+
resources = await options.resources(bindings);
|
|
910
|
+
} else {
|
|
911
|
+
resources = await resolveArray<CreatedResource>(
|
|
912
|
+
options.resources,
|
|
913
|
+
bindings,
|
|
914
|
+
);
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
const result = {
|
|
918
|
+
tools,
|
|
919
|
+
prompts,
|
|
920
|
+
resources,
|
|
921
|
+
workflows: resolvedWorkflows,
|
|
922
|
+
};
|
|
923
|
+
cached = result;
|
|
924
|
+
return result;
|
|
925
|
+
} catch (err) {
|
|
926
|
+
inflightResolve = null;
|
|
927
|
+
throw err;
|
|
928
|
+
}
|
|
929
|
+
})();
|
|
847
930
|
|
|
848
|
-
|
|
931
|
+
return inflightResolve;
|
|
932
|
+
};
|
|
933
|
+
|
|
934
|
+
const registerAll = (server: McpServer, registrations: Registrations) => {
|
|
935
|
+
for (const tool of registrations.tools) {
|
|
849
936
|
server.registerTool(
|
|
850
937
|
tool.id,
|
|
851
938
|
{
|
|
852
|
-
_meta:
|
|
853
|
-
streamable: isStreamableTool(tool),
|
|
854
|
-
...(tool._meta ?? {}),
|
|
855
|
-
},
|
|
939
|
+
_meta: tool._meta,
|
|
856
940
|
description: tool.description,
|
|
857
941
|
annotations: tool.annotations,
|
|
858
942
|
inputSchema:
|
|
859
943
|
tool.inputSchema && "shape" in tool.inputSchema
|
|
860
944
|
? (tool.inputSchema.shape as ZodRawShape)
|
|
861
945
|
: z.object({}).shape,
|
|
862
|
-
outputSchema:
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
"shape" in tool.outputSchema
|
|
946
|
+
outputSchema:
|
|
947
|
+
tool.outputSchema &&
|
|
948
|
+
typeof tool.outputSchema === "object" &&
|
|
949
|
+
"shape" in tool.outputSchema
|
|
867
950
|
? (tool.outputSchema.shape as ZodRawShape)
|
|
868
951
|
: undefined,
|
|
869
952
|
},
|
|
870
953
|
async (args) => {
|
|
871
|
-
const
|
|
872
|
-
|
|
873
|
-
runtimeContext:
|
|
874
|
-
|
|
954
|
+
const ctx = createRuntimeContext();
|
|
955
|
+
const result = await tool.execute(
|
|
956
|
+
{ context: args, runtimeContext: ctx },
|
|
957
|
+
ctx,
|
|
958
|
+
);
|
|
875
959
|
|
|
876
|
-
// For streamable tools, the Response is handled at the transport layer
|
|
877
|
-
// Do NOT call result.bytes() - it buffers the entire response in memory
|
|
878
|
-
// causing massive memory leaks (2GB+ Uint8Array accumulation)
|
|
879
|
-
if (isStreamableTool(tool) && result instanceof Response) {
|
|
880
|
-
return {
|
|
881
|
-
structuredContent: {
|
|
882
|
-
streamable: true,
|
|
883
|
-
status: result.status,
|
|
884
|
-
statusText: result.statusText,
|
|
885
|
-
},
|
|
886
|
-
content: [
|
|
887
|
-
{
|
|
888
|
-
type: "text",
|
|
889
|
-
text: `Streaming response: ${result.status} ${result.statusText}`,
|
|
890
|
-
},
|
|
891
|
-
],
|
|
892
|
-
};
|
|
893
|
-
}
|
|
894
960
|
return {
|
|
895
961
|
structuredContent: result as Record<string, unknown>,
|
|
896
962
|
content: [
|
|
@@ -904,28 +970,7 @@ export const createMCPServer = <
|
|
|
904
970
|
);
|
|
905
971
|
}
|
|
906
972
|
|
|
907
|
-
|
|
908
|
-
const promptsFn =
|
|
909
|
-
typeof options.prompts === "function"
|
|
910
|
-
? options.prompts
|
|
911
|
-
: async (bindings: TEnv) => {
|
|
912
|
-
if (typeof options.prompts === "function") {
|
|
913
|
-
return await options.prompts(bindings);
|
|
914
|
-
}
|
|
915
|
-
return await Promise.all(
|
|
916
|
-
options.prompts?.flatMap(async (prompt) => {
|
|
917
|
-
const promptResult = prompt(bindings);
|
|
918
|
-
const awaited = await promptResult;
|
|
919
|
-
if (Array.isArray(awaited)) {
|
|
920
|
-
return awaited;
|
|
921
|
-
}
|
|
922
|
-
return [awaited];
|
|
923
|
-
}) ?? [],
|
|
924
|
-
).then((p) => p.flat());
|
|
925
|
-
};
|
|
926
|
-
const prompts = await promptsFn(bindings);
|
|
927
|
-
|
|
928
|
-
for (const prompt of prompts) {
|
|
973
|
+
for (const prompt of registrations.prompts) {
|
|
929
974
|
server.registerPrompt(
|
|
930
975
|
prompt.name,
|
|
931
976
|
{
|
|
@@ -936,36 +981,19 @@ export const createMCPServer = <
|
|
|
936
981
|
: z.object({}).shape,
|
|
937
982
|
},
|
|
938
983
|
async (args) => {
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
984
|
+
const ctx = createRuntimeContext();
|
|
985
|
+
return await prompt.execute(
|
|
986
|
+
{
|
|
987
|
+
args: args as Record<string, string | undefined>,
|
|
988
|
+
runtimeContext: ctx,
|
|
989
|
+
},
|
|
990
|
+
ctx,
|
|
991
|
+
);
|
|
943
992
|
},
|
|
944
993
|
);
|
|
945
994
|
}
|
|
946
995
|
|
|
947
|
-
|
|
948
|
-
const resourcesFn =
|
|
949
|
-
typeof options.resources === "function"
|
|
950
|
-
? options.resources
|
|
951
|
-
: async (bindings: TEnv) => {
|
|
952
|
-
if (typeof options.resources === "function") {
|
|
953
|
-
return await options.resources(bindings);
|
|
954
|
-
}
|
|
955
|
-
return await Promise.all(
|
|
956
|
-
options.resources?.flatMap(async (resource) => {
|
|
957
|
-
const resourceResult = resource(bindings);
|
|
958
|
-
const awaited = await resourceResult;
|
|
959
|
-
if (Array.isArray(awaited)) {
|
|
960
|
-
return awaited;
|
|
961
|
-
}
|
|
962
|
-
return [awaited];
|
|
963
|
-
}) ?? [],
|
|
964
|
-
).then((r) => r.flat());
|
|
965
|
-
};
|
|
966
|
-
const resources = await resourcesFn(bindings);
|
|
967
|
-
|
|
968
|
-
for (const resource of resources) {
|
|
996
|
+
for (const resource of registrations.resources) {
|
|
969
997
|
server.resource(
|
|
970
998
|
resource.name,
|
|
971
999
|
resource.uri,
|
|
@@ -974,23 +1002,9 @@ export const createMCPServer = <
|
|
|
974
1002
|
mimeType: resource.mimeType,
|
|
975
1003
|
},
|
|
976
1004
|
async (uri) => {
|
|
977
|
-
const
|
|
978
|
-
|
|
979
|
-
runtimeContext: createRuntimeContext(),
|
|
980
|
-
});
|
|
981
|
-
// Build content object based on what's provided (text or blob, not both)
|
|
982
|
-
const content: {
|
|
983
|
-
uri: string;
|
|
984
|
-
mimeType?: string;
|
|
985
|
-
text?: string;
|
|
986
|
-
blob?: string;
|
|
987
|
-
} = { uri: result.uri };
|
|
988
|
-
|
|
989
|
-
if (result.mimeType) {
|
|
990
|
-
content.mimeType = result.mimeType;
|
|
991
|
-
}
|
|
1005
|
+
const ctx = createRuntimeContext();
|
|
1006
|
+
const result = await resource.read({ uri, runtimeContext: ctx }, ctx);
|
|
992
1007
|
|
|
993
|
-
// MCP SDK expects either text or blob content, not both
|
|
994
1008
|
const meta =
|
|
995
1009
|
(result as { _meta?: Record<string, unknown> | null })._meta ??
|
|
996
1010
|
undefined;
|
|
@@ -1018,7 +1032,6 @@ export const createMCPServer = <
|
|
|
1018
1032
|
};
|
|
1019
1033
|
}
|
|
1020
1034
|
|
|
1021
|
-
// Fallback to empty text if neither provided
|
|
1022
1035
|
return {
|
|
1023
1036
|
contents: [
|
|
1024
1037
|
{ uri: result.uri, mimeType: result.mimeType, text: "" },
|
|
@@ -1027,8 +1040,28 @@ export const createMCPServer = <
|
|
|
1027
1040
|
},
|
|
1028
1041
|
);
|
|
1029
1042
|
}
|
|
1043
|
+
};
|
|
1044
|
+
|
|
1045
|
+
const createServer = async (bindings: TEnv) => {
|
|
1046
|
+
await options.before?.(bindings);
|
|
1047
|
+
|
|
1048
|
+
const { instructions, ...serverInfoOverrides } = options.serverInfo ?? {};
|
|
1049
|
+
const server = new McpServer(
|
|
1050
|
+
{
|
|
1051
|
+
...serverInfoOverrides,
|
|
1052
|
+
name: serverInfoOverrides.name ?? "@deco/mcp-api",
|
|
1053
|
+
version: serverInfoOverrides.version ?? "1.0.0",
|
|
1054
|
+
},
|
|
1055
|
+
{
|
|
1056
|
+
capabilities: { tools: {}, prompts: {}, resources: {} },
|
|
1057
|
+
...(instructions && { instructions }),
|
|
1058
|
+
},
|
|
1059
|
+
);
|
|
1060
|
+
|
|
1061
|
+
const registrations = await resolveRegistrations(bindings);
|
|
1062
|
+
registerAll(server, registrations);
|
|
1030
1063
|
|
|
1031
|
-
return { server,
|
|
1064
|
+
return { server, ...registrations };
|
|
1032
1065
|
};
|
|
1033
1066
|
|
|
1034
1067
|
const fetch = async (req: Request, env: TEnv) => {
|
|
@@ -1037,34 +1070,48 @@ export const createMCPServer = <
|
|
|
1037
1070
|
|
|
1038
1071
|
await server.connect(transport);
|
|
1039
1072
|
|
|
1073
|
+
const cleanup = () => {
|
|
1074
|
+
try {
|
|
1075
|
+
transport.close?.();
|
|
1076
|
+
} catch {
|
|
1077
|
+
/* ignore */
|
|
1078
|
+
}
|
|
1079
|
+
try {
|
|
1080
|
+
server.close?.();
|
|
1081
|
+
} catch {
|
|
1082
|
+
/* ignore */
|
|
1083
|
+
}
|
|
1084
|
+
};
|
|
1085
|
+
|
|
1040
1086
|
try {
|
|
1041
1087
|
const response = await transport.handleRequest(req);
|
|
1042
1088
|
|
|
1043
|
-
// Check if this is a streaming response (SSE or streamable tool)
|
|
1044
|
-
// SSE responses have text/event-stream content-type
|
|
1045
|
-
// Note: response.body is always non-null for all HTTP responses, so we can't use it to detect streaming
|
|
1046
1089
|
const contentType = response.headers.get("content-type");
|
|
1047
1090
|
const isStreaming =
|
|
1048
1091
|
contentType?.includes("text/event-stream") ||
|
|
1049
1092
|
contentType?.includes("application/json-rpc");
|
|
1050
1093
|
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
await transport.close?.();
|
|
1055
|
-
} catch {
|
|
1056
|
-
// Ignore close errors
|
|
1057
|
-
}
|
|
1094
|
+
if (!isStreaming || !response.body) {
|
|
1095
|
+
cleanup();
|
|
1096
|
+
return response;
|
|
1058
1097
|
}
|
|
1059
1098
|
|
|
1060
|
-
|
|
1099
|
+
// Pipe the SSE body through a passthrough so that when the stream
|
|
1100
|
+
// finishes (server sent the response) or the client disconnects
|
|
1101
|
+
// (cancel), the server and transport are always cleaned up.
|
|
1102
|
+
const { readable, writable } = new TransformStream();
|
|
1103
|
+
response.body
|
|
1104
|
+
.pipeTo(writable)
|
|
1105
|
+
.catch(() => {})
|
|
1106
|
+
.finally(cleanup);
|
|
1107
|
+
|
|
1108
|
+
return new Response(readable, {
|
|
1109
|
+
status: response.status,
|
|
1110
|
+
statusText: response.statusText,
|
|
1111
|
+
headers: response.headers,
|
|
1112
|
+
});
|
|
1061
1113
|
} catch (error) {
|
|
1062
|
-
|
|
1063
|
-
try {
|
|
1064
|
-
await transport.close?.();
|
|
1065
|
-
} catch {
|
|
1066
|
-
// Ignore close errors
|
|
1067
|
-
}
|
|
1114
|
+
cleanup();
|
|
1068
1115
|
throw error;
|
|
1069
1116
|
}
|
|
1070
1117
|
};
|
|
@@ -1075,7 +1122,9 @@ export const createMCPServer = <
|
|
|
1075
1122
|
throw new Error("Missing state, did you forget to call State.bind?");
|
|
1076
1123
|
}
|
|
1077
1124
|
const env = currentState?.env;
|
|
1078
|
-
const { tools } = await
|
|
1125
|
+
const { tools } = await resolveRegistrations(
|
|
1126
|
+
env as TEnv & DefaultEnv<TSchema>,
|
|
1127
|
+
);
|
|
1079
1128
|
const tool = tools.find((t) => t.id === toolCallId);
|
|
1080
1129
|
const execute = tool?.execute;
|
|
1081
1130
|
if (!execute) {
|
|
@@ -1084,10 +1133,8 @@ export const createMCPServer = <
|
|
|
1084
1133
|
);
|
|
1085
1134
|
}
|
|
1086
1135
|
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
runtimeContext: createRuntimeContext(),
|
|
1090
|
-
});
|
|
1136
|
+
const ctx = createRuntimeContext();
|
|
1137
|
+
return execute({ context: toolCallInput, runtimeContext: ctx }, ctx);
|
|
1091
1138
|
};
|
|
1092
1139
|
|
|
1093
1140
|
return {
|
package/src/workflows.ts
CHANGED
|
@@ -24,6 +24,12 @@ export interface WorkflowDefinition {
|
|
|
24
24
|
* Defaults to START_WORKFLOW_<TITLE_SLUG> (e.g. START_WORKFLOW_FETCH_USERS).
|
|
25
25
|
*/
|
|
26
26
|
toolId?: string;
|
|
27
|
+
/**
|
|
28
|
+
* JSON Schema describing the expected input for this workflow.
|
|
29
|
+
* When set, the mesh validates execution input against this schema
|
|
30
|
+
* before creating an execution.
|
|
31
|
+
*/
|
|
32
|
+
inputSchema?: Record<string, unknown> | null;
|
|
27
33
|
}
|
|
28
34
|
|
|
29
35
|
interface WorkflowCollectionItem {
|
|
@@ -67,6 +73,7 @@ interface MeshWorkflowClient {
|
|
|
67
73
|
description?: string;
|
|
68
74
|
virtual_mcp_id?: string;
|
|
69
75
|
steps: Step[];
|
|
76
|
+
input_schema?: Record<string, unknown> | null;
|
|
70
77
|
};
|
|
71
78
|
}) => Promise<{ item: WorkflowCollectionItem }>;
|
|
72
79
|
COLLECTION_WORKFLOW_UPDATE: (input: {
|
|
@@ -76,6 +83,7 @@ interface MeshWorkflowClient {
|
|
|
76
83
|
description?: string;
|
|
77
84
|
virtual_mcp_id?: string;
|
|
78
85
|
steps?: Step[];
|
|
86
|
+
input_schema?: Record<string, unknown> | null;
|
|
79
87
|
};
|
|
80
88
|
}) => Promise<{ success: boolean; error?: string }>;
|
|
81
89
|
COLLECTION_WORKFLOW_DELETE: (input: {
|
|
@@ -254,10 +262,11 @@ function fingerprintWorkflows(declared: WorkflowDefinition[]): string {
|
|
|
254
262
|
return JSON.stringify(
|
|
255
263
|
declared.map((w) => ({
|
|
256
264
|
title: w.title,
|
|
257
|
-
description: w.description ??
|
|
258
|
-
virtual_mcp_id: w.virtual_mcp_id ??
|
|
265
|
+
description: w.description ?? undefined,
|
|
266
|
+
virtual_mcp_id: w.virtual_mcp_id ?? undefined,
|
|
259
267
|
steps: w.steps,
|
|
260
|
-
toolId: w.toolId ??
|
|
268
|
+
toolId: w.toolId ?? undefined,
|
|
269
|
+
inputSchema: w.inputSchema ?? undefined,
|
|
261
270
|
})),
|
|
262
271
|
);
|
|
263
272
|
}
|
|
@@ -383,6 +392,10 @@ async function doSyncWorkflows(
|
|
|
383
392
|
virtual_mcp_id: resolvedVmcpId,
|
|
384
393
|
}),
|
|
385
394
|
steps: wf.steps,
|
|
395
|
+
input_schema:
|
|
396
|
+
wf.inputSchema === undefined
|
|
397
|
+
? undefined
|
|
398
|
+
: (wf.inputSchema ?? null),
|
|
386
399
|
},
|
|
387
400
|
});
|
|
388
401
|
if (!result.success) {
|
|
@@ -402,6 +415,7 @@ async function doSyncWorkflows(
|
|
|
402
415
|
description: wf.description,
|
|
403
416
|
virtual_mcp_id: resolvedVmcpId,
|
|
404
417
|
steps: wf.steps,
|
|
418
|
+
input_schema: wf.inputSchema ?? null,
|
|
405
419
|
},
|
|
406
420
|
});
|
|
407
421
|
console.log(`${tag} CREATE "${wf.title}" OK`);
|
|
@@ -596,8 +610,30 @@ type InputForTool<
|
|
|
596
610
|
: StepInput<TSteps>
|
|
597
611
|
: StepInput<TSteps>;
|
|
598
612
|
|
|
599
|
-
|
|
600
|
-
|
|
613
|
+
/**
|
|
614
|
+
* Typed bail condition with @ref autocomplete.
|
|
615
|
+
* Self-references (e.g. `@thisStep.field` on step "thisStep") are valid for
|
|
616
|
+
* bail — the condition is evaluated after the step completes — but the current
|
|
617
|
+
* step name isn't in TSteps yet. Use the `(string & {})` escape hatch.
|
|
618
|
+
*/
|
|
619
|
+
type TypedBail<TSteps extends string> =
|
|
620
|
+
| true
|
|
621
|
+
| {
|
|
622
|
+
ref: KnownRefs<TSteps>;
|
|
623
|
+
eq?: unknown;
|
|
624
|
+
neq?: unknown;
|
|
625
|
+
gt?: number;
|
|
626
|
+
lt?: number;
|
|
627
|
+
};
|
|
628
|
+
|
|
629
|
+
type BaseStepFields<TSteps extends string> = Omit<
|
|
630
|
+
Step,
|
|
631
|
+
"name" | "input" | "action" | "bail"
|
|
632
|
+
> & { bail?: TypedBail<TSteps> };
|
|
633
|
+
type BaseForEachFields<TSteps extends string> = Omit<
|
|
634
|
+
Step,
|
|
635
|
+
"name" | "forEach" | "input" | "action" | "bail"
|
|
636
|
+
> & { bail?: TypedBail<TSteps> };
|
|
601
637
|
|
|
602
638
|
/**
|
|
603
639
|
* Tool-call variants of StepOpts — one discriminated member per tool ID so
|
|
@@ -608,12 +644,12 @@ type ToolCallStepOpts<
|
|
|
608
644
|
TSteps extends string,
|
|
609
645
|
TTools extends readonly ToolLike[],
|
|
610
646
|
> = [TTools[number]] extends [never]
|
|
611
|
-
? BaseStepFields & {
|
|
647
|
+
? BaseStepFields<TSteps> & {
|
|
612
648
|
action: { toolName: string & {}; transformCode?: string };
|
|
613
649
|
input?: StepInput<TSteps>;
|
|
614
650
|
}
|
|
615
651
|
: {
|
|
616
|
-
[TId in TTools[number]["id"]]: BaseStepFields & {
|
|
652
|
+
[TId in TTools[number]["id"]]: BaseStepFields<TSteps> & {
|
|
617
653
|
action: { toolName: TId; transformCode?: string };
|
|
618
654
|
input?: InputForTool<TTools, TId, TSteps>;
|
|
619
655
|
};
|
|
@@ -621,19 +657,22 @@ type ToolCallStepOpts<
|
|
|
621
657
|
|
|
622
658
|
type StepOpts<TSteps extends string, TTools extends readonly ToolLike[]> =
|
|
623
659
|
| ToolCallStepOpts<TSteps, TTools>
|
|
624
|
-
| (BaseStepFields & {
|
|
660
|
+
| (BaseStepFields<TSteps> & {
|
|
661
|
+
action: { code: string };
|
|
662
|
+
input?: StepInput<TSteps>;
|
|
663
|
+
});
|
|
625
664
|
|
|
626
665
|
type ToolCallForEachOpts<
|
|
627
666
|
TSteps extends string,
|
|
628
667
|
TTools extends readonly ToolLike[],
|
|
629
668
|
> = [TTools[number]] extends [never]
|
|
630
|
-
? BaseForEachFields & {
|
|
669
|
+
? BaseForEachFields<TSteps> & {
|
|
631
670
|
action: { toolName: string & {}; transformCode?: string };
|
|
632
671
|
input?: StepInput<TSteps>;
|
|
633
672
|
concurrency?: number;
|
|
634
673
|
}
|
|
635
674
|
: {
|
|
636
|
-
[TId in TTools[number]["id"]]: BaseForEachFields & {
|
|
675
|
+
[TId in TTools[number]["id"]]: BaseForEachFields<TSteps> & {
|
|
637
676
|
action: { toolName: TId; transformCode?: string };
|
|
638
677
|
input?: InputForTool<TTools, TId, TSteps>;
|
|
639
678
|
concurrency?: number;
|
|
@@ -645,7 +684,7 @@ type ForEachItemOpts<
|
|
|
645
684
|
TTools extends readonly ToolLike[],
|
|
646
685
|
> =
|
|
647
686
|
| ToolCallForEachOpts<TSteps, TTools>
|
|
648
|
-
| (BaseForEachFields & {
|
|
687
|
+
| (BaseForEachFields<TSteps> & {
|
|
649
688
|
action: { code: string };
|
|
650
689
|
input?: StepInput<TSteps>;
|
|
651
690
|
concurrency?: number;
|