diff --git a/.changeset/workerd-shim-vendors-cfworker.md b/.changeset/workerd-shim-vendors-cfworker.md new file mode 100644 index 0000000000..075d1da463 --- /dev/null +++ b/.changeset/workerd-shim-vendors-cfworker.md @@ -0,0 +1,11 @@ +--- +'@modelcontextprotocol/core': minor +'@modelcontextprotocol/client': patch +'@modelcontextprotocol/server': patch +--- + +Make validator backends symmetrical in core and bundle automatic defaults in client/server runtime shims. + +Core no longer re-exports concrete validator providers as runtime values from the root/public barrels. AJV/AJV formats and `@cfworker/json-schema` are optional peer backends behind explicit core validator provider subpaths, used internally by client/server shims. + +Client/server continue to select defaults automatically: Node shims use AJV, while browser/workerd shims use `@cfworker/json-schema`. Those backends are bundled into the shim chunks that select them, so users do not need to install validator packages or import explicit validators for default behavior. Advanced users can still pass their own `jsonSchemaValidator` implementation. diff --git a/docs/migration-SKILL.md b/docs/migration-SKILL.md index 9cff719bbc..592542fce6 100644 --- a/docs/migration-SKILL.md +++ b/docs/migration-SKILL.md @@ -518,11 +518,11 @@ new McpServer( new McpServer({ name: 'server', version: '1.0.0' }, {}); ``` -Access validators explicitly: +Validator behavior: -- Runtime-aware default: `import { DefaultJsonSchemaValidator } from '@modelcontextprotocol/server/_shims';` -- AJV (Node.js): `import { AjvJsonSchemaValidator } from '@modelcontextprotocol/server';` -- CF Worker: `import { CfWorkerJsonSchemaValidator } from '@modelcontextprotocol/server/validators/cf-worker';` +- Do not add validator imports for normal migrations. +- Do not install `ajv`, `ajv-formats`, or `@cfworker/json-schema`; client/server bundle the runtime-selected defaults. +- Advanced users may pass `jsonSchemaValidator: myCustomValidator` with their own validator implementation. ## 15. Migration Steps (apply in this order) diff --git a/docs/migration.md b/docs/migration.md index fecf185996..4926557c15 100644 --- a/docs/migration.md +++ b/docs/migration.md @@ -932,15 +932,18 @@ const server = new McpServer( ); ``` -You can still explicitly override the validator if needed: +You do not need to install or import validator packages for the default behavior. The client and server packages bundle the validator backend selected by the runtime shim. -```typescript -// Runtime-aware default (auto-selects AjvJsonSchemaValidator or CfWorkerJsonSchemaValidator) -import { DefaultJsonSchemaValidator } from '@modelcontextprotocol/server/_shims'; +Advanced users can still override validation by passing an object that implements the SDK's JSON Schema validator interface: -// Specific validators -import { AjvJsonSchemaValidator } from '@modelcontextprotocol/server'; -import { CfWorkerJsonSchemaValidator } from '@modelcontextprotocol/server/validators/cf-worker'; +```typescript +const server = new McpServer( + { name: 'my-server', version: '1.0.0' }, + { + capabilities: { tools: {} }, + jsonSchemaValidator: myCustomValidator + } +); ``` ## Unchanged APIs diff --git a/packages/client/package.json b/packages/client/package.json index 537804b732..d436c04493 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -28,10 +28,6 @@ "types": "./dist/stdio.d.mts", "import": "./dist/stdio.mjs" }, - "./validators/cf-worker": { - "types": "./dist/validators/cfWorker.d.mts", - "import": "./dist/validators/cfWorker.mjs" - }, "./_shims": { "workerd": { "types": "./dist/shimsWorkerd.d.mts", @@ -54,9 +50,6 @@ "types": "./dist/index.d.mts", "typesVersions": { "*": { - "validators/cf-worker": [ - "dist/validators/cfWorker.d.mts" - ], "stdio": [ "dist/stdio.d.mts" ] @@ -93,6 +86,8 @@ "@modelcontextprotocol/eslint-config": "workspace:^", "@modelcontextprotocol/test-helpers": "workspace:^", "@cfworker/json-schema": "catalog:runtimeShared", + "ajv": "catalog:runtimeShared", + "ajv-formats": "catalog:runtimeShared", "@types/content-type": "catalog:devTools", "@types/cross-spawn": "catalog:devTools", "@types/eventsource": "catalog:devTools", diff --git a/packages/client/src/client/client.ts b/packages/client/src/client/client.ts index 5fa2e14d94..009fd14fdd 100644 --- a/packages/client/src/client/client.ts +++ b/packages/client/src/client/client.ts @@ -161,7 +161,7 @@ export type ClientOptions = ProtocolOptions & { * The validator is used to validate structured content returned by tools * against their declared output schemas. * - * @default {@linkcode DefaultJsonSchemaValidator} ({@linkcode index.AjvJsonSchemaValidator | AjvJsonSchemaValidator} on Node.js, `CfWorkerJsonSchemaValidator` on Cloudflare Workers) + * @default Runtime-selected validator (`AjvJsonSchemaValidator` on Node.js, `CfWorkerJsonSchemaValidator` on browser/workerd runtimes) */ jsonSchemaValidator?: jsonSchemaValidator; diff --git a/packages/client/src/shimsNode.ts b/packages/client/src/shimsNode.ts index 00b80abe05..de48ea2de6 100644 --- a/packages/client/src/shimsNode.ts +++ b/packages/client/src/shimsNode.ts @@ -3,7 +3,7 @@ * * This file is selected via package.json export conditions when running in Node.js. */ -export { AjvJsonSchemaValidator as DefaultJsonSchemaValidator } from '@modelcontextprotocol/core'; +export { AjvJsonSchemaValidator as DefaultJsonSchemaValidator } from '@modelcontextprotocol/core/validators/ajv'; /** * Whether `fetch()` may throw `TypeError` due to CORS. CORS is a browser-only concept — diff --git a/packages/client/src/validators/cfWorker.ts b/packages/client/src/validators/cfWorker.ts deleted file mode 100644 index 8d66770e0d..0000000000 --- a/packages/client/src/validators/cfWorker.ts +++ /dev/null @@ -1,10 +0,0 @@ -/** - * Cloudflare Workers JSON Schema validator, available as a sub-path export. - * - * @example - * ```ts - * import { CfWorkerJsonSchemaValidator } from '@modelcontextprotocol/client/validators/cf-worker'; - * ``` - */ -export type { CfWorkerSchemaDraft } from '@modelcontextprotocol/core/validators/cfWorker'; -export { CfWorkerJsonSchemaValidator } from '@modelcontextprotocol/core/validators/cfWorker'; diff --git a/packages/client/test/client/barrelClean.test.ts b/packages/client/test/client/barrelClean.test.ts index 6a7dc02b7f..b8dd65bbb0 100644 --- a/packages/client/test/client/barrelClean.test.ts +++ b/packages/client/test/client/barrelClean.test.ts @@ -8,6 +8,7 @@ import { beforeAll, describe, expect, test } from 'vitest'; const pkgDir = join(dirname(fileURLToPath(import.meta.url)), '../..'); const distDir = join(pkgDir, 'dist'); const NODE_ONLY = /\b(child_process|cross-spawn|node:stream|node:child_process)\b/; +const VALIDATOR_BACKEND_IMPORT = /from\s+["'](?:ajv|ajv-formats|@cfworker\/json-schema)["']/; function chunkImportsOf(entryPath: string): string[] { const visited = new Set(); @@ -52,4 +53,17 @@ describe('@modelcontextprotocol/client root entry is browser-safe', () => { expect(stdio).toMatch(/\bgetDefaultEnvironment\b/); expect(stdio).toMatch(/\bDEFAULT_INHERITED_ENV_VARS\b/); }); + + test('runtime shims vendor default validator backends instead of requiring consumers to install them', () => { + for (const shim of ['shimsNode.mjs', 'shimsWorkerd.mjs', 'shimsBrowser.mjs']) { + const entry = join(distDir, shim); + expect(readFileSync(entry, 'utf8')).not.toMatch(VALIDATOR_BACKEND_IMPORT); + + for (const chunk of chunkImportsOf(entry)) { + expect({ chunk, content: readFileSync(chunk, 'utf8') }).not.toEqual( + expect.objectContaining({ content: expect.stringMatching(VALIDATOR_BACKEND_IMPORT) }) + ); + } + } + }); }); diff --git a/packages/client/test/client/jsonSchemaValidatorOverride.test.ts b/packages/client/test/client/jsonSchemaValidatorOverride.test.ts new file mode 100644 index 0000000000..2e38f618c5 --- /dev/null +++ b/packages/client/test/client/jsonSchemaValidatorOverride.test.ts @@ -0,0 +1,110 @@ +import type { JSONRPCMessage, JsonSchemaType, JsonSchemaValidatorResult, jsonSchemaValidator } from '@modelcontextprotocol/core'; +import { InMemoryTransport, LATEST_PROTOCOL_VERSION } from '@modelcontextprotocol/core'; +import { Client } from '../../src/client/client.js'; +import { fromJsonSchema } from '../../src/fromJsonSchema.js'; + +class RecordingValidator implements jsonSchemaValidator { + schemas: JsonSchemaType[] = []; + values: unknown[] = []; + + getValidator(schema: JsonSchemaType) { + this.schemas.push(schema); + return (value: unknown): JsonSchemaValidatorResult => { + this.values.push(value); + return { valid: true, data: value as T, errorMessage: undefined }; + }; + } +} + +async function connectInitializedClient(client: Client) { + const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); + serverTransport.onmessage = async message => { + if ('method' in message && 'id' in message && message.method === 'initialize') { + await serverTransport.send({ + jsonrpc: '2.0', + id: message.id, + result: { + protocolVersion: LATEST_PROTOCOL_VERSION, + capabilities: { tools: {} }, + serverInfo: { name: 'test-server', version: '1.0.0' } + } + }); + } else if ('method' in message && 'id' in message && message.method === 'tools/list') { + await serverTransport.send({ + jsonrpc: '2.0', + id: message.id, + result: { + tools: [ + { + name: 'structured-tool', + description: 'A tool with structured output', + inputSchema: { type: 'object' }, + outputSchema: { + type: 'object', + properties: { count: { type: 'number' } }, + required: ['count'] + } + } + ] + } + } satisfies JSONRPCMessage); + } + }; + + await Promise.all([client.connect(clientTransport), serverTransport.start()]); + return { clientTransport, serverTransport }; +} + +describe('client JSON Schema validator overrides', () => { + test('Client constructor uses a custom validator for tool output schema caching', async () => { + const validator = new RecordingValidator(); + const client = new Client( + { name: 'test-client', version: '1.0.0' }, + { + capabilities: {}, + jsonSchemaValidator: validator + } + ); + const { clientTransport, serverTransport } = await connectInitializedClient(client); + + await expect(client.listTools()).resolves.toMatchObject({ + tools: [ + { + name: 'structured-tool', + outputSchema: { + type: 'object', + properties: { count: { type: 'number' } }, + required: ['count'] + } + } + ] + }); + + expect(validator.schemas).toEqual([ + { + type: 'object', + properties: { count: { type: 'number' } }, + required: ['count'] + } + ]); + + await client.close(); + await clientTransport.close(); + await serverTransport.close(); + }); + + test('fromJsonSchema uses an explicitly supplied custom validator', async () => { + const validator = new RecordingValidator(); + const schema: JsonSchemaType = { + type: 'object', + properties: { name: { type: 'string' } }, + required: ['name'] + }; + + const standardSchema = fromJsonSchema<{ name: string }>(schema, validator); + expect(standardSchema['~standard'].validate({ name: 123 })).toEqual({ value: { name: 123 } }); + + expect(validator.schemas).toEqual([schema]); + expect(validator.values).toEqual([{ name: 123 }]); + }); +}); diff --git a/packages/client/tsconfig.json b/packages/client/tsconfig.json index a40ee9fd5e..5f47efeceb 100644 --- a/packages/client/tsconfig.json +++ b/packages/client/tsconfig.json @@ -7,6 +7,7 @@ "*": ["./*"], "@modelcontextprotocol/core": ["./node_modules/@modelcontextprotocol/core/src/index.ts"], "@modelcontextprotocol/core/public": ["./node_modules/@modelcontextprotocol/core/src/exports/public/index.ts"], + "@modelcontextprotocol/core/validators/ajv": ["./node_modules/@modelcontextprotocol/core/src/validators/ajvProvider.ts"], "@modelcontextprotocol/core/validators/cfWorker": [ "./node_modules/@modelcontextprotocol/core/src/validators/cfWorkerProvider.ts" ], diff --git a/packages/client/tsdown.config.ts b/packages/client/tsdown.config.ts index c547e6ec9a..83b69f4fca 100644 --- a/packages/client/tsdown.config.ts +++ b/packages/client/tsdown.config.ts @@ -4,7 +4,7 @@ export default defineConfig({ failOnWarn: 'ci-only', // 1. Entry Points // Directly matches package.json include/exclude globs - entry: ['src/index.ts', 'src/stdio.ts', 'src/shimsNode.ts', 'src/shimsWorkerd.ts', 'src/shimsBrowser.ts', 'src/validators/cfWorker.ts'], + entry: ['src/index.ts', 'src/stdio.ts', 'src/shimsNode.ts', 'src/shimsWorkerd.ts', 'src/shimsBrowser.ts'], // 2. Output Configuration format: ['esm'], @@ -27,13 +27,19 @@ export default defineConfig({ paths: { '@modelcontextprotocol/core': ['../core/src/index.ts'], '@modelcontextprotocol/core/public': ['../core/src/exports/public/index.ts'], + '@modelcontextprotocol/core/validators/ajv': ['../core/src/validators/ajvProvider.ts'], '@modelcontextprotocol/core/validators/cfWorker': ['../core/src/validators/cfWorkerProvider.ts'] } } }, - // 5. Vendoring Strategy - Bundle the code for this specific package into the output, - // but treat all other dependencies as external (require/import). - noExternal: ['@modelcontextprotocol/core'], + // 5. Vendoring Strategy - Bundle this package's core implementation into the output, + // but treat most dependencies as external (require/import). + // + // The runtime `_shims` entries choose default JSON Schema validators: AJV on Node and + // @cfworker/json-schema on workerd/browser. Client users should not have to install a + // validator backend just to use the runtime default, so bundle the default backends into + // the shim chunks that select them. + noExternal: ['@modelcontextprotocol/core', 'ajv', 'ajv-formats', '@cfworker/json-schema'], // 6. External packages - keep self-reference imports external for runtime resolution external: ['@modelcontextprotocol/client/_shims'] diff --git a/packages/core/package.json b/packages/core/package.json index 201773736f..beb46ccb88 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -33,6 +33,10 @@ "types": "./src/exports/public/index.ts", "import": "./src/exports/public/index.ts" }, + "./validators/ajv": { + "types": "./src/validators/ajvProvider.ts", + "import": "./src/validators/ajvProvider.ts" + }, "./validators/cfWorker": { "types": "./src/validators/cfWorkerProvider.ts", "import": "./src/validators/cfWorkerProvider.ts" @@ -49,19 +53,25 @@ "client": "tsx scripts/cli.ts client" }, "dependencies": { - "ajv": "catalog:runtimeShared", - "ajv-formats": "catalog:runtimeShared", "json-schema-typed": "catalog:runtimeShared", "zod": "catalog:runtimeShared" }, "peerDependencies": { "@cfworker/json-schema": "catalog:runtimeShared", + "ajv": "catalog:runtimeShared", + "ajv-formats": "catalog:runtimeShared", "zod": "catalog:runtimeShared" }, "peerDependenciesMeta": { "@cfworker/json-schema": { "optional": true }, + "ajv": { + "optional": true + }, + "ajv-formats": { + "optional": true + }, "zod": { "optional": false } @@ -71,6 +81,8 @@ "@modelcontextprotocol/vitest-config": "workspace:^", "@modelcontextprotocol/eslint-config": "workspace:^", "@cfworker/json-schema": "catalog:runtimeShared", + "ajv": "catalog:runtimeShared", + "ajv-formats": "catalog:runtimeShared", "@eslint/js": "catalog:devTools", "@types/content-type": "catalog:devTools", "@types/cors": "catalog:devTools", diff --git a/packages/core/src/exports/public/index.ts b/packages/core/src/exports/public/index.ts index 5c1689ca60..8f9d392735 100644 --- a/packages/core/src/exports/public/index.ts +++ b/packages/core/src/exports/public/index.ts @@ -141,8 +141,8 @@ export { InMemoryTaskMessageQueue, InMemoryTaskStore } from '../../experimental/ export type { SpecTypeName, SpecTypes } from '../../types/specTypeSchema.js'; export { isSpecType, specTypeSchemas } from '../../types/specTypeSchema.js'; export type { StandardSchemaV1, StandardSchemaWithJSON } from '../../util/standardSchema.js'; -export { AjvJsonSchemaValidator } from '../../validators/ajvProvider.js'; -export type { CfWorkerSchemaDraft } from '../../validators/cfWorkerProvider.js'; +export type { AjvJsonSchemaValidator } from '../../validators/ajvProvider.js'; +export type { CfWorkerJsonSchemaValidator, CfWorkerSchemaDraft } from '../../validators/cfWorkerProvider.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/index.ts b/packages/core/src/index.ts index 8bcc9c9591..232bb071f5 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -19,12 +19,12 @@ export * from './util/zodCompat.js'; // experimental exports export * from './experimental/index.js'; -export * from './validators/ajvProvider.js'; -// cfWorkerProvider is intentionally NOT re-exported here: it statically imports -// `@cfworker/json-schema` (an optional peer), and bundling it into the main barrel -// would force that import on all Node consumers. Import via `@modelcontextprotocol/core/validators/cfWorker` -// (used by the workerd/browser `_shims` and the public `/validators/cf-worker` subpaths). -export type { CfWorkerSchemaDraft } from './validators/cfWorkerProvider.js'; +export type { AjvJsonSchemaValidator } from './validators/ajvProvider.js'; +// Validator providers are intentionally NOT re-exported as runtime values here: AJV +// and @cfworker/json-schema are optional peers, and importing either provider from +// the root barrel would force that backend on all consumers. Internal runtime shims +// import concrete defaults via explicit core validator subpaths. +export type { CfWorkerJsonSchemaValidator, CfWorkerSchemaDraft } from './validators/cfWorkerProvider.js'; export * from './validators/fromJsonSchema.js'; /** * JSON Schema validation @@ -32,12 +32,15 @@ export * from './validators/fromJsonSchema.js'; * This module provides configurable JSON Schema validation for the MCP SDK. * Choose a validator based on your runtime environment: * - * - {@linkcode AjvJsonSchemaValidator}: Best for Node.js (default, fastest) - * Bundled — no additional dependencies required. + * - `AjvJsonSchemaValidator`: Best for Node.js (default, fastest) + * Used automatically by client/server Node shims. * * - `CfWorkerJsonSchemaValidator`: Best for edge runtimes - * Import from: `@modelcontextprotocol/server/validators/cf-worker` or `@modelcontextprotocol/client/validators/cf-worker` - * Bundled — no additional dependencies required. + * Used automatically by client/server browser/workerd shims. + * + * Client and server packages bundle their runtime default validator backends, so most users should + * rely on automatic runtime selection. Advanced users can pass their own validator implementation + * through client/server options. * * @example For Node.js with AJV * ```ts source="./index.examples.ts#validation_ajv" diff --git a/packages/core/src/validators/ajvProvider.ts b/packages/core/src/validators/ajvProvider.ts index 820a3d6618..b2e6bbcf18 100644 --- a/packages/core/src/validators/ajvProvider.ts +++ b/packages/core/src/validators/ajvProvider.ts @@ -33,7 +33,7 @@ function createDefaultAjvInstance(): Ajv { * const validator = new AjvJsonSchemaValidator(ajv); * ``` * - * @see `CfWorkerJsonSchemaValidator` for an edge-runtime-compatible alternative (import from `@modelcontextprotocol/server/validators/cf-worker` or `@modelcontextprotocol/client/validators/cf-worker`) + * @see `CfWorkerJsonSchemaValidator` for the edge-runtime-compatible validator selected automatically by client/server browser/workerd shims. */ export class AjvJsonSchemaValidator implements jsonSchemaValidator { private _ajv: Ajv; diff --git a/packages/server/package.json b/packages/server/package.json index 20195e7101..98e58f498b 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -28,10 +28,6 @@ "types": "./dist/stdio.d.mts", "import": "./dist/stdio.mjs" }, - "./validators/cf-worker": { - "types": "./dist/validators/cfWorker.d.mts", - "import": "./dist/validators/cfWorker.mjs" - }, "./_shims": { "workerd": { "types": "./dist/shimsWorkerd.d.mts", @@ -54,9 +50,6 @@ "types": "./dist/index.d.mts", "typesVersions": { "*": { - "validators/cf-worker": [ - "dist/validators/cfWorker.d.mts" - ], "zod-schemas": [ "dist/zodSchemas.d.mts" ], @@ -86,6 +79,8 @@ }, "devDependencies": { "@cfworker/json-schema": "catalog:runtimeShared", + "ajv": "catalog:runtimeShared", + "ajv-formats": "catalog:runtimeShared", "@eslint/js": "catalog:devTools", "@modelcontextprotocol/core": "workspace:^", "@modelcontextprotocol/eslint-config": "workspace:^", diff --git a/packages/server/src/server/server.ts b/packages/server/src/server/server.ts index f6a34f02da..b19b50dafc 100644 --- a/packages/server/src/server/server.ts +++ b/packages/server/src/server/server.ts @@ -83,7 +83,7 @@ export type ServerOptions = ProtocolOptions & { * The validator is used to validate user input returned from elicitation * requests against the requested schema. * - * @default {@linkcode DefaultJsonSchemaValidator} ({@linkcode index.AjvJsonSchemaValidator | AjvJsonSchemaValidator} on Node.js, `CfWorkerJsonSchemaValidator` on Cloudflare Workers) + * @default Runtime-selected validator (`AjvJsonSchemaValidator` on Node.js, `CfWorkerJsonSchemaValidator` on browser/workerd runtimes) */ jsonSchemaValidator?: jsonSchemaValidator; }; diff --git a/packages/server/src/shimsNode.ts b/packages/server/src/shimsNode.ts index 09283a40de..9354850b6e 100644 --- a/packages/server/src/shimsNode.ts +++ b/packages/server/src/shimsNode.ts @@ -3,5 +3,5 @@ * * This file is selected via package.json export conditions when running in Node.js. */ -export { AjvJsonSchemaValidator as DefaultJsonSchemaValidator } from '@modelcontextprotocol/core'; +export { AjvJsonSchemaValidator as DefaultJsonSchemaValidator } from '@modelcontextprotocol/core/validators/ajv'; export { default as process } from 'node:process'; diff --git a/packages/server/src/validators/cfWorker.ts b/packages/server/src/validators/cfWorker.ts deleted file mode 100644 index f804b768eb..0000000000 --- a/packages/server/src/validators/cfWorker.ts +++ /dev/null @@ -1,10 +0,0 @@ -/** - * Cloudflare Workers JSON Schema validator, available as a sub-path export. - * - * @example - * ```ts - * import { CfWorkerJsonSchemaValidator } from '@modelcontextprotocol/server/validators/cf-worker'; - * ``` - */ -export type { CfWorkerSchemaDraft } from '@modelcontextprotocol/core/validators/cfWorker'; -export { CfWorkerJsonSchemaValidator } from '@modelcontextprotocol/core/validators/cfWorker'; diff --git a/packages/server/test/server/barrelClean.test.ts b/packages/server/test/server/barrelClean.test.ts index e7f3e33c50..df4c940903 100644 --- a/packages/server/test/server/barrelClean.test.ts +++ b/packages/server/test/server/barrelClean.test.ts @@ -8,6 +8,7 @@ import { beforeAll, describe, expect, test } from 'vitest'; const pkgDir = join(dirname(fileURLToPath(import.meta.url)), '../..'); const distDir = join(pkgDir, 'dist'); const NODE_ONLY = /\b(child_process|cross-spawn|node:stream|node:child_process)\b/; +const VALIDATOR_BACKEND_IMPORT = /from\s+["'](?:ajv|ajv-formats|@cfworker\/json-schema)["']/; function chunkImportsOf(entryPath: string): string[] { const visited = new Set(); @@ -53,4 +54,17 @@ describe('@modelcontextprotocol/server root entry is browser-safe', () => { const stdio = readFileSync(join(distDir, 'stdio.mjs'), 'utf8'); expect(stdio).toMatch(/\bStdioServerTransport\b/); }); + + test('runtime shims vendor default validator backends instead of requiring consumers to install them', () => { + for (const shim of ['shimsNode.mjs', 'shimsWorkerd.mjs']) { + const entry = join(distDir, shim); + expect(readFileSync(entry, 'utf8')).not.toMatch(VALIDATOR_BACKEND_IMPORT); + + for (const chunk of chunkImportsOf(entry)) { + expect({ chunk, content: readFileSync(chunk, 'utf8') }).not.toEqual( + expect.objectContaining({ content: expect.stringMatching(VALIDATOR_BACKEND_IMPORT) }) + ); + } + } + }); }); diff --git a/packages/server/test/server/jsonSchemaValidatorOverride.test.ts b/packages/server/test/server/jsonSchemaValidatorOverride.test.ts new file mode 100644 index 0000000000..729111d9a8 --- /dev/null +++ b/packages/server/test/server/jsonSchemaValidatorOverride.test.ts @@ -0,0 +1,97 @@ +import type { JsonSchemaType, JsonSchemaValidatorResult, jsonSchemaValidator } from '@modelcontextprotocol/core'; +import { InMemoryTransport, LATEST_PROTOCOL_VERSION } from '@modelcontextprotocol/core'; +import { fromJsonSchema } from '../../src/fromJsonSchema.js'; +import { Server } from '../../src/server/server.js'; + +class RecordingValidator implements jsonSchemaValidator { + schemas: JsonSchemaType[] = []; + values: unknown[] = []; + + getValidator(schema: JsonSchemaType) { + this.schemas.push(schema); + return (value: unknown): JsonSchemaValidatorResult => { + this.values.push(value); + return { valid: true, data: value as T, errorMessage: undefined }; + }; + } +} + +describe('server JSON Schema validator overrides', () => { + test('Server constructor uses a custom validator for elicitation response validation', async () => { + const validator = new RecordingValidator(); + const server = new Server( + { name: 'test-server', version: '1.0.0' }, + { + capabilities: {}, + jsonSchemaValidator: validator + } + ); + + const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); + await server.connect(serverTransport); + await clientTransport.start(); + + const initializeResponse = new Promise(resolve => { + clientTransport.onmessage = message => resolve(message); + }); + await clientTransport.send({ + jsonrpc: '2.0', + id: 1, + method: 'initialize', + params: { + protocolVersion: LATEST_PROTOCOL_VERSION, + capabilities: { elicitation: { form: {} } }, + clientInfo: { name: 'test-client', version: '1.0.0' } + } + }); + await initializeResponse; + + clientTransport.onmessage = async message => { + if ('method' in message && 'id' in message && message.method === 'elicitation/create') { + await clientTransport.send({ + jsonrpc: '2.0', + id: message.id, + result: { action: 'accept', content: { name: 123 } } + }); + } + }; + + await expect( + server.elicitInput({ + message: 'What is your name?', + requestedSchema: { + type: 'object', + properties: { name: { type: 'string' } }, + required: ['name'] + } + }) + ).resolves.toEqual({ action: 'accept', content: { name: 123 } }); + + expect(validator.schemas).toEqual([ + { + type: 'object', + properties: { name: { type: 'string' } }, + required: ['name'] + } + ]); + expect(validator.values).toEqual([{ name: 123 }]); + + await server.close(); + await clientTransport.close(); + }); + + test('fromJsonSchema uses an explicitly supplied custom validator', async () => { + const validator = new RecordingValidator(); + const schema: JsonSchemaType = { + type: 'object', + properties: { name: { type: 'string' } }, + required: ['name'] + }; + + const standardSchema = fromJsonSchema<{ name: string }>(schema, validator); + expect(standardSchema['~standard'].validate({ name: 123 })).toEqual({ value: { name: 123 } }); + + expect(validator.schemas).toEqual([schema]); + expect(validator.values).toEqual([{ name: 123 }]); + }); +}); diff --git a/packages/server/tsconfig.json b/packages/server/tsconfig.json index 7ab6d79a56..24da6e426d 100644 --- a/packages/server/tsconfig.json +++ b/packages/server/tsconfig.json @@ -7,6 +7,7 @@ "*": ["./*"], "@modelcontextprotocol/core": ["./node_modules/@modelcontextprotocol/core/src/index.ts"], "@modelcontextprotocol/core/public": ["./node_modules/@modelcontextprotocol/core/src/exports/public/index.ts"], + "@modelcontextprotocol/core/validators/ajv": ["./node_modules/@modelcontextprotocol/core/src/validators/ajvProvider.ts"], "@modelcontextprotocol/core/validators/cfWorker": [ "./node_modules/@modelcontextprotocol/core/src/validators/cfWorkerProvider.ts" ], diff --git a/packages/server/tsdown.config.ts b/packages/server/tsdown.config.ts index 25a65f4e16..995357d4d7 100644 --- a/packages/server/tsdown.config.ts +++ b/packages/server/tsdown.config.ts @@ -4,7 +4,7 @@ export default defineConfig({ failOnWarn: 'ci-only', // 1. Entry Points // Directly matches package.json include/exclude globs - entry: ['src/index.ts', 'src/stdio.ts', 'src/shimsNode.ts', 'src/shimsWorkerd.ts', 'src/validators/cfWorker.ts'], + entry: ['src/index.ts', 'src/stdio.ts', 'src/shimsNode.ts', 'src/shimsWorkerd.ts'], // 2. Output Configuration format: ['esm'], @@ -27,13 +27,19 @@ export default defineConfig({ paths: { '@modelcontextprotocol/core': ['../core/src/index.ts'], '@modelcontextprotocol/core/public': ['../core/src/exports/public/index.ts'], + '@modelcontextprotocol/core/validators/ajv': ['../core/src/validators/ajvProvider.ts'], '@modelcontextprotocol/core/validators/cfWorker': ['../core/src/validators/cfWorkerProvider.ts'] } } }, - // 5. Vendoring Strategy - Bundle the code for this specific package into the output, - // but treat all other dependencies as external (require/import). - noExternal: ['@modelcontextprotocol/core'], + // 5. Vendoring Strategy - Bundle this package's core implementation into the output, + // but treat most dependencies as external (require/import). + // + // The runtime `_shims` entries choose default JSON Schema validators: AJV on Node and + // @cfworker/json-schema on workerd/browser. Server users should not have to install a + // validator backend just to use the runtime default, so bundle the default backends into + // the shim chunks that select them. + noExternal: ['@modelcontextprotocol/core', 'ajv', 'ajv-formats', '@cfworker/json-schema'], // 6. External packages - keep self-reference imports external for runtime resolution external: ['@modelcontextprotocol/server/_shims'] diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4baf23d803..5d52cda35f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -544,6 +544,12 @@ importers: '@typescript/native-preview': specifier: catalog:devTools version: 7.0.0-dev.20260327.2 + ajv: + specifier: catalog:runtimeShared + version: 8.18.0 + ajv-formats: + specifier: catalog:runtimeShared + version: 3.0.1(ajv@8.18.0) eslint: specifier: catalog:devTools version: 9.39.4 @@ -574,12 +580,6 @@ importers: packages/core: dependencies: - ajv: - specifier: catalog:runtimeShared - version: 8.18.0 - ajv-formats: - specifier: catalog:runtimeShared - version: 3.0.1(ajv@8.18.0) json-schema-typed: specifier: catalog:runtimeShared version: 8.0.2 @@ -623,6 +623,12 @@ importers: '@typescript/native-preview': specifier: catalog:devTools version: 7.0.0-dev.20260327.2 + ajv: + specifier: catalog:runtimeShared + version: 8.18.0 + ajv-formats: + specifier: catalog:runtimeShared + version: 3.0.1(ajv@8.18.0) eslint: specifier: catalog:devTools version: 9.39.4 @@ -907,6 +913,12 @@ importers: '@typescript/native-preview': specifier: catalog:devTools version: 7.0.0-dev.20260327.2 + ajv: + specifier: catalog:runtimeShared + version: 8.18.0 + ajv-formats: + specifier: catalog:runtimeShared + version: 3.0.1(ajv@8.18.0) eslint: specifier: catalog:devTools version: 9.39.4 diff --git a/test/integration/test/server/elicitation.test.ts b/test/integration/test/server/elicitation.test.ts index 640e7b6378..84bb071f1b 100644 --- a/test/integration/test/server/elicitation.test.ts +++ b/test/integration/test/server/elicitation.test.ts @@ -9,7 +9,8 @@ import { Client } from '@modelcontextprotocol/client'; import type { ElicitRequestFormParams } from '@modelcontextprotocol/core'; -import { AjvJsonSchemaValidator, InMemoryTransport } from '@modelcontextprotocol/core'; +import { InMemoryTransport } from '@modelcontextprotocol/core'; +import { AjvJsonSchemaValidator } from '@modelcontextprotocol/core/validators/ajv'; import { CfWorkerJsonSchemaValidator } from '@modelcontextprotocol/core/validators/cfWorker'; import { Server } from '@modelcontextprotocol/server'; diff --git a/test/integration/test/standardSchema.test.ts b/test/integration/test/standardSchema.test.ts index 67f16c5fa7..8764a76ed5 100644 --- a/test/integration/test/standardSchema.test.ts +++ b/test/integration/test/standardSchema.test.ts @@ -5,7 +5,7 @@ import { Client } from '@modelcontextprotocol/client'; import type { TextContent } from '@modelcontextprotocol/core'; -import { AjvJsonSchemaValidator, fromJsonSchema, InMemoryTransport } from '@modelcontextprotocol/core'; +import { InMemoryTransport } from '@modelcontextprotocol/core'; import { completable, fromJsonSchema as serverFromJsonSchema, McpServer } from '@modelcontextprotocol/server'; import { toStandardJsonSchema } from '@valibot/to-json-schema'; import { type } from 'arktype'; @@ -382,13 +382,12 @@ describe('Standard Schema Support', () => { }); describe('Raw JSON Schema via fromJsonSchema', () => { - const validator = new AjvJsonSchemaValidator(); - test('should register tool with raw JSON Schema input', async () => { - const inputSchema = fromJsonSchema<{ name: string }>( - { type: 'object', properties: { name: { type: 'string' } }, required: ['name'] }, - validator - ); + const inputSchema = serverFromJsonSchema<{ name: string }>({ + type: 'object', + properties: { name: { type: 'string' } }, + required: ['name'] + }); mcpServer.registerTool('greet', { inputSchema }, async ({ name }) => ({ content: [{ type: 'text', text: `Hello, ${name}!` }] @@ -407,11 +406,12 @@ describe('Standard Schema Support', () => { expect((result.content[0] as TextContent).text).toBe('Hello, World!'); }); - test('should reject invalid input via AJV validation', async () => { - const inputSchema = fromJsonSchema( - { type: 'object', properties: { count: { type: 'number' } }, required: ['count'] }, - validator - ); + test('should reject invalid input via default validation', async () => { + const inputSchema = serverFromJsonSchema({ + type: 'object', + properties: { count: { type: 'number' } }, + required: ['count'] + }); mcpServer.registerTool('double', { inputSchema }, async args => { const { count } = args as { count: number }; diff --git a/test/integration/tsconfig.json b/test/integration/tsconfig.json index 4a2820da3f..26c4fb83e0 100644 --- a/test/integration/tsconfig.json +++ b/test/integration/tsconfig.json @@ -7,6 +7,7 @@ "*": ["./*"], "@modelcontextprotocol/core": ["./node_modules/@modelcontextprotocol/core/src/index.ts"], "@modelcontextprotocol/core/public": ["./node_modules/@modelcontextprotocol/core/src/exports/public/index.ts"], + "@modelcontextprotocol/core/validators/ajv": ["./node_modules/@modelcontextprotocol/core/src/validators/ajvProvider.ts"], "@modelcontextprotocol/core/validators/cfWorker": [ "./node_modules/@modelcontextprotocol/core/src/validators/cfWorkerProvider.ts" ],