diff --git a/src/tool/factory.test.ts b/src/tool/factory.test.ts index fc47c98054..33c3a3bf0e 100644 --- a/src/tool/factory.test.ts +++ b/src/tool/factory.test.ts @@ -167,4 +167,80 @@ describe("tool factory", () => { assertStringIncludes(t.id, "tool_"); }); }); + + describe("dynamicTool input validation", () => { + it("should validate input with Zod schema", async () => { + const t = dynamicTool({ + id: "zod-validate", + description: "desc", + inputSchema: z.object({ query: z.string() }), + execute: async (input) => input, + }); + const result = await t.execute({ query: "test" }); + assertEquals(result, { query: "test" }); + }); + + it("should reject invalid input when Zod schema is provided", async () => { + const t = dynamicTool({ + id: "zod-reject", + description: "desc", + inputSchema: z.object({ query: z.string() }), + execute: async () => "ok", + }); + await assertRejects( + () => t.execute({ query: 123 }), + Error, + ); + }); + + it("should accept valid object input without Zod schema", async () => { + const t = dynamicTool({ + id: "no-schema-obj", + description: "desc", + inputSchema: {}, + execute: async (input) => input, + }); + const result = await t.execute({ foo: "bar" }); + assertEquals(result, { foo: "bar" }); + }); + + it("should reject null input without schema", async () => { + const t = dynamicTool({ + id: "no-schema-null", + description: "desc", + inputSchema: {}, + execute: async (input) => input, + }); + await assertRejects( + () => t.execute(null), + Error, + "input must be a non-null object", + ); + }); + + it("should coerce undefined input to empty object for zero-input tools", async () => { + const t = dynamicTool({ + id: "no-schema-undef", + description: "desc", + inputSchema: {}, + execute: async (input) => input, + }); + const result = await t.execute(undefined); + assertEquals(result, {}); + }); + + it("should reject primitive input without schema", async () => { + const t = dynamicTool({ + id: "no-schema-prim", + description: "desc", + inputSchema: {}, + execute: async (input) => input, + }); + await assertRejects( + () => t.execute("string-input"), + Error, + "input must be a non-null object", + ); + }); + }); }); diff --git a/src/tool/factory.ts b/src/tool/factory.ts index 69d0d9e3fe..b6d3ed61af 100644 --- a/src/tool/factory.ts +++ b/src/tool/factory.ts @@ -186,6 +186,16 @@ export function dynamicTool(config: DynamicToolConfig): Tool { inputSchema: config.inputSchema as z.ZodSchema, inputSchemaJson, execute: async (input: unknown, context?: ToolExecutionContext) => { + if ( + config.inputSchema && + typeof (config.inputSchema as { parse?: unknown }).parse === "function" + ) { + (config.inputSchema as { parse: (v: unknown) => unknown }).parse(input); + } else if (input === undefined) { + input = {}; + } else if (input === null || typeof input !== "object") { + throw new Error("dynamicTool: input must be a non-null object"); + } const result = await config.execute(input, context); return config.toModelOutput ? config.toModelOutput(result) : result; },