🧩 fix: Normalize Malformed MCP Required Schemas (#14771)

This commit is contained in:
Danny Avila 2026-08-12 22:29:25 -04:00 committed by GitHub
parent 8f1f43f33e
commit 861cfe8a3c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 70 additions and 0 deletions

View file

@ -2121,6 +2121,50 @@ describe('normalizeJsonSchema', () => {
expect(result.additionalProperties).toEqual({ type: 'string', enum: ['val'] });
});
it('should drop malformed required keywords recursively', () => {
const schema = {
type: 'object',
properties: {
required: { type: 'boolean' },
config: {
type: 'object',
properties: { host: { type: 'string' } },
required: { host: true },
},
},
oneOf: [{ type: 'object', required: 'name' }],
items: { type: 'object', required: null },
required: {},
};
expect(normalizeJsonSchema(schema)).toEqual({
type: 'object',
properties: {
required: { type: 'boolean' },
config: {
type: 'object',
properties: { host: { type: 'string' } },
},
},
oneOf: [{ type: 'object' }],
items: { type: 'object' },
});
});
it('should normalize required arrays to unique property names', () => {
const schema = {
type: 'object',
properties: {
name: { type: 'string' },
age: { type: 'number' },
},
required: ['name', 42, null, 'age', 'name', { key: 'value' }],
};
expect(normalizeJsonSchema(schema).required).toEqual(['name', 'age']);
expect(normalizeJsonSchema({ type: 'object', required: [] }).required).toEqual([]);
});
it('should handle null, undefined, and primitive inputs safely', () => {
expect(normalizeJsonSchema(null as any)).toBeNull();
expect(normalizeJsonSchema(undefined as any)).toBeUndefined();

View file

@ -337,6 +337,7 @@ export function resolveJsonSchemaRefs<T extends Record<string, unknown>>(
* - Strips vendor extension fields (`x-*` prefixed keys, e.g. `x-google-enum-descriptions`)
* - Strips `definitions` and `$`-prefixed schema keywords (`$defs`, `$schema`,
* `$id`, `$comment`, ...) that may survive ref resolution
* - Drops malformed `required` values and filters arrays to property names
*
* Beyond LLM compatibility, dropping every `$`-prefixed keyword also makes the
* output safe to persist: MongoDB rejects field names beginning with `$`, so a
@ -375,6 +376,23 @@ const SCHEMA_MAP_KEYWORDS = new Set([
/** Keywords whose value is an array of subschemas. */
const SCHEMA_LIST_KEYWORDS = new Set(['oneOf', 'anyOf', 'allOf', 'prefixItems']);
function normalizeRequired(value: unknown): string[] | undefined {
if (!Array.isArray(value)) {
return undefined;
}
const required: string[] = [];
const seen = new Set<string>();
for (const entry of value) {
if (typeof entry !== 'string' || seen.has(entry)) {
continue;
}
seen.add(entry);
required.push(entry);
}
return required;
}
export function normalizeJsonSchema<T extends Record<string, unknown>>(schema: T): T {
if (!schema || typeof schema !== 'object') {
return schema;
@ -416,6 +434,14 @@ export function normalizeJsonSchema<T extends Record<string, unknown>>(schema: T
continue;
}
if (key === 'required') {
const required = normalizeRequired(value);
if (required) {
result[key] = required;
}
continue;
}
if (
SCHEMA_MAP_KEYWORDS.has(key) &&
value &&