MCP tool call cannot pass JSON null for a required parameter typed anyOf:[integer,null] — arrives as the string "null"
Summary
An MCP tool whose input schema declares a required parameter as anyOf: [{"type": "integer"}, {"type": "null"}] cannot be called with null from Claude Code. The value arrives at the server as the string "null", so the tool is effectively uncallable with its documented null value.
I verified the server side is correct by issuing the byte-identical tools/call over raw HTTP JSON-RPC to the same server process — with a genuine JSON null it succeeds. Only the Claude Code call path fails.
Environment
- Claude Code 2.1.220
- Linux 7.1.4 x86_64
- MCP server: local stdio server built with the TypeScript MCP SDK, schema via Zod (
z.number().int().positive().nullable()), protocol2025-06-18
Advertised schema (from tools/list on the same server)
{
"anyOf": [
{ "type": "integer", "exclusiveMinimum": 0, "maximum": 9007199254740991 },
{ "type": "null" }
]
}
The parameter is listed in the schema's required array, so it must be present and may be null.
Reproduction
- Expose an MCP tool with a required parameter shaped exactly as above (e.g. a compare-and-set "expected generation" fence where
nullmeans "no generation yet"). - From Claude Code, call the tool and pass
nullfor that parameter.
Observed, for each attempt:
| Value passed | Server receives | Error |
|---|---|---|
| null | string "null" | expected "number", code "invalid_type", message "Invalid input: expected number, received string" |
| (omitted) | undefined | expected "number", ... received undefined — correct, since nullable() is not optional() |
| "" (empty) | string "" | ... received string |
| 0 | number 0 | too_small — correct, schema requires > 0. Numeric values coerce properly. |
That last row is the key contrast: numeric-looking values are converted to JSON numbers, but null is not converted to JSON null.
Control: the same call succeeds over raw JSON-RPC
Against the same server, same tool, same arguments:
POST /mcp
{"jsonrpc":"2.0","id":2,"method":"tools/call",
"params":{"name":"claim_plan","arguments":{
"goalId":"G120","purpose":"initial",
"claimRequestId":"<uuid>",
"ownerFenceToken":"<redacted>",
"expectedGeneration":null,
"author":"...","session":"..."}}}
Response:
{"ok":true,"replayed":false,"acknowledgement":{"goalId":"G120","claimId":"claim_G120_1","generation":1,...}}
So the schema, the server, and the value are all fine; the failure is in how Claude Code serializes null for the tool call.
Expected behavior
When a parameter's schema admits {"type": "null"}, Claude Code should be able to send JSON null for it.
Impact
There is no workaround from inside Claude Code — omitting the parameter is rejected (nullable is not optional), and every spellable value is either a string or a non-null number. Any MCP tool that uses a required nullable field to distinguish "absent" from "some value" becomes uncallable at exactly that boundary. In my case this is a compare-and-set fence where null is the only legal value for a first claim, which blocked the initial call entirely until I went around Claude Code with a direct HTTP client.