Skip to content

Commit b80761d

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 b80761d

File tree

5 files changed

+63
-3
lines changed

5 files changed

+63
-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.2.0. The SDK requires `~standard.jsonSchema` (StandardJSONSchemaV1, added in zod 4.2.0); previously a missing `jsonSchema` crashed at `undefined[io]`. `standardSchemaToJsonSchema` now detects `vendor: 'zod'` without `jsonSchema` and falls back to the SDK-bundled `z.toJSONSchema()`, emitting a one-time console warning. 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: 25 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> {
@@ -148,8 +150,30 @@ export function isStandardSchemaWithJSON(schema: unknown): schema is StandardSch
148150
* Throws if the schema has an explicit non-object `type` (e.g. `z.string()`),
149151
* since that cannot satisfy the MCP spec.
150152
*/
153+
let warnedZodFallback = false;
154+
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.2.0 implements StandardSchemaV1 but not StandardJSONSchemaV1 (`~standard.jsonSchema`).
162+
// The SDK already bundles zod, so fall back to its converter rather than crashing on tools/list.
163+
if (!warnedZodFallback) {
164+
warnedZodFallback = true;
165+
console.warn(
166+
'[@modelcontextprotocol/sdk] Your zod version does not implement `~standard.jsonSchema` (added in zod 4.2.0). ' +
167+
'Falling back to z.toJSONSchema(). Upgrade to zod >=4.2.0 to silence this warning.'
168+
);
169+
}
170+
result = z.toJSONSchema(schema as unknown as z.ZodType, { target: 'draft-2020-12', io }) as Record<string, unknown>;
171+
} else {
172+
throw new Error(
173+
`Schema library "${std.vendor}" does not implement StandardJSONSchemaV1 (\`~standard.jsonSchema\`). ` +
174+
`Upgrade to a version that does, or wrap your JSON Schema with fromJsonSchema().`
175+
);
176+
}
153177
if (result.type !== undefined && result.type !== 'object') {
154178
throw new Error(
155179
`MCP tool and prompt schemas must describe objects (got type: ${JSON.stringify(result.type)}). ` +
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
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 <4.2.0 fallback', () => {
8+
it('falls back to z.toJSONSchema when ~standard.jsonSchema is absent (vendor=zod)', () => {
9+
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
10+
const real = z.object({ a: z.string() });
11+
// Simulate zod <4.2.0: shadow `~standard` on the real instance with `jsonSchema` removed.
12+
// Keeps the rest of the zod object 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 non-zod libraries without ~standard.jsonSchema', () => {
26+
const fake = { '~standard': { version: 1, vendor: 'mylib', validate: () => ({ value: {} }) } };
27+
expect(() => standardSchemaToJsonSchema(fake as unknown as SchemaArg)).toThrow(/mylib/);
28+
expect(() => standardSchemaToJsonSchema(fake as unknown as SchemaArg)).toThrow(/fromJsonSchema/);
29+
});
30+
});

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)