diff --git a/.changeset/zod-jsonschema-fallback.md b/.changeset/zod-jsonschema-fallback.md
new file mode 100644
index 000000000..97026cf21
--- /dev/null
+++ b/.changeset/zod-jsonschema-fallback.md
@@ -0,0 +1,6 @@
+---
+'@modelcontextprotocol/server': patch
+'@modelcontextprotocol/client': patch
+---
+
+Fix runtime crash on `tools/list` when a tool's `inputSchema` comes from zod 4.0–4.1. The SDK requires `~standard.jsonSchema` (StandardJSONSchemaV1, added in zod 4.2.0); previously a missing `jsonSchema` crashed at `undefined[io]`. `standardSchemaToJsonSchema` now detects zod 4 schemas lacking `jsonSchema` and falls back to the SDK-bundled `z.toJSONSchema()`, emitting a one-time console warning. zod 3 schemas (which the bundled zod 4 converter cannot introspect) and non-zod schema libraries without `jsonSchema` get a clear error pointing to `fromJsonSchema()`. The workspace zod catalog is also bumped to `^4.2.0`.
diff --git a/packages/core/src/util/standardSchema.ts b/packages/core/src/util/standardSchema.ts
index 9817dc39a..2e4737647 100644
--- a/packages/core/src/util/standardSchema.ts
+++ b/packages/core/src/util/standardSchema.ts
@@ -6,6 +6,8 @@
/* eslint-disable @typescript-eslint/no-namespace */
+import * as z from 'zod/v4';
+
// Standard Schema interfaces — vendored from https://standardschema.dev (spec v1, Jan 2025)
export interface StandardTypedV1 {
@@ -138,6 +140,8 @@ export function isStandardSchemaWithJSON(schema: unknown): schema is StandardSch
// JSON Schema conversion
+let warnedZodFallback = false;
+
/**
* Converts a StandardSchema to JSON Schema for use as an MCP tool/prompt schema.
*
@@ -149,7 +153,35 @@ export function isStandardSchemaWithJSON(schema: unknown): schema is StandardSch
* since that cannot satisfy the MCP spec.
*/
export function standardSchemaToJsonSchema(schema: StandardJSONSchemaV1, io: 'input' | 'output' = 'input'): Record {
- const result = schema['~standard'].jsonSchema[io]({ target: 'draft-2020-12' });
+ const std = schema['~standard'];
+ let result: Record;
+ if (std.jsonSchema) {
+ result = std.jsonSchema[io]({ target: 'draft-2020-12' });
+ } else if (std.vendor === 'zod') {
+ // zod 4.0–4.1 implements StandardSchemaV1 but not StandardJSONSchemaV1 (`~standard.jsonSchema`).
+ // The SDK already bundles zod 4, so fall back to its converter rather than crashing on tools/list.
+ // zod 3 schemas (which also report vendor 'zod') have `_def` but not `_zod`; the SDK-bundled
+ // zod 4 `z.toJSONSchema()` cannot introspect them, so throw a clear error instead of crashing.
+ if (!('_zod' in (schema as object))) {
+ throw new Error(
+ 'Schema appears to be from zod 3, which the SDK cannot convert to JSON Schema. ' +
+ 'Upgrade to zod >=4.2.0, or wrap your JSON Schema with fromJsonSchema().'
+ );
+ }
+ if (!warnedZodFallback) {
+ warnedZodFallback = true;
+ console.warn(
+ '[@modelcontextprotocol/sdk] Your zod version does not implement `~standard.jsonSchema` (added in zod 4.2.0). ' +
+ 'Falling back to z.toJSONSchema(). Upgrade to zod >=4.2.0 to silence this warning.'
+ );
+ }
+ result = z.toJSONSchema(schema as unknown as z.ZodType, { target: 'draft-2020-12', io }) as Record;
+ } else {
+ throw new Error(
+ `Schema library "${std.vendor}" does not implement StandardJSONSchemaV1 (\`~standard.jsonSchema\`). ` +
+ `Upgrade to a version that does, or wrap your JSON Schema with fromJsonSchema().`
+ );
+ }
if (result.type !== undefined && result.type !== 'object') {
throw new Error(
`MCP tool and prompt schemas must describe objects (got type: ${JSON.stringify(result.type)}). ` +
diff --git a/packages/core/test/util/standardSchema.zodFallback.test.ts b/packages/core/test/util/standardSchema.zodFallback.test.ts
new file mode 100644
index 000000000..d825a3271
--- /dev/null
+++ b/packages/core/test/util/standardSchema.zodFallback.test.ts
@@ -0,0 +1,37 @@
+import { describe, expect, it, vi } from 'vitest';
+import * as z from 'zod/v4';
+import { standardSchemaToJsonSchema } from '../../src/util/standardSchema.js';
+
+type SchemaArg = Parameters[0];
+
+describe('standardSchemaToJsonSchema — zod fallback paths', () => {
+ it('falls back to z.toJSONSchema for zod 4.0–4.1 (vendor=zod, no ~standard.jsonSchema, has _zod)', () => {
+ const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
+ const real = z.object({ a: z.string() });
+ // Simulate zod 4.0–4.1: shadow `~standard` on the real instance with `jsonSchema` removed.
+ // Keeps the rest of the zod 4 object (including `_zod`) intact so z.toJSONSchema can introspect it.
+ const { jsonSchema: _drop, ...stdNoJson } = real['~standard'] as unknown as Record;
+ void _drop;
+ Object.defineProperty(real, '~standard', { value: { ...stdNoJson, vendor: 'zod' }, configurable: true });
+
+ const result = standardSchemaToJsonSchema(real as unknown as SchemaArg);
+ expect(result.type).toBe('object');
+ expect((result.properties as unknown as Record)?.a).toBeDefined();
+ expect(warn).toHaveBeenCalledOnce();
+ expect(warn.mock.calls[0]?.[0]).toContain('zod 4.2.0');
+ warn.mockRestore();
+ });
+
+ it('throws a clear error for zod 3 (vendor=zod, no ~standard.jsonSchema, no _zod)', () => {
+ // zod 3.24+ reports `~standard.vendor === 'zod'` but has no `_zod` internal marker.
+ const zod3ish = { _def: {}, '~standard': { version: 1, vendor: 'zod', validate: () => ({ value: {} }) } };
+ expect(() => standardSchemaToJsonSchema(zod3ish as unknown as SchemaArg)).toThrow(/zod 3/);
+ expect(() => standardSchemaToJsonSchema(zod3ish as unknown as SchemaArg)).toThrow(/4\.2\.0/);
+ });
+
+ it('throws a clear error for non-zod libraries without ~standard.jsonSchema', () => {
+ const fake = { '~standard': { version: 1, vendor: 'mylib', validate: () => ({ value: {} }) } };
+ expect(() => standardSchemaToJsonSchema(fake as unknown as SchemaArg)).toThrow(/mylib/);
+ expect(() => standardSchemaToJsonSchema(fake as unknown as SchemaArg)).toThrow(/fromJsonSchema/);
+ });
+});
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 899586750..5b2312c22 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -127,7 +127,7 @@ catalogs:
specifier: ^5.0.0
version: 5.0.1
zod:
- specifier: ^4.0
+ specifier: ^4.2.0
version: 4.3.6
overrides:
diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml
index ae812a28b..cdfb340b6 100644
--- a/pnpm-workspace.yaml
+++ b/pnpm-workspace.yaml
@@ -50,7 +50,7 @@ catalogs:
ajv-formats: ^3.0.1
json-schema-typed: ^8.0.2
pkce-challenge: ^5.0.0
- zod: ^4.0
+ zod: ^4.2.0
enableGlobalVirtualStore: false