[Bug] MCP tool-call arguments coerced to JSON-string on the wire regardless of schema type
Summary
The Claude Code MCP client appears to coerce non-string scalar inputs (numbers, booleans, arrays, objects) to their JSON-string representation before sending tool-call payloads to the MCP server, regardless of the tool's declared input schema type. This makes schema-side type declarations cosmetic for serialization purposes — they may appear in the catalog view via ToolSearch but don't affect wire behavior.
Reproduction
Tested against AI Engine Pro 3.4.8 (a WordPress MCP server) on 2026-05-10. Two tools registered on the same MCP server:
- Tool A:
set_context_settingswithvalue: { type: "string" } - Tool B:
set_theme_optionswithvalue: { /* no type declared */ }
Test inputs:
- Send
value: 42(number literal) to both tools. - Send
value: ["a", "b"](array literal) to both tools.
Observed
Both tools' server-side handlers received string values in every case:
42→ arrived as"42"in PHP, regardless of schema.["a","b"]→ arrived as"[\"a\",\"b\"]"(JSON-encoded string).
The schema-declared type: "string" on Tool A had no effect — same coercion happened on the untyped Tool B.
Expected
Either:
- A. Validate-and-reject: Tool A receives
value: 42(number) → client rejects the call as schema-violating (42 is not a string), surfacing the schema mismatch to the caller LLM as a clear error. - B. Pass-through: Tool A receives
value: 42(number) → client forwards42as a JSON-number; server's untyped/lenient handlers see actual numbers, typed handlers can return a clear error from their own validation.
The current silent coercion to string-of-JSON is the worst of both worlds: schema appears to enforce a type but doesn't, and the server-side handler can't distinguish "caller passed a number" from "caller passed a string-shaped number."
Why this matters
I spent 5 rounds of release cycles on a downstream plugin (ws404-thegem-mcp v0.4.6 → v0.4.9, ~3 weeks of audit work) trying to leverage schema-side type declarations to enforce input refusal on a server-side check. The fixes always shipped looking correct (catalog view via ToolSearch showed the type field) but had no behavioral effect. Empirical verification only came from a two-tool discriminator test (described below) that should have been the first step.
Discriminator (proposed regression test for the harness)
If Claude Code's MCP client adopts validate-and-reject behavior in a future version, downstream consumers should run this test to detect the change:
- Pick a tool with
value: { type: "string" }and a control tool with notypedeclaration on the comparable param. - Send
value: 42to both. - If both arrive as
"42"(string) → current silent-coercion behavior. - If typed tool errors and control accepts
42→ validate-reject behavior.
Related upstream issue
There's an adjacent issue on the AI Engine (server) side: normalize_input_schema appears to silently strip ALL union-array type declarations (e.g., type: ["string","number","boolean"]), not just the documented ["string","object"] / ["string","array"] patterns that "break ChatGPT." Already filed with Meow Apps; this report is specifically about the client-side coercion behavior.
Environment
- Claude Code (current as of 2026-05-20)
- AI Engine Pro 3.4.8 (target MCP server)
- WordPress 6.x on PHP 8.1
- Single-admin connection over the MCP transport
This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗