anthropics / anthropics/claude-code

MCP tool-call validation treats optional/nullable parameters as required, and literal "null" is never transmitted as JSON null

Open
#95,239 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
TypeScript
Stars
146k
Forks
23.8k
PR merge metrics
PR metrics pending

Description

## 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)

1. 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" }
```
2. Call it with no arguments at all.
3. **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`)

4. Retry, this time typing the literal text `null` for both parameters:
```
ha_list_floors_areas(fields=null, area_fields=null)
```
5. **Actual:** the call now passes client-side validation, but the server's own response proves it received the array `["null"]`, not an absent/`None` value:
```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: [...]"
]}
```
6. 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", ...]}
```
The echoed `"label_id": "null"` 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 no `required` entry should be omittable from the call.
- If a parameter's declared type permits `null` (or has no `type` at all, as in these examples), typing the literal `null` should be transmitted as the JSON value `null`, not as the string `"null"` or an array containing it.

Contributor guide

No contributing guide indexed for this repository

Research direction

Start at the MCP tool-call validation entry point and reproduce both cases described: omitted optional parameters and the literal text null. Trace JSON Schema handling and argument serialization, then verify that optional keys can be omitted and that a permitted null reaches the server as JSON null rather than a string or array.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
api, tooling
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
52/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.