diff --git a/src/core/tools/UseMcpToolTool.ts b/src/core/tools/UseMcpToolTool.ts index 7cbc09bfd7..cc1ee7a96b 100644 --- a/src/core/tools/UseMcpToolTool.ts +++ b/src/core/tools/UseMcpToolTool.ts @@ -111,7 +111,14 @@ export class UseMcpToolTool extends BaseTool<"use_mcp_tool"> { return { isValid: false } } - // Native-only: arguments are already a structured object. + // Some LLMs emit arguments as JSON-encoded strings rather than objects. + // Parse them early so the type check below sees the unwrapped object. + if (typeof params.arguments === "string") { + try { + params.arguments = JSON.parse(params.arguments) + } catch {} + } + let parsedArguments: Record | undefined if (params.arguments !== undefined) { if (typeof params.arguments !== "object" || params.arguments === null || Array.isArray(params.arguments)) { diff --git a/src/core/tools/__tests__/useMcpToolTool.spec.ts b/src/core/tools/__tests__/useMcpToolTool.spec.ts index 5ee826774f..1ec6f548f9 100644 --- a/src/core/tools/__tests__/useMcpToolTool.spec.ts +++ b/src/core/tools/__tests__/useMcpToolTool.spec.ts @@ -254,6 +254,53 @@ describe("useMcpToolTool", () => { expect(mockPushToolResult).toHaveBeenCalledWith("Tool result: Tool executed successfully") }) + it("should parse JSON-string arguments and pass parsed object to callTool", async () => { + const callToolMock = vi.fn().mockResolvedValue({ + content: [{ type: "text", text: "Browser session started" }], + isError: false, + }) + + mockProviderRef.deref.mockReturnValue({ + getMcpHub: () => ({ + callTool: callToolMock, + getAllServers: vi.fn().mockReturnValue([ + { name: "test_server", tools: [{ name: "test_tool", description: "Test Tool" }] }, + ]), + }), + postMessageToWebview: vi.fn(), + }) + + const block: ToolUse = { + type: "tool_use", + name: "use_mcp_tool", + params: { + server_name: "test_server", + tool_name: "test_tool", + arguments: '{"headless": true}', + }, + nativeArgs: { + server_name: "test_server", + tool_name: "test_tool", + arguments: '{"headless": true}' as unknown as Record, + }, + partial: false, + } + + mockAskApproval.mockResolvedValue(true) + + await useMcpToolTool.handle(mockTask as Task, block as any, { + askApproval: mockAskApproval, + handleError: mockHandleError, + pushToolResult: mockPushToolResult, + }) + + expect(mockTask.consecutiveMistakeCount).toBe(0) + expect(mockTask.recordToolError).not.toHaveBeenCalled() + expect(callToolMock).toHaveBeenCalledWith("test_server", "test_tool", { headless: true }) + expect(mockTask.say).toHaveBeenCalledWith("mcp_server_request_started") + expect(mockTask.say).toHaveBeenCalledWith("mcp_server_response", "Browser session started", []) + }) + it("should handle user rejection", async () => { const block: ToolUse = { type: "tool_use",