fix(mcp): restore tools/list, broken for every client since 2.2.0 - #2
Merged
Conversation
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What was wrong
tools/listdid not return an empty catalog. It threw:Every MCP host — Claude Code, Codex, Cursor, Wayland — saw zero tools. The CLI was unaffected, which is why this went unnoticed.
Root cause
strategy_sweepdeclaredinputsas a one-argumentz.record(...). Valid under Zod 3, invalid under Zod 4, which requires an explicit key type. The one-argument form leaves the value typeundefined, and the SDK's schema conversion dereferences_zodon it.The deeper cause:
zodis 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.zod: ^3.23.8and nested its own copy.tools/listworked.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 cifrom that commit's lockfile and drivingtools/listagainst 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/listhandler. The CLI and core paths never trigger it, so the whole offline suite stayed green while the MCP server was unusable.tool_count.test.jscountsserver.tool(by regex; it never starts a server.Changes
src/tools/sweep.js— two-argumentz.record(z.string(), ...).package.json— declarezodat4.3.6so a dependency's range cannot pick our schema library again.tests/mcp_stdio.test.js— the first test that speaks MCP. Drivesinitializeandtools/listover 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.Verification
npm run test:offlinenpm run linttools/listover stdio, bare env,cwd=/-32603