Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fix-zod-44-registertool-types.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@modelcontextprotocol/sdk': patch
---

fix(server): accept structurally compatible Zod v4 schemas
22 changes: 16 additions & 6 deletions src/server/zod-compat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Output = unknown, Input = unknown> {
_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<string, AnySchema>;

Expand All @@ -30,6 +38,8 @@ export interface ZodV3Internal {

export interface ZodV4Internal {
_zod?: {
output?: unknown;
input?: unknown;
def?: {
type?: string;
value?: unknown;
Expand All @@ -41,9 +51,9 @@ export interface ZodV4Internal {
}

// --- Type inference helpers ---
export type SchemaOutput<S> = S extends z3.ZodTypeAny ? z3.infer<S> : S extends z4.$ZodType ? z4.output<S> : never;
export type SchemaOutput<S> = S extends z3.ZodTypeAny ? z3.infer<S> : S extends ZodV4TypeLike<infer Output, unknown> ? Output : never;

export type SchemaInput<S> = S extends z3.ZodTypeAny ? z3.input<S> : S extends z4.$ZodType ? z4.input<S> : never;
export type SchemaInput<S> = S extends z3.ZodTypeAny ? z3.input<S> : S extends ZodV4TypeLike<unknown, infer Input> ? Input : never;

/**
* Infers the output type from a ZodRawShapeCompat (raw shape object).
Expand All @@ -54,7 +64,7 @@ export type ShapeOutput<Shape extends ZodRawShapeCompat> = {
};

// --- 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;
Expand All @@ -81,7 +91,7 @@ export function safeParse<S extends AnySchema>(
): { success: true; data: SchemaOutput<S> } | { 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<S> } | { success: false; error: unknown };
}
const v3Schema = schema as z3.ZodTypeAny;
Expand All @@ -95,7 +105,7 @@ export async function safeParseAsync<S extends AnySchema>(
): Promise<{ success: true; data: SchemaOutput<S> } | { 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<S> } | { success: false; error: unknown };
}
const v3Schema = schema as z3.ZodTypeAny;
Expand Down
36 changes: 36 additions & 0 deletions test/issues/test_1987_zod_v4_type_identity.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { McpServer } from '../../src/server/mcp.js';

type ExternalZodV4Schema<Output, Input = Output> = {
_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<string>;
const age = {} as ExternalZodV4Schema<number | undefined, number | undefined>;

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 ?? ''}` }] };
}
);
});
});
});
Loading