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