Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
6 changes: 6 additions & 0 deletions .changeset/zod-jsonschema-fallback.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@modelcontextprotocol/server': patch
'@modelcontextprotocol/client': patch
---
Comment thread
felixweinberger marked this conversation as resolved.

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`.
34 changes: 33 additions & 1 deletion packages/core/src/util/standardSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Input = unknown, Output = Input> {
Expand Down Expand Up @@ -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.
*
Expand All @@ -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<string, unknown> {
const result = schema['~standard'].jsonSchema[io]({ target: 'draft-2020-12' });
const std = schema['~standard'];
let result: Record<string, unknown>;
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.'
Comment thread
felixweinberger marked this conversation as resolved.
);
}
result = z.toJSONSchema(schema as unknown as z.ZodType, { target: 'draft-2020-12', io }) as Record<string, unknown>;
Comment thread
claude[bot] marked this conversation as resolved.
} 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)}). ` +
Expand Down
37 changes: 37 additions & 0 deletions packages/core/test/util/standardSchema.zodFallback.test.ts
Original file line number Diff line number Diff line change
@@ -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<typeof standardSchemaToJsonSchema>[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<string, unknown>;
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<string, unknown>)?.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/);
});
});
2 changes: 1 addition & 1 deletion pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion pnpm-workspace.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Loading