From 9f9c65f8b9977e25dcd733c2b383a8e25108125a Mon Sep 17 00:00:00 2001 From: Pajito_S Date: Fri, 22 May 2026 18:48:03 +0530 Subject: [PATCH 1/2] fix: parse JSON-string MCP tool arguments before type check Some LLMs (DeepSeek V4 Pro, others) emit MCP tool call arguments as JSON-encoded strings (e.g. '{"headless": true}') rather than as native objects. This causes validateParams() to reject valid MCP tool calls with 'Invalid JSON argument' errors. The fix adds a JSON.parse() guard before the existing type check, falling through silently if parsing fails. The existing code path then handles it as before (either accepts the object or rejects malformed input). This matches the fix applied to Roo Code v3.54.0 which was field- tested across multiple MCP providers (playwright-stealth etc). --- src/core/tools/UseMcpToolTool.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) 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)) { From c92ab3f1793b09047f9c1e5639c7d145bff7ae32 Mon Sep 17 00:00:00 2001 From: Pajito_S Date: Fri, 5 Jun 2026 18:27:32 +0530 Subject: [PATCH 2/2] test: add coverage for JSON-string MCP tool argument parsing Add a test that passes nativeArgs.arguments as a JSON-encoded string (e.g. '{"headless": true}') and verifies that callTool receives the parsed object rather than the raw string. This addresses the review feedback from @edelauna on PR #255, ensuring the primary behavior change has regression coverage. --- .../tools/__tests__/useMcpToolTool.spec.ts | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) 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",