diff --git a/src/gateway/mcp-http.schema.test.ts b/src/gateway/mcp-http.schema.test.ts new file mode 100644 index 00000000000..64feb1144f5 --- /dev/null +++ b/src/gateway/mcp-http.schema.test.ts @@ -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; 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; 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([]); + }); +}); diff --git a/src/gateway/mcp-http.schema.ts b/src/gateway/mcp-http.schema.ts index 3ad0d21c002..558cecb503e 100644 --- a/src/gateway/mcp-http.schema.ts +++ b/src/gateway/mcp-http.schema.ts @@ -67,7 +67,7 @@ function flattenUnionSchema( if (!Array.isArray(variants) || variants.length === 0) { return raw; } - const mergedProps: Record = {}; + const mergedProps = Object.create(null) as Record; const requiredSets: Set[] = []; for (const variant of variants) { if (variant === true) { @@ -86,7 +86,7 @@ function flattenUnionSchema( ); continue; } - if (!(key in mergedProps)) { + if (!Object.hasOwn(mergedProps, key)) { mergedProps[key] = schema; continue; } @@ -139,7 +139,7 @@ function flattenUnionSchema( const required = requiredSets.length > 0 ? [...(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;