[BUG] MCP tool parameters with type: number are sent as strings to MCP servers

Status Closed — not planned
Reported on v2.1.71
Maintainer reply None cached
Activity 11 comments · opened Mar 9, 2026 · closed Jul 3, 2026

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:

  1. MCP server exposes tools/list with JSON Schema declaring "type": "number" for parameters
  2. Claude Code presents the schema to the Claude model
  3. Claude model generates tool calls in XML format: <parameter name="x">330</parameter>
  4. 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

  1. Configure @mobilenext/mobile-mcp in .mcp.json:
{
  "mobile-mcp": {
    "type": "stdio",
    "command": "npx",
    "args": ["-y", "@mobilenext/mobile-mcp@latest"]
  }
}
  1. Call mobile_click_on_screen_at_coordinates with any numeric x/y values
  1. Expected: Parameters sent as JSON numbers ({"x": 330, "y": 200})
  2. 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.

View original on GitHub ↗

11 Comments

tnull · 5 months ago

+1, also just hit this.

m13v · 5 months ago

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.

m13v · 5 months ago

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

Rennding · 4 months ago

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, and list_issues.

Reproduction

  1. Open Cowork session with GitHub MCP connected
  2. Call add_issue_comment with issue_number: 72 (a JSON number)
  3. Server receives it as a string and rejects:
MCP error -32602: Input validation error: Invalid arguments for tool add_issue_comment: [
  {
    "code": "invalid_type",
    "expected": "number",
    "received": "string",
    "path": ["issue_number"],
    "message": "Expected number, received string"
  }
]

Affected tools (GitHub MCP)

  • add_issue_commentissue_number parameter
  • update_issueissue_number parameter
  • list_issueslabels array serialization (related but distinct: array elements become a single comma-joined string instead of an array)
  • Any GitHub MCP tool with a numeric parameter

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

  • Interface: Cowork (Claude Desktop)
  • Model: Claude Opus 4.6
  • GitHub MCP: built-in connector
  • OS: Windows
  • Frequency: ~30-50% of sessions, persistent once it starts in a given session

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.

m13v · 4 months ago

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.

madisonrickert · 4 months ago

+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/number params. In one MCP server, two tools registered together in the same /mcp reconnect cycle, both declaring a boolean param:

  • A pre-existing tool whose schema was modified in the current build to add the boolean param → request body sent the param as a string, upstream API rejected with type-mismatch error
  • A newly registered tool with the same boolean param → request did not 400 (consistent with either correct boolean handling or the param being dropped — can't distinguish without body-level logging)

Refresh behavior:

  • /mcp reconnect within the same Claude Code session: does NOT fix it.
  • Full Claude Code restart: does fix it. New session, modified tool's boolean param now sent as boolean.

Suggests schemas are cached per tool name at session start; the cache survives /mcp reconnect; modified tools keep the stale cached schema (without the new param) while newly-registered tools get a fresh fetch. Consistent with the prior observation that ToolSearch at 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/sdk Client + InMemoryTransport: handlers receive correctly-typed values for type: "boolean", type: "number", and type: "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.

WebSearching404 · 3 months ago

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 type for 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:

  1. Pick a tool with value: { type: "string" } and a control tool with no type declaration on the comparable param.
  2. Send value: 42 (or value: [1, 2, 3]) to both.
  3. If both arrive as "42" (or "[1,2,3]") on the MCP server side → current silent-coercion behavior reproduces.
  4. If typed tool errors (or rejects) and control accepts the raw number/array → validate-and-reject behavior has landed.

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-array type declarations (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-mcp v0.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.

WebSearching404 · 3 months ago

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/list JSON-RPC endpoint directly via curl, extracted the probe's inputSchema.properties from the raw response:

{
  "scalar_only_union": {
    "type": ["string", "number", "boolean", "null"]
  },
  "object_array_union": {
  },
  "single_string": {
    "type": "string"
  }
}

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 no type field 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 type when 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"], fetch tools/list via curl directly (bypassing the client), and compare the wire response against what Claude Code surfaces via ToolSearch. 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.

clifguy · 3 months ago

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.

github-actions[bot] · 1 month ago

Closing for now — inactive for too long. Please open a new issue if this is still relevant.

WebSearching404 · 1 month ago

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 inputSchema declares type: number, integer, boolean, a typed array, and two union type arrays (["string","number"], ["string","null"]). A headless claude -p session was asked to (a) report the schema it sees and (b) call the tool; the server logged the raw tools/call line off the wire.

Results:

  1. The model-visible schema preserved both union type arrays verbatim — no strip.
  2. The wire arguments arrived fully typed, no JSON-string coercion:
{"num":4.5,"int_p":7,"union_scalar":3,"union_nullable":null,"arr":[1,2,3],"flag":true,"s":"x"}

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.