feat(client,server): bundle default validators, expose customisation via subpaths (#2088)
This commit is contained in:
parent
4a5c863a21
commit
16d13abf78
2 changed files with 749 additions and 314 deletions
|
|
@ -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 }]);
|
||||
});
|
||||
});
|
||||
Loading…
Reference in a new issue