fix(schemas): import from zod/v4 to match SDK's zod API (#548)

The generated schemas compose with schemas imported from
@modelcontextprotocol/sdk/types.js (e.g. z.array(ContentBlockSchema)).
The SDK constructs its schemas via `import * as z from 'zod/v4'`, but
our generated schema.ts used `import { z } from "zod"` — which returns
the v3 API when the consumer has zod 3.25.x installed.

Mixing v3-created and v4-created schema instances throws at parse time
with errors like `keyValidator._parse is not a function` because zod v4
dropped the `_parse` / `_parseSync` internals that v3 wrappers call.

This was masked before #534 because zod was bundled — all schema
creation shared the same inlined zod copy. Externalizing zod exposed
the mismatch on consumers pinned to zod 3.x (e.g. anthropics/apps).

The `zod/v4` subpath is exported by both zod 3.25+ and zod 4.x, so the
existing peerDependency range `^3.25.0 || ^4.0.0` is preserved.

https://claude.ai/code/session_013xQUegrvVGct5xpqeSL5av

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Olivier Chafik 2026-03-10 19:10:36 +00:00 • committed by GitHub
commit eeb81c08bc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -8,10 +8,15 @@
*
* ts-to-zod is a powerful tool but has limitations that require post-processing:
*
* ### 1. Zod Import Path (keep standard `"zod"` for version agnosticism)
* ### 1. Zod Import Path (`"zod"` → `"zod/v4"`)
*
* ts-to-zod generates `import { z } from "zod"` which works with both
* Zod v3.25+ and v4. We keep this standard import to support both versions.
* ts-to-zod generates `import { z } from "zod"`. We rewrite this to
* `import { z } from "zod/v4"` because the generated schemas compose with
* schemas imported from `@modelcontextprotocol/sdk/types.js`, which the SDK
* constructs via `zod/v4`. Mixing schemas from the v3 and v4 APIs at runtime
* fails with errors like `keyValidator._parse is not a function` (v3 internals
* calling into v4 objects, or vice versa). The `zod/v4` subpath is exported by
* both zod 3.25+ and zod 4.x, so the peerDependency range is preserved.
*
* ### 2. External Type References (`z.any()` → actual schemas)
*
@ -179,11 +184,13 @@ async function generateJsonSchema() {
* Post-process generated schemas for project compatibility.
*/
function postProcess(content: string): string {
// 1. Add MCP SDK schema imports (keep standard zod import for v3/v4 compatibility)
// 1. Rewrite to zod/v4 and add MCP SDK schema imports.
// zod/v4 aligns with the SDK's own zod import — composing v3 and v4
// schema instances throws at parse time. See header comment for details.
const mcpImports = EXTERNAL_TYPE_SCHEMAS.join(",\n ");
content = content.replace(
'import { z } from "zod";',
`import { z } from "zod";
`import { z } from "zod/v4";
import {
${mcpImports},
} from "@modelcontextprotocol/sdk/types.js";`,
@ -257,7 +264,12 @@ function replaceRecordAndWithPassthrough(content: string): string {
* Post-process generated integration tests.
*/
function postProcessTests(content: string): string {
// Keep standard zod import for v3/v4 compatibility
// Rewrite to zod/v4 so z.infer matches the v4 schemas in schema.ts.
content = content.replace(
'import { z } from "zod";',
'import { z } from "zod/v4";',
);
content = content.replace(
"// Generated by ts-to-zod",
`// Generated by ts-to-zod