diff --git a/.changeset/cfworker-out-of-barrel.md b/.changeset/cfworker-out-of-barrel.md index 9a35b845dc..f9e366acfe 100644 --- a/.changeset/cfworker-out-of-barrel.md +++ b/.changeset/cfworker-out-of-barrel.md @@ -3,4 +3,4 @@ '@modelcontextprotocol/client': patch --- -Stop bundling `@cfworker/json-schema` into the main package barrel. Previously `CfWorkerJsonSchemaValidator` was re-exported from the core internal barrel, so tsdown inlined the `@cfworker/json-schema` dev dependency into every consumer's bundle even when it was never used. The validator is now reachable only via the `_shims` conditional (workerd/browser) and the explicit `@modelcontextprotocol/{server,client}/validators/cf-worker` subpath, so consumers that don't opt into it no longer ship that code. No public API change. +Stop bundling `@cfworker/json-schema` into the main package barrel. Previously `CfWorkerJsonSchemaValidator` was re-exported from the core internal barrel, so tsdown inlined the `@cfworker/json-schema` dependency into every consumer's bundle even when it was never used. The named validator classes are now reachable only via the explicit `@modelcontextprotocol/{client,server}/validators/{ajv,cf-worker}` subpaths and the runtime `_shims` conditional, so consumers that import only from the root entry point no longer ship the validator dep. diff --git a/.changeset/support-standard-json-schema.md b/.changeset/support-standard-json-schema.md index 1ceff35844..792e15f1f7 100644 --- a/.changeset/support-standard-json-schema.md +++ b/.changeset/support-standard-json-schema.md @@ -21,10 +21,10 @@ server.registerTool('greet', { For raw JSON Schema (e.g. TypeBox output), use the new `fromJsonSchema` adapter: ```typescript -import { fromJsonSchema, AjvJsonSchemaValidator } from '@modelcontextprotocol/core'; +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); ``` diff --git a/.changeset/workerd-shim-vendors-cfworker.md b/.changeset/workerd-shim-vendors-cfworker.md new file mode 100644 index 0000000000..9759e73009 --- /dev/null +++ b/.changeset/workerd-shim-vendors-cfworker.md @@ -0,0 +1,18 @@ +--- +'@modelcontextprotocol/core': minor +'@modelcontextprotocol/client': patch +'@modelcontextprotocol/server': patch +--- + +Bundle automatic JSON Schema validator defaults in `@modelcontextprotocol/client` and `@modelcontextprotocol/server` runtime shims. + +Client and server pick the right validator automatically based on the runtime: the Node shim uses AJV, the browser/workerd shim uses `@cfworker/json-schema`. Both backends are bundled into the shim chunks that select them, so the default code path needs no extra installs — `import { McpServer } from '@modelcontextprotocol/server'` does not pull `ajv` or `@cfworker/json-schema` into the root entry chunk. + +The named validator classes remain part of the public surface for consumers who want to customize the built-in backend (pre-register schemas by `$id`, register custom AJV formats, switch dialects, change `@cfworker/json-schema` draft). They are exposed through explicit subpaths so they do not bloat the root index chunk: + +- `import { AjvJsonSchemaValidator } from '@modelcontextprotocol/{client,server}/validators/ajv'` +- `import { CfWorkerJsonSchemaValidator } from '@modelcontextprotocol/{client,server}/validators/cf-worker'` + +Importing from one of these subpaths means the corresponding peer dep (`ajv` + `ajv-formats`, or `@cfworker/json-schema`) must be in your `package.json`. The shim keeps its own vendored copy for the default path, so a project can use the subpath in some files and rely on the default in others. + +The `jsonSchemaValidator` interface remains the public extension point for replacing validation entirely with a custom implementation. diff --git a/docs/migration-SKILL.md b/docs/migration-SKILL.md index e2fbf71455..b2066f0363 100644 --- a/docs/migration-SKILL.md +++ b/docs/migration-SKILL.md @@ -509,8 +509,8 @@ Type changes in handler context: The SDK now auto-selects the appropriate JSON Schema validator based on runtime: -- Node.js → `AjvJsonSchemaValidator` (no change from v1) -- Cloudflare Workers (workerd) → `CfWorkerJsonSchemaValidator` (previously required manual config) +- Node.js → AJV (no change from v1) +- Cloudflare Workers (workerd) → `@cfworker/json-schema` (previously required manual config) **No action required** for most users. Cloudflare Workers users can remove explicit `jsonSchemaValidator` configuration: @@ -527,11 +527,12 @@ 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` for the default path; client/server bundle the runtime-selected defaults and the root entry point does not pull either dep in. +- To customize the built-in backend (e.g. register custom AJV formats, change `@cfworker/json-schema` draft), import the named class from the package subpath: `@modelcontextprotocol/{client,server}/validators/ajv` for `AjvJsonSchemaValidator`, `@modelcontextprotocol/{client,server}/validators/cf-worker` for `CfWorkerJsonSchemaValidator`. Importing from a subpath means the corresponding peer dep must be in your `package.json`. +- To replace validation entirely, pass `jsonSchemaValidator: myCustomValidator` with your own implementation of the `jsonSchemaValidator` interface. ## 15. Migration Steps (apply in this order) diff --git a/docs/migration.md b/docs/migration.md index 9fd029ef82..7affd77675 100644 --- a/docs/migration.md +++ b/docs/migration.md @@ -901,8 +901,8 @@ server.setRequestHandler('tools/call', async (request, ctx) => { The SDK now automatically selects the appropriate JSON Schema validator based on your runtime environment: -- **Node.js**: Uses `AjvJsonSchemaValidator` (same as v1 default) -- **Cloudflare Workers**: Uses `CfWorkerJsonSchemaValidator` (previously required manual configuration) +- **Node.js**: Uses AJV (same as v1 default) +- **Cloudflare Workers**: Uses `@cfworker/json-schema` (previously required manual configuration) This means Cloudflare Workers users no longer need to explicitly pass the validator: @@ -933,17 +933,45 @@ 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, so a normal `import { McpServer } from '@modelcontextprotocol/server'` does not pull `ajv` or `@cfworker/json-schema` into your bundle until you choose to customize. + +If you want to customize the **built-in** backend (for example, pre-register schemas by `$id`, register custom AJV formats, or change the `@cfworker/json-schema` draft), import the named class from the explicit subpath and pass an instance through `jsonSchemaValidator`: ```typescript -// Runtime-aware default (auto-selects AjvJsonSchemaValidator or CfWorkerJsonSchemaValidator) -import { DefaultJsonSchemaValidator } from '@modelcontextprotocol/server/_shims'; +import { Ajv } from 'ajv'; +import addFormats from 'ajv-formats'; +import { AjvJsonSchemaValidator } from '@modelcontextprotocol/server/validators/ajv'; + +const ajv = new Ajv({ strict: true, allErrors: true }); +addFormats(ajv); -// Specific validators -import { AjvJsonSchemaValidator } from '@modelcontextprotocol/server'; +const server = new McpServer( + { name: 'my-server', version: '1.0.0' }, + { + capabilities: { tools: {} }, + jsonSchemaValidator: new AjvJsonSchemaValidator(ajv) + } +); +``` + +```typescript import { CfWorkerJsonSchemaValidator } from '@modelcontextprotocol/server/validators/cf-worker'; + +const server = new McpServer( + { name: 'my-server', version: '1.0.0' }, + { + capabilities: { tools: {} }, + jsonSchemaValidator: new CfWorkerJsonSchemaValidator({ draft: '2020-12', shortcircuit: false }) + } +); ``` +(both subpaths are also available on `@modelcontextprotocol/client/validators/...`) + +If you import from one of these subpaths in your own code, the corresponding peer dep (`ajv` + `ajv-formats`, or `@cfworker/json-schema`) needs to be installed in your `package.json`. The runtime shim continues to vendor a copy for the default code path, so you can use the subpath in some files and rely on the default in others. + +To replace validation wholesale rather than customizing the built-in classes, implement the `jsonSchemaValidator` interface and pass your own implementation through the option above. + ## Unchanged APIs The following APIs are unchanged between v1 and v2 (only the import paths changed): diff --git a/packages/client/package.json b/packages/client/package.json index 537804b732..4362c4fe86 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -28,6 +28,10 @@ "types": "./dist/stdio.d.mts", "import": "./dist/stdio.mjs" }, + "./validators/ajv": { + "types": "./dist/validators/ajv.d.mts", + "import": "./dist/validators/ajv.mjs" + }, "./validators/cf-worker": { "types": "./dist/validators/cfWorker.d.mts", "import": "./dist/validators/cfWorker.mjs" @@ -54,6 +58,9 @@ "types": "./dist/index.d.mts", "typesVersions": { "*": { + "validators/ajv": [ + "dist/validators/ajv.d.mts" + ], "validators/cf-worker": [ "dist/validators/cfWorker.d.mts" ], @@ -93,6 +100,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..92a25cea09 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 (AJV-backed on Node.js, `@cfworker/json-schema`-backed 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/ajv.ts b/packages/client/src/validators/ajv.ts new file mode 100644 index 0000000000..770df3f57a --- /dev/null +++ b/packages/client/src/validators/ajv.ts @@ -0,0 +1,14 @@ +/** + * Customisation entry point for the AJV validator. Re-exports `Ajv` + `addFormats` from the + * SDK's bundled copy, so customising the validator needs no extra installs. + * + * @example + * ```ts + * import { Ajv, addFormats, AjvJsonSchemaValidator } from '@modelcontextprotocol/client/validators/ajv'; + * + * const ajv = new Ajv({ strict: true, allErrors: true }); + * addFormats(ajv); + * const validator = new AjvJsonSchemaValidator(ajv); + * ``` + */ +export { addFormats, Ajv, AjvJsonSchemaValidator } from '@modelcontextprotocol/core/validators/ajv'; diff --git a/packages/client/src/validators/cfWorker.ts b/packages/client/src/validators/cfWorker.ts index 8d66770e0d..2969b4dc9d 100644 --- a/packages/client/src/validators/cfWorker.ts +++ b/packages/client/src/validators/cfWorker.ts @@ -1,10 +1,3 @@ -/** - * Cloudflare Workers JSON Schema validator, available as a sub-path export. - * - * @example - * ```ts - * import { CfWorkerJsonSchemaValidator } from '@modelcontextprotocol/client/validators/cf-worker'; - * ``` - */ +/** Customisation entry point for the `@cfworker/json-schema` validator. */ 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..741528b6f6 100644 --- a/packages/client/test/client/barrelClean.test.ts +++ b/packages/client/test/client/barrelClean.test.ts @@ -8,6 +8,8 @@ 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/; +// Anchored at start-of-line so JSDoc-example `from 'ajv'` strings in vendored chunks don't match. +const VALIDATOR_BACKEND_IMPORT = /^import[^\n]*?from\s+["'](?:ajv|ajv-formats|@cfworker\/json-schema)["']/m; function chunkImportsOf(entryPath: string): string[] { const visited = new Set(); @@ -52,4 +54,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..773e07c920 100644 --- a/packages/client/tsdown.config.ts +++ b/packages/client/tsdown.config.ts @@ -2,39 +2,35 @@ import { defineConfig } from 'tsdown'; 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'], - - // 2. Output Configuration + entry: [ + 'src/index.ts', + 'src/stdio.ts', + 'src/shimsNode.ts', + 'src/shimsWorkerd.ts', + 'src/shimsBrowser.ts', + 'src/validators/ajv.ts', + 'src/validators/cfWorker.ts' + ], format: ['esm'], outDir: 'dist', - clean: true, // Recommended: Cleans 'dist' before building + clean: true, sourcemap: true, - - // 3. Platform & Target target: 'esnext', platform: 'node', - shims: true, // Polyfills common Node.js shims (__dirname, etc.) - - // 4. Type Definitions - // Bundles d.ts files into a single output + shims: true, dts: { resolver: 'tsc', - // override just for DTS generation: + resolve: ['ajv', 'ajv-formats'], compilerOptions: { baseUrl: '.', 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'], - - // 6. External packages - keep self-reference imports external for runtime resolution + noExternal: ['@modelcontextprotocol/core', 'ajv', 'ajv-formats', '@cfworker/json-schema'], external: ['@modelcontextprotocol/client/_shims'] }); diff --git a/packages/codemod/src/migrations/v1-to-v2/mappings/importMap.ts b/packages/codemod/src/migrations/v1-to-v2/mappings/importMap.ts index bc69faf6cc..84dc90782c 100644 --- a/packages/codemod/src/migrations/v1-to-v2/mappings/importMap.ts +++ b/packages/codemod/src/migrations/v1-to-v2/mappings/importMap.ts @@ -7,6 +7,11 @@ export interface ImportMapping { removalMessage?: string; /** No entries currently set this; scaffolding for when a v1 symbol has no v2 equivalent yet. */ isV2Gap?: boolean; + /** + * Subpath suffix appended after `RESOLVE_BY_CONTEXT` resolves the base package (e.g. `/validators/ajv`). + * The final target becomes `@modelcontextprotocol/{client,server}`. + */ + subpathSuffix?: string; } export const IMPORT_MAP: Record = { @@ -163,6 +168,27 @@ export const IMPORT_MAP: Record = { } }; +// v1 `validation/*` paths → v2 `validators/*` subpaths. The canonical `*-provider.js` filename and +// the short aliases from the v1 README, with and without `.js` suffix. +const VALIDATOR_V1_VARIANTS: Record = { + '/validators/ajv': ['validation/ajv-provider.js', 'validation/ajv.js', 'validation/ajv'], + '/validators/cf-worker': ['validation/cfworker-provider.js', 'validation/cfworker.js', 'validation/cfworker'] +}; +for (const [subpathSuffix, v1Specifiers] of Object.entries(VALIDATOR_V1_VARIANTS)) { + for (const v1Specifier of v1Specifiers) { + IMPORT_MAP[`@modelcontextprotocol/sdk/${v1Specifier}`] = { + target: 'RESOLVE_BY_CONTEXT', + status: 'moved', + subpathSuffix + }; + } +} + +// `validation/index` / `validation/types` carry only the `jsonSchemaValidator` interface + helpers. +for (const barrelSpecifier of ['@modelcontextprotocol/sdk/validation/index.js', '@modelcontextprotocol/sdk/validation/types.js']) { + IMPORT_MAP[barrelSpecifier] = { target: 'RESOLVE_BY_CONTEXT', status: 'moved' }; +} + export function isAuthImport(specifier: string): boolean { return specifier.includes('/server/auth/') || specifier.includes('/server/auth.'); } diff --git a/packages/codemod/src/migrations/v1-to-v2/transforms/importPaths.ts b/packages/codemod/src/migrations/v1-to-v2/transforms/importPaths.ts index 96e9308bfd..e49858719e 100644 --- a/packages/codemod/src/migrations/v1-to-v2/transforms/importPaths.ts +++ b/packages/codemod/src/migrations/v1-to-v2/transforms/importPaths.ts @@ -102,6 +102,9 @@ export const importPathsTransform: Transform = { line, diagnostics }); + if (mapping.subpathSuffix) { + targetPackage = `${targetPackage}${mapping.subpathSuffix}`; + } } const symbolsToRenameInFile: Array<[string, string]> = []; @@ -250,6 +253,9 @@ function rewriteExportDeclarations( return spec.includes('/server/') || spec === '@modelcontextprotocol/server'; }); targetPackage = resolveTypesPackage(context, hasClientImport, hasServerImport); + if (mapping.subpathSuffix) { + targetPackage = `${targetPackage}${mapping.subpathSuffix}`; + } } if (mapping.symbolTargetOverrides) { diff --git a/packages/codemod/src/migrations/v1-to-v2/transforms/mockPaths.ts b/packages/codemod/src/migrations/v1-to-v2/transforms/mockPaths.ts index e857b38adb..f5247dc01b 100644 --- a/packages/codemod/src/migrations/v1-to-v2/transforms/mockPaths.ts +++ b/packages/codemod/src/migrations/v1-to-v2/transforms/mockPaths.ts @@ -64,6 +64,9 @@ function resolveTarget( return s.includes('/server/') || s === '@modelcontextprotocol/server'; }); target = resolveTypesPackage(context, hasClient, hasServer, diagnosticSink); + if (mapping.subpathSuffix) { + target = `${target}${mapping.subpathSuffix}`; + } } return { target, renamedSymbols: mapping.renamedSymbols, symbolTargetOverrides: mapping.symbolTargetOverrides }; diff --git a/packages/codemod/test/v1-to-v2/transforms/importPaths.test.ts b/packages/codemod/test/v1-to-v2/transforms/importPaths.test.ts index 7068919077..2c661aa2db 100644 --- a/packages/codemod/test/v1-to-v2/transforms/importPaths.test.ts +++ b/packages/codemod/test/v1-to-v2/transforms/importPaths.test.ts @@ -448,4 +448,85 @@ describe('import-paths transform', () => { expect(result).toContain(`from "@modelcontextprotocol/client"`); expect(result).toContain('InMemoryTransport'); }); + + describe('validator subpath rewrites', () => { + it('rewrites CfWorkerJsonSchemaValidator from v1 cfworker-provider to client subpath', () => { + const input = [ + `import { Client } from '@modelcontextprotocol/sdk/client/index.js';`, + `import { CfWorkerJsonSchemaValidator } from '@modelcontextprotocol/sdk/validation/cfworker-provider.js';`, + '' + ].join('\n'); + const result = applyTransform(input, { projectType: 'client' }); + expect(result).toContain(`from "@modelcontextprotocol/client/validators/cf-worker"`); + expect(result).toContain('CfWorkerJsonSchemaValidator'); + expect(result).not.toContain('@modelcontextprotocol/sdk'); + }); + + it('rewrites CfWorkerJsonSchemaValidator from v1 cfworker short alias to server subpath', () => { + const input = [ + `import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';`, + `import { CfWorkerJsonSchemaValidator } from '@modelcontextprotocol/sdk/validation/cfworker';`, + '' + ].join('\n'); + const result = applyTransform(input, { projectType: 'server' }); + expect(result).toContain(`from "@modelcontextprotocol/server/validators/cf-worker"`); + expect(result).toContain('CfWorkerJsonSchemaValidator'); + }); + + it('rewrites AjvJsonSchemaValidator from v1 ajv-provider to server subpath', () => { + const input = [ + `import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';`, + `import { AjvJsonSchemaValidator } from '@modelcontextprotocol/sdk/validation/ajv-provider.js';`, + '' + ].join('\n'); + const result = applyTransform(input, { projectType: 'server' }); + expect(result).toContain(`from "@modelcontextprotocol/server/validators/ajv"`); + expect(result).toContain('AjvJsonSchemaValidator'); + }); + + it('rewrites AjvJsonSchemaValidator from v1 ajv short alias to server subpath', () => { + const input = [ + `import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';`, + `import { AjvJsonSchemaValidator } from '@modelcontextprotocol/sdk/validation/ajv';`, + '' + ].join('\n'); + const result = applyTransform(input, { projectType: 'server' }); + expect(result).toContain(`from "@modelcontextprotocol/server/validators/ajv"`); + expect(result).toContain('AjvJsonSchemaValidator'); + }); + + it('routes validator subpath to client when only client siblings exist', () => { + const input = [ + `import { Client } from '@modelcontextprotocol/sdk/client/index.js';`, + `import { AjvJsonSchemaValidator } from '@modelcontextprotocol/sdk/validation/ajv-provider.js';`, + '' + ].join('\n'); + const result = applyTransform(input, { projectType: 'both' }); + expect(result).toContain(`from "@modelcontextprotocol/client/validators/ajv"`); + }); + + it('routes validator subpath via project type when no SDK siblings', () => { + const input = `import { CfWorkerJsonSchemaValidator } from '@modelcontextprotocol/sdk/validation/cfworker-provider.js';\n`; + const result = applyTransform(input, { projectType: 'server' }); + expect(result).toContain(`from "@modelcontextprotocol/server/validators/cf-worker"`); + }); + + it('includes the validator subpath in usedPackages', () => { + const project = new Project({ useInMemoryFileSystem: true }); + const sourceFile = project.createSourceFile( + 'test.ts', + `import { CfWorkerJsonSchemaValidator } from '@modelcontextprotocol/sdk/validation/cfworker-provider.js';\n` + ); + const result = importPathsTransform.apply(sourceFile, { projectType: 'client' }); + expect(result.usedPackages).toBeDefined(); + expect(result.usedPackages!.has('@modelcontextprotocol/client/validators/cf-worker')).toBe(true); + }); + + it('rewrites the validation/index type-only import to the resolved base package', () => { + const input = `import type { jsonSchemaValidator } from '@modelcontextprotocol/sdk/validation/index.js';\n`; + const result = applyTransform(input, { projectType: 'server' }); + expect(result).toContain(`from "@modelcontextprotocol/server"`); + expect(result).toContain('jsonSchemaValidator'); + }); + }); }); diff --git a/packages/codemod/test/v1-to-v2/transforms/mockPaths.test.ts b/packages/codemod/test/v1-to-v2/transforms/mockPaths.test.ts index 3cb1aec7ba..864a4742ad 100644 --- a/packages/codemod/test/v1-to-v2/transforms/mockPaths.test.ts +++ b/packages/codemod/test/v1-to-v2/transforms/mockPaths.test.ts @@ -315,4 +315,51 @@ describe('mock-paths transform', () => { expect(result).toContain('new MyError('); }); }); + + describe('validator subpath rewrites', () => { + it('rewrites vi.mock of validator provider to the subpath', () => { + const input = [ + `vi.mock('@modelcontextprotocol/sdk/validation/cfworker-provider.js', () => ({`, + ` CfWorkerJsonSchemaValidator: vi.fn()`, + `}));`, + '' + ].join('\n'); + const result = applyTransform(input); + expect(result).toContain(`'@modelcontextprotocol/server/validators/cf-worker'`); + expect(result).not.toContain('@modelcontextprotocol/sdk'); + }); + + it('rewrites vi.doMock of ajv provider with sibling client import', () => { + const input = [ + `import { Client } from '@modelcontextprotocol/sdk/client/index.js';`, + `vi.doMock('@modelcontextprotocol/sdk/validation/ajv-provider.js', () => ({`, + ` AjvJsonSchemaValidator: vi.fn()`, + `}));`, + '' + ].join('\n'); + const result = applyTransform(input, { projectType: 'both' }); + expect(result).toContain(`'@modelcontextprotocol/client/validators/ajv'`); + }); + + it('rewrites dynamic import of validator provider to the subpath', () => { + const input = [ + `const { AjvJsonSchemaValidator } = await import('@modelcontextprotocol/sdk/validation/ajv-provider.js');`, + '' + ].join('\n'); + const result = applyTransform(input); + expect(result).toContain(`'@modelcontextprotocol/server/validators/ajv'`); + expect(result).toContain('AjvJsonSchemaValidator'); + }); + + it('rewrites jest.mock of validator short alias to the subpath', () => { + const input = [ + `jest.mock('@modelcontextprotocol/sdk/validation/cfworker', () => ({`, + ` CfWorkerJsonSchemaValidator: jest.fn()`, + `}));`, + '' + ].join('\n'); + const result = applyTransform(input); + expect(result).toContain(`'@modelcontextprotocol/server/validators/cf-worker'`); + }); + }); }); 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 f73ab2d2e7..913b948ac8 100644 --- a/packages/core/src/exports/public/index.ts +++ b/packages/core/src/exports/public/index.ts @@ -142,8 +142,10 @@ export { InMemoryTaskMessageQueue, InMemoryTaskStore } from '../../experimental/ export type { SpecTypeName, SpecTypes } from '../../types/specTypeSchema.js'; export { isSpecType, specTypeSchemas } from '../../types/specTypeSchema.js'; export type { StandardSchemaV1, StandardSchemaV1Sync, StandardSchemaWithJSON } from '../../util/standardSchema.js'; -export { AjvJsonSchemaValidator } from '../../validators/ajvProvider.js'; -export type { CfWorkerSchemaDraft } from '../../validators/cfWorkerProvider.js'; +// Validator providers are type-only here — import the runtime classes from the explicit +// `@modelcontextprotocol/{client,server}/validators/{ajv,cf-worker}` subpaths to customise. +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.examples.ts b/packages/core/src/index.examples.ts deleted file mode 100644 index 531f512113..0000000000 --- a/packages/core/src/index.examples.ts +++ /dev/null @@ -1,31 +0,0 @@ -/** - * Type-checked examples for `index.ts`. - * - * These examples are synced into JSDoc comments via the sync-snippets script. - * Each function's region markers define the code snippet that appears in the docs. - * - * @module - */ - -import { AjvJsonSchemaValidator } from './validators/ajvProvider.js'; -import { CfWorkerJsonSchemaValidator } from './validators/cfWorkerProvider.js'; - -/** - * Example: AJV validator for Node.js. - */ -function validation_ajv() { - //#region validation_ajv - const validator = new AjvJsonSchemaValidator(); - //#endregion validation_ajv - return validator; -} - -/** - * Example: CfWorker validator for edge runtimes. - */ -function validation_cfWorker() { - //#region validation_cfWorker - const validator = new CfWorkerJsonSchemaValidator(); - //#endregion validation_cfWorker - return validator; -} diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 8bcc9c9591..0f83b8fa2a 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -19,38 +19,9 @@ 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'; +// Validator providers are type-only here — import the runtime classes from the explicit +// `@modelcontextprotocol/{core,client,server}/validators/{ajv,cf-worker}` subpaths to customise. +export type { AjvJsonSchemaValidator } from './validators/ajvProvider.js'; +export type { CfWorkerJsonSchemaValidator, CfWorkerSchemaDraft } from './validators/cfWorkerProvider.js'; export * from './validators/fromJsonSchema.js'; -/** - * JSON Schema validation - * - * 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. - * - * - `CfWorkerJsonSchemaValidator`: Best for edge runtimes - * Import from: `@modelcontextprotocol/server/validators/cf-worker` or `@modelcontextprotocol/client/validators/cf-worker` - * Bundled — no additional dependencies required. - * - * @example For Node.js with AJV - * ```ts source="./index.examples.ts#validation_ajv" - * const validator = new AjvJsonSchemaValidator(); - * ``` - * - * @example For Cloudflare Workers - * ```ts source="./index.examples.ts#validation_cfWorker" - * const validator = new CfWorkerJsonSchemaValidator(); - * ``` - * - * @module validation - */ - -// Core types only - implementations are exported via separate entry points export type { JsonSchemaType, JsonSchemaValidator, jsonSchemaValidator, JsonSchemaValidatorResult } from './validators/types.js'; diff --git a/packages/core/src/validators/ajvProvider.examples.ts b/packages/core/src/validators/ajvProvider.examples.ts index eea45bf15c..abf8d1572a 100644 --- a/packages/core/src/validators/ajvProvider.examples.ts +++ b/packages/core/src/validators/ajvProvider.examples.ts @@ -7,12 +7,7 @@ * @module */ -import { Ajv } from 'ajv'; -import _addFormats from 'ajv-formats'; - -import { AjvJsonSchemaValidator } from './ajvProvider.js'; - -const addFormats = _addFormats as unknown as typeof _addFormats.default; +import { addFormats, Ajv, AjvJsonSchemaValidator } from './ajvProvider.js'; /** * Example: Default AJV instance. @@ -36,13 +31,16 @@ function AjvJsonSchemaValidator_customInstance() { } /** - * Example: Constructor with advanced AJV configuration including formats. + * Example: Custom AJV instance with formats registered. + * + * `Ajv` and `addFormats` are re-exported from this module so customising the validator + * requires no extra `package.json` dependencies — both come from the SDK's bundled copy. */ -function AjvJsonSchemaValidator_constructor_withFormats() { - //#region AjvJsonSchemaValidator_constructor_withFormats - const ajv = new Ajv({ validateFormats: true }); +function AjvJsonSchemaValidator_withFormats() { + //#region AjvJsonSchemaValidator_withFormats + const ajv = new Ajv({ strict: true, allErrors: true }); addFormats(ajv); const validator = new AjvJsonSchemaValidator(ajv); - //#endregion AjvJsonSchemaValidator_constructor_withFormats + //#endregion AjvJsonSchemaValidator_withFormats return validator; } diff --git a/packages/core/src/validators/ajvProvider.ts b/packages/core/src/validators/ajvProvider.ts index 820a3d6618..f62a8469ae 100644 --- a/packages/core/src/validators/ajvProvider.ts +++ b/packages/core/src/validators/ajvProvider.ts @@ -7,6 +7,20 @@ import _addFormats from 'ajv-formats'; import type { JsonSchemaType, JsonSchemaValidator, jsonSchemaValidator, JsonSchemaValidatorResult } from './types.js'; +/** Structural subset of the AJV interface used by {@link AjvJsonSchemaValidator}. */ +interface AjvLike { + compile: (schema: unknown) => AjvValidateFunction; + getSchema: (keyRef: string) => AjvValidateFunction | undefined; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + errorsText: (errors?: any) => string; +} + +interface AjvValidateFunction { + (input: unknown): boolean; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + errors?: any; +} + function createDefaultAjvInstance(): Ajv { const ajv = new Ajv({ strict: false, @@ -22,54 +36,41 @@ function createDefaultAjvInstance(): Ajv { } /** - * @example Use with default AJV instance (recommended) + * AJV-backed JSON Schema validator. See `@modelcontextprotocol/{client,server}/validators/ajv` + * for the customisation entry point (re-exports `Ajv` and `addFormats` from the bundled copy). + * + * @example Use with default configuration * ```ts source="./ajvProvider.examples.ts#AjvJsonSchemaValidator_default" * const validator = new AjvJsonSchemaValidator(); * ``` * - * @example Use with custom AJV instance + * @example Use with a custom AJV instance * ```ts source="./ajvProvider.examples.ts#AjvJsonSchemaValidator_customInstance" * const ajv = new Ajv({ strict: true, allErrors: true }); * 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`) + * @example Register ajv-formats + * ```ts source="./ajvProvider.examples.ts#AjvJsonSchemaValidator_withFormats" + * const ajv = new Ajv({ strict: true, allErrors: true }); + * addFormats(ajv); + * const validator = new AjvJsonSchemaValidator(ajv); + * ``` */ export class AjvJsonSchemaValidator implements jsonSchemaValidator { - private _ajv: Ajv; + private _ajv: AjvLike; /** - * Create an AJV validator - * - * @param ajv - Optional pre-configured AJV instance. If not provided, a default instance will be created. - * - * @example Use default configuration (recommended for most cases) - * ```ts source="./ajvProvider.examples.ts#AjvJsonSchemaValidator_default" - * const validator = new AjvJsonSchemaValidator(); - * ``` - * - * @example Provide custom AJV instance for advanced configuration - * ```ts source="./ajvProvider.examples.ts#AjvJsonSchemaValidator_constructor_withFormats" - * const ajv = new Ajv({ validateFormats: true }); - * addFormats(ajv); - * const validator = new AjvJsonSchemaValidator(ajv); - * ``` + * @param ajv - Optional pre-configured AJV-compatible instance. If omitted, a default instance is + * created with `strict: false`, `validateFormats: true`, `validateSchema: false`, `allErrors: true`, + * and `ajv-formats` registered. The parameter is typed structurally so consumers who don't pass + * an instance need not have `ajv` installed. */ - constructor(ajv?: Ajv) { + constructor(ajv?: AjvLike) { this._ajv = ajv ?? createDefaultAjvInstance(); } - /** - * Create a validator for the given JSON Schema - * - * The validator is compiled once and can be reused multiple times. - * If the schema has an `$id`, it will be cached by AJV automatically. - * - * @param schema - Standard JSON Schema object - * @returns A validator function that validates input data - */ getValidator(schema: JsonSchemaType): JsonSchemaValidator { - // Check if schema has $id and is already compiled/cached const ajvValidator = '$id' in schema && typeof schema.$id === 'string' ? (this._ajv.getSchema(schema.$id) ?? this._ajv.compile(schema)) @@ -92,3 +93,7 @@ export class AjvJsonSchemaValidator implements jsonSchemaValidator { }; } } + +export { Ajv } from 'ajv'; +/** `ajv-formats` default export, normalised through the CJS/ESM interop wrapper. */ +export const addFormats = _addFormats as unknown as typeof _addFormats.default; diff --git a/packages/core/src/validators/cfWorkerProvider.examples.ts b/packages/core/src/validators/cfWorkerProvider.examples.ts index a347f9b7cf..c166c18dd6 100644 --- a/packages/core/src/validators/cfWorkerProvider.examples.ts +++ b/packages/core/src/validators/cfWorkerProvider.examples.ts @@ -10,7 +10,7 @@ import { CfWorkerJsonSchemaValidator } from './cfWorkerProvider.js'; /** - * Example: Default configuration. + * Example: Default configuration (draft 2020-12, shortcircuit on). */ function CfWorkerJsonSchemaValidator_default() { //#region CfWorkerJsonSchemaValidator_default diff --git a/packages/core/src/validators/cfWorkerProvider.ts b/packages/core/src/validators/cfWorkerProvider.ts index f2cce37e8b..6fcc3d507e 100644 --- a/packages/core/src/validators/cfWorkerProvider.ts +++ b/packages/core/src/validators/cfWorkerProvider.ts @@ -13,13 +13,15 @@ import { Validator } from '@cfworker/json-schema'; import type { JsonSchemaType, JsonSchemaValidator, jsonSchemaValidator, JsonSchemaValidatorResult } from './types.js'; /** - * JSON Schema draft version supported by @cfworker/json-schema + * JSON Schema draft version supported by `@cfworker/json-schema`. */ export type CfWorkerSchemaDraft = '4' | '7' | '2019-09' | '2020-12'; /** + * `@cfworker/json-schema`-backed JSON Schema validator. See + * `@modelcontextprotocol/{client,server}/validators/cf-worker` for the customisation entry point. * - * @example Use with default configuration (2020-12, shortcircuit) + * @example Use with default configuration (draft 2020-12, shortcircuit on) * ```ts source="./cfWorkerProvider.examples.ts#CfWorkerJsonSchemaValidator_default" * const validator = new CfWorkerJsonSchemaValidator(); * ``` diff --git a/packages/core/src/validators/fromJsonSchema.examples.ts b/packages/core/src/validators/fromJsonSchema.examples.ts index 22ff4a9d60..af72b5b036 100644 --- a/packages/core/src/validators/fromJsonSchema.examples.ts +++ b/packages/core/src/validators/fromJsonSchema.examples.ts @@ -6,17 +6,23 @@ * @module */ -import { AjvJsonSchemaValidator } from './ajvProvider.js'; import { fromJsonSchema } from './fromJsonSchema.js'; +import type { jsonSchemaValidator } from './types.js'; + +declare const validator: jsonSchemaValidator; /** * Example: wrap a raw JSON Schema object for use with registerTool. + * + * Consumers importing `fromJsonSchema` from `@modelcontextprotocol/server` or + * `@modelcontextprotocol/client` omit the second argument — the runtime shim + * supplies the appropriate default validator. */ function fromJsonSchema_basicUsage() { //#region fromJsonSchema_basicUsage const inputSchema = fromJsonSchema<{ name: string }>( { type: 'object', properties: { name: { type: 'string' } }, required: ['name'] }, - new AjvJsonSchemaValidator() + validator ); // Use with server.registerTool('greet', { inputSchema }, handler) //#endregion fromJsonSchema_basicUsage diff --git a/packages/core/src/validators/fromJsonSchema.ts b/packages/core/src/validators/fromJsonSchema.ts index 73db24e8cc..4b7a6f11a3 100644 --- a/packages/core/src/validators/fromJsonSchema.ts +++ b/packages/core/src/validators/fromJsonSchema.ts @@ -19,7 +19,7 @@ import type { JsonSchemaType, jsonSchemaValidator } from './types.js'; * ```ts source="./fromJsonSchema.examples.ts#fromJsonSchema_basicUsage" * const inputSchema = fromJsonSchema<{ name: string }>( * { type: 'object', properties: { name: { type: 'string' } }, required: ['name'] }, - * new AjvJsonSchemaValidator() + * validator * ); * // Use with server.registerTool('greet', { inputSchema }, handler) * ``` diff --git a/packages/server/package.json b/packages/server/package.json index 20195e7101..d7bce70fa4 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -28,6 +28,10 @@ "types": "./dist/stdio.d.mts", "import": "./dist/stdio.mjs" }, + "./validators/ajv": { + "types": "./dist/validators/ajv.d.mts", + "import": "./dist/validators/ajv.mjs" + }, "./validators/cf-worker": { "types": "./dist/validators/cfWorker.d.mts", "import": "./dist/validators/cfWorker.mjs" @@ -54,6 +58,9 @@ "types": "./dist/index.d.mts", "typesVersions": { "*": { + "validators/ajv": [ + "dist/validators/ajv.d.mts" + ], "validators/cf-worker": [ "dist/validators/cfWorker.d.mts" ], @@ -86,6 +93,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..70e7cba486 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 (AJV-backed on Node.js, `@cfworker/json-schema`-backed 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/ajv.ts b/packages/server/src/validators/ajv.ts new file mode 100644 index 0000000000..31f0fed3b3 --- /dev/null +++ b/packages/server/src/validators/ajv.ts @@ -0,0 +1,14 @@ +/** + * Customisation entry point for the AJV validator. Re-exports `Ajv` + `addFormats` from the + * SDK's bundled copy, so customising the validator needs no extra installs. + * + * @example + * ```ts + * import { Ajv, addFormats, AjvJsonSchemaValidator } from '@modelcontextprotocol/server/validators/ajv'; + * + * const ajv = new Ajv({ strict: true, allErrors: true }); + * addFormats(ajv); + * const validator = new AjvJsonSchemaValidator(ajv); + * ``` + */ +export { addFormats, Ajv, AjvJsonSchemaValidator } from '@modelcontextprotocol/core/validators/ajv'; diff --git a/packages/server/src/validators/cfWorker.ts b/packages/server/src/validators/cfWorker.ts index f804b768eb..2969b4dc9d 100644 --- a/packages/server/src/validators/cfWorker.ts +++ b/packages/server/src/validators/cfWorker.ts @@ -1,10 +1,3 @@ -/** - * Cloudflare Workers JSON Schema validator, available as a sub-path export. - * - * @example - * ```ts - * import { CfWorkerJsonSchemaValidator } from '@modelcontextprotocol/server/validators/cf-worker'; - * ``` - */ +/** Customisation entry point for the `@cfworker/json-schema` validator. */ 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..bf3924e484 100644 --- a/packages/server/test/server/barrelClean.test.ts +++ b/packages/server/test/server/barrelClean.test.ts @@ -8,6 +8,8 @@ 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/; +// Anchored at start-of-line so JSDoc-example `from 'ajv'` strings in vendored chunks don't match. +const VALIDATOR_BACKEND_IMPORT = /^import[^\n]*?from\s+["'](?:ajv|ajv-formats|@cfworker\/json-schema)["']/m; function chunkImportsOf(entryPath: string): string[] { const visited = new Set(); @@ -53,4 +55,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..891ce49641 100644 --- a/packages/server/tsdown.config.ts +++ b/packages/server/tsdown.config.ts @@ -2,39 +2,34 @@ import { defineConfig } from 'tsdown'; 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'], - - // 2. Output Configuration + entry: [ + 'src/index.ts', + 'src/stdio.ts', + 'src/shimsNode.ts', + 'src/shimsWorkerd.ts', + 'src/validators/ajv.ts', + 'src/validators/cfWorker.ts' + ], format: ['esm'], outDir: 'dist', - clean: true, // Recommended: Cleans 'dist' before building + clean: true, sourcemap: true, - - // 3. Platform & Target target: 'esnext', platform: 'node', - shims: true, // Polyfills common Node.js shims (__dirname, etc.) - - // 4. Type Definitions - // Bundles d.ts files into a single output + shims: true, dts: { resolver: 'tsc', - // override just for DTS generation: + resolve: ['ajv', 'ajv-formats'], compilerOptions: { baseUrl: '.', 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'], - - // 6. External packages - keep self-reference imports external for runtime resolution + noExternal: ['@modelcontextprotocol/core', 'ajv', 'ajv-formats', '@cfworker/json-schema'], external: ['@modelcontextprotocol/server/_shims'] }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1dab7759f0..2e0326f2b3 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 @@ -626,12 +632,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 @@ -675,6 +675,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 @@ -959,6 +965,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/e2e/scenarios/tools.test.ts b/test/e2e/scenarios/tools.test.ts index 31a7c7da84..408712f23a 100644 --- a/test/e2e/scenarios/tools.test.ts +++ b/test/e2e/scenarios/tools.test.ts @@ -22,7 +22,6 @@ import { Client } from '@modelcontextprotocol/client'; import type { JsonSchemaType } from '@modelcontextprotocol/core'; -import { AjvJsonSchemaValidator } from '@modelcontextprotocol/core'; import type { CreateMessageRequest, CreateMessageResult, @@ -33,6 +32,7 @@ import type { Tool } from '@modelcontextprotocol/server'; import { McpServer, ProtocolError, ProtocolErrorCode, Server, UrlElicitationRequiredError } from '@modelcontextprotocol/server'; +import { AjvJsonSchemaValidator } from '@modelcontextprotocol/server/validators/ajv'; import { expect, vi } from 'vitest'; import { z } from 'zod/v4'; diff --git a/test/e2e/scenarios/validation.test.ts b/test/e2e/scenarios/validation.test.ts index 6c1b004d90..21121240e7 100644 --- a/test/e2e/scenarios/validation.test.ts +++ b/test/e2e/scenarios/validation.test.ts @@ -14,8 +14,6 @@ import { Client } from '@modelcontextprotocol/client'; import type { JsonSchemaType, JsonSchemaValidator, jsonSchemaValidator, StandardSchemaWithJSON } from '@modelcontextprotocol/core'; -import { AjvJsonSchemaValidator } from '@modelcontextprotocol/core'; -import { CfWorkerJsonSchemaValidator } from '@modelcontextprotocol/core/validators/cfWorker'; import type { Tool } from '@modelcontextprotocol/server'; import { fromJsonSchema, @@ -26,6 +24,8 @@ import { Server, specTypeSchemas } from '@modelcontextprotocol/server'; +import { AjvJsonSchemaValidator } from '@modelcontextprotocol/server/validators/ajv'; +import { CfWorkerJsonSchemaValidator } from '@modelcontextprotocol/server/validators/cf-worker'; import { expect } from 'vitest'; import { z } from 'zod/v4'; diff --git a/test/e2e/tsconfig.json b/test/e2e/tsconfig.json index f1b80046e0..439e737eab 100644 --- a/test/e2e/tsconfig.json +++ b/test/e2e/tsconfig.json @@ -13,9 +13,13 @@ "@modelcontextprotocol/client": ["./node_modules/@modelcontextprotocol/client/src/index.ts"], "@modelcontextprotocol/client/stdio": ["./node_modules/@modelcontextprotocol/client/src/stdio.ts"], "@modelcontextprotocol/client/_shims": ["./node_modules/@modelcontextprotocol/client/src/shimsNode.ts"], + "@modelcontextprotocol/client/validators/ajv": ["./node_modules/@modelcontextprotocol/client/src/validators/ajv.ts"], + "@modelcontextprotocol/client/validators/cf-worker": ["./node_modules/@modelcontextprotocol/client/src/validators/cfWorker.ts"], "@modelcontextprotocol/server": ["./node_modules/@modelcontextprotocol/server/src/index.ts"], "@modelcontextprotocol/server/stdio": ["./node_modules/@modelcontextprotocol/server/src/stdio.ts"], "@modelcontextprotocol/server/_shims": ["./node_modules/@modelcontextprotocol/server/src/shimsNode.ts"], + "@modelcontextprotocol/server/validators/ajv": ["./node_modules/@modelcontextprotocol/server/src/validators/ajv.ts"], + "@modelcontextprotocol/server/validators/cf-worker": ["./node_modules/@modelcontextprotocol/server/src/validators/cfWorker.ts"], "@modelcontextprotocol/express": ["./node_modules/@modelcontextprotocol/express/src/index.ts"], "@modelcontextprotocol/fastify": ["./node_modules/@modelcontextprotocol/fastify/src/index.ts"], "@modelcontextprotocol/hono": ["./node_modules/@modelcontextprotocol/hono/src/index.ts"], diff --git a/test/integration/test/server/cloudflareWorkers.test.ts b/test/integration/test/server/cloudflareWorkers.test.ts index 9c2d73a40e..c32f1dc96c 100644 --- a/test/integration/test/server/cloudflareWorkers.test.ts +++ b/test/integration/test/server/cloudflareWorkers.test.ts @@ -15,15 +15,77 @@ import { Client, StreamableHTTPClientTransport } from '@modelcontextprotocol/cli import { afterAll, beforeAll, describe, expect, it } from 'vitest'; const PORT = 8787; +const READINESS_TIMEOUT_MS = 60_000; +const READINESS_POLL_INTERVAL_MS = 100; -interface TestEnv { - tempDir: string; - process: ChildProcess; - cleanup: () => Promise; +/** + * Wait until the worker can serve a real MCP `initialize` request. + * + * Wrangler's "Ready on …" stdout line is unreliable: miniflare can print it before the user + * worker is actually wired, and subsequent POSTs come back as `500 Network connection lost` or + * `ECONNREFUSED`. The only signal we can trust is "the server returned an MCP-shaped response + * to a protocol request". + * + * Polls the configured port with an MCP `initialize` POST every {@link READINESS_POLL_INTERVAL_MS}ms + * until either a JSON-RPC result body comes back, the wrangler process exits, or + * {@link READINESS_TIMEOUT_MS} elapses. + */ +async function waitForMcpReady(proc: ChildProcess): Promise { + let stderrTail = ''; + proc.stderr?.on('data', d => { + stderrTail = (stderrTail + d.toString()).slice(-2048); + }); + + let processExitedWithCode: number | null = null; + proc.on('exit', code => { + processExitedWithCode = code ?? -1; + }); + + const deadline = Date.now() + READINESS_TIMEOUT_MS; + let lastFailure = 'no attempts made'; + + while (Date.now() < deadline) { + if (processExitedWithCode !== null) { + throw new Error(`wrangler dev exited with code ${processExitedWithCode} before becoming ready.\nstderr tail:\n${stderrTail}`); + } + + try { + const response = await fetch(`http://127.0.0.1:${PORT}/`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json, text/event-stream' + }, + body: JSON.stringify({ + jsonrpc: '2.0', + id: 'readiness-probe', + method: 'initialize', + params: { + protocolVersion: '2025-06-18', + capabilities: {}, + clientInfo: { name: 'readiness-probe', version: '0' } + } + }) + }); + const body = await response.text(); + if (response.ok && body.includes('"jsonrpc"') && body.includes('"result"')) { + return; + } + lastFailure = `status=${response.status} body=${body.slice(0, 200)}`; + } catch (error) { + lastFailure = (error as { cause?: { code?: string }; message: string }).cause?.code ?? (error as Error).message; + } + + await new Promise(resolve => setTimeout(resolve, READINESS_POLL_INTERVAL_MS)); + } + + throw new Error( + `Worker did not become ready within ${READINESS_TIMEOUT_MS}ms.\nLast probe: ${lastFailure}\nstderr tail:\n${stderrTail}` + ); } describe('Cloudflare Workers compatibility (no nodejs_compat)', () => { - let env: TestEnv | null = null; + let cleanup: (() => Promise) | null = null; beforeAll(async () => { const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cf-worker-test-')); @@ -42,8 +104,7 @@ describe('Cloudflare Workers compatibility (no nodejs_compat)', () => { private: true, type: 'module', dependencies: { - '@modelcontextprotocol/server': `file:./${tarballName}`, - '@cfworker/json-schema': '^4.1.1' + '@modelcontextprotocol/server': `file:./${tarballName}` }, devDependencies: { wrangler: '^4.14.4' @@ -84,50 +145,22 @@ export default { // Install dependencies execSync('npm install', { cwd: tempDir, stdio: 'pipe', timeout: 60_000 }); - // Start wrangler dev server + // Start wrangler dev server. Readiness is determined by probing the MCP endpoint, not by + // parsing wrangler's stdout — see waitForMcpReady for the reasoning. const proc = spawn('npx', ['wrangler', 'dev', '--local', '--port', String(PORT)], { cwd: tempDir, shell: true, stdio: 'pipe' }); - // Wait for server to be ready - await new Promise((resolve, reject) => { - const timeout = setTimeout(() => reject(new Error('Wrangler startup timeout')), 60_000); - let stderrData = ''; - - proc.stdout?.on('data', data => { - const output = data.toString(); - if (/Ready on|Listening on/.test(output)) { - clearTimeout(timeout); - // Extra delay for wrangler to fully initialize - setTimeout(resolve, 1000); - } - }); - - proc.stderr?.on('data', data => { - stderrData += data.toString(); - // Check for fatal errors like missing node: modules - if (/No such module "node:/.test(stderrData)) { - clearTimeout(timeout); - reject(new Error(`Wrangler fatal error: ${stderrData}`)); - } - }); - - proc.on('error', err => { - clearTimeout(timeout); - reject(err); - }); - - proc.on('close', code => { - if (code !== 0 && code !== null) { - clearTimeout(timeout); - reject(new Error(`Wrangler exited with code ${code}. stderr: ${stderrData}`)); - } - }); - }); + try { + await waitForMcpReady(proc); + } catch (error) { + proc.kill('SIGTERM'); + throw error; + } - const cleanup = async () => { + cleanup = async () => { proc.kill('SIGTERM'); await new Promise(resolve => { proc.on('close', () => resolve()); @@ -139,35 +172,16 @@ export default { // Ignore cleanup errors } }; - - env = { tempDir, process: proc, cleanup }; }, 120_000); afterAll(async () => { - await env?.cleanup(); + await cleanup?.(); }); it('should handle MCP requests', async () => { - expect(env).not.toBeNull(); - - // Retry connection — wrangler may report "Ready" before it can handle requests - let client!: Client; - let lastError: unknown; - for (let attempt = 0; attempt < 5; attempt++) { - try { - client = new Client({ name: 'test-client', version: '1.0.0' }); - const transport = new StreamableHTTPClientTransport(new URL(`http://127.0.0.1:${PORT}/`)); - await client.connect(transport); - lastError = undefined; - break; - } catch (error) { - lastError = error; - await new Promise(resolve => setTimeout(resolve, 1000)); - } - } - if (lastError) { - throw lastError; - } + const client = new Client({ name: 'test-client', version: '1.0.0' }); + const transport = new StreamableHTTPClientTransport(new URL(`http://127.0.0.1:${PORT}/`)); + await client.connect(transport); const result = await client.callTool({ name: 'greet', arguments: { name: 'World' } }); expect(result.content).toEqual([{ type: 'text', text: 'Hello, World!' }]); 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..ffc41ce4d8 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 }; @@ -428,44 +428,6 @@ 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 diff --git a/test/integration/tsconfig.json b/test/integration/tsconfig.json index 4a2820da3f..64391c93e0 100644 --- a/test/integration/tsconfig.json +++ b/test/integration/tsconfig.json @@ -7,15 +7,20 @@ "*": ["./*"], "@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" ], "@modelcontextprotocol/client": ["./node_modules/@modelcontextprotocol/client/src/index.ts"], "@modelcontextprotocol/client/stdio": ["./node_modules/@modelcontextprotocol/client/src/stdio.ts"], "@modelcontextprotocol/client/_shims": ["./node_modules/@modelcontextprotocol/client/src/shimsNode.ts"], + "@modelcontextprotocol/client/validators/ajv": ["./node_modules/@modelcontextprotocol/client/src/validators/ajv.ts"], + "@modelcontextprotocol/client/validators/cf-worker": ["./node_modules/@modelcontextprotocol/client/src/validators/cfWorker.ts"], "@modelcontextprotocol/server": ["./node_modules/@modelcontextprotocol/server/src/index.ts"], "@modelcontextprotocol/server/stdio": ["./node_modules/@modelcontextprotocol/server/src/stdio.ts"], "@modelcontextprotocol/server/_shims": ["./node_modules/@modelcontextprotocol/server/src/shimsNode.ts"], + "@modelcontextprotocol/server/validators/ajv": ["./node_modules/@modelcontextprotocol/server/src/validators/ajv.ts"], + "@modelcontextprotocol/server/validators/cf-worker": ["./node_modules/@modelcontextprotocol/server/src/validators/cfWorker.ts"], "@modelcontextprotocol/express": ["./node_modules/@modelcontextprotocol/express/src/index.ts"], "@modelcontextprotocol/node": ["./node_modules/@modelcontextprotocol/node/src/index.ts"], "@modelcontextprotocol/vitest-config": ["./node_modules/@modelcontextprotocol/vitest-config/tsconfig.json"],