MCP client rejects tool calls omitting optional parameters; deadlocks on tools with mutually-exclusive parameter groups

Status Closed — duplicate
Maintainer reply None cached
Activity 8 comments · opened Sep 16, 2026 · closed Oct 11, 2026

Summary

Every MCP tool call that omits an optional parameter is rejected with a Zod-style
invalid_type / expected: "nonoptional", received: "undefined" error for that
parameter — even though the tool's own JSON Schema declares a default for it.
Supplying the parameter explicitly (including as null/empty string) works, but
for tools that also enforce "provide exactly one of A or B" / "field X must be
omitted when Y is set" business rules, this creates a genuine deadlock: the outer
layer requires every optional field to be present, while the tool's own logic then
rejects that same field's presence. There is no parameter combination that
satisfies both, so some tool actions (e.g. HACS update_information, ws_command
calls with entity_id, entity-registry unique_id lookups) cannot be invoked at
all right now.

This affects an MCP server ("ha-mcp") connected via a local stdio subprocess
(uvx ha-mcp@latest), and reproduces identically against two completely
different Home Assistant instances (the user's real instance and an unrelated
public demo instance), and across at least two ha-mcp server versions (8.4.2 and
8.4.3). That rules out the HA instance and the server package version as the
cause and points at the client-side tool-calling/validation layer.

Environment

  • Client: Claude Code, running inside the Claude Desktop app ("Code" tab), macOS
  • macOS: updated the day before this bug first appeared, to macOS 27 "Golden

Gate" (exact build/beta number not captured — please fill in from sw_vers)

  • MCP server: ha-mcp (PyPI), launched via:

``
~/.local/bin/uvx --python 3.13 --refresh ha-mcp@latest
`
configured in
claude_desktop_config.json under mcpServers.home-assistant`

  • ha-mcp versions tested: 8.4.3 (original), 8.4.2 (pinned to rule out a

regression) — bug identical on both

  • Home Assistant instances tested: a primary self-hosted instance (HA core

2026.9.2) and an unrelated public demo instance (HA core 2026.6.4) — bug
identical on both, ruling out anything instance-specific

  • Companion HACS integration (ha_mcp_tools / homeassistant-ai/ha-mcp-integration):

updated from stable v2.1.3 to dev snapshot v2.2.0-dev.386 mid-investigation,
in case of a client↔server↔component version-skew issue — bug persisted
unchanged afterward

Steps to reproduce

  1. Connect to any MCP server exposing a tool with one or more optional

parameters that have declared JSON Schema default values.

  1. Call the tool supplying only the required parameters, omitting the optional

ones (i.e. the normal/expected calling convention).

  1. Observe the call is rejected before it reaches the server's own business

logic.

Example 1 — basic optional-field rejection

Call:

ha_get_state(entity_id="sun.sun")

(fields and attribute_keys are both declared as {"default": null} in the
tool's schema.)

Result:

{
  "error": "MCP error -32602: Input validation error: Invalid arguments for tool ha_get_state: [
    {\"code\":\"invalid_type\",\"expected\":\"nonoptional\",\"path\":[\"fields\"],\"message\":\"Invalid input: expected nonoptional, received undefined\"},
    {\"code\":\"invalid_type\",\"expected\":\"nonoptional\",\"path\":[\"attribute_keys\"],\"message\":\"Invalid input: expected nonoptional, received undefined\"}
  ]"
}

Workaround: explicitly pass fields and attribute_keys (e.g. as a real list,
or as the literal string null for list-typed fields — note this is
not JSON null, it becomes the one-element list ["null"] server-side,
which is itself a separate minor bug but harmless for read calls).

Example 2 — the deadlock (no workaround exists)

Tool ha_call_service accepts either a normal domain/service/entity_id
service call, or a ws_command one-shot WebSocket command — mutually exclusive,
enforced by the tool's own validation:

{
  "error": "These parameters apply only to service calls and must be omitted when ws_command is set: entity_id, result_fields, result_attribute_keys."
}

But the outer layer separately requires ws_command to be present on every
call:

{
  "code": "invalid_type",
  "expected": "nonoptional",
  "path": ["ws_command"],
  "message": "Invalid input: expected nonoptional, received undefined"
}

There is no value for ws_command (empty string, JSON null, omitted) that
satisfies both "must be present" and "must be absent when doing a plain service
call" simultaneously. Plain service calls (domain+service+entity_id, no
ws_command) are therefore currently impossible to make without hitting one
error or the other.

The same deadlock shape reproduces on:

  • ha_get_integration (entry_id vs. domain query modes)
  • ha_get_entity ("provide exactly one of entity_id or unique_id")
  • ha_manage_hacs (each action value has its own required/forbidden

parameter subset, e.g. update_information forbids category/repository/
version, but the outer layer demands they be present)

What was ruled out

  • Not the HA instance: identical failure on two unrelated HA installs.
  • Not the ha-mcp server version: identical on 8.4.2 and 8.4.3.
  • Not the companion HACS component version: identical before and after

updating ha_mcp_tools from v2.1.3 to v2.2.0-dev.386 (+ HA restart).

  • Not a one-off stale connection: identical immediately after multiple full

restarts of the local uvx ha-mcp@latest subprocess (via killing/relaunching
the Claude Desktop app), and after removing a stray second/duplicate
mcpServers entry for the same server (which had inadvertently been the one
reconnected after one restart, briefly pointing the session at the wrong
backend — a separate, secondary finding worth a look: on restart the client
reconnected to a different-but-similarly-named mcpServers entry than the one
most recently active, rather than the one used before the restart).

Suspected cause

Given the failure reproduces across every tested combination of server
version, HA backend, and companion-component version, but is 100% consistent
in shape (missing-optional-field rejection, plus the exact-one-of deadlock)
across many different tools from the same server, the most likely location is
the client-side code that takes each tool's declared JSON Schema and constructs
the outbound tool-call arguments / validates them before dispatch — i.e.
something in this session's MCP tool-calling layer is not respecting a
schema property's default/optional-ness and is instead requiring literal
presence of every declared key.

Workaround in use

Passing an explicit, schema-valid value for every declared optional parameter
(never omitting any) avoids the first class of error for tools that don't also
have mutually-exclusive parameter groups. Tools with genuine "exactly one of"
requirements remain unusable via this MCP connection until this is fixed.

Request

Please advise whether this is a known issue, and if not, what additional
diagnostics (e.g. the raw outbound JSON-RPC tools/call payload for a failing
request) would help pin down where the extra/missing parameters are being
introduced.

View original on GitHub ↗

7 Comments

jamescra · 28 days ago

Update: reproduces identically on ha-mcp 8.5.0 as well (in addition to 8.4.2 and 8.4.3 noted above). Same exact error shape on the same basic call (ha_get_state omitting the optional fields/attribute_keys params):

Input validation error: Invalid arguments for tool ha_get_state: [
  {"code":"invalid_type","expected":"nonoptional","path":["fields"],"message":"Invalid input: expected nonoptional, received undefined"},
  {"code":"invalid_type","expected":"nonoptional","path":["attribute_keys"],"message":"Invalid input: expected nonoptional, received undefined"}
]

Now confirmed across three consecutive server releases with no change in behavior, which further reinforces that this sits in the client-side MCP tool-calling layer rather than anything the ha-mcp server can address.

waseemnabi · 28 days ago
Update: reproduces identically on ha-mcp 8.5.0 as well (in addition to 8.4.2 and 8.4.3 noted above). Same exact error shape on the same basic call (ha_get_state omitting the optional fields/attribute_keys params): `` Input validation error: Invalid arguments for tool ha_get_state: [ {"code":"invalid_type","expected":"nonoptional","path":["fields"],"message":"Invalid input: expected nonoptional, received undefined"}, {"code":"invalid_type","expected":"nonoptional","path":["attribute_keys"],"message":"Invalid input: expected nonoptional, received undefined"} ] `` Now confirmed across three consecutive server releases with no change in behavior, which further reinforces that this sits in the client-side MCP tool-calling layer rather than anything the ha-mcp server can address.

we are receiving same error after v2.110.0 update

spongetimblin · 28 days ago

Root cause, from reading the Claude desktop 2.110.0 bundle on macOS: for local stdio MCP servers, the Code tab re-registers each tool through the bundled Agent SDK's tool() / createSdkMcpServer. It first converts the tool's JSON Schema to a Zod shape with the app's own Zod 4.5.4 (.vite/build/index.chunk-DKIrdFGR.js): a property's default becomes .prefault(default), and non-required properties get .optional(). In Zod 4.5 that optional wrapper reports _zod.optin === "defaulted". The Agent SDK chunk (index.chunk-BKvKVjFV.js) bundles its own Zod 4.4.3, and McpServer.validateToolInput turns the shape into an object schema with it. Zod 4.4.3's handlePropertyResult treats a key as optional only when optin === "optional", so every absent key whose JSON Schema had a default fails with expected: "nonoptional". Properties without a default are unaffected, which matches every report here.

Standalone repro: z.object({ limit: z.number().prefault(null).optional() }), with the inner schema built by zod@4.5.4 and z.object from zod@4.4.3 (zod/mini), rejects {} with nonoptional. With both from 4.5.4, it parses. Running the app's converter this way reproduces the exact error lists for things-mcp on FastMCP 3.4.7 (24 of 26 tools reject calls with only required arguments) and for a server on the mcp SDK's FastMCP 1.30.0 (15 of 33 tools). I didn't trace why sessions still on bundled Claude Code 2.1.270 avoided this path, as noted in #94608.

Likely fix: convert and validate with the same Zod instance, or stop mapping JSON Schema default to prefault in the converter.

Workaround until a fix ships: run the server through a small stdio wrapper that deletes default from each tool's top-level inputSchema.properties in tools/list responses. The converter then emits a plain .optional(), validation passes, and the server still applies its own defaults. The app keeps claude_desktop_config.json in memory and rewrites it, so change the server command while the app is quit.

zzangho · 28 days ago

Same regression on Windows 11 (build 26200), Claude Desktop 2.110.0.0 (MSIX), Code tab. Still reproducing as of 2026-09-17T04:10Z, after #94608 was closed.

Version boundary (from session transcripts):

  • Bundled Claude Code 2.1.270: last successful call omitting optional params, 2026-09-16T05:08:19Z
  • Bundled Claude Code 2.1.271: first expected nonoptional rejection for the same call shape, 2026-09-16T14:57:36Z

Server: joplin-mcp 0.7.1 (Python, FastMCP 2.14.7, mcp 1.27.0), local stdio from claude_desktop_config.json. Server packages unchanged since April.

Direct stdio probe against the same server executable, bypassing the desktop app:

| Optional params sent as | Result |
|---|---|
| omitted | OK |
| JSON null | OK |
| the string "null" | fails with the same server-side errors seen from the Code tab |

The Code tab sent the string "null" (not JSON null) for params declared as anyOf: [{"type": X}, {"type": "null"}], so the workaround of passing every param explicitly does not help for these.

Typed params with a default are rejected too, consistent with the correction in #94608: find_notes.offset is {"type": "integer", "default": 0, "minimum": 0} in the server's tools/list and is reported as nonoptional when omitted.

Tools with no callable argument combination:

  • edit_note: old_string and position are mutually exclusive (is not None check). Omitting either is rejected as nonoptional; passing "null" or "" returns Cannot specify both old_string and position.
  • get_note: omitting section/start_line is rejected as nonoptional; start_line: "null" returns start_line must be an integer or string representation of an integer; section: "" with start_line: 1 returns Cannot specify both start_line and section.
alexnbg88 · 27 days ago

Confirming @spongetimblin's root cause above, with two additions (macOS, 2026-09-17):

  1. Claude Desktop 2.110.1 does not fix it. In the downloaded 2.110.1 bundle the JSON Schema to zod converter is unchanged apart from minified names (still .prefault(default) then .optional()), the app chunk still bundles zod 4.5.4, and the Agent SDK chunk that exports createSdkMcpServer still bundles zod 4.4.3. #94608 was closed as completed, but this still reproduces today.
  1. Server-side workaround for servers you control, verified with a control: list tool input schemas without the default annotation. The app then builds a plain .optional() (optin "optional"), which zod 4.4.3 accepts, and the server still applies its own defaults, so calls behave the same. For FastMCP 3.x this is a small on_list_tools middleware returning tool.model_copy(update={"parameters": schema_without_property_defaults}), the same pattern as FastMCP's own DereferenceRefsMiddleware. Walk the schema by structure, not key names (a parameter can itself be named properties). After the change, a call omitting three defaulted parameters succeeded; in the same Code session an unpatched Desktop extension still failed with expected nonoptional.
hstrick2000 · 27 days ago

Reproduced on macOS with a remote HTTP MCP server (not a local Python/stdio one), and traced the two zod copies in the 2.110.1 bundle — adding it because the reports above are local stdio servers and this shows the transport doesn't matter.

Setup

  • Claude Desktop 2.110.1 (macOS 27), Code tab.
  • Server: a Next.js app using @modelcontextprotocol/sdk 1.29.0 + zod 3.25.76 (v3 classic), served over Streamable HTTP; tools declare params with z.number().default(...) / z.boolean().default(...).
  • Reached from the Code tab via claude_desktop_config.json → npx -y mcp-remote <url> --header "Authorization: Bearer …" (mcp-remote 0.14.2).

Symptom — omitting any .default() param:

MCP error -32602: Input validation error: Invalid arguments for tool summarize_invoices: [
 {"code":"invalid_type","expected":"nonoptional","path":["limit"],"message":"Invalid input: expected nonoptional, received undefined"},
 {"code":"invalid_type","expected":"nonoptional","path":["include_voided"],"message":"Invalid input: expected nonoptional, received undefined"}]

Plain .optional() params omitted in the same call are accepted.

Controls that rule the server out

  • The identical call with the same params omitted succeeds through the claude.ai web connector against the same production deployment (68 groups returned).
  • The production Next bundle, run locally and probed through the SDK's own normalizeObjectSchema → safeParseAsync, applies both defaults.
  • mcp-remote 0.14.2 validates only the wire shape (arguments: record(string(), unknown()).optional(), dist/proxy.js) and forwards tools/call unchanged; it contains no validateToolInput.

Where it happens in the 2.110.1 app.asar (main-process chunks under .vite/build/) — both zod copies carry version literals, so this is not inferred from behaviour:

  • index.chunk-D6ATlqeD.js — MCP TypeScript SDK bundle with zod 4.5.4 (ite={major:4,minor:5,patch:4}). Its $ZodOptional sets optin = innerType._zod.optin==="defaulted" ? "defaulted" : "optional"; its object-parse helper rejects an absent key only when optin===void 0, so "defaulted" passes.
  • index.chunk-BehGPm8o.js — JSON-Schema→zod converter (exports.t), requires D6ATlqeD and builds shapes from that zod. Object properties: a[e] = required.includes(e) ? t : t.optional(); then "default" in r && r.default !== void 0 && (o = o.prefault(r.default)). A defaulted, non-required property therefore becomes prefault(x).optional() with optin:"defaulted".
  • index.chunk-B4biYxEs.js (LocalMcpServerManager.createSdkServer({createSdkMcpServer, tool}, …)) maps every listTools result: tool(e.name, e.description ?? e.name, <converter>.t(e.inputSchema ?? {}), async args => client.callTool({name: e.name, arguments: args})), then createSdkMcpServer({name, version:"1.0.0", instructions, tools}). index.chunk-D7HolO_j.js does the same for its transport.
  • index.chunk-CCB3eLtQ.js — the Agent SDK (exports.createSdkMcpServer → registerTool → McpServer.validateToolInput) with a separate zod 4.4.3 (UQ={major:4,minor:4,patch:3}). Its object parser computes r = n._zod.optin==="optional" and the helper h$ pushes {code:"invalid_type", expected:"nonoptional", path:[key]} whenever the key is absent and !r. A "defaulted" key is therefore treated as required.
  • The resulting SDK server is what reaches the Claude Code runtime as the mcpServers option (index.chunk-CEZNDsuL.js), so the rejection is generated locally before tools/call is ever forwarded.

Live path: LocalMcpServerManager.listTools → converter.t (zod 4.5.4) → tool() → createSdkMcpServer → registerTool → validateToolInput (zod 4.4.3).

Minimal cross-version repro (node) — shape from zod 4.5.4, object from zod 4.4.3:

const z45 = require('zod'  /* 4.5.4 */), z44 = require('./z443/node_modules/zod' /* 4.4.3 */);
const limit = z45.z.number().prefault(200).optional();   // optin "defaulted" under 4.5.4
const obj = z44.z.object({ tenant: z44.z.string(), limit, customer_code: z45.z.string().optional() });
obj.safeParse({ tenant: 'mrs' });
// → issues: [{ code:'invalid_type', expected:'nonoptional', path:['limit'], … }]  (customer_code not flagged)
// same zod both sides → success, limit = 200

"defaulted" is a usable fingerprint when version literals are stripped: 0 occurrences in zod 4.4.3, 40 in 4.5.4.

Workaround verified end-to-end on this server (same day): changed ONE tool (summarize_invoices) to declare limit/include_voided as .optional() with the defaults applied in the handler, so the advertised JSON Schema carries no default keyword (required unchanged). Deployed; the Code tab still rejected the omitted params until Claude Desktop was fully quit and relaunched (the tool list is cached per app-level connection), then the identical call succeeded (68 groups, matching the connector path). Control: an unchanged tool on the same server that still advertises default is still rejected. So on the current Desktop build, default in the advertised schema is both necessary and sufficient for the rejection.

Independently re-verified by a second model reading the same extracted chunks (the trace above is from that pass). Confirms the server-side workaround already noted above: publish the input schema without the default keyword (keep applying the default server-side) and the converter emits a plain .optional(), which the 4.4-series validator accepts.

alexnbg88 · 27 days ago

Update from macOS, 2026-09-18: fixed for me after Claude Desktop auto-updated to 2.2553.0 (bundled Claude Code 2.1.274, up from 2.1.271).

Same setup as my comment above: Code tab, a local Desktop extension (Apify), defaulted parameters left out:

  • search-apify-docs with only query (omitting docSource, limit and offset, all with a JSON Schema default) now returns results. On 2026-09-17 the identical call failed with expected nonoptional, received undefined.
  • search-actors with only keywords (omitting limit and offset) now returns results.

I did not test 2.110.1 live between the two updates, so I can't say for certain which build carried the fix; my reading of the 2.110.1 bundle above still showed the old converter. Not tested: remote HTTP servers (@hstrick2000's case) or Windows.

Showing cached comments. Read the full discussion on GitHub ↗