Object argument to a deferred (ToolSearch-loaded) MCP tool appears dropped — POST calls with JSON-object body fail via chat, succeed via standalone MCP client

Status Open
Maintainer reply None cached
Activity 0 comments · opened Jul 29, 2026

Title

Object argument to a deferred (ToolSearch-loaded) local MCP tool appears to be dropped/mangled — POST calls with a JSON-object body fail, identical calls succeed via a standalone MCP client

Summary

A locally-run, stdio-based MCP server (custom Node.js server, @modelcontextprotocol/sdk ^1.12, actually resolved 1.29.0, zod 3.25.76) exposes a generic HTTP-passthrough tool with this shape:

inputSchema: {
  method: z.enum(['GET', 'POST', 'PUT', 'PATCH', 'DELETE']).default('GET'),
  path: z.string(),
  query: z.record(z.string()).optional(),
  body: z.record(z.unknown()).optional(),
}

The tool was loaded mid-conversation as a deferred tool via ToolSearch (it was not available from the start of the session). Every call made through the chat interface with method: "POST" and an object body (e.g. {"clientId": "67"}) fails. The error returned looks exactly like a legitimate downstream API rejection (a well-formed JSON error body with an HTTP-400-shaped message), which strongly suggested — incorrectly — that the target API was rejecting the request.

What I verified (ruling out the server/API as the cause)

  1. Direct curl to the target API with the exact same body/headers/auth → succeeds (200).
  2. Directly invoking the server's tool handler function in a Node REPL with the same arguments → succeeds.
  3. A full round-trip through a standalone @modelcontextprotocol/sdk Client + StdioClientTransport, spawning the exact same server process and calling the exact same tool with an object body → succeeds.
  4. The server's own automated test suite (which already specifically guards against a related "body" schema regression, with an object-typed body and a rejection test for pre-stringified JSON) passes in full.

So the server, its input schema, and the target API are all confirmed correct and working. The only environment where the call fails is the live chat tool-call path in this session.

Suspicious detail

When inspecting the tool's schema as exposed to the model via ToolSearch (deferred-tool loading), the body parameter's JSON Schema rendered as a completely empty object: "body": {} — no type, no additionalProperties. A structurally similar query field (z.record(z.string()).optional()) rendered correctly as {"type":"object","additionalProperties":{"type":"string"},"properties":{}}.

Tracing this: zod-to-json-schema (via the SDK's toJsonSchemaCompat) renders an optional z.record(z.unknown()) as:

{"anyOf":[{"not":{}},{"type":"object","additionalProperties":{}}]}

i.e. an anyOf with an empty/tautological branch ({} as the additionalProperties value, because the record's value type is unknown). This is different from query's schema, whose inner value type (string) is concrete rather than empty. This looks like a plausible trigger for whatever step flattens/summarizes MCP tool schemas for a deferred ToolSearch result — an anyOf branch with an all-empty leaf schema may be getting simplified away entirely, and something downstream may be using that flattened/lossy schema (rather than the real one used for validation) to decide how to encode the body argument when dispatching the tool call.

I want to flag this is circumstantial, not a proven root cause — I don't have visibility into how the harness serializes tool-call arguments internally. But it's a strong, reproducible correlation: the one field that renders as a totally empty schema is the one field whose values consistently fail to reach the server correctly, while a same-conversation, same-tool call with only primitive/string arguments (query, path, method) works fine.

Impact

Any locally-hosted MCP tool with an object/record-typed parameter (very common for generic "call this REST API" style tools) that gets loaded as a deferred tool via ToolSearch may silently fail to pass that parameter correctly, while surfacing a misleading, plausible-looking downstream error — which sends debugging effort toward the wrong target (the remote API / the tool's own server code) instead of the actual cause.

Environment

  • Claude Code CLI, Windows 11
  • Node.js (LTS), locally-run stdio MCP server
  • Issue observed specifically with a tool loaded via ToolSearch mid-conversation (deferred tool), not confirmed whether natively-available tools are affected

Repro sketch

  1. Register a local stdio MCP server with a tool that has an optional z.record(z.unknown()) (or similarly "empty-leaf" object) parameter, alongside another optional z.record(z.string()) parameter for comparison.
  2. In a Claude Code session, don't reference the tool early — let it appear as a deferred tool, then load it via ToolSearch.
  3. Call the tool passing a non-empty object for the "unknown"-valued record parameter.
  4. Compare against calling a standalone @modelcontextprotocol/sdk Client/StdioClientTransport against the same server with the same arguments (works), and against inspecting the tool's schema as shown by ToolSearch (renders as {} for that parameter).

View original on GitHub ↗