fix: MCP tool schema transformation for OpenAI/Azure providers

Fixes schema compatibility issues where MCP tools fail with OpenAI/Azure
providers due to missing required parameter transformations.

Changes:
- Move transformation logic from session to MCP.tools() for better architecture
- Add transformJsonSchema() to handle simple object schemas with optional→nullable conversion
- Transform AI SDK's parameters.jsonSchema structure for OpenAI/Azure compatibility
- Add graceful fallback for complex schemas (oneOf, anyOf, etc.)
- Preserve error handling with fallback to original tools

This ensures MCP tools like context7 and clojure-mcp work seamlessly with
OpenAI/Azure providers while maintaining compatibility with other providers.
This commit is contained in:
Rafał Krzyważnia 2025-07-05 14:46:18 +02:00
parent ea6bfef21a
commit 7dcac56114
No known key found for this signature in database
GPG key ID: 056C31DF20A2DF32
3 changed files with 97 additions and 5 deletions

View file

@ -98,13 +98,45 @@ export namespace MCP {
return state().then((state) => state.clients)
}
export async function tools() {
export async function tools(providerID?: string) {
const result: Record<string, Tool> = {}
for (const [clientName, client] of Object.entries(await clients())) {
for (const [toolName, tool] of Object.entries(await client.tools())) {
result[clientName + "_" + toolName] = tool
const clientEntries = Object.entries(await clients())
for (const [clientName, client] of clientEntries) {
try {
const clientTools = await client.tools()
for (const [toolName, tool] of Object.entries(clientTools)) {
const toolKey = clientName + "_" + toolName
if (providerID) {
const transformedTool = await transformToolForProvider(tool, providerID)
result[toolKey] = transformedTool
} else {
result[toolKey] = tool
}
}
} catch (error: any) {
log.error('Failed to get tools from MCP client', { clientName, error: error.message })
}
}
return result
}
async function transformToolForProvider(tool: any, providerID: string): Promise<any> {
if (!['openai', 'azure'].includes(providerID)) return tool
try {
const { Provider } = await import("../provider/provider")
return Provider.transformMCPToolForProvider(tool, providerID)
} catch (error: any) {
log.warn('Could not transform MCP tool schema, using as-is', {
toolId: tool.id || 'unknown',
providerID,
error: error.message
})
return tool
}
}
}

View file

@ -545,6 +545,66 @@ export namespace Provider {
return schema
}
function transformJsonSchema(schema: any): any {
// Only handle simple object schemas
if (!schema || schema.type !== 'object' || !schema.properties) {
return schema // Return as-is for anything we don't understand
}
// If it has oneOf, anyOf, allOf, or other complex stuff - bail out
if (schema.oneOf || schema.anyOf || schema.allOf) {
log.warn('Complex JSON schema detected, skipping transformation', { schema })
return schema
}
// Simple transformation: all properties become required + nullable
const required = Object.keys(schema.properties)
const properties: Record<string, any> = {}
for (const [key, prop] of Object.entries(schema.properties)) {
if (!schema.required?.includes(key)) {
// Make it nullable
properties[key] = {
anyOf: [prop, { type: 'null' }]
}
} else {
properties[key] = prop
}
}
return {
...schema,
required,
properties
}
}
export function transformMCPToolForProvider(tool: any, providerID: string): any {
if (!['openai', 'azure'].includes(providerID)) return tool
try {
// AI SDK converts MCP tools to: { parameters: { jsonSchema: <original_inputSchema> } }
if (tool.parameters?.jsonSchema) {
const transformedSchema = transformJsonSchema(tool.parameters.jsonSchema)
return {
...tool,
parameters: {
...tool.parameters,
jsonSchema: transformedSchema
}
}
}
return tool
} catch (error: any) {
log.warn('Could not transform MCP tool schema, using as-is', {
toolId: tool.id || 'unknown',
error: error.message
})
return tool
}
}
export const ModelNotFoundError = NamedError.create(
"ProviderModelNotFoundError",
z.object({

View file

@ -549,7 +549,7 @@ export namespace Session {
})
}
for (const [key, item] of Object.entries(await MCP.tools())) {
for (const [key, item] of Object.entries(await MCP.tools(input.providerID))) {
const execute = item.execute
if (!execute) continue
item.execute = async (args, opts) => {