MCP tool call cannot pass JSON null for a required parameter typed anyOf:[integer,null] — arrives as the string "null"

Status Open
Reported on v2.1.220
Maintainer reply None cached
Activity 0 comments · opened Jul 28, 2026

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()), protocol 2025-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

  1. Expose an MCP tool with a required parameter shaped exactly as above (e.g. a compare-and-set "expected generation" fence where null means "no generation yet").
  2. From Claude Code, call the tool and pass null for 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.

View original on GitHub ↗