Skip to content

Commit 98c629f

Browse files
authored
fix: parse JSON-string MCP tool arguments before type check (#255)
* 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). * 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.
1 parent 79faecd commit 98c629f

2 files changed

Lines changed: 55 additions & 1 deletion

File tree

src/core/tools/UseMcpToolTool.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,14 @@ export class UseMcpToolTool extends BaseTool<"use_mcp_tool"> {
111111
return { isValid: false }
112112
}
113113

114-
// Native-only: arguments are already a structured object.
114+
// Some LLMs emit arguments as JSON-encoded strings rather than objects.
115+
// Parse them early so the type check below sees the unwrapped object.
116+
if (typeof params.arguments === "string") {
117+
try {
118+
params.arguments = JSON.parse(params.arguments)
119+
} catch {}
120+
}
121+
115122
let parsedArguments: Record<string, unknown> | undefined
116123
if (params.arguments !== undefined) {
117124
if (typeof params.arguments !== "object" || params.arguments === null || Array.isArray(params.arguments)) {

src/core/tools/__tests__/useMcpToolTool.spec.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,53 @@ describe("useMcpToolTool", () => {
254254
expect(mockPushToolResult).toHaveBeenCalledWith("Tool result: Tool executed successfully")
255255
})
256256

257+
it("should parse JSON-string arguments and pass parsed object to callTool", async () => {
258+
const callToolMock = vi.fn().mockResolvedValue({
259+
content: [{ type: "text", text: "Browser session started" }],
260+
isError: false,
261+
})
262+
263+
mockProviderRef.deref.mockReturnValue({
264+
getMcpHub: () => ({
265+
callTool: callToolMock,
266+
getAllServers: vi.fn().mockReturnValue([
267+
{ name: "test_server", tools: [{ name: "test_tool", description: "Test Tool" }] },
268+
]),
269+
}),
270+
postMessageToWebview: vi.fn(),
271+
})
272+
273+
const block: ToolUse = {
274+
type: "tool_use",
275+
name: "use_mcp_tool",
276+
params: {
277+
server_name: "test_server",
278+
tool_name: "test_tool",
279+
arguments: '{"headless": true}',
280+
},
281+
nativeArgs: {
282+
server_name: "test_server",
283+
tool_name: "test_tool",
284+
arguments: '{"headless": true}' as unknown as Record<string, unknown>,
285+
},
286+
partial: false,
287+
}
288+
289+
mockAskApproval.mockResolvedValue(true)
290+
291+
await useMcpToolTool.handle(mockTask as Task, block as any, {
292+
askApproval: mockAskApproval,
293+
handleError: mockHandleError,
294+
pushToolResult: mockPushToolResult,
295+
})
296+
297+
expect(mockTask.consecutiveMistakeCount).toBe(0)
298+
expect(mockTask.recordToolError).not.toHaveBeenCalled()
299+
expect(callToolMock).toHaveBeenCalledWith("test_server", "test_tool", { headless: true })
300+
expect(mockTask.say).toHaveBeenCalledWith("mcp_server_request_started")
301+
expect(mockTask.say).toHaveBeenCalledWith("mcp_server_response", "Browser session started", [])
302+
})
303+
257304
it("should handle user rejection", async () => {
258305
const block: ToolUse = {
259306
type: "tool_use",

0 commit comments

Comments
 (0)