Skip to content

fix(mcp-http): ensure MCP tool schemas always include properties field#69956

Closed
Chevron7Locked wants to merge 3 commits into
openclaw:mainfrom
Chevron7Locked:fix/mcp-tool-schema-missing-properties
Closed

fix(mcp-http): ensure MCP tool schemas always include properties field#69956
Chevron7Locked wants to merge 3 commits into
openclaw:mainfrom
Chevron7Locked:fix/mcp-tool-schema-missing-properties

Conversation

@Chevron7Locked

Copy link
Copy Markdown
Contributor

Summary

  • Problem: MCP servers can emit no-arg tools with inputSchema: {"type":"object"} and no properties key. OpenAI and OpenRouter reject this as invalid JSON Schema (object schema missing properties), returning a 400 on every request that includes the offending tool — causing the entire model call to fail and fall through to the next fallback.
  • Why it matters: One bad tool poisons the entire tool list for the turn. The user sees silent fallback to weaker models with no indication why. Hard to debug because the error is at the API level, not visible in normal logs.
  • What changed: Moved if (!raw.properties) raw.properties = {} outside the raw.type !== "object" condition in buildMcpToolSchema. Now properties: {} is always added to any object-typed schema that lacks it, regardless of whether type needed correction.
  • What did NOT change: Behavior for schemas that already have properties, or schemas with anyOf/oneOf that go through flattenUnionSchema.

Root cause

// Before — only added properties when type was wrong:
if (raw.type !== "object") {
  raw.type = "object";
  if (!raw.properties) raw.properties = {};  // never reached for {"type":"object"} no-properties
}

// After — always ensures properties is present:
if (raw.type !== "object") {
  raw.type = "object";
}
if (!raw.properties) raw.properties = {};

Reproduces with

mcpproxy-go v0.23.2, which emits {"type":"object"} for no-argument tools (e.g. list_registries). Affects any strict OpenAI-compatible provider.

Change Type

  • Bug fix

Scope

  • Gateway / orchestration

…ried on each turn

When runWithModelFallback falls back to a secondary provider it writes
providerOverride/modelOverride/modelOverrideSource:"auto" to the session.
On subsequent turns createModelSelectionState read this stored override and
passed the fallback provider directly to runWithModelFallback, so the
configured primary was never retried — the session was permanently pinned to
the fallback even after the primary recovered.

Fix: at model-selection ingress, when the direct session override has
modelOverrideSource "auto" (set by a previous automatic fallback, not a user
/model command), clear the override and retry the configured primary. If the
primary is still down runWithModelFallback will fall back and re-set the auto
override for that turn. Once the primary recovers the override stays clear.

User-selected overrides (modelOverrideSource "user" or legacy undefined+model)
are preserved unchanged.

Covered by four new unit tests in model-selection.test.ts:
- auto-failover override cleared and primary retried
- user-selected override preserved
- legacy override without source field preserved
- parent-session auto-override applied to child (not cleared by child logic)
…verride clearing

Three corrections to the auto-failover self-healing introduced in the prior commit:

1. Reset in-memory provider/model to configured primary after clearing auto override.
   get-reply-directives.ts preloads provider/model from the stored override before
   calling createModelSelectionState, so clearing only session state still ran the
   current turn on the fallback. Now provider/model are reset to defaultProvider/
   defaultModel so this turn retries the primary immediately, not on the next turn.

2. Remove resetModelOverride = true from the auto-heal path. That flag triggers a
   "Model override not allowed for this agent" system event in
   applyInlineDirectiveOverrides, which is incorrect: the override was valid and set
   by the fallback loop — it just expired once the primary recovered. Auto-heal is
   not an allowlist violation.

3. Add a test case that verifies the in-memory reset when the caller pre-loads the
   fallback provider/model (simulating the get-reply-directives.ts preload path).

Known limitation (noted in comment): channel model overrides (channels.modelByChannel)
are skipped on the recovery turn because hasSessionModelOverride was true when they
were evaluated at preload time. They resume on the following turn once session state
is clear. Fixing this cleanly requires changes to the get-reply-directives preload
flow and is out of scope for this PR.
MCP servers sometimes emit no-arg tools with inputSchema {"type":"object"}
and no "properties" key. OpenAI and OpenRouter reject this as an invalid
JSON Schema — the error is "object schema missing properties" and manifests
as a 400 on every request that includes the offending tool, causing the
entire model call to fail and fall through to the next fallback.

The fix: move the `if (!raw.properties) raw.properties = {}` guard outside
the `raw.type !== "object"` condition so it applies to schemas that already
have the correct type but are missing the required properties key.

Before: only added properties:{} when type was wrong
After:  always ensures properties:{} is present on any object-typed schema

Reproduces with mcpproxy-go v0.23.2 which emits {"type":"object"} for
no-argument tools like list_registries. Affects any strict OpenAI-compatible
provider (OpenRouter, direct OpenAI API).
@openclaw-barnacle openclaw-barnacle Bot added gateway Gateway runtime size: S labels Apr 22, 2026
@greptile-apps

greptile-apps Bot commented Apr 22, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR bundles two independent fixes: (1) the titled MCP schema fix in mcp-http.schema.ts — moving properties: {} initialization outside the type-correction guard so {"type":"object"} schemas without properties pass OpenAI/OpenRouter validation — and (2) an auto-failover self-healing feature in model-selection.ts that clears auto-sourced session overrides on the next turn so the session retries the primary model instead of permanently using the fallback. Both changes are logically correct and well-tested; the only nit is a duplicated comment in the test file.

Confidence Score: 5/5

Safe to merge — both fixes are correct, tested, and non-breaking

The MCP schema fix is a minimal, targeted one-liner with no risk of regression. The model-selection auto-healing logic is more substantial but includes thorough tests covering all branch conditions (user override, legacy override, parent-inherited override). All remaining findings are P2 style issues.

No files require special attention

Prompt To Fix All With AI
This is a comment left during a code review.
Path: src/auto-reply/reply/model-selection.test.ts
Line: 571-572

Comment:
**Duplicate comment**

The same comment is copy-pasted on consecutive lines.

```suggestion
    // Provider/model should revert to the configured primary, not the fallback.
```

How can I resolve this? If you propose a fix, please make it concise.

Reviews (1): Last reviewed commit: "fix(mcp-http): ensure MCP tool schemas a..." | Re-trigger Greptile

Comment on lines +571 to +572
// Provider/model should revert to the configured primary, not the fallback.
// Provider/model should revert to the configured primary, not the fallback.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Duplicate comment

The same comment is copy-pasted on consecutive lines.

Suggested change
// Provider/model should revert to the configured primary, not the fallback.
// Provider/model should revert to the configured primary, not the fallback.
// Provider/model should revert to the configured primary, not the fallback.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/auto-reply/reply/model-selection.test.ts
Line: 571-572

Comment:
**Duplicate comment**

The same comment is copy-pasted on consecutive lines.

```suggestion
    // Provider/model should revert to the configured primary, not the fallback.
```

How can I resolve this? If you propose a fix, please make it concise.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8167229af8

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +389 to +390
if (isAutoSessionOverride && sessionEntry && sessionStore && sessionKey && !resetModelOverride) {
const { updated } = applyModelOverrideToSessionEntry({

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Respect heartbeat override before auto-clearing model fallback

This branch runs even when hasResolvedHeartbeatModelOverride is true, so heartbeat turns that intentionally selected heartbeat.model can still enter auto-heal, clear the session override, and then force provider/model back to defaultProvider/defaultModel later in the block. In that scenario the heartbeat no longer uses the configured heartbeat model and mutates session state unexpectedly; this regresses the existing skipStoredOverride contract for heartbeat runs. Add a !skipStoredOverride guard (or otherwise preserve the current provider/model) before applying this auto-clear path.

Useful? React with 👍 / 👎.

@prtags

prtags Bot commented Apr 23, 2026

Copy link
Copy Markdown

Related work from PRtags group unique-heron-2zsc

Title: Open PR duplicate: MCP schema properties + auto-reset override self-heal composite

Number Title
#58299 fix: normalize MCP tool schemas missing properties field for OpenAI Responses API
#60176 fix(tools): normalize truly empty MCP tool schemas for OpenAI
#69419 session: clear auto-sourced model/auth overrides on /new and /reset
#69956* fix(mcp-http): ensure MCP tool schemas always include properties field

* This PR

@steipete

Copy link
Copy Markdown
Contributor

Codex maintainer rewrite landed as #70763.

I kept the minimal MCP schema fix from this PR: loopback MCP object schemas now always include properties, including no-argument tools with { type: "object" }. The two auto-failover commits in this branch were already on main through #69365, so I left them out of the replacement.

While verifying this, I also chased the live Docker failure path: Codex was cancelling MCP tool calls when the generated loopback server inherited local approval prompts. #70763 marks only the generated OpenClaw loopback MCP server with Codex's per-server default_tools_approval_mode = "approve", pins the Docker smoke to Codex CLI 0.124.0, keeps the Docker Codex default on codex-cli/gpt-5.5, and adds a live Docker schema probe.

Verified with focused tests, pnpm check:changed, and Docker live Codex e2e using staged .codex/auth.json, codex-cli 0.124.0, codex-cli/gpt-5.5, live tools/list, direct cron, and model-driven cron tool call.

Thanks for spotting the MCP schema issue and for the original fix direction.

@steipete

Copy link
Copy Markdown
Contributor

Closing in favor of the narrower maintainer rewrite in #70763. Thanks again for the original MCP schema fix direction.

@steipete steipete closed this Apr 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

gateway Gateway runtime size: S

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants