You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
When an LLM client (currently observed with Claude Code) calls an MCP tool with an integer parameter, the value sometimes arrives as a JSON string instead of a JSON number. The host's JSON Schema validator rejects the call before it reaches our server with errors like:
The model attempted the call twice in the same stringified shape and gave up. The agent reports this is non-deterministic — sometimes correct, sometimes stringified — so retries don't reliably self-correct.
Root cause
This is not a server bug. Option<usize> in SearchInput makes schemars emit:
The schema is correct per JSON Schema / MCP spec. The error message format (params/limit must be integer,null) is AJV's signature, meaning a JS-based validator in the MCP host (Claude Code) rejects the call before it reaches our server.
This is a known, widespread ecosystem issue caused by Claude Code's tool-argument serialization:
Adopt Option E from the design discussion: introduce a single helper (newtype or deserialize_with + schema_with pair) that:
Declares the JSON Schema as type: [\"integer\", \"string\", \"null\"] with a pattern: \"^[0-9]+$\" constraint on the string form, so the host's pre-flight AJV/Zod validator accepts both shapes.
Implements Deserialize to parse string-encoded integers into the underlying numeric type.
Applies to every integer input field across the tool surface.
Initial scope (integer inputs in crates/rimap-server/src/tools/):
retrieval/search.rs: limit, offset
retrieval/fetch_message.rs: max_body_bytes
Any other Option<u32> / Option<usize> / Option<u64> input field on a tool's *Input struct (audit the full set during implementation).
String fields. FastMCP issue #1873 documented data loss when string-typed inputs were aggressively coerced (UUIDs starting with digits got truncated). Strings stay strict.
Output schemas. Server controls its own output types; coercion only applies to input validation.
Acceptance criteria
search tool accepts \"limit\": 100, \"limit\": \"100\", and \"limit\": null.
search tool rejects \"limit\": \"abc\" and \"limit\": -1 with a clear error.
fetch_message.max_body_bytes behaves identically.
All other integer input fields on tool input structs use the same helper.
dump-tool-catalog --features test-support confirms each affected field has schema type: [\"integer\", \"string\", \"null\"] with a digits-only pattern on the string form.
Unit tests cover: int-form, string-form, null, invalid string, overflow.
Footguns to avoid
Don't coerce booleans with naive truthy semantics. Boolean(\"false\") === true in JS; if we ever do booleans, use the preprocess-style mapping that handles \"true\"/\"false\" explicitly.
Don't coerce string-typed fields. Only fields whose canonical type is integer.
Don't infer types from content. FastMCP-style "if it looks like a number, parse it" corrupts UUID/phone-number strings. The schema annotation is authoritative.
Pattern-constrain the string form.\"^[0-9]+$\" (or \"^-?[0-9]+$\" for signed types) so AJV continues to reject non-numeric strings client-side.
Problem
When an LLM client (currently observed with Claude Code) calls an MCP tool with an integer parameter, the value sometimes arrives as a JSON string instead of a JSON number. The host's JSON Schema validator rejects the call before it reaches our server with errors like:
Observed in the wild on
searchcalls:{"folder":"DPDK Mailing Lists.Dev","limit":"100","to":"drc@linux.ibm.com"}The model attempted the call twice in the same stringified shape and gave up. The agent reports this is non-deterministic — sometimes correct, sometimes stringified — so retries don't reliably self-correct.
Root cause
This is not a server bug.
Option<usize>inSearchInputmakesschemarsemit:{ "type": ["integer", "null"], "format": "uint", "minimum": 0 }The schema is correct per JSON Schema / MCP spec. The error message format (
params/limit must be integer,null) is AJV's signature, meaning a JS-based validator in the MCP host (Claude Code) rejects the call before it reaches our server.This is a known, widespread ecosystem issue caused by Claude Code's tool-argument serialization:
number,object, andarrayparameters during MCP tool calls.How other MCP servers handle it
The ecosystem has converged on lenient server-side coercion:
strict_input_validation=Falseis the default.mapstructure.WeakDecode; parses numeric strings inRequiredInt/OptionalIntParam.z.coerce.number()for ints;z.preprocessfor booleans (avoiding theBoolean(\"false\") === truefootgun).Decision
Adopt Option E from the design discussion: introduce a single helper (newtype or
deserialize_with+schema_withpair) that:type: [\"integer\", \"string\", \"null\"]with apattern: \"^[0-9]+$\"constraint on the string form, so the host's pre-flight AJV/Zod validator accepts both shapes.Deserializeto parse string-encoded integers into the underlying numeric type.Initial scope (integer inputs in
crates/rimap-server/src/tools/):retrieval/search.rs:limit,offsetretrieval/fetch_message.rs:max_body_bytesOption<u32>/Option<usize>/Option<u64>input field on a tool's*Inputstruct (audit the full set during implementation).Out of scope:
z.coerce.boolean(\"false\") === truefootgun (fix(sequential-thinking): use z.coerce for number and boolean params modelcontextprotocol/servers#3533 review thread) makes the cost/benefit worse, and we have not seen booleans rejected in the wild yet. Defer.Acceptance criteria
searchtool accepts\"limit\": 100,\"limit\": \"100\", and\"limit\": null.searchtool rejects\"limit\": \"abc\"and\"limit\": -1with a clear error.fetch_message.max_body_bytesbehaves identically.dump-tool-catalog --features test-supportconfirms each affected field has schematype: [\"integer\", \"string\", \"null\"]with a digits-only pattern on the string form.inputSchema.typeremains\"object\").Footguns to avoid
Boolean(\"false\") === truein JS; if we ever do booleans, use thepreprocess-style mapping that handles\"true\"/\"false\"explicitly.\"^[0-9]+$\"(or\"^-?[0-9]+$\"for signed types) so AJV continues to reject non-numeric strings client-side.References