@decocms/runtime 1.3.1 → 1.5.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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decocms/runtime",
3
- "version": "1.3.1",
3
+ "version": "1.5.0",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "check": "tsc --noEmit",
@@ -7,7 +7,7 @@ import {
7
7
  type MCPClientFetchStub,
8
8
  type ToolBinder,
9
9
  } from "../mcp.ts";
10
- import { createPrivateTool, createStreamableTool } from "../tools.ts";
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, streamable, ...toolLike } = {
109
+ const { name, handler, ...toolLike } = {
121
110
  ...toolSchema,
122
111
  ...toolImplementation,
123
112
  };
124
- if (streamable) {
125
- impl.push(
126
- createStreamableToolFn({
127
- ...toolLike,
128
- streamable,
129
- id: name,
130
- execute: ({ context }) => Promise.resolve(handler(context)),
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,10 @@ 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("readonly"),
164
+ toolApprovalLevel: z.enum(["auto", "readonly"]).default("auto"),
165
+ mode: z
166
+ .enum(["default", "plan", "web-search", "gen-image"])
167
+ .default("default"),
165
168
  temperature: z.number().default(0.5),
166
169
  });
167
170
 
package/src/decopilot.ts CHANGED
@@ -31,7 +31,9 @@ export interface AgentBindingConfig {
31
31
  thinking?: AgentModelInfo;
32
32
  coding?: AgentModelInfo;
33
33
  fast?: AgentModelInfo;
34
- toolApprovalLevel?: "auto" | "readonly" | "plan";
34
+ toolApprovalLevel?: "auto" | "readonly";
35
+ /** Decopilot stream mode — default, plan, web-search, gen-image */
36
+ mode?: "default" | "plan" | "web-search" | "gen-image";
35
37
  temperature?: number;
36
38
  }
37
39
 
@@ -45,7 +47,9 @@ export interface AgentStreamParams {
45
47
  thinking?: AgentModelInfo;
46
48
  coding?: AgentModelInfo;
47
49
  fast?: AgentModelInfo;
48
- toolApprovalLevel?: "auto" | "readonly" | "plan";
50
+ toolApprovalLevel?: "auto" | "readonly";
51
+ /** Decopilot stream mode — default, plan, web-search, gen-image */
52
+ mode?: "default" | "plan" | "web-search" | "gen-image";
49
53
  temperature?: number;
50
54
  memory?: { windowSize: number; thread_id: string };
51
55
  thread_id?: string;
@@ -126,6 +130,7 @@ export async function streamAgent(
126
130
  agent: { id: agentId },
127
131
  temperature: params.temperature ?? config.temperature,
128
132
  toolApprovalLevel: params.toolApprovalLevel ?? config.toolApprovalLevel,
133
+ mode: params.mode ?? config.mode ?? "default",
129
134
  ...(params.memory ? { memory: params.memory } : {}),
130
135
  ...(params.thread_id ? { thread_id: params.thread_id } : {}),
131
136
  };
@@ -188,6 +193,7 @@ export function createDecopilotClient(options: DecopilotClientOptions) {
188
193
  coding: request.coding,
189
194
  fast: request.fast,
190
195
  toolApprovalLevel: request.toolApprovalLevel,
196
+ mode: request.mode,
191
197
  temperature: request.temperature,
192
198
  };
193
199
  return streamAgent(streamUrl, token, config, request, opts);
package/src/index.ts CHANGED
@@ -29,6 +29,7 @@ export {
29
29
  type ResourceContents,
30
30
  type CreatedResource,
31
31
  type WorkflowDefinition,
32
+ ensureAuthenticated,
32
33
  } from "./tools.ts";
33
34
  export { createWorkflow } from "./workflows.ts";
34
35
  import type { Binding } from "./wrangler.ts";
package/src/tools.ts CHANGED
@@ -8,6 +8,7 @@ import {
8
8
  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
+ CallToolResult,
11
12
  GetPromptResult,
12
13
  Implementation,
13
14
  ToolAnnotations,
@@ -16,7 +17,7 @@ import { z } from "zod";
16
17
  import type { ZodRawShape, ZodSchema, ZodTypeAny } from "zod";
17
18
  import { BindingRegistry, injectBindingSchemas } from "./bindings.ts";
18
19
  import { Event, type EventHandlers } from "./events.ts";
19
- import type { DefaultEnv } from "./index.ts";
20
+ import type { DefaultEnv, User } from "./index.ts";
20
21
  import { State } from "./state.ts";
21
22
  import {
22
23
  type WorkflowDefinition,
@@ -67,25 +68,14 @@ export interface Tool<
67
68
  outputSchema?: TSchemaOut;
68
69
  execute(
69
70
  context: ToolExecutionContext<TSchemaIn>,
71
+ ctx?: AppContext,
70
72
  ): TSchemaOut extends ZodSchema
71
73
  ? Promise<z.infer<TSchemaOut>>
72
74
  : Promise<unknown>;
73
75
  }
74
76
 
75
77
  /**
76
- * Streamable tool interface for tools that return Response streams.
77
- */
78
- export interface StreamableTool<TSchemaIn extends ZodSchema = ZodSchema> {
79
- _meta?: Record<string, unknown>;
80
- id: string;
81
- inputSchema: TSchemaIn;
82
- streamable?: true;
83
- description?: string;
84
- execute(input: ToolExecutionContext<TSchemaIn>): Promise<Response>;
85
- }
86
-
87
- /**
88
- * CreatedTool is a permissive type that any Tool or StreamableTool can be assigned to.
78
+ * CreatedTool is a permissive type that any Tool can be assigned to.
89
79
  * Uses a structural type with relaxed execute signature to allow tools with any schema.
90
80
  */
91
81
  export type CreatedTool = {
@@ -95,12 +85,14 @@ export type CreatedTool = {
95
85
  annotations?: ToolAnnotations;
96
86
  inputSchema: ZodTypeAny;
97
87
  outputSchema?: ZodTypeAny;
98
- streamable?: true;
99
88
  // Use a permissive execute signature - accepts any context shape
100
- execute(context: {
101
- context: unknown;
102
- runtimeContext: AppContext;
103
- }): Promise<unknown>;
89
+ execute(
90
+ context: {
91
+ context: unknown;
92
+ runtimeContext: AppContext;
93
+ },
94
+ ctx?: AppContext,
95
+ ): Promise<unknown>;
104
96
  };
105
97
 
106
98
  // Re-export types for external use
@@ -137,6 +129,7 @@ export interface Prompt<TArgs extends PromptArgsRawShape = PromptArgsRawShape> {
137
129
  argsSchema?: TArgs;
138
130
  execute(
139
131
  context: PromptExecutionContext<TArgs>,
132
+ ctx?: AppContext,
140
133
  ): Promise<GetPromptResult> | GetPromptResult;
141
134
  }
142
135
 
@@ -150,10 +143,13 @@ export type CreatedPrompt = {
150
143
  description?: string;
151
144
  argsSchema?: PromptArgsRawShape;
152
145
  // Use a permissive execute signature - accepts any args shape
153
- execute(context: {
154
- args: Record<string, string | undefined>;
155
- runtimeContext: AppContext;
156
- }): Promise<GetPromptResult> | GetPromptResult;
146
+ execute(
147
+ context: {
148
+ args: Record<string, string | undefined>;
149
+ runtimeContext: AppContext;
150
+ },
151
+ ctx?: AppContext,
152
+ ): Promise<GetPromptResult> | GetPromptResult;
157
153
  };
158
154
 
159
155
  // ============================================================================
@@ -199,6 +195,7 @@ export interface Resource {
199
195
  /** Handler function to read the resource content */
200
196
  read(
201
197
  context: ResourceExecutionContext,
198
+ ctx?: AppContext,
202
199
  ): Promise<ResourceContents> | ResourceContents;
203
200
  }
204
201
 
@@ -211,48 +208,58 @@ export type CreatedResource = {
211
208
  name: string;
212
209
  description?: string;
213
210
  mimeType?: string;
214
- read(context: {
215
- uri: URL;
216
- runtimeContext: AppContext;
217
- }): Promise<ResourceContents> | ResourceContents;
211
+ read(
212
+ context: {
213
+ uri: URL;
214
+ runtimeContext: AppContext;
215
+ },
216
+ ctx?: AppContext,
217
+ ): Promise<ResourceContents> | ResourceContents;
218
218
  };
219
219
 
220
220
  /**
221
- * creates a private tool that always ensure for athentication before being executed
221
+ * Ensure the current request is authenticated.
222
+ * Reads from the per-request AppContext (AsyncLocalStorage), not from a cached env.
223
+ *
224
+ * @param ctx - Per-request AppContext from the second arg of execute/read handlers
225
+ * @returns The authenticated User
226
+ * @throws Error if no request context or user is not authenticated
227
+ */
228
+ export function ensureAuthenticated(ctx: AppContext): User {
229
+ const reqCtx = ctx?.env?.MESH_REQUEST_CONTEXT;
230
+ if (!reqCtx) {
231
+ throw new Error("Unauthorized: missing request context");
232
+ }
233
+ const user = reqCtx.ensureAuthenticated();
234
+ if (!user) {
235
+ throw new Error("Unauthorized");
236
+ }
237
+ return user;
238
+ }
239
+
240
+ let _warnedPrivateTool = false;
241
+
242
+ /**
243
+ * @deprecated Use `createTool` with `ensureAuthenticated(ctx)` instead.
244
+ *
245
+ * Creates a private tool that ensures authentication before execution.
222
246
  */
223
247
  export function createPrivateTool<
224
248
  TSchemaIn extends ZodSchema = ZodSchema,
225
249
  TSchemaOut extends ZodSchema | undefined = undefined,
226
250
  >(opts: Tool<TSchemaIn, TSchemaOut>): Tool<TSchemaIn, TSchemaOut> {
227
- const execute = opts.execute;
228
- if (typeof execute === "function") {
229
- opts.execute = (input: ToolExecutionContext<TSchemaIn>) => {
230
- const env = input.runtimeContext.env;
231
- if (env) {
232
- env.MESH_REQUEST_CONTEXT?.ensureAuthenticated();
233
- }
234
- return execute(input);
235
- };
251
+ if (!_warnedPrivateTool) {
252
+ console.warn(
253
+ "[runtime] createPrivateTool is deprecated. Use createTool with ensureAuthenticated(ctx) instead.",
254
+ );
255
+ _warnedPrivateTool = true;
236
256
  }
237
- return createTool(opts);
238
- }
239
-
240
- export function createStreamableTool<TSchemaIn extends ZodSchema = ZodSchema>(
241
- streamableTool: StreamableTool<TSchemaIn>,
242
- ): StreamableTool<TSchemaIn> {
243
- return {
244
- ...streamableTool,
245
- execute: (input: ToolExecutionContext<TSchemaIn>) => {
246
- const env = input.runtimeContext.env;
247
- if (env) {
248
- env.MESH_REQUEST_CONTEXT?.ensureAuthenticated();
249
- }
250
- return streamableTool.execute({
251
- ...input,
252
- runtimeContext: createRuntimeContext(input.runtimeContext),
253
- });
254
- },
257
+ const execute = opts.execute;
258
+ opts.execute = (input: ToolExecutionContext<TSchemaIn>, ctx: AppContext) => {
259
+ ensureAuthenticated(ctx);
260
+ return execute(input, ctx);
255
261
  };
262
+ return createTool(opts);
256
263
  }
257
264
 
258
265
  export function createTool<
@@ -263,10 +270,8 @@ export function createTool<
263
270
  return {
264
271
  ...opts,
265
272
  execute: (input: ToolExecutionContext<TSchemaIn>) => {
266
- return opts.execute({
267
- ...input,
268
- runtimeContext: createRuntimeContext(input.runtimeContext),
269
- });
273
+ const ctx = createRuntimeContext(input.runtimeContext);
274
+ return opts.execute({ ...input, runtimeContext: ctx }, ctx);
270
275
  },
271
276
  };
272
277
  }
@@ -280,10 +285,8 @@ export function createPublicPrompt<TArgs extends PromptArgsRawShape>(
280
285
  return {
281
286
  ...opts,
282
287
  execute: (input: PromptExecutionContext<TArgs>) => {
283
- return opts.execute({
284
- ...input,
285
- runtimeContext: createRuntimeContext(input.runtimeContext),
286
- });
288
+ const ctx = createRuntimeContext(input.runtimeContext);
289
+ return opts.execute({ ...input, runtimeContext: ctx }, ctx);
287
290
  },
288
291
  };
289
292
  }
@@ -298,12 +301,9 @@ export function createPrompt<TArgs extends PromptArgsRawShape>(
298
301
  const execute = opts.execute;
299
302
  return createPublicPrompt({
300
303
  ...opts,
301
- execute: (input: PromptExecutionContext<TArgs>) => {
302
- const env = input.runtimeContext.env;
303
- if (env) {
304
- env.MESH_REQUEST_CONTEXT?.ensureAuthenticated();
305
- }
306
- return execute(input);
304
+ execute: (input: PromptExecutionContext<TArgs>, ctx: AppContext) => {
305
+ ensureAuthenticated(ctx);
306
+ return execute(input, ctx);
307
307
  },
308
308
  });
309
309
  }
@@ -315,10 +315,8 @@ export function createPublicResource(opts: Resource): Resource {
315
315
  return {
316
316
  ...opts,
317
317
  read: (input: ResourceExecutionContext) => {
318
- return opts.read({
319
- ...input,
320
- runtimeContext: createRuntimeContext(input.runtimeContext),
321
- });
318
+ const ctx = createRuntimeContext(input.runtimeContext);
319
+ return opts.read({ ...input, runtimeContext: ctx }, ctx);
322
320
  },
323
321
  };
324
322
  }
@@ -331,12 +329,9 @@ export function createResource(opts: Resource): Resource {
331
329
  const read = opts.read;
332
330
  return createPublicResource({
333
331
  ...opts,
334
- read: (input: ResourceExecutionContext) => {
335
- const env = input.runtimeContext.env;
336
- if (env) {
337
- env.MESH_REQUEST_CONTEXT?.ensureAuthenticated();
338
- }
339
- return read(input);
332
+ read: (input: ResourceExecutionContext, ctx: AppContext) => {
333
+ ensureAuthenticated(ctx);
334
+ return read(input, ctx);
340
335
  },
341
336
  });
342
337
  }
@@ -355,12 +350,6 @@ export interface Integration {
355
350
  appId: string;
356
351
  }
357
352
 
358
- export function isStreamableTool(
359
- tool: CreatedTool,
360
- ): tool is StreamableTool & CreatedTool {
361
- return tool && "streamable" in tool && tool.streamable === true;
362
- }
363
-
364
353
  export interface OnChangeCallback<TState> {
365
354
  state: TState;
366
355
  scopes: string[];
@@ -500,35 +489,38 @@ export interface CreateMCPServerOptions<
500
489
  };
501
490
  tools?:
502
491
  | Array<
503
- (
504
- env: TEnv,
505
- ) =>
506
- | Promise<CreatedTool>
507
- | CreatedTool
508
- | CreatedTool[]
509
- | Promise<CreatedTool[]>
492
+ | CreatedTool
493
+ | ((
494
+ env: TEnv,
495
+ ) =>
496
+ | Promise<CreatedTool>
497
+ | CreatedTool
498
+ | CreatedTool[]
499
+ | Promise<CreatedTool[]>)
510
500
  >
511
501
  | ((env: TEnv) => CreatedTool[] | Promise<CreatedTool[]>);
512
502
  prompts?:
513
503
  | Array<
514
- (
515
- env: TEnv,
516
- ) =>
517
- | Promise<CreatedPrompt>
518
- | CreatedPrompt
519
- | CreatedPrompt[]
520
- | Promise<CreatedPrompt[]>
504
+ | CreatedPrompt
505
+ | ((
506
+ env: TEnv,
507
+ ) =>
508
+ | Promise<CreatedPrompt>
509
+ | CreatedPrompt
510
+ | CreatedPrompt[]
511
+ | Promise<CreatedPrompt[]>)
521
512
  >
522
513
  | ((env: TEnv) => CreatedPrompt[] | Promise<CreatedPrompt[]>);
523
514
  resources?:
524
515
  | Array<
525
- (
526
- env: TEnv,
527
- ) =>
528
- | Promise<CreatedResource>
529
- | CreatedResource
530
- | CreatedResource[]
531
- | Promise<CreatedResource[]>
516
+ | CreatedResource
517
+ | ((
518
+ env: TEnv,
519
+ ) =>
520
+ | Promise<CreatedResource>
521
+ | CreatedResource
522
+ | CreatedResource[]
523
+ | Promise<CreatedResource[]>)
532
524
  >
533
525
  | ((env: TEnv) => CreatedResource[] | Promise<CreatedResource[]>);
534
526
  workflows?:
@@ -819,7 +811,8 @@ export const createMCPServer = <
819
811
  ): MCPServer<TEnv, TSchema, TBindings> => {
820
812
  // Tool/prompt/resource definitions are resolved once on first request and
821
813
  // cached for the lifetime of the process. Tool *execution* reads per-request
822
- // context from State (AsyncLocalStorage), so reusing definitions is safe.
814
+ // context from State (AsyncLocalStorage) via the second `ctx` argument, so
815
+ // reusing definitions is safe.
823
816
  type Registrations = {
824
817
  tools: CreatedTool[];
825
818
  prompts: CreatedPrompt[];
@@ -830,6 +823,51 @@ export const createMCPServer = <
830
823
  let cached: Registrations | null = null;
831
824
  let inflightResolve: Promise<Registrations> | null = null;
832
825
 
826
+ let _warnedFactoryDeprecation = false;
827
+ const warnFactoryDeprecation = () => {
828
+ if (!_warnedFactoryDeprecation) {
829
+ console.warn(
830
+ "[runtime] Passing factory functions to tools/prompts/resources is deprecated. " +
831
+ "Pass createTool()/createPrompt()/createResource() instances directly.",
832
+ );
833
+ _warnedFactoryDeprecation = true;
834
+ }
835
+ };
836
+
837
+ /**
838
+ * Check whether a value is an already-created instance (has an `id` or `name` property)
839
+ * rather than a factory function.
840
+ */
841
+ const isInstance = (v: unknown): boolean =>
842
+ typeof v === "object" &&
843
+ v !== null &&
844
+ ("id" in v || "name" in v || "uri" in v);
845
+
846
+ /**
847
+ * Resolve an array that may contain both direct instances and factory functions.
848
+ * Factories are called with `bindings` and trigger a deprecation warning.
849
+ */
850
+ async function resolveArray<T>(
851
+ items: Array<unknown> | undefined,
852
+ bindings: TEnv,
853
+ ): Promise<T[]> {
854
+ if (!items) return [];
855
+ return (
856
+ await Promise.all(
857
+ items.flatMap(async (item) => {
858
+ if (isInstance(item)) {
859
+ return [item as T];
860
+ }
861
+ // Factory function — deprecated path
862
+ warnFactoryDeprecation();
863
+ const result = await (item as (env: TEnv) => unknown)(bindings);
864
+ if (Array.isArray(result)) return result as T[];
865
+ return [result as T];
866
+ }),
867
+ )
868
+ ).flat();
869
+ }
870
+
833
871
  const resolveRegistrations = async (
834
872
  bindings: TEnv,
835
873
  ): Promise<Registrations> => {
@@ -838,25 +876,13 @@ export const createMCPServer = <
838
876
 
839
877
  inflightResolve = (async (): Promise<Registrations> => {
840
878
  try {
841
- const toolsFn =
842
- typeof options.tools === "function"
843
- ? options.tools
844
- : async (bindings: TEnv) => {
845
- if (typeof options.tools === "function") {
846
- return await options.tools(bindings);
847
- }
848
- return await Promise.all(
849
- options.tools?.flatMap(async (tool) => {
850
- const toolResult = tool(bindings);
851
- const awaited = await toolResult;
852
- if (Array.isArray(awaited)) {
853
- return awaited;
854
- }
855
- return [awaited];
856
- }) ?? [],
857
- ).then((t) => t.flat());
858
- };
859
- const tools = await toolsFn(bindings);
879
+ let tools: CreatedTool[];
880
+ if (typeof options.tools === "function") {
881
+ warnFactoryDeprecation();
882
+ tools = await options.tools(bindings);
883
+ } else {
884
+ tools = await resolveArray<CreatedTool>(options.tools, bindings);
885
+ }
860
886
 
861
887
  const resolvedWorkflows =
862
888
  typeof options.workflows === "function"
@@ -867,45 +893,27 @@ export const createMCPServer = <
867
893
  ...toolsFor<TSchema>({ ...options, workflows: resolvedWorkflows }),
868
894
  );
869
895
 
870
- const promptsFn =
871
- typeof options.prompts === "function"
872
- ? options.prompts
873
- : async (bindings: TEnv) => {
874
- if (typeof options.prompts === "function") {
875
- return await options.prompts(bindings);
876
- }
877
- return await Promise.all(
878
- options.prompts?.flatMap(async (prompt) => {
879
- const promptResult = prompt(bindings);
880
- const awaited = await promptResult;
881
- if (Array.isArray(awaited)) {
882
- return awaited;
883
- }
884
- return [awaited];
885
- }) ?? [],
886
- ).then((p) => p.flat());
887
- };
888
- const prompts = await promptsFn(bindings);
889
-
890
- const resourcesFn =
891
- typeof options.resources === "function"
892
- ? options.resources
893
- : async (bindings: TEnv) => {
894
- if (typeof options.resources === "function") {
895
- return await options.resources(bindings);
896
- }
897
- return await Promise.all(
898
- options.resources?.flatMap(async (resource) => {
899
- const resourceResult = resource(bindings);
900
- const awaited = await resourceResult;
901
- if (Array.isArray(awaited)) {
902
- return awaited;
903
- }
904
- return [awaited];
905
- }) ?? [],
906
- ).then((r) => r.flat());
907
- };
908
- const resources = await resourcesFn(bindings);
896
+ let prompts: CreatedPrompt[];
897
+ if (typeof options.prompts === "function") {
898
+ warnFactoryDeprecation();
899
+ prompts = await options.prompts(bindings);
900
+ } else {
901
+ prompts = await resolveArray<CreatedPrompt>(
902
+ options.prompts,
903
+ bindings,
904
+ );
905
+ }
906
+
907
+ let resources: CreatedResource[];
908
+ if (typeof options.resources === "function") {
909
+ warnFactoryDeprecation();
910
+ resources = await options.resources(bindings);
911
+ } else {
912
+ resources = await resolveArray<CreatedResource>(
913
+ options.resources,
914
+ bindings,
915
+ );
916
+ }
909
917
 
910
918
  const result = {
911
919
  tools,
@@ -924,55 +932,48 @@ export const createMCPServer = <
924
932
  return inflightResolve;
925
933
  };
926
934
 
927
- const registerAll = (
928
- server: McpServer,
929
- registrations: Registrations,
930
- ) => {
935
+ const registerAll = (server: McpServer, registrations: Registrations) => {
931
936
  for (const tool of registrations.tools) {
932
937
  server.registerTool(
933
938
  tool.id,
934
939
  {
935
- _meta: {
936
- streamable: isStreamableTool(tool),
937
- ...(tool._meta ?? {}),
938
- },
940
+ _meta: tool._meta,
939
941
  description: tool.description,
940
942
  annotations: tool.annotations,
941
943
  inputSchema:
942
944
  tool.inputSchema && "shape" in tool.inputSchema
943
945
  ? (tool.inputSchema.shape as ZodRawShape)
944
946
  : z.object({}).shape,
945
- outputSchema: isStreamableTool(tool)
946
- ? z.object({ bytes: z.record(z.string(), z.number()) }).shape
947
- : tool.outputSchema &&
948
- typeof tool.outputSchema === "object" &&
949
- "shape" in tool.outputSchema
947
+ outputSchema:
948
+ tool.outputSchema &&
949
+ typeof tool.outputSchema === "object" &&
950
+ "shape" in tool.outputSchema
950
951
  ? (tool.outputSchema.shape as ZodRawShape)
951
952
  : undefined,
952
953
  },
953
954
  async (args) => {
954
- const result = await tool.execute({
955
- context: args,
956
- runtimeContext: createRuntimeContext(),
957
- });
958
-
959
- // For streamable tools, the Response is handled at the transport layer.
960
- // Do NOT call result.bytes() — it buffers the entire body in memory.
961
- if (isStreamableTool(tool) && result instanceof Response) {
962
- return {
963
- structuredContent: {
964
- streamable: true,
965
- status: result.status,
966
- statusText: result.statusText,
967
- },
968
- content: [
969
- {
970
- type: "text",
971
- text: `Streaming response: ${result.status} ${result.statusText}`,
972
- },
973
- ],
974
- };
955
+ const ctx = createRuntimeContext();
956
+ const result = await tool.execute(
957
+ { context: args, runtimeContext: ctx },
958
+ ctx,
959
+ );
960
+
961
+ if (
962
+ result != null &&
963
+ typeof result === "object" &&
964
+ "content" in result &&
965
+ Array.isArray(result.content) &&
966
+ result.content.every(
967
+ (item: unknown) =>
968
+ item != null &&
969
+ typeof item === "object" &&
970
+ "type" in item &&
971
+ typeof (item as Record<string, unknown>).type === "string",
972
+ )
973
+ ) {
974
+ return result as CallToolResult;
975
975
  }
976
+
976
977
  return {
977
978
  structuredContent: result as Record<string, unknown>,
978
979
  content: [
@@ -997,10 +998,14 @@ export const createMCPServer = <
997
998
  : z.object({}).shape,
998
999
  },
999
1000
  async (args) => {
1000
- return await prompt.execute({
1001
- args: args as Record<string, string | undefined>,
1002
- runtimeContext: createRuntimeContext(),
1003
- });
1001
+ const ctx = createRuntimeContext();
1002
+ return await prompt.execute(
1003
+ {
1004
+ args: args as Record<string, string | undefined>,
1005
+ runtimeContext: ctx,
1006
+ },
1007
+ ctx,
1008
+ );
1004
1009
  },
1005
1010
  );
1006
1011
  }
@@ -1014,10 +1019,8 @@ export const createMCPServer = <
1014
1019
  mimeType: resource.mimeType,
1015
1020
  },
1016
1021
  async (uri) => {
1017
- const result = await resource.read({
1018
- uri,
1019
- runtimeContext: createRuntimeContext(),
1020
- });
1022
+ const ctx = createRuntimeContext();
1023
+ const result = await resource.read({ uri, runtimeContext: ctx }, ctx);
1021
1024
 
1022
1025
  const meta =
1023
1026
  (result as { _meta?: Record<string, unknown> | null })._meta ??
@@ -1114,7 +1117,10 @@ export const createMCPServer = <
1114
1117
  // finishes (server sent the response) or the client disconnects
1115
1118
  // (cancel), the server and transport are always cleaned up.
1116
1119
  const { readable, writable } = new TransformStream();
1117
- response.body.pipeTo(writable).catch(() => {}).finally(cleanup);
1120
+ response.body
1121
+ .pipeTo(writable)
1122
+ .catch(() => {})
1123
+ .finally(cleanup);
1118
1124
 
1119
1125
  return new Response(readable, {
1120
1126
  status: response.status,
@@ -1144,10 +1150,8 @@ export const createMCPServer = <
1144
1150
  );
1145
1151
  }
1146
1152
 
1147
- return execute({
1148
- context: toolCallInput,
1149
- runtimeContext: createRuntimeContext(),
1150
- });
1153
+ const ctx = createRuntimeContext();
1154
+ return execute({ context: toolCallInput, runtimeContext: ctx }, ctx);
1151
1155
  };
1152
1156
 
1153
1157
  return {