mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-19 21:46:49 +00:00
102 lines
4.3 KiB
TypeScript
102 lines
4.3 KiB
TypeScript
/**
|
|
* The advertised schema is the enforced schema — MCPENF-001 to MCPENF-010.
|
|
*
|
|
* `.meta()` shows the model the plugin's declaration verbatim. The SDK contract,
|
|
* and every client, read that as "arguments are validated before the handler
|
|
* runs". An earlier version advertised types, enums, ranges and
|
|
* `additionalProperties: false` while accepting literally anything, which is
|
|
* the worst of both: the model formats its call to a contract nothing checks.
|
|
*/
|
|
import {
|
|
McpToolSchemaError,
|
|
buildToolInputSchema,
|
|
normaliseToolSchema,
|
|
} from '../../../src/nest/plugins/mcp-tool-schema';
|
|
|
|
import { describe, expect, it } from 'vitest';
|
|
import { z } from 'zod';
|
|
|
|
const build = (declared: unknown) => buildToolInputSchema(normaliseToolSchema(declared)) as unknown as z.ZodType;
|
|
|
|
const advertised = (declared: unknown) =>
|
|
z.toJSONSchema(buildToolInputSchema(normaliseToolSchema(declared)) as unknown as z.ZodType, {
|
|
io: 'input',
|
|
}) as Record<string, unknown>;
|
|
|
|
const strictSchema = {
|
|
type: 'object',
|
|
properties: {
|
|
days: { type: 'integer', minimum: 1, maximum: 14 },
|
|
mode: { type: 'string', enum: ['fast', 'slow'] },
|
|
email: { type: 'string', format: 'email' },
|
|
tags: { type: 'array', items: { type: 'string' }, maxItems: 3 },
|
|
},
|
|
required: ['days'],
|
|
additionalProperties: false,
|
|
};
|
|
|
|
describe('enforcement matches the advertisement', () => {
|
|
it('MCPENF-001: rejects a value of the wrong type', () => {
|
|
expect(build(strictSchema).safeParse({ days: 'seven' }).success).toBe(false);
|
|
});
|
|
|
|
it('MCPENF-002: rejects a value outside the declared range', () => {
|
|
expect(build(strictSchema).safeParse({ days: 999 }).success).toBe(false);
|
|
expect(build(strictSchema).safeParse({ days: 7 }).success).toBe(true);
|
|
});
|
|
|
|
it('MCPENF-003: rejects a value outside the declared enum', () => {
|
|
expect(build(strictSchema).safeParse({ days: 3, mode: 'teleport' }).success).toBe(false);
|
|
expect(build(strictSchema).safeParse({ days: 3, mode: 'fast' }).success).toBe(true);
|
|
});
|
|
|
|
it('MCPENF-004: honours additionalProperties: false', () => {
|
|
expect(build(strictSchema).safeParse({ days: 3, surprise: true }).success).toBe(false);
|
|
});
|
|
|
|
it('MCPENF-005: still enforces required', () => {
|
|
expect(build(strictSchema).safeParse({ mode: 'fast' }).success).toBe(false);
|
|
});
|
|
|
|
it('MCPENF-006: enforces string formats and array bounds', () => {
|
|
expect(build(strictSchema).safeParse({ days: 1, email: 'nope' }).success).toBe(false);
|
|
expect(build(strictSchema).safeParse({ days: 1, email: 'a@b.co' }).success).toBe(true);
|
|
expect(build(strictSchema).safeParse({ days: 1, tags: ['a', 'b', 'c', 'd'] }).success).toBe(false);
|
|
});
|
|
|
|
it('MCPENF-007: an open object still accepts extra keys', () => {
|
|
// additionalProperties is absent, so the plugin may accept more than it
|
|
// advertises. That is a choice the declaration makes, not an accident.
|
|
const open = { type: 'object', properties: { a: { type: 'string' } } };
|
|
expect(build(open).safeParse({ a: 'x', extra: 1 }).success).toBe(true);
|
|
});
|
|
|
|
it('MCPENF-008: the advertisement is still the plugin declaration verbatim', () => {
|
|
// Enforcement must not have cost us the reason we advertise .meta() at all.
|
|
const out = advertised(strictSchema);
|
|
expect(out.properties).toEqual(strictSchema.properties);
|
|
expect(out.required).toEqual(['days']);
|
|
expect(out.additionalProperties).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('unenforceable declarations are refused, not advertised', () => {
|
|
it('MCPENF-009: rejects keywords the validator cannot express', () => {
|
|
// oneOf/anyOf/allOf/not would otherwise reach the model and bind nothing.
|
|
for (const keyword of ['oneOf', 'anyOf', 'allOf', 'not', 'patternProperties']) {
|
|
const schema = { type: 'object', properties: { a: { [keyword]: [{ type: 'string' }] } } };
|
|
expect(() => build(schema), keyword).toThrow(McpToolSchemaError);
|
|
}
|
|
});
|
|
|
|
it('MCPENF-010: rejects an unsupported type, format, or broken pattern', () => {
|
|
expect(() => build({ type: 'object', properties: { a: { type: 'bigint' } } })).toThrow(/unsupported type/);
|
|
expect(() => build({ type: 'object', properties: { a: { type: 'string', format: 'ipv6' } } })).toThrow(
|
|
/unsupported format/,
|
|
);
|
|
expect(() => build({ type: 'object', properties: { a: { type: 'string', pattern: '([' } } })).toThrow(
|
|
/regular expression/,
|
|
);
|
|
});
|
|
});
|