From 57999f99651bd441a1e14c6db802445ec3f1cdf3 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 3 Apr 2026 20:43:05 +0900 Subject: [PATCH] fix: narrow empty MCP tool schema normalization (#60176) (thanks @Bartok9) --- CHANGELOG.md | 1 + src/agents/pi-tools.schema.test.ts | 14 ++++++++++++++ src/agents/pi-tools.schema.ts | 10 +++++----- 3 files changed, 20 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ae028f7773..132e469abfe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,6 +39,7 @@ Docs: https://docs.openclaw.ai - Android/assistant: keep queued App Actions prompts pending when auto-send enqueue is rejected, so transient chat-health drops do not silently lose the assistant request. Thanks @obviyus. - Plugins/Google: separate OAuth CSRF state from PKCE code verifier during Gemini browser sign-in so state validation and token exchange use independent values. (#59116) Thanks @eleqtrizit. - Agents/subagents: honor `agents.defaults.subagents.allowAgents` for `sessions_spawn` and `agents_list`, so default cross-agent allowlists work without duplicating per-agent config. (#59944) Thanks @hclsys. +- Agents/tools: normalize only truly empty MCP tool schemas to `{ type: "object", properties: {} }` so OpenAI accepts parameter-free tools without rewriting unrelated conditional schemas. (#60176) Thanks @Bartok9. - Plugins/browser: block SSRF redirect bypass by installing a real-time Playwright route handler before `page.goto()` so navigation to private/internal IPs is intercepted and aborted mid-redirect instead of checked post-hoc. (#58771) Thanks @pgondhi987. - Android/gateway: require TLS for non-loopback remote gateway endpoints while still allowing local loopback and emulator cleartext setup flows. (#58475) Thanks @eleqtrizit. - Exec/Windows: hide transient console windows for `runExec` and `runCommandWithTimeout` child-process launches, matching other Windows exec paths and stopping visible shell flashes during tool runs. (#59466) Thanks @lawrence3699. diff --git a/src/agents/pi-tools.schema.test.ts b/src/agents/pi-tools.schema.test.ts index bcaa1074402..e53a977735f 100644 --- a/src/agents/pi-tools.schema.test.ts +++ b/src/agents/pi-tools.schema.test.ts @@ -20,6 +20,20 @@ describe("normalizeToolParameters", () => { expect(parameters.properties).toEqual({}); }); + it("does not rewrite non-empty schemas that still lack type/properties", () => { + const tool: AnyAgentTool = { + name: "conditional", + label: "conditional", + description: "Conditional schema stays untouched", + parameters: { allOf: [] }, + execute: vi.fn(), + }; + + const normalized = normalizeToolParameters(tool); + + expect(normalized.parameters).toEqual({ allOf: [] }); + }); + it("injects properties:{} for type:object schemas missing properties (MCP no-param tools)", () => { const tool: AnyAgentTool = { name: "list_regions", diff --git a/src/agents/pi-tools.schema.ts b/src/agents/pi-tools.schema.ts index ea6d4145b71..98bb7098f0e 100644 --- a/src/agents/pi-tools.schema.ts +++ b/src/agents/pi-tools.schema.ts @@ -133,11 +133,11 @@ export function normalizeToolParameterSchema( ? "oneOf" : null; if (!variantKey) { - // Handle truly empty schemas (no type, no properties, no unions) — - // OpenAI requires `type: "object"` with `properties` for tool schemas. - // MCP tools with parameter-free schemas may return `{}` or minimal objects. - if (!("type" in schemaRecord) && !("properties" in schemaRecord)) { - return applyProviderCleaning({ type: "object", properties: {}, ...schemaRecord }); + // Handle the proven MCP no-parameter case: a truly empty schema object. + // Keep other non-empty shapes unchanged so we do not silently bless + // unsupported top-level conditionals like `allOf`. + if (Object.keys(schemaRecord).length === 0) { + return applyProviderCleaning({ type: "object", properties: {} }); } return schema; }