MCP tool calls: list/dict arguments sent as JSON-encoded strings instead of native arrays/objects
Summary
When Claude Code (or Claude Desktop, which bundles the same @anthropic-ai/claude-agent-sdk) calls an MCP tool whose schema declares an array or object parameter, the argument is sent on the wire as a JSON-encoded string instead of a native JSON array/object. This violates the MCP specification (which declares CallToolRequestParams.arguments as Record<string, unknown>, native types allowed) and breaks any MCP server that uses standard JSON-schema validation against its declared inputSchema.
Environment
- Claude Desktop
1.7196.0on Windows 11 (Claude Code CLI2.1.138). @anthropic-ai/claude-agent-sdk0.2.138(bundled).- Transport: stdio →
mcp-remote@0.1.37/0.1.38→ Streamable HTTP to remote MCP server. - Remote server: FastMCP
3.2.4(Python). Verified the same wire shape reaches a custom Python stdio MCP probe with nomcp-remotein the path when run inside Claude Desktop — meaning the stringification is upstream ofmcp-remote.
Reproduction
- Build a minimal MCP server exposing one tool:
``python``
@server.call_tool()
async def echo(name, arguments):
return [TextContent(type="text", text=json.dumps({
k: {"python_type": type(v).__name__, "value": v} for k, v in arguments.items()
}))]
# with inputSchema declaring items as type=array, obj as type=object
- Run via Claude Code / Claude Desktop with
arguments = {"items": ["a","b"], "obj": {"k":"v"}}. - The server's call-tool handler observes
itemsaspython_type=strwith value'["a","b"]'(i.e. a JSON-encoded string) — NOTpython_type=list. Likewise forobj.
Expected
The server should observe items: list, obj: dict — native types per the MCP spec (Record<string, unknown>).
Control (proves the bug is in claude-agent-sdk, not mcp-remote or the MCP spec)
Running the same probe via mcp-inspector CLI (the official @modelcontextprotocol/inspector test harness):
npx -y @modelcontextprotocol/inspector --cli py probe_server.py \
--method tools/call --tool-name echo \
--tool-arg 'items=["one","two","three"]'
produces a wire frame with arguments.items as a native JSON array:
{"method":"tools/call","params":{"name":"echo","arguments":{"items":["one","two","three"]}}, ...}
and the server's handler observes items: list. So the MCP spec and Inspector are correct; the Inspector is a known-spec-compliant control.
Source-code inspection of mcp-remote@0.1.37/0.1.38 (chunk-65X3S4HB.js lines 20330+, mcpProxy) confirms mcp-remote passes messages through transportToServer.send(message) unchanged — the only mutation is clientInfo.name on the initialize handshake. The JSON.stringify(message) at lines 19536 / 19842 is envelope-level serialization, not per-arg coercion.
Source-code inspection of Claude Desktop's directMcpHost.js (asar-extracted from Claude_1.7196.0.0 at .vite/build/mcp-runtime/directMcpHost.js lines 15759-15777) confirms directMcpHost is also a relay: const { name, arguments: args } = request.params; await remoteClient.callTool({ name, arguments: args ?? {} }, ...); — args pass through unchanged.
So the stringification happens UPSTREAM of directMcpHost, in the path from the Anthropic API's tool_use.input JSON object → the CallToolRequest{params.arguments} that directMcpHost receives. That conversion lives inside @anthropic-ai/claude-agent-sdk.
Why this matters
Servers that strictly validate against their declared MCP inputSchema (which the spec encourages) reject the entire tool call. The error class is "validation error: <stringified-list-value> is not of type 'array'" — surfaced at the SDK's auto-generated input validator before the tool's user code runs.
Workarounds that other MCP servers may need to deploy:
- Add a coerce layer that JSON-parses string-valued arguments when the schema expects array/object.
- Loosen schemas to
oneOf: [array, string](defeats the validation guarantee).
Both are user-server burdens that wouldn't exist if the client sent native types per the spec.
Suggested fix
Wherever the SDK converts tool_use.input (a JSON object delivered by the Anthropic API as a structured payload) into the arguments field of an MCP CallToolRequest, pass the native object through unchanged. Likely a stray JSON.stringify on container-valued properties was added (possibly to handle string-coerced primitives) and ended up applying to lists/dicts as well.
Filing this from an internal MCP-server investigation. Happy to share the probe repro on request.
This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗