From c5b119aca0a9b20703219068578c5ec20947aab9 Mon Sep 17 00:00:00 2001 From: Genmin Date: Wed, 29 Apr 2026 22:38:40 -0700 Subject: [PATCH 1/3] fix(server): accept structurally compatible Zod v4 schemas --- src/server/zod-compat.ts | 22 ++++++++---- .../test_1987_zod_v4_type_identity.test.ts | 36 +++++++++++++++++++ 2 files changed, 52 insertions(+), 6 deletions(-) create mode 100644 test/issues/test_1987_zod_v4_type_identity.test.ts diff --git a/src/server/zod-compat.ts b/src/server/zod-compat.ts index d95ee79080..2d2f640300 100644 --- a/src/server/zod-compat.ts +++ b/src/server/zod-compat.ts @@ -10,7 +10,15 @@ import * as z3rt from 'zod/v3'; import * as z4mini from 'zod/v4-mini'; // --- Unified schema types --- -export type AnySchema = z3.ZodTypeAny | z4.$ZodType; +export interface ZodV4TypeLike { + _zod: { + output: Output; + input: Input; + def?: unknown; + }; +} + +export type AnySchema = z3.ZodTypeAny | ZodV4TypeLike; export type AnyObjectSchema = z3.AnyZodObject | z4.$ZodObject | AnySchema; export type ZodRawShapeCompat = Record; @@ -30,6 +38,8 @@ export interface ZodV3Internal { export interface ZodV4Internal { _zod?: { + output?: unknown; + input?: unknown; def?: { type?: string; value?: unknown; @@ -41,9 +51,9 @@ export interface ZodV4Internal { } // --- Type inference helpers --- -export type SchemaOutput = S extends z3.ZodTypeAny ? z3.infer : S extends z4.$ZodType ? z4.output : never; +export type SchemaOutput = S extends z3.ZodTypeAny ? z3.infer : S extends ZodV4TypeLike ? Output : never; -export type SchemaInput = S extends z3.ZodTypeAny ? z3.input : S extends z4.$ZodType ? z4.input : never; +export type SchemaInput = S extends z3.ZodTypeAny ? z3.input : S extends ZodV4TypeLike ? Input : never; /** * Infers the output type from a ZodRawShapeCompat (raw shape object). @@ -54,7 +64,7 @@ export type ShapeOutput = { }; // --- Runtime detection --- -export function isZ4Schema(s: AnySchema): s is z4.$ZodType { +export function isZ4Schema(s: AnySchema): s is ZodV4TypeLike { // Present on Zod 4 (Classic & Mini) schemas; absent on Zod 3 const schema = s as unknown as ZodV4Internal; return !!schema._zod; @@ -81,7 +91,7 @@ export function safeParse( ): { success: true; data: SchemaOutput } | { success: false; error: unknown } { if (isZ4Schema(schema)) { // Mini exposes top-level safeParse - const result = z4mini.safeParse(schema, data); + const result = z4mini.safeParse(schema as z4.$ZodType, data); return result as { success: true; data: SchemaOutput } | { success: false; error: unknown }; } const v3Schema = schema as z3.ZodTypeAny; @@ -95,7 +105,7 @@ export async function safeParseAsync( ): Promise<{ success: true; data: SchemaOutput } | { success: false; error: unknown }> { if (isZ4Schema(schema)) { // Mini exposes top-level safeParseAsync - const result = await z4mini.safeParseAsync(schema, data); + const result = await z4mini.safeParseAsync(schema as z4.$ZodType, data); return result as { success: true; data: SchemaOutput } | { success: false; error: unknown }; } const v3Schema = schema as z3.ZodTypeAny; diff --git a/test/issues/test_1987_zod_v4_type_identity.test.ts b/test/issues/test_1987_zod_v4_type_identity.test.ts new file mode 100644 index 0000000000..1cfa338158 --- /dev/null +++ b/test/issues/test_1987_zod_v4_type_identity.test.ts @@ -0,0 +1,36 @@ +import { McpServer } from '../../src/server/mcp.js'; + +type ExternalZodV4Schema = { + _zod: { + output: Output; + input: Input; + def: { type: string }; + }; +}; + +function assertTypechecks(callback: () => void): void { + expect(typeof callback).toBe('function'); +} + +describe('Issue #1987: externally resolved Zod v4 schema types', () => { + it('accepts raw shapes whose fields come from another compatible Zod v4 module identity', () => { + assertTypechecks(() => { + const server = new McpServer({ name: 'test', version: '1.0.0' }); + const name = {} as ExternalZodV4Schema; + const age = {} as ExternalZodV4Schema; + + server.registerTool( + 'example', + { + inputSchema: { name, age } + }, + async ({ name, age }) => { + const upperName: string = name.toUpperCase(); + const maybeAge: number | undefined = age; + + return { content: [{ type: 'text', text: `${upperName} ${maybeAge ?? ''}` }] }; + } + ); + }); + }); +}); From 52de3ad46c98b50f8bc319decc7495744a5fb626 Mon Sep 17 00:00:00 2001 From: Genmin Date: Thu, 30 Apr 2026 07:32:52 -0700 Subject: [PATCH 2/3] chore: add zod register tool changeset --- .changeset/fix-zod-44-registertool-types.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/fix-zod-44-registertool-types.md diff --git a/.changeset/fix-zod-44-registertool-types.md b/.changeset/fix-zod-44-registertool-types.md new file mode 100644 index 0000000000..f8ee8b36b6 --- /dev/null +++ b/.changeset/fix-zod-44-registertool-types.md @@ -0,0 +1,5 @@ +--- +"@modelcontextprotocol/sdk": patch +--- + +fix(server): accept structurally compatible Zod v4 schemas From bfc7eb8e4c8405c53e46112e010287d0a6d91276 Mon Sep 17 00:00:00 2001 From: Genmin Date: Thu, 30 Apr 2026 07:53:14 -0700 Subject: [PATCH 3/3] chore: format zod register tool changeset --- .changeset/fix-zod-44-registertool-types.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/fix-zod-44-registertool-types.md b/.changeset/fix-zod-44-registertool-types.md index f8ee8b36b6..3a990b6ee1 100644 --- a/.changeset/fix-zod-44-registertool-types.md +++ b/.changeset/fix-zod-44-registertool-types.md @@ -1,5 +1,5 @@ --- -"@modelcontextprotocol/sdk": patch +'@modelcontextprotocol/sdk': patch --- fix(server): accept structurally compatible Zod v4 schemas