MCP tool calls: list/dict arguments sent as JSON-encoded strings instead of native arrays/objects

Resolved 💬 3 comments Opened May 14, 2026 by Hani-AMJ Closed May 18, 2026

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.0 on Windows 11 (Claude Code CLI 2.1.138).
  • @anthropic-ai/claude-agent-sdk 0.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 no mcp-remote in the path when run inside Claude Desktop — meaning the stringification is upstream of mcp-remote.

Reproduction

  1. 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
``

  1. Run via Claude Code / Claude Desktop with arguments = {"items": ["a","b"], "obj": {"k":"v"}}.
  2. The server's call-tool handler observes items as python_type=str with value '["a","b"]' (i.e. a JSON-encoded string) — NOT python_type=list. Likewise for obj.

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.

View original on GitHub ↗

This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗