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>();
|
||||
|
|
@ -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) })
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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 }]);
|
||||
});
|
||||
});
|
||||
Loading…
Reference in a new issue