From 1313b782cab886e9448d7dff9f52f938d8fa8140 Mon Sep 17 00:00:00 2001 From: Felix Weinberger Date: Wed, 15 Apr 2026 11:57:32 +0000 Subject: [PATCH] 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. --- .changeset/zod-jsonschema-fallback.md | 6 +++ packages/core/src/util/standardSchema.ts | 34 ++++++++++++++++- .../util/standardSchema.zodFallback.test.ts | 37 +++++++++++++++++++ pnpm-lock.yaml | 2 +- pnpm-workspace.yaml | 2 +- 5 files changed, 78 insertions(+), 3 deletions(-) create mode 100644 .changeset/zod-jsonschema-fallback.md create mode 100644 packages/core/test/util/standardSchema.zodFallback.test.ts 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