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
This commit is contained in:
@@ -78,3 +78,28 @@ test('toolToAPISchema keeps skill required for SkillTool', async () => {
|
|||||||
required: ['skill'],
|
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'])
|
||||||
|
})
|
||||||
|
|||||||
@@ -111,11 +111,60 @@ function filterSwarmFieldsFromSchema(
|
|||||||
delete filteredProps[field]
|
delete filteredProps[field]
|
||||||
}
|
}
|
||||||
filtered.properties = filteredProps
|
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
|
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<string, unknown> | 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<string, unknown>).type === 'object'
|
||||||
|
) {
|
||||||
|
sanitizedProps[key] = sanitizeSchemaRequired(
|
||||||
|
value as Anthropic.Tool.InputSchema,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result.properties = sanitizedProps
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
export async function toolToAPISchema(
|
export async function toolToAPISchema(
|
||||||
tool: Tool,
|
tool: Tool,
|
||||||
options: {
|
options: {
|
||||||
@@ -156,7 +205,7 @@ export async function toolToAPISchema(
|
|||||||
// Use tool's JSON schema directly if provided, otherwise convert Zod schema
|
// Use tool's JSON schema directly if provided, otherwise convert Zod schema
|
||||||
let input_schema = (
|
let input_schema = (
|
||||||
'inputJSONSchema' in tool && tool.inputJSONSchema
|
'inputJSONSchema' in tool && tool.inputJSONSchema
|
||||||
? tool.inputJSONSchema
|
? sanitizeSchemaRequired(tool.inputJSONSchema as Anthropic.Tool.InputSchema)
|
||||||
: zodToJsonSchema(tool.inputSchema)
|
: zodToJsonSchema(tool.inputSchema)
|
||||||
) as Anthropic.Tool.InputSchema
|
) as Anthropic.Tool.InputSchema
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user