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
2 changed files with 749 additions and 314 deletions

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

View file

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