@cloudflare/codemode 0.1.0 → 0.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.
@@ -4,8 +4,16 @@
4
4
  import { describe, it, expect } from "vitest";
5
5
  import { generateTypes, sanitizeToolName } from "../types";
6
6
  import { z } from "zod";
7
+ import { fromJSONSchema } from "zod/v4";
8
+ import { jsonSchema } from "ai";
9
+ import type { ToolSet } from "ai";
7
10
  import type { ToolDescriptors } from "../types";
8
11
 
12
+ // Helper: cast loosely-typed tool objects for generateTypes
13
+ function genTypes(tools: Record<string, unknown>): string {
14
+ return generateTypes(tools as unknown as ToolSet);
15
+ }
16
+
9
17
  describe("sanitizeToolName", () => {
10
18
  it("should replace hyphens with underscores", () => {
11
19
  expect(sanitizeToolName("get-weather")).toBe("get_weather");
@@ -168,4 +176,271 @@ describe("generateTypes", () => {
168
176
  // toCamelCase("get_weather") → "GetWeather"
169
177
  expect(result).toContain("GetWeatherInput");
170
178
  });
179
+
180
+ it("should handle MCP tools with input and output schemas (fromJSONSchema)", () => {
181
+ // MCP tools use JSON Schema format for both input and output
182
+ const inputSchema = {
183
+ type: "object" as const,
184
+ properties: {
185
+ city: { type: "string" as const, description: "City name" },
186
+ units: {
187
+ type: "string" as const,
188
+ enum: ["celsius", "fahrenheit"],
189
+ description: "Temperature units"
190
+ },
191
+ includeForecast: { type: "boolean" as const }
192
+ },
193
+ required: ["city"]
194
+ };
195
+
196
+ const outputSchema = {
197
+ type: "object" as const,
198
+ properties: {
199
+ temperature: { type: "number" as const, description: "Current temp" },
200
+ humidity: { type: "number" as const },
201
+ conditions: { type: "string" as const },
202
+ forecast: {
203
+ type: "array" as const,
204
+ items: {
205
+ type: "object" as const,
206
+ properties: {
207
+ day: { type: "string" as const },
208
+ high: { type: "number" as const },
209
+ low: { type: "number" as const }
210
+ }
211
+ }
212
+ }
213
+ },
214
+ required: ["temperature", "conditions"]
215
+ };
216
+
217
+ const tools: ToolDescriptors = {
218
+ getWeather: {
219
+ description: "Get weather for a city",
220
+ inputSchema: fromJSONSchema(inputSchema),
221
+ outputSchema: fromJSONSchema(outputSchema)
222
+ }
223
+ };
224
+
225
+ const result = generateTypes(tools);
226
+
227
+ // Input schema types
228
+ expect(result).toContain("type GetWeatherInput");
229
+ expect(result).toContain("city: string");
230
+ expect(result).toContain("units?:");
231
+ expect(result).toContain("includeForecast?: boolean");
232
+
233
+ // Output schema types (not unknown)
234
+ expect(result).toContain("type GetWeatherOutput");
235
+ expect(result).not.toContain("GetWeatherOutput = unknown");
236
+ expect(result).toContain("temperature: number");
237
+ expect(result).toContain("humidity?: number");
238
+ expect(result).toContain("conditions: string");
239
+ expect(result).toContain("forecast?:");
240
+ expect(result).toContain("day?: string");
241
+ expect(result).toContain("high?: number");
242
+ expect(result).toContain("low?: number");
243
+
244
+ // JSDoc
245
+ expect(result).toContain("@param input.city - City name");
246
+ expect(result).toContain("/** Current temp */");
247
+ });
248
+
249
+ it("should handle Zod schemas with input and output schemas", () => {
250
+ // Direct ToolDescriptors with Zod schemas (what generateTypes operates on)
251
+ const tools: ToolDescriptors = {
252
+ getWeather: {
253
+ description: "Get weather for a city",
254
+ inputSchema: z.object({
255
+ city: z.string().describe("City name"),
256
+ units: z.enum(["celsius", "fahrenheit"]).optional()
257
+ }),
258
+ outputSchema: z.object({
259
+ temperature: z.number().describe("Current temperature"),
260
+ humidity: z.number().describe("Humidity percentage"),
261
+ conditions: z.string().describe("Weather conditions"),
262
+ forecast: z.array(
263
+ z.object({
264
+ day: z.string(),
265
+ high: z.number(),
266
+ low: z.number()
267
+ })
268
+ )
269
+ })
270
+ }
271
+ };
272
+
273
+ const result = generateTypes(tools);
274
+
275
+ // Verify input schema
276
+ expect(result).toContain("type GetWeatherInput");
277
+ expect(result).toContain("city: string");
278
+ expect(result).toContain("units?:");
279
+ expect(result).toContain('"celsius"');
280
+ expect(result).toContain('"fahrenheit"');
281
+
282
+ // Verify output schema is properly typed (not unknown)
283
+ expect(result).toContain("type GetWeatherOutput");
284
+ expect(result).not.toContain("GetWeatherOutput = unknown");
285
+ expect(result).toContain("temperature: number");
286
+ expect(result).toContain("humidity: number");
287
+ expect(result).toContain("conditions: string");
288
+ expect(result).toContain("forecast:");
289
+ expect(result).toContain("day: string");
290
+ expect(result).toContain("high: number");
291
+ expect(result).toContain("low: number");
292
+
293
+ // Verify JSDoc comments from .describe()
294
+ expect(result).toContain("/** City name */");
295
+ expect(result).toContain("/** Current temperature */");
296
+ expect(result).toContain("@param input.city - City name");
297
+ });
298
+
299
+ it("should handle null inputSchema gracefully", () => {
300
+ const tools = {
301
+ broken: {
302
+ description: "Broken tool",
303
+ inputSchema: null
304
+ }
305
+ };
306
+
307
+ const result = genTypes(tools);
308
+
309
+ expect(result).toContain("type BrokenInput = unknown");
310
+ expect(result).toContain("type BrokenOutput = unknown");
311
+ expect(result).toContain("broken:");
312
+ });
313
+
314
+ it("should handle undefined inputSchema gracefully", () => {
315
+ const tools = {
316
+ broken: {
317
+ description: "Broken tool",
318
+ inputSchema: undefined
319
+ }
320
+ };
321
+
322
+ const result = genTypes(tools);
323
+
324
+ expect(result).toContain("type BrokenInput = unknown");
325
+ expect(result).toContain("type BrokenOutput = unknown");
326
+ expect(result).toContain("broken:");
327
+ });
328
+
329
+ it("should handle string inputSchema gracefully", () => {
330
+ const tools = {
331
+ broken: {
332
+ description: "Broken tool",
333
+ inputSchema: "not a schema"
334
+ }
335
+ };
336
+
337
+ const result = genTypes(tools);
338
+
339
+ expect(result).toContain("type BrokenInput = unknown");
340
+ expect(result).toContain("broken:");
341
+ });
342
+
343
+ it("should isolate errors: one throwing tool does not break others", () => {
344
+ // Create a tool with a getter that throws
345
+ const throwingSchema = {
346
+ get jsonSchema(): never {
347
+ throw new Error("Schema explosion");
348
+ }
349
+ };
350
+
351
+ const tools = {
352
+ good1: {
353
+ description: "Good first",
354
+ inputSchema: jsonSchema({
355
+ type: "object" as const,
356
+ properties: { a: { type: "string" as const } }
357
+ })
358
+ },
359
+ bad: {
360
+ description: "Bad tool",
361
+ inputSchema: throwingSchema
362
+ },
363
+ good2: {
364
+ description: "Good second",
365
+ inputSchema: jsonSchema({
366
+ type: "object" as const,
367
+ properties: { b: { type: "number" as const } }
368
+ })
369
+ }
370
+ };
371
+
372
+ const result = genTypes(tools);
373
+
374
+ // Good tools should work fine
375
+ expect(result).toContain("type Good1Input");
376
+ expect(result).toContain("a?: string;");
377
+ expect(result).toContain("type Good2Input");
378
+ expect(result).toContain("b?: number;");
379
+
380
+ // Bad tool should degrade to unknown
381
+ expect(result).toContain("type BadInput = unknown");
382
+ expect(result).toContain("type BadOutput = unknown");
383
+
384
+ // All three tools should appear in the codemode declaration
385
+ expect(result).toContain("good1:");
386
+ expect(result).toContain("bad:");
387
+ expect(result).toContain("good2:");
388
+ });
389
+
390
+ it("should handle AI SDK jsonSchema wrapper (MCP tools)", () => {
391
+ // This is what MCP tools look like when using the AI SDK jsonSchema wrapper
392
+ const inputJsonSchema = {
393
+ type: "object" as const,
394
+ properties: {
395
+ query: { type: "string" as const, description: "Search query" },
396
+ limit: { type: "number" as const, description: "Max results" }
397
+ },
398
+ required: ["query"]
399
+ };
400
+
401
+ const outputJsonSchema = {
402
+ type: "object" as const,
403
+ properties: {
404
+ results: {
405
+ type: "array" as const,
406
+ items: {
407
+ type: "object" as const,
408
+ properties: {
409
+ title: { type: "string" as const },
410
+ url: { type: "string" as const }
411
+ }
412
+ }
413
+ },
414
+ total: { type: "number" as const }
415
+ }
416
+ };
417
+
418
+ // Use AI SDK jsonSchema wrapper (what MCP client returns)
419
+ const tools = {
420
+ search: {
421
+ description: "Search the web",
422
+ inputSchema: jsonSchema(inputJsonSchema),
423
+ outputSchema: jsonSchema(outputJsonSchema)
424
+ }
425
+ };
426
+
427
+ const result = generateTypes(tools as unknown as ToolDescriptors);
428
+
429
+ // Input schema types
430
+ expect(result).toContain("type SearchInput");
431
+ expect(result).toContain("query: string");
432
+ expect(result).toContain("limit?: number");
433
+
434
+ // Output schema types (not unknown)
435
+ expect(result).toContain("type SearchOutput");
436
+ expect(result).not.toContain("SearchOutput = unknown");
437
+ expect(result).toContain("results?:");
438
+ expect(result).toContain("title?: string");
439
+ expect(result).toContain("url?: string");
440
+ expect(result).toContain("total?: number");
441
+
442
+ // JSDoc from JSON Schema descriptions
443
+ expect(result).toContain("@param input.query - Search query");
444
+ expect(result).toContain("@param input.limit - Max results");
445
+ });
171
446
  });
package/src/tool.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { tool, type Tool } from "ai";
1
+ import { tool, type Tool, asSchema } from "ai";
2
2
  import { z } from "zod";
3
3
  import type { ToolSet } from "ai";
4
4
  import * as acorn from "acorn";
@@ -10,7 +10,8 @@ const DEFAULT_DESCRIPTION = `Execute code to achieve a goal.
10
10
  Available:
11
11
  {{types}}
12
12
 
13
- Write an async arrow function that returns the result.
13
+ Write an async arrow function in JavaScript that returns the result.
14
+ Do NOT use TypeScript syntax — no type annotations, interfaces, or generics.
14
15
  Do NOT define named functions then call them — just write the arrow function body directly.
15
16
 
16
17
  Example: async () => { const r = await codemode.searchWeb({ query: "test" }); return r; }`;
@@ -97,7 +98,9 @@ export function createCodeTool(
97
98
  description,
98
99
  inputSchema: codeSchema,
99
100
  execute: async ({ code }) => {
100
- // Extract execute functions from tools, keyed by name
101
+ // Extract execute functions from tools, keyed by name.
102
+ // Wrap each with its schema so arguments from the sandbox
103
+ // are validated before reaching the tool function.
101
104
  const fns: Record<string, (...args: unknown[]) => Promise<unknown>> = {};
102
105
 
103
106
  for (const [name, t] of Object.entries(tools)) {
@@ -106,7 +109,25 @@ export function createCodeTool(
106
109
  ? (t.execute as (args: unknown) => Promise<unknown>)
107
110
  : undefined;
108
111
  if (execute) {
109
- fns[sanitizeToolName(name)] = execute;
112
+ const rawSchema =
113
+ "inputSchema" in t
114
+ ? t.inputSchema
115
+ : "parameters" in t
116
+ ? (t as Record<string, unknown>).parameters
117
+ : undefined;
118
+
119
+ // Use AI SDK's asSchema() to normalize any schema type
120
+ // (Zod v3/v4, Standard Schema, JSON Schema) into a unified
121
+ // Schema with an optional .validate() method.
122
+ const schema = rawSchema != null ? asSchema(rawSchema) : undefined;
123
+
124
+ fns[sanitizeToolName(name)] = schema?.validate
125
+ ? async (args: unknown) => {
126
+ const result = await schema.validate!(args);
127
+ if (!result.success) throw result.error;
128
+ return execute(result.value);
129
+ }
130
+ : execute;
110
131
  }
111
132
  }
112
133