Skip to content

fix: parse JSON-string MCP tool arguments before type check#255

Merged
edelauna merged 2 commits into
Zoo-Code-Org:mainfrom
pajitosingh:fix/mcp-json-string-arguments
Jun 6, 2026
Merged

fix: parse JSON-string MCP tool arguments before type check#255
edelauna merged 2 commits into
Zoo-Code-Org:mainfrom
pajitosingh:fix/mcp-json-string-arguments

Conversation

@pajitosingh

@pajitosingh pajitosingh commented May 22, 2026

Copy link
Copy Markdown
Contributor

Related GitHub Issue

Port of the fix for Roo Code issue #10919 — the same JSON-string arguments bug affects Zoo Code when used with DeepSeek V4 Pro and other LLMs that emit MCP tool call arguments as JSON-encoded strings.

Description

Problem

Some LLMs (DeepSeek V4 Pro and others) emit MCP tool call arguments as JSON-encoded strings rather than as native objects:

{ "arguments": "{\"headless\": true}" }

instead of:

{ "arguments": { "headless": true } }

The existing validateParams() method in UseMcpToolTool.ts rejects these with "Invalid JSON argument" errors because the type check (typeof params.arguments !== "object") fails on strings.

This is the same bug that plagued Roo Code v3.54.0 (see Roo-Code issues #10919 and the archive discussion).

Fix

Adds a JSON.parse() guard before the existing type check. If arguments arrives as a string, attempt to parse it into an object. If parsing fails (malformed JSON), we fall through silently and the existing code path handles it as before (rejects with the appropriate error).

Safety

  • The guard is safe for all input types:
    • Object arguments (existing correct format): typeof check passes the string guard, goes straight to the object type check → no change
    • String arguments (the bug case): JSON.parse() unwraps the string into an object → type check passes → fix
    • Malformed strings: JSON.parse() throws, catch {} swallows it → falls through to the existing error path → graceful rejection
    • null / undefined / array: Not strings → string guard skipped → existing error path → unchanged

Test Procedure

  1. Unit test: Run npx vitest run src/core/tools/__tests__/useMcpToolTool.spec.ts

    • The new test "should parse JSON-string arguments and pass parsed object to callTool" passes nativeArgs.arguments as the JSON-encoded string '{"headless": true}' and verifies callTool receives the parsed object { headless: true }
    • All existing tests continue to pass unchanged
  2. Manual integration test (DeepSeek V4 Pro or similar model):

    • Configure an MCP server (e.g., playwright-stealth)
    • Ask the model to call an MCP tool (e.g., browser_navigate to a URL)
    • Verify no "Invalid JSON argument" errors appear
    • The tool should execute successfully on the first attempt without retries
  3. Regression check: Existing tool calls with object-format arguments (the normal case) must continue to work without any behavioral change.

Pre-Submission Checklist

  • Issue Linked: This PR addresses the same bug pattern as Roo Code #10919, ported to Zoo Code
  • Scope: My changes are focused on the linked issue (one major feature/fix per PR)
  • Self-Review: I have performed a thorough self-review of my code
  • Testing: New tests have been added to cover the JSON-string argument parsing path
  • Documentation Impact: No documentation updates required
  • Contribution Guidelines: I have read and agree to the Contributor Guidelines

Documentation Updates

  • No documentation updates are required.

Additional Notes

This fix has been field-tested on Roo Code v3.54.0 with the identical fix across multiple MCP providers (playwright-stealth, Context7, etc.). The root cause is model behavior, not server-specific — any MCP server can be affected when the LLM emits string-encoded arguments.

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).
@coderabbitai

coderabbitai Bot commented May 22, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The PR makes UseMcpToolTool.validateParams accept params.arguments as a JSON-encoded string by attempting to parse it before existing type checks; a test verifies the parsed object is forwarded to the MCP hub's callTool.

Changes

Arguments JSON Parsing

Layer / File(s) Summary
JSON string argument deserialization
src/core/tools/UseMcpToolTool.ts
If params.arguments is a string, the code attempts JSON.parse in a try/catch, uses the parsed object on success (or leaves the string on failure), then proceeds with the existing object/null/array validation.
Test: parsed JSON-string arguments forwarded to callTool
src/core/tools/__tests__/useMcpToolTool.spec.ts
Adds a test that sends JSON-string params.arguments, mocks MCP discovery and callTool, and asserts the parsed { headless: true } object is passed to callTool and the expected say calls occur.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 I nibble at strings wrapped tight,
Unwrap JSON in morning light.
Parse with care, then onward hop—
Arguments tidy, ready to stop.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: parsing JSON-string MCP tool arguments before the type check.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description check ✅ Passed The PR description is comprehensive, well-structured, and closely follows the template with all key sections completed.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@edelauna edelauna left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for your contribution! I had one comment around increasing test coverage, but this looks great!

try {
params.arguments = JSON.parse(params.arguments)
} catch {}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we add a test in useMcpToolTool.spec.ts that passes nativeArgs.arguments as a JSON-encoded string (e.g. '{"headless": true}' as unknown as Record<string, unknown>) and verifies callTool receives the parsed object? This is the primary behavior change and currently has no regression coverage.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@edelauna Done! Added the test in useMcpToolTool.spec.ts — the new test "should parse JSON-string arguments and pass parsed object to callTool" passes nativeArgs.arguments as the JSON string '{"headless": true}' and verifies via callToolMock that callTool receives the parsed object { headless: true }, not the raw string.

The test has been pushed to this PR branch (commit c92ab3f). Ready for re-review.

@codecov

codecov Bot commented May 23, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 80.00000% with 1 line in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/core/tools/UseMcpToolTool.ts 80.00% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

@pajitosingh

Copy link
Copy Markdown
Contributor Author

Was this fixed in main branch? Just switched over from Roo to Zoo today.
Sorry if this is the wrong place to ask, I'm new to all this but trying ot help where I can.

@edelauna

edelauna commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

Was this fixed in main branch? Just switched over from Roo to Zoo today. Sorry if this is the wrong place to ask, I'm new to all this but trying ot help where I can.

See: #255 (review)

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 Zoo-Code-Org#255, ensuring
the primary behavior change has regression coverage.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
src/core/tools/__tests__/useMcpToolTool.spec.ts (1)

257-302: ⚡ Quick win

Consider adding a test case for malformed JSON strings.

The new test comprehensively covers the happy path (valid JSON string → parsed object → forwarded to callTool). To ensure complete coverage of the JSON-string parsing logic, consider adding a test case that verifies malformed JSON strings (e.g., '{"invalid}', 'not json') are properly rejected.

According to the implementation in context snippet 1, if JSON.parse fails, the catch is silent and the string remains a string, which then fails the typeof params.arguments !== "object" check. An explicit test would confirm this error path works correctly with the new parsing logic.

🧪 Suggested test case for malformed JSON
it("should reject malformed JSON-string arguments", async () => {
	const callToolMock = vi.fn()
	
	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: '{"invalid json}',
		},
		nativeArgs: {
			server_name: "test_server",
			tool_name: "test_tool",
			arguments: '{"invalid json}' as unknown as Record<string, unknown>,
		},
		partial: false,
	}
	
	await useMcpToolTool.handle(mockTask as Task, block as any, {
		askApproval: mockAskApproval,
		handleError: mockHandleError,
		pushToolResult: mockPushToolResult,
	})
	
	expect(mockTask.consecutiveMistakeCount).toBe(1)
	expect(mockTask.recordToolError).toHaveBeenCalledWith("use_mcp_tool")
	expect(mockTask.say).toHaveBeenCalledWith("error", expect.stringContaining("invalid JSON argument"))
	expect(callToolMock).not.toHaveBeenCalled()
})

Based on learnings: Use package-local unit tests for pure logic, parsing, state transitions, validation, serialization, request construction, retry decisions, and error handling.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/core/tools/__tests__/useMcpToolTool.spec.ts` around lines 257 - 302, Add
a unit test that verifies malformed JSON strings in ToolUse.params.arguments are
rejected by useMcpToolTool.handle: create a block where params.arguments is an
invalid JSON string (e.g. '{"invalid json}'), mock providerRef.deref to return a
hub with a callTool spy, call useMcpToolTool.handle with the usual helpers, and
assert that mockTask.consecutiveMistakeCount increments to 1,
mockTask.recordToolError was called with "use_mcp_tool", mockTask.say was called
with "error" and a message containing "invalid JSON argument", and callTool was
NOT invoked; this mirrors the existing happy-path test but asserts the error
path for malformed JSON parsing.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/core/tools/__tests__/useMcpToolTool.spec.ts`:
- Around line 257-302: Add a unit test that verifies malformed JSON strings in
ToolUse.params.arguments are rejected by useMcpToolTool.handle: create a block
where params.arguments is an invalid JSON string (e.g. '{"invalid json}'), mock
providerRef.deref to return a hub with a callTool spy, call
useMcpToolTool.handle with the usual helpers, and assert that
mockTask.consecutiveMistakeCount increments to 1, mockTask.recordToolError was
called with "use_mcp_tool", mockTask.say was called with "error" and a message
containing "invalid JSON argument", and callTool was NOT invoked; this mirrors
the existing happy-path test but asserts the error path for malformed JSON
parsing.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: b8c2ffa6-a66a-470f-8c9e-329ed2bf674b

📥 Commits

Reviewing files that changed from the base of the PR and between 9f9c65f and c92ab3f.

📒 Files selected for processing (1)
  • src/core/tools/__tests__/useMcpToolTool.spec.ts

@edelauna edelauna left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for the contribution and adding in the test.

@edelauna edelauna enabled auto-merge June 6, 2026 13:19
@edelauna edelauna added this pull request to the merge queue Jun 6, 2026
Merged via the queue into Zoo-Code-Org:main with commit 98c629f Jun 6, 2026
10 checks passed
edelauna pushed a commit that referenced this pull request Jun 17, 2026
* 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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants