Skip to content

Commit 1313b78

Browse files
fix(core): fall back to z.toJSONSchema for zod schemas without ~standard.jsonSchema
Schemas from zod <4.2.0 implement StandardSchemaV1 but not StandardJSONSchemaV1, so standardSchemaToJsonSchema crashed at `undefined[io]` on tools/list. Detect `vendor: 'zod'` without `jsonSchema` and fall back to the bundled z.toJSONSchema() with a one-time warning. Non-zod libraries without jsonSchema get a clear error pointing at fromJsonSchema(). Bumps catalog zod to ^4.2.0.
1 parent 9ed62fe commit 1313b78

File tree

5 files changed

+78
-3
lines changed

5 files changed

+78
-3
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@modelcontextprotocol/server': patch
3+
'@modelcontextprotocol/client': patch
4+
---
5+
6+
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`.

packages/core/src/util/standardSchema.ts

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66

77
/* eslint-disable @typescript-eslint/no-namespace */
88

9+
import * as z from 'zod/v4';
10+
911
// Standard Schema interfaces — vendored from https://standardschema.dev (spec v1, Jan 2025)
1012

1113
export interface StandardTypedV1<Input = unknown, Output = Input> {
@@ -138,6 +140,8 @@ export function isStandardSchemaWithJSON(schema: unknown): schema is StandardSch
138140

139141
// JSON Schema conversion
140142

143+
let warnedZodFallback = false;
144+
141145
/**
142146
* Converts a StandardSchema to JSON Schema for use as an MCP tool/prompt schema.
143147
*
@@ -149,7 +153,35 @@ export function isStandardSchemaWithJSON(schema: unknown): schema is StandardSch
149153
* since that cannot satisfy the MCP spec.
150154
*/
151155
export function standardSchemaToJsonSchema(schema: StandardJSONSchemaV1, io: 'input' | 'output' = 'input'): Record<string, unknown> {
152-
const result = schema['~standard'].jsonSchema[io]({ target: 'draft-2020-12' });
156+
const std = schema['~standard'];
157+
let result: Record<string, unknown>;
158+
if (std.jsonSchema) {
159+
result = std.jsonSchema[io]({ target: 'draft-2020-12' });
160+
} else if (std.vendor === 'zod') {
161+
// zod 4.0–4.1 implements StandardSchemaV1 but not StandardJSONSchemaV1 (`~standard.jsonSchema`).
162+
// The SDK already bundles zod 4, so fall back to its converter rather than crashing on tools/list.
163+
// zod 3 schemas (which also report vendor 'zod') have `_def` but not `_zod`; the SDK-bundled
164+
// zod 4 `z.toJSONSchema()` cannot introspect them, so throw a clear error instead of crashing.
165+
if (!('_zod' in (schema as object))) {
166+
throw new Error(
167+
'Schema appears to be from zod 3, which the SDK cannot convert to JSON Schema. ' +
168+
'Upgrade to zod >=4.2.0, or wrap your JSON Schema with fromJsonSchema().'
169+
);
170+
}
171+
if (!warnedZodFallback) {
172+
warnedZodFallback = true;
173+
console.warn(
174+
'[@modelcontextprotocol/sdk] Your zod version does not implement `~standard.jsonSchema` (added in zod 4.2.0). ' +
175+
'Falling back to z.toJSONSchema(). Upgrade to zod >=4.2.0 to silence this warning.'
176+
);
177+
}
178+
result = z.toJSONSchema(schema as unknown as z.ZodType, { target: 'draft-2020-12', io }) as Record<string, unknown>;
179+
} else {
180+
throw new Error(
181+
`Schema library "${std.vendor}" does not implement StandardJSONSchemaV1 (\`~standard.jsonSchema\`). ` +
182+
`Upgrade to a version that does, or wrap your JSON Schema with fromJsonSchema().`
183+
);
184+
}
153185
if (result.type !== undefined && result.type !== 'object') {
154186
throw new Error(
155187
`MCP tool and prompt schemas must describe objects (got type: ${JSON.stringify(result.type)}). ` +
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { describe, expect, it, vi } from 'vitest';
2+
import * as z from 'zod/v4';
3+
import { standardSchemaToJsonSchema } from '../../src/util/standardSchema.js';
4+
5+
type SchemaArg = Parameters<typeof standardSchemaToJsonSchema>[0];
6+
7+
describe('standardSchemaToJsonSchema — zod fallback paths', () => {
8+
it('falls back to z.toJSONSchema for zod 4.0–4.1 (vendor=zod, no ~standard.jsonSchema, has _zod)', () => {
9+
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
10+
const real = z.object({ a: z.string() });
11+
// Simulate zod 4.0–4.1: shadow `~standard` on the real instance with `jsonSchema` removed.
12+
// Keeps the rest of the zod 4 object (including `_zod`) intact so z.toJSONSchema can introspect it.
13+
const { jsonSchema: _drop, ...stdNoJson } = real['~standard'] as unknown as Record<string, unknown>;
14+
void _drop;
15+
Object.defineProperty(real, '~standard', { value: { ...stdNoJson, vendor: 'zod' }, configurable: true });
16+
17+
const result = standardSchemaToJsonSchema(real as unknown as SchemaArg);
18+
expect(result.type).toBe('object');
19+
expect((result.properties as unknown as Record<string, unknown>)?.a).toBeDefined();
20+
expect(warn).toHaveBeenCalledOnce();
21+
expect(warn.mock.calls[0]?.[0]).toContain('zod 4.2.0');
22+
warn.mockRestore();
23+
});
24+
25+
it('throws a clear error for zod 3 (vendor=zod, no ~standard.jsonSchema, no _zod)', () => {
26+
// zod 3.24+ reports `~standard.vendor === 'zod'` but has no `_zod` internal marker.
27+
const zod3ish = { _def: {}, '~standard': { version: 1, vendor: 'zod', validate: () => ({ value: {} }) } };
28+
expect(() => standardSchemaToJsonSchema(zod3ish as unknown as SchemaArg)).toThrow(/zod 3/);
29+
expect(() => standardSchemaToJsonSchema(zod3ish as unknown as SchemaArg)).toThrow(/4\.2\.0/);
30+
});
31+
32+
it('throws a clear error for non-zod libraries without ~standard.jsonSchema', () => {
33+
const fake = { '~standard': { version: 1, vendor: 'mylib', validate: () => ({ value: {} }) } };
34+
expect(() => standardSchemaToJsonSchema(fake as unknown as SchemaArg)).toThrow(/mylib/);
35+
expect(() => standardSchemaToJsonSchema(fake as unknown as SchemaArg)).toThrow(/fromJsonSchema/);
36+
});
37+
});

pnpm-lock.yaml

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pnpm-workspace.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ catalogs:
5050
ajv-formats: ^3.0.1
5151
json-schema-typed: ^8.0.2
5252
pkce-challenge: ^5.0.0
53-
zod: ^4.0
53+
zod: ^4.2.0
5454

5555
enableGlobalVirtualStore: false
5656

0 commit comments

Comments
 (0)