[BUG] MCP tool parameters with type: number are sent as strings to MCP servers
Environment
- Claude Code version: 2.1.71
- OS: macOS (Darwin 25.3.0)
- MCP Server: @mobilenext/mobile-mcp v0.0.46 (MCP SDK v1.26.0)
Description
When Claude Code calls an MCP tool that declares parameters with "type": "number" in its JSON Schema, the values are sent as strings in the JSON-RPC call. MCP servers using strict Zod validation (z.number()) rightfully reject these with "expected number, received string".
Root Cause Analysis
The issue is in the XML-to-JSON-RPC pipeline:
- MCP server exposes
tools/listwith JSON Schema declaring"type": "number"for parameters - Claude Code presents the schema to the Claude model
- Claude model generates tool calls in XML format:
<parameter name="x">330</parameter> - Claude Code parses the XML and sends JSON-RPC to the MCP server
The problem is at step 4. Everything in XML is inherently a string — there's no type distinction. Claude Code needs to use the JSON Schema from step 1 to coerce parameter values to their declared types before sending the JSON-RPC request. Currently, it passes raw strings through without coercion.
Reproduction
- Configure
@mobilenext/mobile-mcpin.mcp.json:
{
"mobile-mcp": {
"type": "stdio",
"command": "npx",
"args": ["-y", "@mobilenext/mobile-mcp@latest"]
}
}
- Call
mobile_click_on_screen_at_coordinateswith any numeric x/y values
- Expected: Parameters sent as JSON numbers (
{"x": 330, "y": 200}) - Actual: Parameters sent as JSON strings (
{"x": "330", "y": "200"})
Error from MCP Server
MCP error -32602: Input validation error: Invalid arguments for tool mobile_click_on_screen_at_coordinates: [
{
"expected": "number",
"code": "invalid_type",
"path": ["x"],
"message": "Invalid input: expected number, received string"
},
{
"expected": "number",
"code": "invalid_type",
"path": ["y"],
"message": "Invalid input: expected number, received string"
}
]
MCP Server Schema
The server uses Zod (z.number()) which converts to JSON Schema {"type": "number"} via zodToJsonSchema. The schema is correct — the server properly declares these as numbers.
// From mobile-mcp server.ts
x: z.number().describe("The x coordinate to click on the screen, in pixels"),
y: z.number().describe("The y coordinate to click on the screen, in pixels"),
Suggested Fix
When constructing the JSON-RPC arguments object for an MCP tool call, Claude Code should walk the tool's JSON Schema and coerce values to their declared types:
"type": "number"→parseFloat(value)"type": "integer"→parseInt(value, 10)"type": "boolean"→value === "true""type": "string"→ no change (default)"type": "array"/"type": "object"→JSON.parse(value)
This is similar to how the existing $ref serialization bug (#18260) manifests, but affects plain primitive types.
Impact
This breaks any MCP tool that uses z.number(), z.boolean(), or z.integer() for parameters, which is extremely common. Affected tools include coordinate-based interactions (click, swipe, long press), pagination parameters, numeric configuration values, etc.
Workaround
MCP server authors can use z.coerce.number() instead of z.number() to accept both strings and numbers, but this shouldn't be necessary — the client should honor the schema.
11 Comments
+1, also just hit this.
we build MCP servers (mcp-server-macos-use) and hit this exact problem. our click/scroll tools take numeric x, y, w, h coordinates and we had to switch everything to z.coerce.number() as the workaround. it works but feels wrong since the schema clearly declares these as numbers.
the suggested fix in the issue is spot on. the XML-to-JSON coercion should be a pretty small change - walk the schema, cast values to their declared types before sending the JSON-RPC request. arrays and objects via JSON.parse would cover the nested cases too.
one additional edge case to watch: enum types where the enum values are numbers (like error codes). those also come through as strings and z.enum() doesn't have a coerce variant.
our MCP server where we use the z.coerce workaround for numeric params (click coordinates, scroll amounts): https://github.com/mediar-ai/mcp-server-macos-use/blob/main/Sources/MCPServer/main.swift
Additional reproduction: GitHub MCP server in Cowork (Claude Desktop)
Adding another affected use case. We hit this consistently in Cowork mode (Claude Desktop app) when calling the built-in GitHub MCP tools — specifically
add_issue_comment,update_issue, andlist_issues.Reproduction
add_issue_commentwithissue_number: 72(a JSON number)Affected tools (GitHub MCP)
add_issue_comment—issue_numberparameterupdate_issue—issue_numberparameterlist_issues—labelsarray serialization (related but distinct: array elements become a single comma-joined string instead of an array)Workaround
Calling
ToolSearch("select:mcp__github__add_issue_comment")at the start of each session to force-load the JSON schema seems to reduce (but not eliminate) the frequency of this bug. When it still fails, we fall back to manual copy-paste of the intended API call.Environment
Notes
This is the same root cause as the OP's analysis — the XML-to-JSON-RPC pipeline drops type information. The suggested fix (schema-driven coercion at step 4) would resolve all of these cases. This bug makes it impossible to reliably automate GitHub workflows from Cowork without human fallback for every MCP call that takes a numeric parameter.
+1 for prioritizing this fix — it affects every MCP server with numeric parameters, which is nearly all of them.
thanks for the additional reproduction. the Cowork mode angle is important since that's probably where most people will hit this. the z.coerce workaround we use on our MCP server handles the string-to-number conversion at the schema level, so the server never sees the wrong type. not ideal that every server needs to do this, but it works reliably until the client-side fix lands.
+1 with another reproduction. One observation that may help triage — possibly extends the prior note about
ToolSearch-at-session-start reducing frequency:The bug appears tied to per-tool schema cache state at session start, not universal across all
boolean/numberparams. In one MCP server, two tools registered together in the same/mcpreconnect cycle, both declaring abooleanparam:Refresh behavior:
/mcpreconnect within the same Claude Code session: does NOT fix it.booleanparam now sent as boolean.Suggests schemas are cached per tool name at session start; the cache survives
/mcpreconnect; modified tools keep the stale cached schema (without the new param) while newly-registered tools get a fresh fetch. Consistent with the prior observation thatToolSearchat session start reduces frequency — that workaround forces a fresh schema fetch, bypassing whatever stale cache state otherwise governs serialization.Bug is in the Claude Code harness, not the MCP SDK. Verified via an in-process round-trip test using
@modelcontextprotocol/sdkClient+InMemoryTransport: handlers receive correctly-typed values fortype: "boolean",type: "number", andtype: "array"fields when the SDK delivers them directly. Matches OP's root cause — information loss is in the XML→JSON-RPC pipeline above the SDK.Environment: Claude Code 2.1.126,
@modelcontextprotocol/sdk^1.29.0, macOS 26.3, Node 25.9.0.Happy to share the round-trip test code or add body-level logging if it would help.
Affected user here — Claude Code's bot just auto-flagged my issue #60963 as a duplicate of this one, and the bot is correct. The root-cause analysis here (XML→JSON-RPC pipeline ignoring schema
typefor value coercion) matches what I observed independently in a WordPress MCP-server context. Closing mine as duplicate; consolidating here.Two additions that might help bump this out of
stale:M13 two-tool discriminator (verification procedure)
Useful as both a current-bug repro AND a regression catcher if a fix lands:
value: { type: "string" }and a control tool with notypedeclaration on the comparable param.value: 42(orvalue: [1, 2, 3]) to both."42"(or"[1,2,3]") on the MCP server side → current silent-coercion behavior reproduces.The discriminator distinguishes "schema declaration has any effect" from "schema declaration is purely cosmetic" — important because tool authors can otherwise be misled by the catalog view showing the declared type while the wire behavior is unchanged.
Related AI Engine (server-side) issue
I filed a parallel item with Meow Apps for AI Engine's
normalize_input_schema, which silently strips all union-arraytypedeclarations (e.g.type: ["string","number","boolean"]), not just the documented["string","object"]/["string","array"]patterns that "break ChatGPT."Combined with the client-side coercion this issue tracks, downstream tool authors get a compound silent-failure surface: declare a scalar-only union → AI Engine strips it → Claude Code sees untyped → coerces non-string inputs to JSON-string. Tool author thinks they've enforced a type; in reality nothing changed.
Operational cost (mine)
5 rounds of release cycles on a downstream plugin (
ws404-thegem-mcpv0.4.6 → v0.4.9, ~3 weeks of intermittent work) trying to "fix" via schema-side type declarations that source inspection said should work. Only an empirical wire-behavior test (the discriminator above) settled it. The audit-checklist convention library for my plugin family now codifies the discriminator as a required pre-ship verification step (M13) specifically to prevent future authors from going down the same path.+1 on this issue. Happy to share the full diagnostic trail if it helps triage.
Quick follow-up on my comment above with new empirical evidence from a controlled wire-dump test against AI Engine's MCP server.
My earlier comment described a union-strip pattern I attributed to AI Engine's
normalize_input_schema. Jordy Meow (AI Engine maintainer) replied with a direct unit invocation against the released code showing that scalar-only unions actually preserve through the normalizer — only object/array unions get stripped per a documented "breaks ChatGPT" branch. He asked for a wire dump to localize where the strip I observed actually happens.I ran the test. Three properties on a passive probe MCP tool:
scalar_only_union:type: ["string","number","boolean","null"]object_array_union:type: ["string","array"](control — server-side strip is documented)single_string:type: "string"(control — should always preserve)POST to the MCP server's
tools/listJSON-RPC endpoint directly via curl, extracted the probe'sinputSchema.propertiesfrom the raw response:The server preserves the scalar-only union exactly as declared. It strips the object/array union (as documented and expected). It preserves the single-string control.
But the catalog view Claude Code surfaces to the model — what I was inspecting in prior diagnostic rounds via
ToolSearch— has notypefield on the scalar-only union. The strip happens between "AI Engine sent the wire response" and "the Claude Code catalog rendered the schema for the LLM."This localizes the union-mangling to the client side, alongside the numeric/array coercion this issue tracks. They're likely the same root cause: the JSON-RPC handling pipeline doesn't fully resolve
typewhen it's an array. For primitives that means losing type info on the way to the server (the original report). For union types in the schema that means dropping the type field entirely from the catalog view.Reproduction is straightforward if useful: register an MCP tool with
inputSchema.properties.X.type: ["string","number","boolean","null"], fetchtools/listvia curl directly (bypassing the client), and compare the wire response against what Claude Code surfaces viaToolSearch. Server response will have the union intact; client catalog view will not.Happy to share the full wire-dump JSON if it would help triage.
Related data point from Cowork. The same wrong-type serialization shows up for deferred tools — tools surfaced by name with their JSON Schema fetched on demand rather than pre-loaded. Because the schema isn't in context when the model emits the first call, there's no declared type to coerce against, and typed array/object parameters go out as JSON strings (
'["a","b"]') or otherwise mis-shaped. The intended load-then-retry handshake (fetch schema → call) doesn't reliably prevent that malformed first call.Note this spans deferred built-in tools (e.g.
TaskCreate/TaskUpdate) as well as deferred MCP tools, so it may be a distinct subsystem (on-demand schema loading) rather than strictly this issue — flagging in case you'd prefer to split it out. Either way: the deferred-schema path needs to block dispatch until the schema loads, or coerce-on-retry after load.Environment: Claude for Desktop (Cowork tab), Claude Code 2.1.156, MCP over HTTP/SSE, macOS.
Closing for now — inactive for too long. Please open a new issue if this is still relevant.
Closing the loop on my May 22 comment: I re-tested both halves of this today on Claude Code v2.1.193 (macOS) and can no longer reproduce either.
Method: a scratch stdio MCP server exposing one tool whose
inputSchemadeclarestype: number,integer,boolean, a typedarray, and two union type arrays (["string","number"],["string","null"]). A headlessclaude -psession was asked to (a) report the schema it sees and (b) call the tool; the server logged the rawtools/callline off the wire.Results:
Whatever fixed this between May and now — thank you. No successor issue needed from my side; leaving this here for anyone else who lands on the stale-close.