Skip to content

Commit ae55343

Browse files
authored
fix(server): handle provider-defined tools in /tools API endpoint (#13507)
The `/tools`, `/tools/:toolId`, and `/agents/:agentId/tools/:toolId` endpoints crash when an agent has provider-defined tools like `google.tools.googleSearch()` or `openai.tools.webSearch()`. These tools have a lazy `inputSchema` function that returns an AI SDK Schema object (not a Zod schema). The handler was blindly calling `zodToJsonSchema(tool.inputSchema)` on them, which blows up with: ``` TypeError: Cannot read properties of undefined (reading 'typeName') ``` The fix detects provider-defined tools (via `tool.type === 'provider-defined' || tool.type === 'provider'`) and resolves their lazy schema to extract the `jsonSchema` property directly, skipping `zodToJsonSchema` entirely. This reuses the same detection pattern already in `prepare-tools.ts`. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Fixed /tools API crash when using provider-defined tools from external AI SDKs (e.g., Google, OpenAI). * **New Features** * Added provider-defined tool detection to ensure correct tool serialization and listing. * **Tests** * Added coverage for provider-defined tool recognition and serialization across handlers. * **Chores** * Released @mastra/server minor and @mastra/core patch; tightened peer dependency to require @mastra/core ≥ 1.7.1. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1 parent b69a004 commit ae55343

8 files changed

Lines changed: 192 additions & 34 deletions

File tree

.changeset/bright-pens-type.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@mastra/core': patch
3+
---
4+
5+
Added `isProviderDefinedTool` helper to detect provider-defined AI SDK tools (e.g. `google.tools.googleSearch()`, `openai.tools.webSearch()`) for proper schema handling during serialization.

.changeset/vast-teams-camp.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@mastra/server': patch
3+
---
4+
5+
Fixed /tools API endpoint crashing with provider-defined tools (e.g. `google.tools.googleSearch()`, `openai.tools.webSearch()`). These tools have a lazy `inputSchema` that is not a Zod schema, which caused `zodToJsonSchema` to throw `"Cannot read properties of undefined (reading 'typeName')"`.
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { google } from '@ai-sdk/google-v5';
2+
import { openai } from '@ai-sdk/openai-v5';
3+
import { describe, expect, it } from 'vitest';
4+
import { isProviderDefinedTool } from '../toolchecks';
5+
6+
describe('isProviderDefinedTool', () => {
7+
it('should identify Google provider-defined tools', () => {
8+
expect(isProviderDefinedTool(google.tools.googleSearch({}))).toBe(true);
9+
expect(isProviderDefinedTool(google.tools.urlContext({}))).toBe(true);
10+
});
11+
12+
it('should identify OpenAI provider-defined tools', () => {
13+
expect(isProviderDefinedTool(openai.tools.webSearch({}))).toBe(true);
14+
});
15+
16+
it('should reject null, undefined, and non-objects', () => {
17+
expect(isProviderDefinedTool(null)).toBe(false);
18+
expect(isProviderDefinedTool(undefined)).toBe(false);
19+
expect(isProviderDefinedTool('string')).toBe(false);
20+
expect(isProviderDefinedTool(42)).toBe(false);
21+
});
22+
23+
it('should reject regular tools and plain objects', () => {
24+
expect(isProviderDefinedTool({})).toBe(false);
25+
expect(isProviderDefinedTool({ type: 'function', id: 'test' })).toBe(false);
26+
expect(isProviderDefinedTool({ type: 'provider-defined' })).toBe(false); // missing id
27+
expect(isProviderDefinedTool({ type: 'provider', id: 123 })).toBe(false); // id not string
28+
});
29+
30+
it('should accept both v5 and v6 type markers', () => {
31+
expect(isProviderDefinedTool({ type: 'provider-defined', id: 'google.search' })).toBe(true);
32+
expect(isProviderDefinedTool({ type: 'provider', id: 'openai.web_search' })).toBe(true);
33+
});
34+
});

packages/core/src/tools/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
export * from './tool';
22
export * from './types';
33
export * from './ui-types';
4-
export { isVercelTool } from './toolchecks';
4+
export { isVercelTool, isProviderDefinedTool } from './toolchecks';
55
export { ToolStream } from './stream';
66
export { type ValidationError } from './validation';

packages/core/src/tools/toolchecks.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,19 @@ export function isVercelTool(tool?: ToolToConvert): tool is VercelTool {
2727
('parameters' in tool || ('execute' in tool && typeof tool.execute === 'function' && 'inputSchema' in tool))
2828
);
2929
}
30+
31+
/**
32+
* Checks if a tool is a provider-defined tool from the AI SDK.
33+
* Provider tools (like google.tools.googleSearch(), openai.tools.webSearch()) have:
34+
* - type: "provider-defined" (AI SDK v5) or "provider" (AI SDK v6)
35+
* - id: in format 'provider.tool_name' (e.g., 'google.google_search')
36+
*
37+
* These tools have a lazy `inputSchema` function that returns an AI SDK Schema
38+
* (not a Zod schema), so they require special handling during serialization.
39+
*/
40+
export function isProviderDefinedTool(tool: unknown): boolean {
41+
if (typeof tool !== 'object' || tool === null) return false;
42+
const t = tool as Record<string, unknown>;
43+
const isProviderType = t.type === 'provider-defined' || t.type === 'provider';
44+
return isProviderType && typeof t.id === 'string';
45+
}

packages/server/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@
8686
"author": "",
8787
"license": "Apache-2.0",
8888
"peerDependencies": {
89-
"@mastra/core": ">=1.1.0-0 <2.0.0-0",
89+
"@mastra/core": ">=1.7.1-0 <2.0.0-0",
9090
"zod": "^3.25.0 || ^4.0.0"
9191
},
9292
"devDependencies": {

packages/server/src/server/handlers/tools.test.ts

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { openai } from '@ai-sdk/openai-v5';
12
import { Agent } from '@mastra/core/agent';
23
import { RequestContext } from '@mastra/core/di';
34
import { Mastra } from '@mastra/core/mastra';
@@ -364,4 +365,81 @@ describe('Tools Handlers', () => {
364365
expect(result).toHaveProperty('description', mockTool.description);
365366
});
366367
});
368+
369+
describe('provider-defined tools serialization', () => {
370+
const providerTool = openai.tools.webSearch({});
371+
372+
const providerTools = {
373+
web_search: providerTool,
374+
};
375+
376+
const mixedTools = {
377+
[mockTool.id]: mockTool,
378+
web_search: providerTool,
379+
};
380+
381+
describe('listToolsHandler', () => {
382+
it('should serialize provider-defined tools without crashing', async () => {
383+
const mastra = new Mastra({ logger: false });
384+
const result = await LIST_TOOLS_ROUTE.handler({
385+
...createTestServerContext({ mastra }),
386+
registeredTools: providerTools as any,
387+
});
388+
expect(result).toHaveProperty('web_search');
389+
expect(result['web_search']).toHaveProperty('type', 'provider-defined');
390+
// Verify the lazy inputSchema was actually resolved and serialized
391+
expect(result['web_search'].inputSchema).toBeDefined();
392+
});
393+
394+
it('should serialize a mix of regular and provider-defined tools', async () => {
395+
const mastra = new Mastra({ logger: false });
396+
const result = await LIST_TOOLS_ROUTE.handler({
397+
...createTestServerContext({ mastra }),
398+
registeredTools: mixedTools as any,
399+
});
400+
expect(result).toHaveProperty(mockTool.id);
401+
expect(result).toHaveProperty('web_search');
402+
expect(result[mockTool.id]).toHaveProperty('id', mockTool.id);
403+
expect(result['web_search']).toHaveProperty('type', 'provider-defined');
404+
});
405+
});
406+
407+
describe('getToolByIdHandler', () => {
408+
it('should serialize a provider-defined tool without crashing', async () => {
409+
const mastra = new Mastra({ logger: false });
410+
const result = await GET_TOOL_BY_ID_ROUTE.handler({
411+
...createTestServerContext({ mastra }),
412+
registeredTools: providerTools as any,
413+
toolId: 'openai.web_search',
414+
});
415+
expect(result).toHaveProperty('type', 'provider-defined');
416+
expect(result).toHaveProperty('id', 'openai.web_search');
417+
});
418+
});
419+
420+
describe('getAgentToolHandler', () => {
421+
it('should serialize a provider-defined agent tool without crashing', async () => {
422+
const agent = new Agent({
423+
id: 'provider-tool-agent',
424+
name: 'provider-tool-agent',
425+
instructions: 'You are a search assistant',
426+
tools: providerTools as any,
427+
model: 'openai/gpt-4o-mini' as any,
428+
});
429+
430+
const result = await GET_AGENT_TOOL_ROUTE.handler({
431+
...createTestServerContext({
432+
mastra: new Mastra({
433+
logger: false,
434+
agents: { 'provider-tool-agent': agent as any },
435+
}),
436+
}),
437+
agentId: 'provider-tool-agent',
438+
toolId: 'openai.web_search',
439+
});
440+
expect(result).toHaveProperty('type', 'provider-defined');
441+
expect(result).toHaveProperty('id', 'openai.web_search');
442+
});
443+
});
444+
});
367445
});

packages/server/src/server/handlers/tools.ts

Lines changed: 52 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { isVercelTool } from '@mastra/core/tools';
1+
import { isVercelTool, isProviderDefinedTool } from '@mastra/core/tools';
22
import { zodToJsonSchema } from '@mastra/core/utils/zod-to-json';
33
import { stringify } from 'superjson';
44
import { HTTPException } from '../http-exception';
@@ -18,6 +18,54 @@ import { getAgentFromSystem } from './agents';
1818
import { handleError } from './error';
1919
import { validateBody } from './utils';
2020

21+
/**
22+
* Resolves a schema value that may be a lazy function (as used by AI SDK provider tools).
23+
* Provider tools use lazy schemas: `inputSchema` is a function that returns an AI SDK Schema
24+
* object with `{ jsonSchema, validate, _type }`.
25+
*/
26+
function resolveSchema(schema: unknown): unknown {
27+
if (typeof schema === 'function') {
28+
try {
29+
return schema();
30+
} catch {
31+
return undefined;
32+
}
33+
}
34+
return schema;
35+
}
36+
37+
/**
38+
* Serializes a tool for API responses, handling both regular tools (with Zod schemas)
39+
* and provider-defined tools (with AI SDK lazy schemas).
40+
*/
41+
function serializeTool(tool: any): any {
42+
// Provider-defined tools (e.g. google.tools.googleSearch(), openai.tools.webSearch())
43+
// have lazy inputSchema functions that return AI SDK Schema objects, not Zod schemas.
44+
// We resolve them and use the jsonSchema property directly.
45+
if (isProviderDefinedTool(tool)) {
46+
const resolvedInput = resolveSchema(tool.inputSchema);
47+
const resolvedOutput = resolveSchema(tool.outputSchema);
48+
return {
49+
...tool,
50+
inputSchema:
51+
resolvedInput && typeof resolvedInput === 'object' && 'jsonSchema' in resolvedInput
52+
? stringify(resolvedInput.jsonSchema)
53+
: undefined,
54+
outputSchema:
55+
resolvedOutput && typeof resolvedOutput === 'object' && 'jsonSchema' in resolvedOutput
56+
? stringify(resolvedOutput.jsonSchema)
57+
: undefined,
58+
};
59+
}
60+
61+
return {
62+
...tool,
63+
inputSchema: tool.inputSchema ? stringify(zodToJsonSchema(tool.inputSchema)) : undefined,
64+
outputSchema: tool.outputSchema ? stringify(zodToJsonSchema(tool.outputSchema)) : undefined,
65+
requestContextSchema: tool.requestContextSchema ? stringify(zodToJsonSchema(tool.requestContextSchema)) : undefined,
66+
};
67+
}
68+
2169
// ============================================================================
2270
// Route Definitions (new pattern - handlers defined inline with createRoute)
2371
// ============================================================================
@@ -38,17 +86,7 @@ export const LIST_TOOLS_ROUTE = createRoute({
3886

3987
const serializedTools = Object.entries(allTools).reduce(
4088
(acc, [id, _tool]) => {
41-
// Cast to any since we're serializing to a generic Record<string, any>
42-
// and the tool types have varying property availability
43-
const tool = _tool as any;
44-
acc[id] = {
45-
...tool,
46-
inputSchema: tool.inputSchema ? stringify(zodToJsonSchema(tool.inputSchema)) : undefined,
47-
outputSchema: tool.outputSchema ? stringify(zodToJsonSchema(tool.outputSchema)) : undefined,
48-
requestContextSchema: tool.requestContextSchema
49-
? stringify(zodToJsonSchema(tool.requestContextSchema))
50-
: undefined,
51-
};
89+
acc[id] = serializeTool(_tool);
5290
return acc;
5391
},
5492
{} as Record<string, any>,
@@ -86,16 +124,7 @@ export const GET_TOOL_BY_ID_ROUTE = createRoute({
86124
throw new HTTPException(404, { message: 'Tool not found' });
87125
}
88126

89-
const serializedTool = {
90-
...tool,
91-
inputSchema: tool.inputSchema ? stringify(zodToJsonSchema(tool.inputSchema)) : undefined,
92-
outputSchema: tool.outputSchema ? stringify(zodToJsonSchema(tool.outputSchema)) : undefined,
93-
requestContextSchema: tool.requestContextSchema
94-
? stringify(zodToJsonSchema(tool.requestContextSchema))
95-
: undefined,
96-
};
97-
98-
return serializedTool;
127+
return serializeTool(tool);
99128
} catch (error) {
100129
return handleError(error, 'Error getting tool');
101130
}
@@ -196,16 +225,7 @@ export const GET_AGENT_TOOL_ROUTE = createRoute({
196225
throw new HTTPException(404, { message: 'Tool not found' });
197226
}
198227

199-
const serializedTool = {
200-
...tool,
201-
inputSchema: tool.inputSchema ? stringify(zodToJsonSchema(tool.inputSchema)) : undefined,
202-
outputSchema: tool.outputSchema ? stringify(zodToJsonSchema(tool.outputSchema)) : undefined,
203-
requestContextSchema: tool.requestContextSchema
204-
? stringify(zodToJsonSchema(tool.requestContextSchema))
205-
: undefined,
206-
};
207-
208-
return serializedTool;
228+
return serializeTool(tool);
209229
} catch (error) {
210230
return handleError(error, 'Error getting agent tool');
211231
}

0 commit comments

Comments
 (0)