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
13 changes: 7 additions & 6 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -158,8 +158,8 @@ describe("getMcpServerTools", () => {
expect(getFunction(result[0]).parameters).toEqual({
type: "object",
properties: {
requiredField: { type: "string" },
optionalField: { type: "number" },
requiredField: { type: "string", additionalProperties: false },
optionalField: { type: "number", additionalProperties: false },
},
additionalProperties: false,
required: ["requiredField"],
Expand All @@ -186,7 +186,7 @@ describe("getMcpServerTools", () => {
expect(getFunction(result[0]).parameters).toEqual({
type: "object",
properties: {
optionalField: { type: "string" },
optionalField: { type: "string", additionalProperties: false },
},
additionalProperties: false,
})
Expand Down
27 changes: 11 additions & 16 deletions src/core/prompts/tools/native-tools/mcp_server.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type OpenAI from "openai"
import { McpHub } from "../../../../services/mcp/McpHub"
import { buildMcpToolName } from "../../../../utils/mcp-name"
import { ToolInputSchema, type JsonSchema } from "../../../../utils/json-schema"

/**
* Dynamically generates native tool definitions for all enabled tools across connected MCP servers.
Expand Down Expand Up @@ -40,30 +41,24 @@ export function getMcpServerTools(mcpHub?: McpHub): OpenAI.Chat.ChatCompletionTo
}
seenToolNames.add(toolName)

const originalSchema = tool.inputSchema as Record<string, any> | undefined
const toolInputProps = originalSchema?.properties ?? {}
const toolInputRequired = (originalSchema?.required ?? []) as string[]
const originalSchema = tool.inputSchema as Record<string, unknown> | undefined

// Build parameters directly from the tool's input schema.
// The server_name and tool_name are encoded in the function name itself
// (e.g., mcp_serverName_toolName), so they don't need to be in the arguments.
const parameters: OpenAI.FunctionParameters = {
type: "object",
properties: toolInputProps,
additionalProperties: false,
}

// Only add required if there are required fields
if (toolInputRequired.length > 0) {
parameters.required = toolInputRequired
// Parse with ToolInputSchema to ensure additionalProperties: false is set recursively
let parameters: JsonSchema
if (originalSchema) {
const result = ToolInputSchema.safeParse(originalSchema)
parameters = result.success ? result.data : (originalSchema as JsonSchema)
} else {
// No schema provided - create a minimal valid schema
parameters = ToolInputSchema.parse({ type: "object" })
}

const toolDefinition: OpenAI.Chat.ChatCompletionTool = {
type: "function",
function: {
name: toolName,
description: tool.description,
parameters: parameters,
parameters: parameters as OpenAI.FunctionParameters,
},
}

Expand Down
2 changes: 1 addition & 1 deletion src/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -507,7 +507,7 @@
"web-tree-sitter": "^0.25.6",
"workerpool": "^9.2.0",
"yaml": "^2.8.0",
"zod": "^3.25.61"
"zod": "3.25.61"
},
"devDependencies": {
"@roo-code/build": "workspace:^",
Expand Down
213 changes: 213 additions & 0 deletions src/utils/__tests__/json-schema.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
import { ToolInputSchema } from "../json-schema"

describe("ToolInputSchema", () => {
it("should validate and default additionalProperties to false", () => {
const schema = {
type: "object",
properties: {
name: { type: "string" },
},
}

const result = ToolInputSchema.parse(schema)

expect(result.type).toBe("object")
expect(result.additionalProperties).toBe(false)
})

it("should recursively apply defaults to nested schemas", () => {
const schema = {
type: "object",
properties: {
user: {
type: "object",
properties: {
name: { type: "string" },
},
},
},
}

const result = ToolInputSchema.parse(schema)

expect(result.additionalProperties).toBe(false)
expect((result.properties as any).user.additionalProperties).toBe(false)
})

it("should apply defaults to object schemas in array items", () => {
const schema = {
type: "object",
properties: {
items: {
type: "array",
items: {
type: "object",
properties: {
id: { type: "number" },
},
},
},
},
}

const result = ToolInputSchema.parse(schema)

expect(result.additionalProperties).toBe(false)
expect((result.properties as any).items.items.additionalProperties).toBe(false)
})

it("should throw on invalid schema", () => {
const invalidSchema = { type: "invalid-type" }

expect(() => ToolInputSchema.parse(invalidSchema)).toThrow()
})

it("should use safeParse for error handling", () => {
const invalidSchema = { type: "invalid-type" }

const result = ToolInputSchema.safeParse(invalidSchema)

expect(result.success).toBe(false)
})

it("should apply defaults in anyOf schemas", () => {
const schema = {
anyOf: [{ type: "object", properties: { a: { type: "string" } } }, { type: "string" }],
}

const result = ToolInputSchema.parse(schema)

expect((result.anyOf as any)[0].additionalProperties).toBe(false)
expect((result.anyOf as any)[1].additionalProperties).toBe(false)
})

it("should apply defaults in oneOf schemas", () => {
const schema = {
oneOf: [{ type: "object", properties: { a: { type: "string" } } }, { type: "number" }],
}

const result = ToolInputSchema.parse(schema)

expect((result.oneOf as any)[0].additionalProperties).toBe(false)
expect((result.oneOf as any)[1].additionalProperties).toBe(false)
})

it("should apply defaults in allOf schemas", () => {
const schema = {
allOf: [
{ type: "object", properties: { a: { type: "string" } } },
{ type: "object", properties: { b: { type: "number" } } },
],
}

const result = ToolInputSchema.parse(schema)

expect((result.allOf as any)[0].additionalProperties).toBe(false)
expect((result.allOf as any)[1].additionalProperties).toBe(false)
})

it("should apply defaults to tuple-style array items", () => {
const schema = {
type: "object",
properties: {
tuple: {
type: "array",
items: [
{ type: "object", properties: { a: { type: "string" } } },
{ type: "object", properties: { b: { type: "number" } } },
],
},
},
}

const result = ToolInputSchema.parse(schema)

const tupleItems = (result.properties as any).tuple.items
expect(tupleItems[0].additionalProperties).toBe(false)
expect(tupleItems[1].additionalProperties).toBe(false)
})

it("should preserve explicit additionalProperties: false", () => {
const schema = {
type: "object",
properties: {
name: { type: "string" },
},
additionalProperties: false,
}

const result = ToolInputSchema.parse(schema)

expect(result.additionalProperties).toBe(false)
})

it("should handle deeply nested complex schemas", () => {
const schema = {
type: "object",
properties: {
level1: {
type: "object",
properties: {
level2: {
type: "array",
items: {
type: "object",
properties: {
level3: {
type: "object",
properties: {
value: { type: "string" },
},
},
},
},
},
},
},
},
}

const result = ToolInputSchema.parse(schema)

expect(result.additionalProperties).toBe(false)
expect((result.properties as any).level1.additionalProperties).toBe(false)
expect((result.properties as any).level1.properties.level2.items.additionalProperties).toBe(false)
expect((result.properties as any).level1.properties.level2.items.properties.level3.additionalProperties).toBe(
false,
)
})

it("should handle the real-world MCP memory create_entities schema", () => {
// This is based on the actual schema that caused the OpenAI error
const schema = {
type: "object",
properties: {
entities: {
type: "array",
items: {
type: "object",
properties: {
name: { type: "string", description: "The name of the entity" },
entityType: { type: "string", description: "The type of the entity" },
observations: {
type: "array",
items: { type: "string" },
description: "An array of observation contents",
},
},
required: ["name", "entityType", "observations"],
},
description: "An array of entities to create",
},
},
required: ["entities"],
}

const result = ToolInputSchema.parse(schema)

// Top-level object should have additionalProperties: false
expect(result.additionalProperties).toBe(false)
// Items in the entities array should have additionalProperties: false
expect((result.properties as any).entities.items.additionalProperties).toBe(false)
})
})
Loading
Loading