MCP tool-call validation treats optional/nullable parameters as required, and literal "null" is never transmitted as JSON null
Summary
When calling MCP tools whose JSON Schema declares optional parameters ("default": null, no entry in a required list), the client-side validation layer rejects the call unless every such parameter is explicitly present — contradicting the schema, which marks them optional. Separately and more subtly: even when a parameter is supplied with the literal text null, it is never converted to the JSON value null. Depending on the parameter's underlying type, it is instead transmitted either as the single-element array ["null"] (for list-typed parameters) or as the 4-character string "null" (for scalar/string-typed parameters). For parameters whose only valid "no-op" representation is a true null (e.g. two mutually-exclusive parameters where exactly one must be absent), this makes the tool impossible to call correctly.
Environment
- Claude Code (desktop app)
- Reproduced against a third-party MCP server (
homeassistant/ha-mcp, reported version 8.5.0), on tools never previously invoked in the session — ruling out per-tool caching as the cause.
Steps to reproduce — Part 1 (missing-key rejection)
- Load any MCP tool whose schema has optional parameters, e.g. a tool with:
``json``
{ "properties": {
"fields": {"default": null},
"area_fields": {"default": null}
}, "type": "object" }
- Call it with no arguments at all.
- Actual: rejected before it reaches the server:
````
MCP error -32602: Input validation error: Invalid arguments for tool ha_list_floors_areas: [
{ "code": "invalid_type", "expected": "nonoptional", "path": ["fields"],
"message": "Invalid input: expected nonoptional, received undefined" },
{ "code": "invalid_type", "expected": "nonoptional", "path": ["area_fields"],
"message": "Invalid input: expected nonoptional, received undefined" }
]
Expected: the call succeeds, using the schema's declared defaults.
Steps to reproduce — Part 2 (null is never transmitted as JSON null)
- Retry, this time typing the literal text
nullfor both parameters:
````
ha_list_floors_areas(fields=null, area_fields=null)
- Actual: the call now passes client-side validation, but the server's own response proves it received the array
["null"], not an absent/Nonevalue:
``json``
{"success": true, "warnings": [
"area_fields ['null'] matched no record keys — records came out empty. Available keys: [...]",
"fields ['null'] not found in response — available keys: [...]"
]}
- Repeat on a scalar string parameter, e.g.
ha_config_get_label(label_id=null):
``json`
{"success": false, "error": {"code": "RESOURCE_NOT_FOUND", "message": "Label not found: null"},
"label_id": "null", "available_label_ids": ["sicherheit", "energiesparen", ...]}
"label_id": "null"
The echoed proves the server received the 4-character string "null", not None`.
Downstream impact
For tools with two mutually-exclusive optional parameters where the server checks presence via is not None (e.g. a hypothetical set_config(config=..., python_transform=...) pair), this is fatal: no representable value — not null, not "", not {}, not [] — is ever treated as "absent" by the server, because the client never transmits a real null for that field. Encountered in practice with ha-mcp's ha_config_set_automation (config vs. python_transform) and ha_set_entity (bulk vs. single-entity-only parameters), both of which became fully uncallable in one of their two modes.
Suspected root cause
The client appears to translate each tool's JSON Schema into an internal (Zod-based, judging by the exact error vocabulary: "code": "invalid_type", "expected": "nonoptional") validation schema that marks every property .nullable() but not .optional() — so the key must be present. Once present, the literal text null is then serialized per the parameter's inferred type (wrapped into a single-element array for list-typed parameters, kept as the raw string for scalar ones) rather than being parsed as the JSON literal null.
Expected behavior
- A parameter with a JSON Schema
"default"and norequiredentry should be omittable from the call. - If a parameter's declared type permits
null(or has notypeat all, as in these examples), typing the literalnullshould be transmitted as the JSON valuenull, not as the string"null"or an array containing it.