feat(client,server): bundle default validators, expose customisation via subpaths (#2088)
This commit is contained in:
parent
4a5c863a21
commit
16d13abf78
33 changed files with 749 additions and 314 deletions
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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 —
|
||||
|
|
|
|||
14
packages/client/src/validators/ajv.ts
Normal file
14
packages/client/src/validators/ajv.ts
Normal 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';
|
||||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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) })
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
|||
110
packages/client/test/client/jsonSchemaValidatorOverride.test.ts
Normal file
110
packages/client/test/client/jsonSchemaValidatorOverride.test.ts
Normal 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 }]);
|
||||
});
|
||||
});
|
||||
|
|
@ -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"
|
||||
],
|
||||
|
|
|
|||
|
|
@ -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']
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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.');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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'`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Reference in a new issue