fix(gateway): merge MCP schema properties by own keys (#102856)

Co-authored-by: Alix-007 <li.long15@xydigit.com>
This commit is contained in:
Peter Steinberger 2026-07-09 15:46:41 +01:00 committed by GitHub
parent aaf5af2fbc
commit 29517fe178
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 87 additions and 3 deletions

View file

@ -0,0 +1,84 @@
// Gateway tests cover MCP loopback schema projection behavior.
import { describe, expect, it } from "vitest";
import { buildMcpToolSchema } from "./mcp-http.schema.js";
describe("buildMcpToolSchema", () => {
it("keeps union schema properties named like Object prototype keys", () => {
const [entry] = buildMcpToolSchema([
{
name: "proof_tool",
description: "proof",
parameters: {
anyOf: [
{
type: "object",
properties: {
toString: { type: "string" },
},
required: ["toString"],
},
],
},
} as never,
]);
const inputSchema = entry?.inputSchema as
| { properties?: Record<string, unknown>; required?: string[] }
| undefined;
const propertySchema = inputSchema?.properties?.["toString"];
expect(Object.hasOwn(inputSchema?.properties ?? {}, "toString")).toBe(true);
expect(propertySchema).toEqual({ type: "string" });
expect(inputSchema?.required).toEqual(["toString"]);
});
it("serializes union schema properties named __proto__ as own keys", () => {
const protoKey = "__proto__";
const [entry] = buildMcpToolSchema([
{
name: "proof_tool",
description: "proof",
parameters: {
anyOf: [
{
type: "object",
properties: Object.fromEntries([[protoKey, { type: "string" }]]),
required: [protoKey],
},
],
},
} as never,
]);
const inputSchema = entry?.inputSchema as
| { properties?: Record<string, unknown>; required?: string[] }
| undefined;
expect(Object.hasOwn(inputSchema?.properties ?? {}, protoKey)).toBe(true);
expect(inputSchema?.properties?.[protoKey]).toEqual({ type: "string" });
expect(JSON.stringify(inputSchema?.properties)).toContain('"__proto__"');
expect(inputSchema?.required).toEqual([protoKey]);
});
it("does not keep inherited prototype names as required schema keys", () => {
const [entry] = buildMcpToolSchema([
{
name: "proof_tool",
description: "proof",
parameters: {
anyOf: [
{
type: "object",
properties: {
value: { type: "string" },
},
required: ["toString"],
},
],
},
} as never,
]);
expect(entry?.inputSchema.required).toEqual([]);
});
});

View file

@ -67,7 +67,7 @@ function flattenUnionSchema(
if (!Array.isArray(variants) || variants.length === 0) { if (!Array.isArray(variants) || variants.length === 0) {
return raw; return raw;
} }
const mergedProps: Record<string, unknown> = {}; const mergedProps = Object.create(null) as Record<string, unknown>;
const requiredSets: Set<string>[] = []; const requiredSets: Set<string>[] = [];
for (const variant of variants) { for (const variant of variants) {
if (variant === true) { if (variant === true) {
@ -86,7 +86,7 @@ function flattenUnionSchema(
); );
continue; continue;
} }
if (!(key in mergedProps)) { if (!Object.hasOwn(mergedProps, key)) {
mergedProps[key] = schema; mergedProps[key] = schema;
continue; continue;
} }
@ -139,7 +139,7 @@ function flattenUnionSchema(
const required = const required =
requiredSets.length > 0 requiredSets.length > 0
? [...(requiredSets[0] ?? [])].filter( ? [...(requiredSets[0] ?? [])].filter(
(key) => key in mergedProps && requiredSets.every((set) => set.has(key)), (key) => Object.hasOwn(mergedProps, key) && requiredSets.every((set) => set.has(key)),
) )
: []; : [];
const { anyOf: _anyOf, oneOf: _oneOf, ...rest } = raw; const { anyOf: _anyOf, oneOf: _oneOf, ...rest } = raw;