From 002a8f1f6de2fcfc917165d828501d3047bad61f Mon Sep 17 00:00:00 2001 From: Kagura Date: Sun, 19 Apr 2026 06:44:25 +0800 Subject: [PATCH] fix(mcp): sync required array with properties in tool schemas (#754) * fix(mcp): sync required array with properties in tool schemas MCP servers can emit schemas where the required array contains keys not present in properties. This causes API 400 errors: "Extra required key 'X' supplied." - Add sanitizeSchemaRequired() to filter required arrays - Apply it to MCP tool inputJSONSchema before sending to API - Also fix filterSwarmFieldsFromSchema to update required after removing properties Fixes #525 * test: add MCP schema required sanitization test --- src/utils/api.test.ts | 25 +++++++++++++++++++++ src/utils/api.ts | 51 ++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 75 insertions(+), 1 deletion(-) diff --git a/src/utils/api.test.ts b/src/utils/api.test.ts index 721746b0..52f8ed4c 100644 --- a/src/utils/api.test.ts +++ b/src/utils/api.test.ts @@ -78,3 +78,28 @@ test('toolToAPISchema keeps skill required for SkillTool', async () => { required: ['skill'], }) }) + +test('toolToAPISchema removes extra required keys not in properties (MCP schema sanitization)', async () => { + const schema = await toolToAPISchema( + { + name: 'mcp__test__create_object', + inputSchema: z.strictObject({}), + inputJSONSchema: { + type: 'object', + properties: { + name: { type: 'string' }, + }, + required: ['name', 'attributes'], + }, + prompt: async () => 'Create an object', + } as unknown as Tool, + { + getToolPermissionContext: async () => getEmptyToolPermissionContext(), + tools: [] as unknown as Tools, + agents: [], + }, + ) + + const inputSchema = (schema as { input_schema: { required?: string[] } }).input_schema + expect(inputSchema.required).toEqual(['name']) +}) diff --git a/src/utils/api.ts b/src/utils/api.ts index 9b66fd79..616b1544 100644 --- a/src/utils/api.ts +++ b/src/utils/api.ts @@ -111,11 +111,60 @@ function filterSwarmFieldsFromSchema( delete filteredProps[field] } filtered.properties = filteredProps + + // Keep `required` in sync after removing properties + if (Array.isArray(filtered.required)) { + filtered.required = filtered.required.filter( + (key: string) => key in filteredProps, + ) + } } return filtered } +/** + * Ensure `required` only lists keys present in `properties`. + * MCP servers may emit schemas where these are out of sync, causing + * API 400 errors ("Extra required key supplied"). + * Recurses into nested object schemas. + */ +function sanitizeSchemaRequired( + schema: Anthropic.Tool.InputSchema, +): Anthropic.Tool.InputSchema { + if (!schema || typeof schema !== 'object') { + return schema + } + + const result = { ...schema } + const props = result.properties as Record | undefined + + if (props && Array.isArray(result.required)) { + result.required = result.required.filter( + (key: string) => key in props, + ) + } + + // Recurse into nested object properties + if (props) { + const sanitizedProps = { ...props } + for (const [key, value] of Object.entries(sanitizedProps)) { + if ( + value && + typeof value === 'object' && + (value as Record).type === 'object' + ) { + sanitizedProps[key] = sanitizeSchemaRequired( + value as Anthropic.Tool.InputSchema, + ) + } + } + result.properties = sanitizedProps + } + + return result +} + export async function toolToAPISchema( tool: Tool, options: { @@ -156,7 +205,7 @@ export async function toolToAPISchema( // Use tool's JSON schema directly if provided, otherwise convert Zod schema let input_schema = ( 'inputJSONSchema' in tool && tool.inputJSONSchema - ? tool.inputJSONSchema + ? sanitizeSchemaRequired(tool.inputJSONSchema as Anthropic.Tool.InputSchema) : zodToJsonSchema(tool.inputSchema) ) as Anthropic.Tool.InputSchema