Skip to content

Commit a43ada2

Browse files
committed
fix: handle inputSchema for MCP tools in schema sanitizer
MCP tools from @ai-sdk/mcp use inputSchema with a jsonSchema getter, not parameters like regular AI SDK tools. The sanitizer now handles both cases to properly strip unsupported properties for OpenAI.
1 parent 75aa643 commit a43ada2

File tree

2 files changed

+105
-2
lines changed

2 files changed

+105
-2
lines changed

src/common/utils/tools/schemaSanitizer.test.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,15 @@ function getParams(tool: Tool): any {
99
return (tool as any).parameters;
1010
}
1111

12+
// Test helper to access tool inputSchema (MCP tools)
13+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
14+
function getInputSchema(tool: Tool): any {
15+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
16+
const inputSchema = (tool as any).inputSchema;
17+
// inputSchema has a jsonSchema getter
18+
return inputSchema?.jsonSchema;
19+
}
20+
1221
describe("schemaSanitizer", () => {
1322
describe("sanitizeToolSchemaForOpenAI", () => {
1423
it("should strip minLength from string properties", () => {
@@ -163,6 +172,65 @@ describe("schemaSanitizer", () => {
163172
// Original should still have minLength
164173
expect(params.properties.content.minLength).toBe(1);
165174
});
175+
176+
it("should sanitize MCP tools with inputSchema", () => {
177+
// MCP tools use inputSchema with a jsonSchema getter instead of parameters
178+
const jsonSchema = {
179+
type: "object",
180+
properties: {
181+
content: { type: "string", minLength: 1, maxLength: 100 },
182+
count: { type: "number", minimum: 0, maximum: 10 },
183+
},
184+
required: ["content"],
185+
};
186+
187+
const mcpTool = {
188+
type: "dynamic",
189+
description: "MCP test tool",
190+
inputSchema: {
191+
// Simulate the jsonSchema getter that @ai-sdk/mcp creates
192+
get jsonSchema() {
193+
return jsonSchema;
194+
},
195+
},
196+
execute: () => Promise.resolve({}),
197+
} as unknown as Tool;
198+
199+
const sanitized = sanitizeToolSchemaForOpenAI(mcpTool);
200+
const schema = getInputSchema(sanitized);
201+
202+
// Unsupported properties should be stripped
203+
expect(schema.properties.content).toEqual({ type: "string" });
204+
expect(schema.properties.count).toEqual({ type: "number" });
205+
// Supported properties should be preserved
206+
expect(schema.type).toBe("object");
207+
expect(schema.required).toEqual(["content"]);
208+
});
209+
210+
it("should not mutate the original MCP tool inputSchema", () => {
211+
const jsonSchema = {
212+
type: "object",
213+
properties: {
214+
content: { type: "string", minLength: 1 },
215+
},
216+
};
217+
218+
const mcpTool = {
219+
type: "dynamic",
220+
description: "MCP test tool",
221+
inputSchema: {
222+
get jsonSchema() {
223+
return jsonSchema;
224+
},
225+
},
226+
execute: () => Promise.resolve({}),
227+
} as unknown as Tool;
228+
229+
sanitizeToolSchemaForOpenAI(mcpTool);
230+
231+
// Original should still have minLength
232+
expect(jsonSchema.properties.content.minLength).toBe(1);
233+
});
166234
});
167235

168236
describe("sanitizeMCPToolsForOpenAI", () => {

src/common/utils/tools/schemaSanitizer.ts

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -104,15 +104,50 @@ function stripUnsupportedProperties(schema: unknown): void {
104104
* This function creates a new tool with sanitized parameters that strips
105105
* unsupported schema properties like minLength, maximum, default, etc.
106106
*
107+
* Tools can have schemas in two places:
108+
* - `parameters`: Used by tools created with ai SDK's `tool()` function
109+
* - `inputSchema`: Used by MCP tools created with `dynamicTool()` from @ai-sdk/mcp
110+
*
107111
* @param tool - The original tool to sanitize
108112
* @returns A new tool with sanitized parameter schema
109113
*/
110114
export function sanitizeToolSchemaForOpenAI(tool: Tool): Tool {
111-
// Access tool internals - the AI SDK tool structure has parameters
115+
// Access tool internals - the AI SDK tool structure varies:
116+
// - Regular tools have `parameters` (Zod schema)
117+
// - MCP/dynamic tools have `inputSchema` (JSON Schema wrapper with getter)
112118
// eslint-disable-next-line @typescript-eslint/no-explicit-any
113119
const toolRecord = tool as any as Record<string, unknown>;
114120

115-
// If no parameters, return as-is
121+
// Check for inputSchema first (MCP tools use this)
122+
// The inputSchema is a wrapper object with a jsonSchema getter
123+
if (toolRecord.inputSchema && typeof toolRecord.inputSchema === "object") {
124+
const inputSchemaWrapper = toolRecord.inputSchema as Record<string, unknown>;
125+
126+
// Get the actual JSON Schema - it's exposed via a getter
127+
const rawJsonSchema = inputSchemaWrapper.jsonSchema;
128+
if (rawJsonSchema && typeof rawJsonSchema === "object") {
129+
// Deep clone and sanitize
130+
const clonedSchema = JSON.parse(JSON.stringify(rawJsonSchema)) as Record<string, unknown>;
131+
stripUnsupportedProperties(clonedSchema);
132+
133+
// Create a new inputSchema wrapper that returns our sanitized schema
134+
const sanitizedInputSchema = {
135+
...inputSchemaWrapper,
136+
// Override the jsonSchema getter with our sanitized version
137+
get jsonSchema() {
138+
return clonedSchema;
139+
},
140+
};
141+
142+
return {
143+
...tool,
144+
inputSchema: sanitizedInputSchema,
145+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
146+
} as any as Tool;
147+
}
148+
}
149+
150+
// Fall back to parameters (regular AI SDK tools)
116151
if (!toolRecord.parameters) {
117152
return tool;
118153
}

0 commit comments

Comments
 (0)