Skip to content

fix(mcp): restore tools/list, broken for every client since 2.2.0 - #2

Merged
FerroxLabs merged 2 commits into
mainfrom
fix/mcp-tools-list-zod-record
Aug 4, 2026
Merged

fix(mcp): restore tools/list, broken for every client since 2.2.0#2
FerroxLabs merged 2 commits into
mainfrom
fix/mcp-tools-list-zod-record

Conversation

@FerroxLabs

Copy link
Copy Markdown
Owner

What was wrong

tools/list did not return an empty catalog. It threw:

{"jsonrpc":"2.0","id":2,"error":{"code":-32603,
 "message":"Cannot read properties of undefined (reading '_zod')"}}

Every MCP host — Claude Code, Codex, Cursor, Wayland — saw zero tools. The CLI was unaffected, which is why this went unnoticed.

Root cause

strategy_sweep declared inputs as a one-argument z.record(...). Valid under Zod 3, invalid under Zod 4, which requires an explicit key type. The one-argument form leaves the value type undefined, and the SDK's schema conversion dereferences _zod on it.

The deeper cause: zod is imported in 17 source files and was declared in none. It resolved transitively through @modelcontextprotocol/sdk, so the SDK's dependency range chose our schema library's major version.

  • SDK 1.12.1 pinned zod: ^3.23.8 and nested its own copy. tools/list worked.
  • SDK 1.29.0 (adopted 2026-07-16, 98bc5e0) widened to ^3.25 || ^4.0. Zod 4 hoisted, and the same unchanged source stopped working.

Verified rather than inferred: rebuilding the pre-bump tree with npm ci from that commit's lockfile and driving tools/list against the unfixed source returns all 82 tools of the then-88-tool catalog with no error.

Why 512 passing tests missed it

Schema conversion happens only inside the SDK's tools/list handler. The CLI and core paths never trigger it, so the whole offline suite stayed green while the MCP server was unusable. tool_count.test.js counts server.tool( by regex; it never starts a server.

Changes

  • src/tools/sweep.js — two-argument z.record(z.string(), ...).
  • package.json — declare zod at 4.3.6 so a dependency's range cannot pick our schema library again.
  • tests/mcp_stdio.test.js — the first test that speaks MCP. Drives initialize and tools/list over real stdio from a bare environment and a foreign working directory, derives the expected count from the catalog rather than hardcoding it, and asserts every published tool converts to a usable JSON Schema.
  • Version to 2.2.1 plus changelog. The published 2.2.0 is broken for all MCP clients, so this wants a release.

Verification

Check Result
npm run test:offline 517/517 pass (512 existing + 5 new)
npm run lint clean
tools/list over stdio, bare env, cwd=/ 101 tools
New tests against unfixed source 5/5 fail with the exact -32603
Pre-bump tree (SDK 1.12.1, unfixed source) 82 tools, no error

tools/list answered -32603 "Cannot read properties of undefined (reading
'_zod')" instead of returning the catalog, so Claude Code, Codex, Cursor
and Wayland all saw zero tools. The CLI was unaffected.

strategy_sweep declared `inputs` as a one-argument z.record(). That is
valid Zod 3 but invalid Zod 4, which requires an explicit key type; the
one-argument form leaves the value type undefined and the SDK's schema
conversion dereferences `_zod` on it. Nothing in the 512-test suite
touches that conversion, because it happens only inside the SDK's
tools/list handler and the CLI never calls it.

The deeper cause is that zod was imported in 17 files and declared in
none. It resolved transitively through @modelcontextprotocol/sdk, so the
SDK's own range picked the major version. Until 2026-07-16 the SDK
pinned zod ^3.23.8 and nested its own copy, and tools/list worked --
verified by rebuilding that tree from the lockfile, where the unfixed
code publishes all 82 tools. SDK 1.29.0 widened to "^3.25 || ^4.0",
Zod 4 hoisted, and the same source stopped working with no change here.

Declaring zod at 4.3.6 stops a dependency's range from choosing our
schema library again.

tests/mcp_stdio.test.js is the first test that speaks MCP: it drives
initialize and tools/list over real stdio from a bare environment and a
foreign working directory, derives the expected tool count from the
catalog rather than hardcoding it, and asserts every published tool
converts to a usable JSON Schema. All five assertions fail against the
unfixed source and pass against this one.
CI runs `npm audit --audit-level=high`, which has been failing on main
since brace-expansion, fast-uri and ip-address advisories were published
against transitive dependencies. Verified pre-existing: the audit exits 1
on unmodified main and on this branch identically.

Lockfile-only. No top-level dependency changes and no resolved-version
change for zod (4.3.6), the MCP SDK (1.29.0) or chrome-remote-interface
(0.34.0) -- deliberately not `--force`, which would pull the SDK to 1.30.0
and repeat the kind of unreviewed SDK jump that broke tools/list.

Two moderate advisories remain, both in the SDK's HTTP server stack
(hono / @hono/node-server / body-parser). They sit below the CI threshold
and are unreachable from this package, which only ever runs the stdio
transport.
@FerroxLabs
FerroxLabs merged commit 94e80df into main Aug 4, 2026
6 checks passed
@FerroxLabs
FerroxLabs deleted the fix/mcp-tools-list-zod-record branch August 4, 2026 03:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants