From 2ac2689dcca5a1c504df51b6e86333ba8ba9fe25 Mon Sep 17 00:00:00 2001 From: Konstantin Konstantinov Date: Tue, 31 Mar 2026 08:48:07 +0300 Subject: [PATCH 1/4] fix(core): make fromJsonSchema() use runtime-aware default validator via _shims --- docs/migration-SKILL.md | 2 +- docs/migration.md | 6 ++-- packages/client/src/fromJsonSchema.ts | 31 +++++++++++++++++++ packages/client/src/index.ts | 3 ++ packages/core/src/exports/public/index.ts | 3 +- .../core/src/validators/fromJsonSchema.ts | 5 +++ packages/server/src/fromJsonSchema.ts | 30 ++++++++++++++++++ packages/server/src/index.ts | 3 ++ 8 files changed, 78 insertions(+), 5 deletions(-) create mode 100644 packages/client/src/fromJsonSchema.ts create mode 100644 packages/server/src/fromJsonSchema.ts diff --git a/docs/migration-SKILL.md b/docs/migration-SKILL.md index c2f42b5f50..a66f9f50d0 100644 --- a/docs/migration-SKILL.md +++ b/docs/migration-SKILL.md @@ -209,7 +209,7 @@ Zod schemas, all callback return types. Note: `callTool()` and `request()` signa The variadic `.tool()`, `.prompt()`, `.resource()` methods are removed. Use the `register*` methods with a config object. -**IMPORTANT**: v2 requires schema objects implementing [Standard Schema](https://standardschema.dev/) — raw shapes like `{ name: z.string() }` are no longer supported. Wrap with `z.object()` (Zod v4), or use ArkType's `type({...})`, or Valibot. For raw JSON Schema, wrap with `fromJsonSchema(schema, validator)` from `@modelcontextprotocol/server`. Applies to `inputSchema`, `outputSchema`, and `argsSchema`. +**IMPORTANT**: v2 requires schema objects implementing [Standard Schema](https://standardschema.dev/) — raw shapes like `{ name: z.string() }` are no longer supported. Wrap with `z.object()` (Zod v4), or use ArkType's `type({...})`, or Valibot. For raw JSON Schema, wrap with `fromJsonSchema(schema)` from `@modelcontextprotocol/server` (validator defaults automatically; pass an explicit validator for custom configurations). Applies to `inputSchema`, `outputSchema`, and `argsSchema`. ### Tools diff --git a/docs/migration.md b/docs/migration.md index 5d7763cbe0..9b91c567e4 100644 --- a/docs/migration.md +++ b/docs/migration.md @@ -250,10 +250,10 @@ server.registerTool('greet', { inputSchema: type({ name: 'string' }) }, async ({ name }) => { ... }); -// Raw JSON Schema via fromJsonSchema -import { fromJsonSchema, AjvJsonSchemaValidator } from '@modelcontextprotocol/server'; +// Raw JSON Schema via fromJsonSchema (validator defaults to runtime-appropriate choice) +import { fromJsonSchema } from '@modelcontextprotocol/server'; server.registerTool('greet', { - inputSchema: fromJsonSchema({ type: 'object', properties: { name: { type: 'string' } } }, new AjvJsonSchemaValidator()) + inputSchema: fromJsonSchema({ type: 'object', properties: { name: { type: 'string' } } }) }, handler); // For tools with no parameters, use z.object({}) diff --git a/packages/client/src/fromJsonSchema.ts b/packages/client/src/fromJsonSchema.ts new file mode 100644 index 0000000000..822eb266c0 --- /dev/null +++ b/packages/client/src/fromJsonSchema.ts @@ -0,0 +1,31 @@ +/** + * Runtime-aware wrapper of {@linkcode coreFromJsonSchema | fromJsonSchema} from core. + * + * Uses the `_shims` pattern to select the default validator: + * - Node.js: {@linkcode index.AjvJsonSchemaValidator | AjvJsonSchemaValidator} + * - Browser: {@linkcode index.CfWorkerJsonSchemaValidator | CfWorkerJsonSchemaValidator} + * - Cloudflare Workers: {@linkcode index.CfWorkerJsonSchemaValidator | CfWorkerJsonSchemaValidator} + */ +import { DefaultJsonSchemaValidator } from '@modelcontextprotocol/client/_shims'; +import type { JsonSchemaType, jsonSchemaValidator, StandardSchemaWithJSON } from '@modelcontextprotocol/core'; +import { fromJsonSchema as coreFromJsonSchema } from '@modelcontextprotocol/core'; + +let _defaultValidator: jsonSchemaValidator | undefined; + +/** + * Wrap a raw JSON Schema object as a {@linkcode StandardSchemaWithJSON} so it can be + * passed to `registerTool` / `registerPrompt`. Use this when you already have JSON + * Schema (e.g. from TypeBox, or hand-written) and want to register it without going + * through a Standard Schema library. + * + * The callback arguments will be typed `unknown` (raw JSON Schema has no TypeScript + * types attached). Cast at the call site, or use the generic `fromJsonSchema(...)`. + * + * @param schema - A JSON Schema object describing the expected shape + * @param validator - Optional validator provider. Defaults to the runtime-appropriate + * validator ({@linkcode index.AjvJsonSchemaValidator | AjvJsonSchemaValidator} on Node.js, + * {@linkcode index.CfWorkerJsonSchemaValidator | CfWorkerJsonSchemaValidator} on browsers/edge runtimes). + */ +export function fromJsonSchema(schema: JsonSchemaType, validator?: jsonSchemaValidator): StandardSchemaWithJSON { + return coreFromJsonSchema(schema, validator ?? (_defaultValidator ??= new DefaultJsonSchemaValidator())); +} diff --git a/packages/client/src/index.ts b/packages/client/src/index.ts index d1af95103d..dcce9aebaa 100644 --- a/packages/client/src/index.ts +++ b/packages/client/src/index.ts @@ -74,5 +74,8 @@ export { WebSocketClientTransport } from './client/websocket.js'; // experimental exports export { ExperimentalClientTasks } from './experimental/tasks/client.js'; +// runtime-aware wrapper (shadows core/public's fromJsonSchema with optional validator) +export { fromJsonSchema } from './fromJsonSchema.js'; + // re-export curated public API from core export * from '@modelcontextprotocol/core/public'; diff --git a/packages/core/src/exports/public/index.ts b/packages/core/src/exports/public/index.ts index 2834702993..7f386ea377 100644 --- a/packages/core/src/exports/public/index.ts +++ b/packages/core/src/exports/public/index.ts @@ -138,5 +138,6 @@ export type { StandardSchemaWithJSON } from '../../util/standardSchema.js'; export { AjvJsonSchemaValidator } from '../../validators/ajvProvider.js'; export type { CfWorkerSchemaDraft } from '../../validators/cfWorkerProvider.js'; export { CfWorkerJsonSchemaValidator } from '../../validators/cfWorkerProvider.js'; -export { fromJsonSchema } from '../../validators/fromJsonSchema.js'; +// fromJsonSchema is intentionally NOT exported here — the server and client packages +// provide runtime-aware wrappers that default to the appropriate validator via _shims. export type { JsonSchemaType, JsonSchemaValidator, jsonSchemaValidator, JsonSchemaValidatorResult } from '../../validators/types.js'; diff --git a/packages/core/src/validators/fromJsonSchema.ts b/packages/core/src/validators/fromJsonSchema.ts index ec4ba05d65..73db24e8cc 100644 --- a/packages/core/src/validators/fromJsonSchema.ts +++ b/packages/core/src/validators/fromJsonSchema.ts @@ -10,6 +10,11 @@ import type { JsonSchemaType, jsonSchemaValidator } from './types.js'; * The callback arguments will be typed `unknown` (raw JSON Schema has no TypeScript * types attached). Cast at the call site, or use the generic `fromJsonSchema(...)`. * + * @param schema - A JSON Schema object describing the expected shape + * @param validator - A validator provider. When importing `fromJsonSchema` from + * `@modelcontextprotocol/server` or `@modelcontextprotocol/client`, a runtime-appropriate + * default is provided automatically (AJV on Node.js, CfWorker on edge runtimes). + * * @example * ```ts source="./fromJsonSchema.examples.ts#fromJsonSchema_basicUsage" * const inputSchema = fromJsonSchema<{ name: string }>( diff --git a/packages/server/src/fromJsonSchema.ts b/packages/server/src/fromJsonSchema.ts new file mode 100644 index 0000000000..892521b084 --- /dev/null +++ b/packages/server/src/fromJsonSchema.ts @@ -0,0 +1,30 @@ +/** + * Runtime-aware wrapper of {@linkcode coreFromJsonSchema | fromJsonSchema} from core. + * + * Uses the `_shims` pattern to select the default validator: + * - Node.js: {@linkcode index.AjvJsonSchemaValidator | AjvJsonSchemaValidator} + * - Cloudflare Workers: {@linkcode index.CfWorkerJsonSchemaValidator | CfWorkerJsonSchemaValidator} + */ +import type { JsonSchemaType, jsonSchemaValidator, StandardSchemaWithJSON } from '@modelcontextprotocol/core'; +import { fromJsonSchema as coreFromJsonSchema } from '@modelcontextprotocol/core'; +import { DefaultJsonSchemaValidator } from '@modelcontextprotocol/server/_shims'; + +let _defaultValidator: jsonSchemaValidator | undefined; + +/** + * Wrap a raw JSON Schema object as a {@linkcode StandardSchemaWithJSON} so it can be + * passed to `registerTool` / `registerPrompt`. Use this when you already have JSON + * Schema (e.g. from TypeBox, or hand-written) and want to register it without going + * through a Standard Schema library. + * + * The callback arguments will be typed `unknown` (raw JSON Schema has no TypeScript + * types attached). Cast at the call site, or use the generic `fromJsonSchema(...)`. + * + * @param schema - A JSON Schema object describing the expected shape + * @param validator - Optional validator provider. Defaults to the runtime-appropriate + * validator ({@linkcode index.AjvJsonSchemaValidator | AjvJsonSchemaValidator} on Node.js, + * {@linkcode index.CfWorkerJsonSchemaValidator | CfWorkerJsonSchemaValidator} on edge runtimes). + */ +export function fromJsonSchema(schema: JsonSchemaType, validator?: jsonSchemaValidator): StandardSchemaWithJSON { + return coreFromJsonSchema(schema, validator ?? (_defaultValidator ??= new DefaultJsonSchemaValidator())); +} diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index c680dffe76..6e1bba28db 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -43,5 +43,8 @@ export type { CreateTaskRequestHandler, TaskRequestHandler, ToolTaskHandler } fr export { ExperimentalMcpServerTasks } from './experimental/tasks/mcpServer.js'; export { ExperimentalServerTasks } from './experimental/tasks/server.js'; +// runtime-aware wrapper (shadows core/public's fromJsonSchema with optional validator) +export { fromJsonSchema } from './fromJsonSchema.js'; + // re-export curated public API from core export * from '@modelcontextprotocol/core/public'; From a5d3980dcf0dd50391cb1400ae5f07d6398f1cd6 Mon Sep 17 00:00:00 2001 From: Konstantin Konstantinov Date: Tue, 31 Mar 2026 08:54:53 +0300 Subject: [PATCH 2/4] clean up --- packages/client/src/fromJsonSchema.ts | 22 ---------------------- packages/server/src/fromJsonSchema.ts | 21 --------------------- 2 files changed, 43 deletions(-) diff --git a/packages/client/src/fromJsonSchema.ts b/packages/client/src/fromJsonSchema.ts index 822eb266c0..575db2a8c4 100644 --- a/packages/client/src/fromJsonSchema.ts +++ b/packages/client/src/fromJsonSchema.ts @@ -1,31 +1,9 @@ -/** - * Runtime-aware wrapper of {@linkcode coreFromJsonSchema | fromJsonSchema} from core. - * - * Uses the `_shims` pattern to select the default validator: - * - Node.js: {@linkcode index.AjvJsonSchemaValidator | AjvJsonSchemaValidator} - * - Browser: {@linkcode index.CfWorkerJsonSchemaValidator | CfWorkerJsonSchemaValidator} - * - Cloudflare Workers: {@linkcode index.CfWorkerJsonSchemaValidator | CfWorkerJsonSchemaValidator} - */ import { DefaultJsonSchemaValidator } from '@modelcontextprotocol/client/_shims'; import type { JsonSchemaType, jsonSchemaValidator, StandardSchemaWithJSON } from '@modelcontextprotocol/core'; import { fromJsonSchema as coreFromJsonSchema } from '@modelcontextprotocol/core'; let _defaultValidator: jsonSchemaValidator | undefined; -/** - * Wrap a raw JSON Schema object as a {@linkcode StandardSchemaWithJSON} so it can be - * passed to `registerTool` / `registerPrompt`. Use this when you already have JSON - * Schema (e.g. from TypeBox, or hand-written) and want to register it without going - * through a Standard Schema library. - * - * The callback arguments will be typed `unknown` (raw JSON Schema has no TypeScript - * types attached). Cast at the call site, or use the generic `fromJsonSchema(...)`. - * - * @param schema - A JSON Schema object describing the expected shape - * @param validator - Optional validator provider. Defaults to the runtime-appropriate - * validator ({@linkcode index.AjvJsonSchemaValidator | AjvJsonSchemaValidator} on Node.js, - * {@linkcode index.CfWorkerJsonSchemaValidator | CfWorkerJsonSchemaValidator} on browsers/edge runtimes). - */ export function fromJsonSchema(schema: JsonSchemaType, validator?: jsonSchemaValidator): StandardSchemaWithJSON { return coreFromJsonSchema(schema, validator ?? (_defaultValidator ??= new DefaultJsonSchemaValidator())); } diff --git a/packages/server/src/fromJsonSchema.ts b/packages/server/src/fromJsonSchema.ts index 892521b084..180ef2defc 100644 --- a/packages/server/src/fromJsonSchema.ts +++ b/packages/server/src/fromJsonSchema.ts @@ -1,30 +1,9 @@ -/** - * Runtime-aware wrapper of {@linkcode coreFromJsonSchema | fromJsonSchema} from core. - * - * Uses the `_shims` pattern to select the default validator: - * - Node.js: {@linkcode index.AjvJsonSchemaValidator | AjvJsonSchemaValidator} - * - Cloudflare Workers: {@linkcode index.CfWorkerJsonSchemaValidator | CfWorkerJsonSchemaValidator} - */ import type { JsonSchemaType, jsonSchemaValidator, StandardSchemaWithJSON } from '@modelcontextprotocol/core'; import { fromJsonSchema as coreFromJsonSchema } from '@modelcontextprotocol/core'; import { DefaultJsonSchemaValidator } from '@modelcontextprotocol/server/_shims'; let _defaultValidator: jsonSchemaValidator | undefined; -/** - * Wrap a raw JSON Schema object as a {@linkcode StandardSchemaWithJSON} so it can be - * passed to `registerTool` / `registerPrompt`. Use this when you already have JSON - * Schema (e.g. from TypeBox, or hand-written) and want to register it without going - * through a Standard Schema library. - * - * The callback arguments will be typed `unknown` (raw JSON Schema has no TypeScript - * types attached). Cast at the call site, or use the generic `fromJsonSchema(...)`. - * - * @param schema - A JSON Schema object describing the expected shape - * @param validator - Optional validator provider. Defaults to the runtime-appropriate - * validator ({@linkcode index.AjvJsonSchemaValidator | AjvJsonSchemaValidator} on Node.js, - * {@linkcode index.CfWorkerJsonSchemaValidator | CfWorkerJsonSchemaValidator} on edge runtimes). - */ export function fromJsonSchema(schema: JsonSchemaType, validator?: jsonSchemaValidator): StandardSchemaWithJSON { return coreFromJsonSchema(schema, validator ?? (_defaultValidator ??= new DefaultJsonSchemaValidator())); } From a677ba3489b26f18ffc696240611ea524d1bfbec Mon Sep 17 00:00:00 2001 From: Konstantin Konstantinov Date: Tue, 31 Mar 2026 09:04:24 +0300 Subject: [PATCH 3/4] add test with default validator --- test/integration/test/standardSchema.test.ts | 37 +++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/test/integration/test/standardSchema.test.ts b/test/integration/test/standardSchema.test.ts index c3817b1208..59fac41a12 100644 --- a/test/integration/test/standardSchema.test.ts +++ b/test/integration/test/standardSchema.test.ts @@ -6,7 +6,7 @@ import { Client } from '@modelcontextprotocol/client'; import type { TextContent } from '@modelcontextprotocol/core'; import { AjvJsonSchemaValidator, fromJsonSchema, InMemoryTransport } from '@modelcontextprotocol/core'; -import { completable, McpServer } from '@modelcontextprotocol/server'; +import { completable, fromJsonSchema as serverFromJsonSchema, McpServer } from '@modelcontextprotocol/server'; import { toStandardJsonSchema } from '@valibot/to-json-schema'; import { type } from 'arktype'; import * as v from 'valibot'; @@ -428,6 +428,41 @@ describe('Standard Schema Support', () => { }); }); + describe('fromJsonSchema with default validator (server wrapper)', () => { + test('should use runtime-appropriate default validator when none is provided', async () => { + const inputSchema = serverFromJsonSchema<{ name: string }>( + { type: 'object', properties: { name: { type: 'string' } }, required: ['name'] } + ); + + mcpServer.registerTool('greet-default', { inputSchema }, async ({ name }) => ({ + content: [{ type: 'text', text: `Hello, ${name}!` }] + })); + + await connectClientAndServer(); + + const result = await client.request({ method: 'tools/call', params: { name: 'greet-default', arguments: { name: 'World' } } }); + expect((result.content[0] as TextContent).text).toBe('Hello, World!'); + }); + + test('should reject invalid input with default validator', async () => { + const inputSchema = serverFromJsonSchema( + { type: 'object', properties: { count: { type: 'number' } }, required: ['count'] } + ); + + mcpServer.registerTool('double-default', { inputSchema }, async args => { + const { count } = args as { count: number }; + return { content: [{ type: 'text', text: `${count * 2}` }] }; + }); + + await connectClientAndServer(); + + const result = await client.request({ method: 'tools/call', params: { name: 'double-default', arguments: { count: 'not a number' } } }); + expect(result.isError).toBe(true); + const errorText = (result.content[0] as TextContent).text; + expect(errorText).toContain('Input validation error'); + }); + }); + describe('Prompt completions with Zod completable', () => { // Note: completable() is currently Zod-specific // These tests verify that Zod schemas with completable still work From 05464f2dae02de0ae2ea3dbab565b61239464e04 Mon Sep 17 00:00:00 2001 From: Konstantin Konstantinov Date: Tue, 31 Mar 2026 09:06:10 +0300 Subject: [PATCH 4/4] add test with default validator --- test/integration/test/standardSchema.test.ts | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/test/integration/test/standardSchema.test.ts b/test/integration/test/standardSchema.test.ts index 59fac41a12..67f16c5fa7 100644 --- a/test/integration/test/standardSchema.test.ts +++ b/test/integration/test/standardSchema.test.ts @@ -430,9 +430,11 @@ describe('Standard Schema Support', () => { describe('fromJsonSchema with default validator (server wrapper)', () => { test('should use runtime-appropriate default validator when none is provided', async () => { - const inputSchema = serverFromJsonSchema<{ name: string }>( - { type: 'object', properties: { name: { type: 'string' } }, required: ['name'] } - ); + const inputSchema = serverFromJsonSchema<{ name: string }>({ + type: 'object', + properties: { name: { type: 'string' } }, + required: ['name'] + }); mcpServer.registerTool('greet-default', { inputSchema }, async ({ name }) => ({ content: [{ type: 'text', text: `Hello, ${name}!` }] @@ -445,9 +447,7 @@ describe('Standard Schema Support', () => { }); test('should reject invalid input with default validator', async () => { - const inputSchema = serverFromJsonSchema( - { type: 'object', properties: { count: { type: 'number' } }, required: ['count'] } - ); + const inputSchema = serverFromJsonSchema({ type: 'object', properties: { count: { type: 'number' } }, required: ['count'] }); mcpServer.registerTool('double-default', { inputSchema }, async args => { const { count } = args as { count: number }; @@ -456,7 +456,10 @@ describe('Standard Schema Support', () => { await connectClientAndServer(); - const result = await client.request({ method: 'tools/call', params: { name: 'double-default', arguments: { count: 'not a number' } } }); + const result = await client.request({ + method: 'tools/call', + params: { name: 'double-default', arguments: { count: 'not a number' } } + }); expect(result.isError).toBe(true); const errorText = (result.content[0] as TextContent).text; expect(errorText).toContain('Input validation error');