[BUG] HTTP MCP array parameters stringified even for plain Zod 3 array schemas (no $ref, no $defs)

Resolved 💬 3 comments Opened Apr 17, 2026 by jns2001 Closed Apr 21, 2026

Bug Description

When an HTTP-type MCP server exposes a tool with a plain z.array(z.string()) parameter (Zod 3, no $ref, no $defs, no nested unions), Claude Code sends the argument as a JSON-encoded string rather than a native JSON array. The server's Zod validator correctly rejects it.

Raw POSTs to the same endpoint with the same arguments succeed, proving the server and MCP transport are fine. The stringification happens inside Claude Code's MCP bridge.

This is the same symptom as #18260 / #22394 / #25475 / #34520, but with a clean non-$ref repro that rules out the z.toJSONSchema / $ref theory (gogakoreli's analysis in #18260). Suggesting the bridge stringifies arrays even when the schema is fully inlined.

Environment

  • Claude Code: current as of 2026-04-17 (Opus 4.7, 1M context)
  • OS: macOS (Darwin 25.5.0)
  • MCP server: custom, @modelcontextprotocol/sdk 1.25.2, zod ^3.25.76 (Zod 3, not Zod 4)
  • MCP transport: type: "http"
  • Endpoint: Streamable HTTP, Bearer auth

Server-side schema (Zod 3)

server.tool(
  "memory_flush",
  "...",
  {
    decisions:    z.array(z.string()).optional(),
    corrections:  z.array(z.string()).optional(),
    open_threads: z.array(z.string()).optional(),
    key_context:  z.array(z.string()).optional(),
    domain:       z.string().optional(),
  },
  async (input) => { ... }
);

tools/list output for the decisions param (verbatim, fetched via raw POST)

"decisions": { "type": "array", "items": { "type": "string" } }

No $ref. No $defs. No oneOf. No z.lazy. No global registry. Plain primitive array.

Reproduction

  1. Configure any HTTP MCP server with a tool that has a z.array(z.string()).optional() parameter.
  2. From Claude Code, call the tool: memory_flush({ decisions: ["a", "b"] }).
  3. Server returns:
MCP error -32602: Input validation error: Invalid arguments for tool memory_flush: [
  {
    "code": "invalid_type",
    "expected": "array",
    "received": "string",
    "path": ["decisions"],
    "message": "Expected array, received string"
  }
]
  1. Repeat the exact same call via raw HTTP (curl / fetch) with {"decisions":["a","b"]} as a native JSON array — succeeds.

Evidence that the server/transport are fine

// Direct POST — works
await fetch("http://host:3333/mcp", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "Accept": "application/json, text/event-stream",
    "Authorization": "Bearer …",
  },
  body: JSON.stringify({
    jsonrpc: "2.0", id: 1, method: "tools/call",
    params: {
      name: "memory_fetch_chunks",
      arguments: { chunk_ids: ["dedup-1775890651841-6541g6"] }
    }
  })
});
// → 200 OK, returns chunk

Same call through Claude Code's MCP bridge → Expected array, received string.

Scope

Affects every tool with an array parameter on this server: memory_flush, memory_fetch_chunks, memory_correct, etc. Completely blocks write flows that require arrays.

Why this is a distinct signal from #18260

#18260's root cause theory (via gogakoreli's SDK analysis) is that Zod v4's z.toJSONSchema() emits $ref for registered/recursive types, and the MCP SDK doesn't resolve them. The in-flight fix is SDK PR #1563 which adds dereferenceLocalRefs() to schemaToJson().

This repro is Zod 3 + no $ref + no $defs, so that fix path does not apply. The bridge-level stringification appears to affect plain inline JSON Schema arrays as well. Either the scope of #18260 is broader than currently believed, or this is a second bug.

Related

  • #18260 — canonical open issue ($ref theory)
  • #22394, #25475, #34520 — closed as duplicates of #18260

Workarounds

  1. Server-side (z.preprocess):

``ts
decisions: z.preprocess(
v => typeof v === "string" ? JSON.parse(v) : v,
z.array(z.string())
).optional()
``

  1. Client-side: call the MCP endpoint directly via HTTP from a sandbox/shell.

View original on GitHub ↗

This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗