diff --git a/.changeset/fix-zod-44-registertool-types.md b/.changeset/fix-zod-44-registertool-types.md new file mode 100644 index 0000000000..3a990b6ee1 --- /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 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 ?? ''}` }] }; + } + ); + }); + }); +});