diff --git a/packages/agent-runtime/src/__tests__/prompts-schema-handling.test.ts b/packages/agent-runtime/src/__tests__/prompts-schema-handling.test.ts index d3ad20b276..89c5a5f3b2 100644 --- a/packages/agent-runtime/src/__tests__/prompts-schema-handling.test.ts +++ b/packages/agent-runtime/src/__tests__/prompts-schema-handling.test.ts @@ -248,6 +248,39 @@ describe('Schema handling error recovery', () => { expect(toolSet['problematic_tool']).toBeDefined() }) + test('getToolSet keeps an MCP tool schema usable after copying the definition', async () => { + // MCP tools arrive as Zod schemas: mcp.ts converts the server's JSON + // Schema with convertJsonSchemaToZod. Building the tool set copies the + // definition, and lodash drops Zod's non-enumerable `_zod`, which left a + // clone that still looked like a schema but threw on any Zod call. + const mcpToolDefs = { + exa__web_search_exa: { + description: 'Search the web with Exa', + inputSchema: convertJsonSchemaToZod({ + type: 'object', + properties: { query: { type: 'string' } }, + required: ['query'], + }), + endsAgentStep: true, + }, + } + + const toolSet = await getToolSet({ + toolNames: [], + windowedFileReads: false, + additionalToolDefinitions: async () => mcpToolDefs, + agentTools: {}, + skills: {}, + }) + + const tool = toolSet['exa__web_search_exa'] as { + inputSchema: { safeParse: (input: unknown) => { success: boolean } } + } + expect(tool).toBeDefined() + // A clone without `_zod` throws here instead of validating. + expect(tool.inputSchema.safeParse({ query: 'hello' }).success).toBe(true) + }) + test('ensureZodSchema converts JSON Schema to Zod schema', () => { const jsonSchema = { type: 'object', diff --git a/packages/agent-runtime/src/tools/prompts.ts b/packages/agent-runtime/src/tools/prompts.ts index d3d9110665..d0c7c8a741 100644 --- a/packages/agent-runtime/src/tools/prompts.ts +++ b/packages/agent-runtime/src/tools/prompts.ts @@ -430,10 +430,19 @@ export async function getToolSet(params: { const toolDefinitions = await additionalToolDefinitions() for (const [toolName, toolDefinition] of Object.entries(toolDefinitions)) { - const clonedDef = cloneDeep(toolDefinition) + // Copy the definition without deep-cloning its inputSchema. lodash copies + // own *enumerable* properties only, and zod keeps its internals on a + // non-enumerable `_zod`, so a deep clone hands back something that still + // looks like a schema (`safeParse` lives on the prototype) but has lost + // `_zod` -- and the next zod call on it throws "undefined is not an object + // (evaluating 'schema._zod.parent')". Every MCP tool hits this, because + // mcp.ts stores its input schema as a zod schema. Schemas are immutable, so + // carry the reference instead. + const { inputSchema, ...restOfDefinition } = toolDefinition + const clonedDef = { ...cloneDeep(restOfDefinition), inputSchema } // Custom tool inputSchema may be JSON Schema (from SDK) or Zod (from MCP) // Ensure it's a Zod schema for the AI SDK - const zodSchema = ensureZodSchema(clonedDef.inputSchema) + const zodSchema = ensureZodSchema(inputSchema) const safeSchema = ensureJsonSchemaCompatible(zodSchema) toolSet[toolName] = { ...clonedDef,