This commit is contained in:
Copilot 2025-12-18 20:17:00 +08:00 committed by GitHub
commit f1d1e8c737
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 135 additions and 7 deletions

View File

@ -0,0 +1,128 @@
import { describe, expect, it } from 'vitest'
import { mcpToolsToOpenAIChatTools, mcpToolsToOpenAIResponseTools } from '../mcp-tools'
import type { MCPTool } from '@renderer/types'
describe('MCP Tools - HTTP MCP Compatibility Fix', () => {
const mockMCPTool: MCPTool = {
id: 'test_server-search_tool',
name: 'search_tool',
description: 'Search for information',
inputSchema: {
type: 'object',
properties: {
query: {
type: 'string',
description: 'Search query'
},
limit: {
type: 'number',
description: 'Optional limit',
minimum: 1
}
},
required: ['query'] // Only query is required, limit is optional
},
serverId: 'test-server-id',
serverName: 'test-server',
type: 'mcp'
}
describe('mcpToolsToOpenAIResponseTools', () => {
it('should preserve original schema without forcing all properties to be required', () => {
const tools = mcpToolsToOpenAIResponseTools([mockMCPTool])
expect(tools).toHaveLength(1)
const tool = tools[0]
// Should use the tool id
expect(tool.name).toBe('test_server-search_tool')
// Should preserve the original required array (only 'query')
expect(tool.parameters.required).toEqual(['query'])
// Should have both properties
expect(tool.parameters.properties).toHaveProperty('query')
expect(tool.parameters.properties).toHaveProperty('limit')
})
it('should not include strict: true', () => {
const tools = mcpToolsToOpenAIResponseTools([mockMCPTool])
expect(tools).toHaveLength(1)
const tool = tools[0] as any
// strict property should not be present
expect(tool.strict).toBeUndefined()
})
})
describe('mcpToolsToOpenAIChatTools', () => {
it('should preserve original schema without forcing all properties to be required', () => {
const tools = mcpToolsToOpenAIChatTools([mockMCPTool])
expect(tools).toHaveLength(1)
const tool = tools[0]
// Should use the tool id
expect(tool.function.name).toBe('test_server-search_tool')
// Should include description
expect(tool.function.description).toBe('Search for information')
// Should preserve the original required array (only 'query')
expect(tool.function.parameters.required).toEqual(['query'])
// Should have both properties
expect(tool.function.parameters.properties).toHaveProperty('query')
expect(tool.function.parameters.properties).toHaveProperty('limit')
})
it('should not include strict: true in function parameters', () => {
const tools = mcpToolsToOpenAIChatTools([mockMCPTool])
expect(tools).toHaveLength(1)
const tool = tools[0] as any
// strict property should not be present
expect(tool.function.strict).toBeUndefined()
})
})
describe('HTTP MCP with complex nested schemas', () => {
it('should handle nested objects with optional fields', () => {
const complexTool: MCPTool = {
id: 'http_server-complex_tool',
name: 'complex_tool',
description: 'Complex tool with nested schemas',
inputSchema: {
type: 'object',
properties: {
required_field: { type: 'string' },
optional_object: {
type: 'object',
properties: {
nested_required: { type: 'string' },
nested_optional: { type: 'number' }
},
required: ['nested_required']
}
},
required: ['required_field']
},
serverId: 'http-server-id',
serverName: 'http-server',
type: 'mcp'
}
const tools = mcpToolsToOpenAIChatTools([complexTool])
const parameters = tools[0].function.parameters
// Top level should only require 'required_field'
expect(parameters.required).toEqual(['required_field'])
// Nested object should only require 'nested_required'
expect(parameters.properties.optional_object.required).toEqual(['nested_required'])
})
})
})

View File

@ -32,13 +32,14 @@ import { nanoid } from 'nanoid'
import { isToolUseModeFunction } from './assistant' import { isToolUseModeFunction } from './assistant'
import { convertBase64ImageToAwsBedrockFormat } from './aws-bedrock-utils' import { convertBase64ImageToAwsBedrockFormat } from './aws-bedrock-utils'
import { filterProperties, processSchemaForO3 } from './mcp-schema' import { filterProperties } from './mcp-schema'
const logger = loggerService.withContext('Utils:MCPTools') const logger = loggerService.withContext('Utils:MCPTools')
export function mcpToolsToOpenAIResponseTools(mcpTools: MCPTool[]): OpenAI.Responses.Tool[] { export function mcpToolsToOpenAIResponseTools(mcpTools: MCPTool[]): OpenAI.Responses.Tool[] {
return mcpTools.map((tool) => { return mcpTools.map((tool) => {
const parameters = processSchemaForO3(tool.inputSchema) // Use the original schema without strict mode processing to maintain compatibility with all MCP types
const parameters = tool.inputSchema
return { return {
type: 'function', type: 'function',
@ -46,15 +47,15 @@ export function mcpToolsToOpenAIResponseTools(mcpTools: MCPTool[]): OpenAI.Respo
parameters: { parameters: {
type: 'object' as const, type: 'object' as const,
...parameters ...parameters
}, }
strict: true
} satisfies OpenAI.Responses.Tool } satisfies OpenAI.Responses.Tool
}) })
} }
export function mcpToolsToOpenAIChatTools(mcpTools: MCPTool[]): Array<ChatCompletionTool> { export function mcpToolsToOpenAIChatTools(mcpTools: MCPTool[]): Array<ChatCompletionTool> {
return mcpTools.map((tool) => { return mcpTools.map((tool) => {
const parameters = processSchemaForO3(tool.inputSchema) // Use the original schema without strict mode processing to maintain compatibility with all MCP types
const parameters = tool.inputSchema
return { return {
type: 'function', type: 'function',
@ -64,8 +65,7 @@ export function mcpToolsToOpenAIChatTools(mcpTools: MCPTool[]): Array<ChatComple
parameters: { parameters: {
type: 'object' as const, type: 'object' as const,
...parameters ...parameters
}, }
strict: true
} }
} as ChatCompletionTool } as ChatCompletionTool
}) })