fix(core): fall back to z.toJSONSchema for zod schemas without ~standard.jsonSchema (#1895)
Co-authored-by: Konstantin Konstantinov <KKonstantinov@users.noreply.github.com>
This commit is contained in:
parent
42cb6b2b72
commit
b256546750
5 changed files with 79 additions and 3 deletions
7
.changeset/zod-jsonschema-fallback.md
Normal file
7
.changeset/zod-jsonschema-fallback.md
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
---
|
||||
'@modelcontextprotocol/core': patch
|
||||
'@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`.
|
||||
|
|
@ -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> {
|
||||
|
|
@ -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<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(
|
||||
'[mcp-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<string, unknown>;
|
||||
} 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)}). ` +
|
||||
|
|
|
|||
37
packages/core/test/util/standardSchema.zodFallback.test.ts
Normal file
37
packages/core/test/util/standardSchema.zodFallback.test.ts
Normal file
|
|
@ -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
pnpm-lock.yaml
generated
2
pnpm-lock.yaml
generated
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue