Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -155,11 +155,12 @@ describe("getMcpServerTools", () => {
const result = getMcpServerTools(mockHub as McpHub)

expect(result).toHaveLength(1)
// additionalProperties: false should only be on the root object type, not on primitive types
expect(getFunction(result[0]).parameters).toEqual({
type: "object",
properties: {
requiredField: { type: "string", additionalProperties: false },
optionalField: { type: "number", additionalProperties: false },
requiredField: { type: "string" },
optionalField: { type: "number" },
},
additionalProperties: false,
required: ["requiredField"],
Expand All @@ -183,10 +184,11 @@ describe("getMcpServerTools", () => {
const result = getMcpServerTools(mockHub as McpHub)

expect(result).toHaveLength(1)
// additionalProperties: false should only be on the root object type, not on primitive types
expect(getFunction(result[0]).parameters).toEqual({
type: "object",
properties: {
optionalField: { type: "string", additionalProperties: false },
optionalField: { type: "string" },
},
additionalProperties: false,
})
Expand Down
39 changes: 19 additions & 20 deletions src/utils/__tests__/json-schema.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ describe("normalizeToolSchema", () => {

const result = normalizeToolSchema(input)

// additionalProperties should NOT be added to non-object types (string, null)
expect(result).toEqual({
anyOf: [{ type: "string" }, { type: "null" }],
description: "Optional field",
additionalProperties: false,
})
})

Expand All @@ -26,11 +26,11 @@ describe("normalizeToolSchema", () => {

const result = normalizeToolSchema(input)

// additionalProperties should NOT be added to array or primitive types
expect(result).toEqual({
anyOf: [{ type: "array" }, { type: "null" }],
items: { type: "string", additionalProperties: false },
items: { type: "string" },
description: "Optional array",
additionalProperties: false,
})
})

Expand All @@ -42,10 +42,10 @@ describe("normalizeToolSchema", () => {

const result = normalizeToolSchema(input)

// additionalProperties should NOT be added to string type
expect(result).toEqual({
type: "string",
description: "Required field",
additionalProperties: false,
})
})

Expand All @@ -64,14 +64,14 @@ describe("normalizeToolSchema", () => {

const result = normalizeToolSchema(input)

// additionalProperties: false should ONLY be on the object type, not on primitives
expect(result).toEqual({
type: "object",
properties: {
name: { type: "string", additionalProperties: false },
name: { type: "string" },
optional: {
anyOf: [{ type: "string" }, { type: "null" }],
description: "Optional nested field",
additionalProperties: false,
},
},
required: ["name"],
Expand All @@ -96,21 +96,20 @@ describe("normalizeToolSchema", () => {

const result = normalizeToolSchema(input)

// additionalProperties: false should ONLY be on object types
expect(result).toEqual({
type: "array",
items: {
type: "object",
properties: {
path: { type: "string", additionalProperties: false },
path: { type: "string" },
line_ranges: {
anyOf: [{ type: "array" }, { type: "null" }],
items: { type: "integer", additionalProperties: false },
additionalProperties: false,
items: { type: "integer" },
},
},
additionalProperties: false,
},
additionalProperties: false,
})
})

Expand Down Expand Up @@ -162,18 +161,18 @@ describe("normalizeToolSchema", () => {

const result = normalizeToolSchema(input)

// additionalProperties: false should ONLY be on object types, not on null or primitive types
expect(result).toEqual({
anyOf: [
{
type: "object",
properties: {
optional: { anyOf: [{ type: "string" }, { type: "null" }], additionalProperties: false },
optional: { anyOf: [{ type: "string" }, { type: "null" }] },
},
additionalProperties: false,
},
{ type: "null", additionalProperties: false },
{ type: "null" },
],
additionalProperties: false,
})
})

Expand All @@ -183,7 +182,9 @@ describe("normalizeToolSchema", () => {
expect(normalizeToolSchema(123 as any)).toBe(123)
})

it("should transform additionalProperties when it is a schema object", () => {
it("should force additionalProperties to false for object types even when set to a schema", () => {
// For strict mode compatibility, we MUST force additionalProperties: false
// even when the original schema allowed arbitrary properties
const input = {
type: "object",
additionalProperties: {
Expand All @@ -193,13 +194,11 @@ describe("normalizeToolSchema", () => {

const result = normalizeToolSchema(input)

// The original additionalProperties schema is replaced with false for strict mode
expect(result).toEqual({
type: "object",
properties: {},
additionalProperties: {
anyOf: [{ type: "string" }, { type: "null" }],
additionalProperties: false,
},
additionalProperties: false,
})
})

Expand Down Expand Up @@ -276,11 +275,11 @@ describe("normalizeToolSchema", () => {

const result = normalizeToolSchema(input)

// additionalProperties should NOT be added to string types
expect(result).toEqual({
type: "string",
format: "date-time",
description: "Timestamp",
additionalProperties: false,
})
})

Expand Down Expand Up @@ -335,10 +334,10 @@ describe("normalizeToolSchema", () => {

const result = normalizeToolSchema(input)

// additionalProperties should NOT be added to string types
expect(result).toEqual({
type: "string",
description: "URL field",
additionalProperties: false,
})
expect(result.format).toBeUndefined()
})
Expand Down
20 changes: 18 additions & 2 deletions src/utils/json-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,8 @@ const NormalizedToolSchemaInternal: z.ZodType<Record<string, unknown>, z.ZodType
properties: z.record(z.string(), NormalizedToolSchemaInternal).optional(),
items: z.union([NormalizedToolSchemaInternal, z.array(NormalizedToolSchemaInternal)]).optional(),
required: z.array(z.string()).optional(),
additionalProperties: z.union([z.boolean(), NormalizedToolSchemaInternal]).default(false),
// Don't set default here - we'll handle it conditionally in the transform
additionalProperties: z.union([z.boolean(), NormalizedToolSchemaInternal]).optional(),
description: z.string().optional(),
default: z.unknown().optional(),
enum: z.array(JsonSchemaEnumValueSchema).optional(),
Expand All @@ -132,9 +133,13 @@ const NormalizedToolSchemaInternal: z.ZodType<Record<string, unknown>, z.ZodType
})
.passthrough()
.transform((schema) => {
const { type, required, properties, format, ...rest } = schema
const { type, required, properties, additionalProperties, format, ...rest } = schema
const result: Record<string, unknown> = { ...rest }

// Determine if this schema represents an object type
const isObjectType =
type === "object" || (Array.isArray(type) && type.includes("object")) || properties !== undefined

// If type is an array, convert to anyOf format (JSON Schema 2020-12)
if (Array.isArray(type)) {
result.anyOf = type.map((t) => ({ type: t }))
Expand Down Expand Up @@ -164,6 +169,17 @@ const NormalizedToolSchemaInternal: z.ZodType<Record<string, unknown>, z.ZodType
result.properties = {}
}

// Only add additionalProperties for object-type schemas
// Adding it to primitive types (string, number, etc.) is invalid JSON Schema
if (isObjectType) {
// For strict mode compatibility, we MUST set additionalProperties to false
// Even if the original schema had {} (any) or true, we force false because
// OpenAI/OpenRouter strict mode rejects schemas with additionalProperties != false
// The original schema intent (allowing arbitrary properties) is incompatible with strict mode
result.additionalProperties = false
}
// For non-object types, don't include additionalProperties at all

return result
}),
)
Expand Down
Loading