import { ChatCompletionFunctionTool } from "openai/resources.js"; import type { ToolPlugin, ToolResult, ToolContext } from "../tools/types.js"; import type { McpTool, McpServerStatus } from "../types/index.js"; import { processToolResult } from "./toolResultStorage.js"; import { DEFAULT_MAX_RESULT_SIZE_CHARS } from "../constants/toolLimits.js"; /** * Recursively clean schema to remove unsupported fields */ function cleanSchema(schema: unknown): unknown { if (typeof schema !== "object" || schema === null) { return schema; } if (Array.isArray(schema)) { return schema.map(cleanSchema); } const newSchema: Record = {}; const obj = schema as Record; for (const key in obj) { // Remove $schema as OpenAI API doesn't accept it // Remove exclusiveMinimum/exclusiveMaximum as some models (e.g. Gemini) don't support them if ( key === "$schema" || key === "exclusiveMinimum" || key === "exclusiveMaximum" ) { continue; } // Handle type as an array (e.g. ["string", "null"]) if (key === "type" && Array.isArray(obj[key])) { const types = obj[key] as string[]; const nonNullTypes = types.filter((t) => t !== "null"); if (nonNullTypes.length > 0) { newSchema["type"] = nonNullTypes[0]; } if (types.includes("null")) { newSchema["nullable"] = true; } continue; } newSchema[key] = cleanSchema(obj[key]); } return newSchema; } /** * Convert MCP tool to OpenAI function tool format */ export function mcpToolToOpenAITool( mcpTool: McpTool, serverName: string, ): ChatCompletionFunctionTool { const cleanInputSchema = cleanSchema(mcpTool.inputSchema) as Record< string, unknown >; const prefixedName = `mcp__${serverName}__${mcpTool.name}`; return { type: "function", function: { name: prefixedName, description: `${mcpTool.description || `Tool from MCP server ${serverName}`} (MCP: ${serverName})`, parameters: cleanInputSchema, }, }; } /** * Create a tool plugin wrapper for an MCP tool */ export function createMcpToolPlugin( mcpTool: McpTool, serverName: string, executeTool: ( name: string, args: Record, context?: ToolContext, ) => Promise<{ success: boolean; content: string; serverName?: string; images?: Array<{ data: string; mediaType?: string }>; }>, ): ToolPlugin { const prefixedName = `mcp__${serverName}__${mcpTool.name}`; return { name: prefixedName, config: mcpToolToOpenAITool(mcpTool, serverName), async execute( args: Record, context?: ToolContext, ): Promise { try { const result = await executeTool(prefixedName, args, context); // Process content for size limits — only text content, not images const processedContent = processToolResult( result.content || `Executed ${mcpTool.name}`, DEFAULT_MAX_RESULT_SIZE_CHARS, `mcp_${serverName}_${mcpTool.name}`, ); return { success: true, content: processedContent, images: result.images, }; } catch (error) { return { success: false, content: "", error: error instanceof Error ? error.message : String(error), }; } }, }; } /** * Find which server a tool belongs to */ export function findToolServer( toolName: string, servers: McpServerStatus[], ): McpServerStatus | undefined { return servers.find( (s) => (s.status === "connected" || s.status === "reconnecting") && s.tools?.some((t) => t.name === toolName), ); }