mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-19 13:36:48 +00:00
724c88c201
A mechanical prettier --write over server/src, server/tests, server/scripts and shared/src with the workspaces' .prettierrc; nothing but layout and import order changes, and every file keeps the same set of imports. shared is formatted throughout and its format baseline is empty. The server baseline keeps the 35 files that Prettier would grow past their lint:size limit (they need a split first, which is not a formatting change), and the generated entity files stay as their generator writes them.
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/,
|
|
);
|
|
});
|
|
});
|