import { type ToolName, toolNames, type FileEntry } from "@roo-code/types" import { type ToolUse, type ToolParamName, toolParamNames, type NativeToolArgs } from "../../shared/tools" /** * Helper type to extract properly typed native arguments for a given tool. * Returns the type from NativeToolArgs if the tool is defined there, otherwise never. */ type NativeArgsFor = TName extends keyof NativeToolArgs ? NativeToolArgs[TName] : never /** * Parser for native tool calls (OpenAI-style function calling). * Converts native tool call format to ToolUse format for compatibility * with existing tool execution infrastructure. * * For tools with refactored parsers (e.g., read_file), this parser provides * typed arguments via nativeArgs. Tool-specific handlers should consume * nativeArgs directly rather than relying on synthesized legacy params. */ export class NativeToolCallParser { /** * Convert a native tool call chunk to a ToolUse object. * * @param toolCall - The native tool call from the API stream * @returns A properly typed ToolUse object */ public static parseToolCall(toolCall: { id: string name: TName arguments: string }): ToolUse | null { // Check if this is a dynamic MCP tool (mcp_serverName_toolName) if (typeof toolCall.name === "string" && toolCall.name.startsWith("mcp_")) { return this.parseDynamicMcpTool(toolCall) as ToolUse | null } // Validate tool name if (!toolNames.includes(toolCall.name as ToolName)) { console.error(`Invalid tool name: ${toolCall.name}`) console.error(`Valid tool names:`, toolNames) return null } try { // Parse the arguments JSON string const args = JSON.parse(toolCall.arguments) // Build legacy params object for backward compatibility with XML protocol and UI. // Native execution path uses nativeArgs instead, which has proper typing. const params: Partial> = {} for (const [key, value] of Object.entries(args)) { // Skip complex parameters that have been migrated to nativeArgs. // For read_file, the 'files' parameter is a FileEntry[] array that can't be // meaningfully stringified. The properly typed data is in nativeArgs instead. if (toolCall.name === "read_file" && key === "files") { continue } // Validate parameter name if (!toolParamNames.includes(key as ToolParamName)) { console.warn(`Unknown parameter '${key}' for tool '${toolCall.name}'`) console.warn(`Valid param names:`, toolParamNames) continue } // Convert to string for legacy params format const stringValue = typeof value === "string" ? value : JSON.stringify(value) params[key as ToolParamName] = stringValue } // Build typed nativeArgs for tools that support it. // This switch statement serves two purposes: // 1. Validation: Ensures required parameters are present before constructing nativeArgs // 2. Transformation: Converts raw JSON to properly typed structures // // Each case validates the minimum required parameters and constructs a properly typed // nativeArgs object. If validation fails, nativeArgs remains undefined and the tool // will fall back to legacy parameter parsing if supported. let nativeArgs: NativeArgsFor | undefined = undefined switch (toolCall.name) { case "read_file": if (args.files && Array.isArray(args.files)) { nativeArgs = { files: args.files } as NativeArgsFor } break case "attempt_completion": if (args.result) { nativeArgs = { result: args.result } as NativeArgsFor } break case "execute_command": if (args.command) { nativeArgs = { command: args.command, cwd: args.cwd, } as NativeArgsFor } break case "insert_content": if (args.path !== undefined && args.line !== undefined && args.content !== undefined) { nativeArgs = { path: args.path, line: typeof args.line === "number" ? args.line : parseInt(String(args.line), 10), content: args.content, } as NativeArgsFor } break case "apply_diff": if (args.path !== undefined && args.diff !== undefined) { nativeArgs = { path: args.path, diff: args.diff, } as NativeArgsFor } break case "ask_followup_question": if (args.question !== undefined && args.follow_up !== undefined) { nativeArgs = { question: args.question, follow_up: args.follow_up, } as NativeArgsFor } break case "browser_action": if (args.action !== undefined) { nativeArgs = { action: args.action, url: args.url, coordinate: args.coordinate, size: args.size, text: args.text, } as NativeArgsFor } break case "codebase_search": if (args.query !== undefined) { nativeArgs = { query: args.query, path: args.path, } as NativeArgsFor } break case "fetch_instructions": if (args.task !== undefined) { nativeArgs = { task: args.task, } as NativeArgsFor } break case "generate_image": if (args.prompt !== undefined && args.path !== undefined) { nativeArgs = { prompt: args.prompt, path: args.path, image: args.image, } as NativeArgsFor } break case "list_code_definition_names": if (args.path !== undefined) { nativeArgs = { path: args.path, } as NativeArgsFor } break case "run_slash_command": if (args.command !== undefined) { nativeArgs = { command: args.command, args: args.args, } as NativeArgsFor } break case "search_files": if (args.path !== undefined && args.regex !== undefined) { nativeArgs = { path: args.path, regex: args.regex, file_pattern: args.file_pattern, } as NativeArgsFor } break case "switch_mode": if (args.mode_slug !== undefined && args.reason !== undefined) { nativeArgs = { mode_slug: args.mode_slug, reason: args.reason, } as NativeArgsFor } break case "update_todo_list": if (args.todos !== undefined) { nativeArgs = { todos: args.todos, } as NativeArgsFor } break case "write_to_file": if (args.path !== undefined && args.content !== undefined && args.line_count !== undefined) { nativeArgs = { path: args.path, content: args.content, line_count: typeof args.line_count === "number" ? args.line_count : parseInt(String(args.line_count), 10), } as NativeArgsFor } break case "use_mcp_tool": if (args.server_name !== undefined && args.tool_name !== undefined) { nativeArgs = { server_name: args.server_name, tool_name: args.tool_name, arguments: args.arguments, } as NativeArgsFor } break default: break } const result: ToolUse = { type: "tool_use" as const, name: toolCall.name, params, partial: false, // Native tool calls are always complete when yielded nativeArgs, } return result } catch (error) { console.error(`Failed to parse tool call arguments:`, error) console.error(`Error details:`, error instanceof Error ? error.message : String(error)) return null } } /** * Parse dynamic MCP tools (named mcp_serverName_toolName). * These are generated dynamically by getMcpServerTools() and need to be * converted back to use_mcp_tool format. */ private static parseDynamicMcpTool(toolCall: { id: string name: string arguments: string }): ToolUse<"use_mcp_tool"> | null { try { const args = JSON.parse(toolCall.arguments) // Extract server_name and tool_name from the arguments // The dynamic tool schema includes these as const properties const serverName = args.server_name const toolName = args.tool_name const toolInputProps = args.toolInputProps if (!serverName || !toolName) { console.error(`Missing server_name or tool_name in dynamic MCP tool`) return null } // Build params for backward compatibility with XML protocol const params: Partial> = { server_name: serverName, tool_name: toolName, } if (toolInputProps) { params.arguments = JSON.stringify(toolInputProps) } // Build nativeArgs with properly typed structure const nativeArgs: NativeToolArgs["use_mcp_tool"] = { server_name: serverName, tool_name: toolName, arguments: toolInputProps, } const result: ToolUse<"use_mcp_tool"> = { type: "tool_use" as const, name: "use_mcp_tool", params, partial: false, nativeArgs, } return result } catch (error) { console.error(`Failed to parse dynamic MCP tool:`, error) return null } } }