feat(client,server): bundle default validators, expose customisation via subpaths (#2088)

This commit is contained in:
Matt 2026-05-29 19:39:32 +01:00 • committed by GitHub
commit 16d13abf78
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
33 changed files with 749 additions and 314 deletions

View file

@ -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",

View file

@ -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;

View file

@ -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 —

View file

@ -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';

View file

@ -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';

View file

@ -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<string>();
@ -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) })
);
}
}
});
});

View file

@ -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<T>(schema: JsonSchemaType) {
this.schemas.push(schema);
return (value: unknown): JsonSchemaValidatorResult<T> => {
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 }]);
});
});

View file

@ -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"
],

View file

@ -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']
});

View file

@ -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>`.
*/
subpathSuffix?: string;
}
export const IMPORT_MAP: Record<string, ImportMapping> = {
@ -163,6 +168,27 @@ export const IMPORT_MAP: Record<string, ImportMapping> = {
}
};
// 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<string, readonly string[]> = {
'/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.');
}

View file

@ -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) {

View file

@ -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 };

View file

@ -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');
});
});
});

View file

@ -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'`);
});
});
});

View file

@ -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",

View file

@ -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';

View file

@ -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;
}

View file

@ -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';

View file

@ -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;
}

View file

@ -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<T>(schema: JsonSchemaType): JsonSchemaValidator<T> {
// 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;

Some files were not shown because too many files have changed in this diff Show more