exec bug - MCP Tool Conversion Error: firecrawl__firecrawl_extract

Resolved 💬 3 comments Opened Sep 26, 2025 by ben-vargas Closed Nov 3, 2025

What version of Codex is running?

codex-cli 0.42.0-alpha.1

Which model were you using?

gpt-5-codex

What platform is your computer?

Linux 6.8.0-84-generic x86_64 x86_64

TL;DR

When Codex bridges MCP tools into OpenAI Responses tools, it translates each server-provided JSON Schema into a narrow internal representation (JsonSchema). The Firecrawl MCP server advertises the tool firecrawl__firecrawl_extract with an object-valued additionalProperties. Our serializer only accepts booleans for that field, so serde_json::from_value::<JsonSchema> fails and we log:

ERROR codex_core::openai_tools: Failed to convert "firecrawl__firecrawl_extract" MCP tool to OpenAI tool: Error("invalid type: map, expected a boolean", line: 0, column: 0)

The rest of codex exec continues, but the Firecrawl tool never reaches the model.

---

How to Reproduce

  1. Ensure Codex is configured with an MCP server that exposes the Firecrawl tool (e.g. via the default firecrawl config).
  2. Run anything through codex exec, for example:

``bash
codex exec --skip-git-repo-check --experimental-json "Implement a nice looking fully featured tetris game in this directory using only html and javascript."
``

  1. Observe the ERROR codex_core::openai_tools log lines before command execution output.

This is deterministic: any command that causes Codex to enumerate MCP tools will hit the same log line as long as the Firecrawl server is reachable.

---

Execution Path Reference

  • codex exec (CLI) -> codex_core -> McpConnectionManager::list_all_tools collects MCP tool metadata.
  • get_openai_tools in codex-rs/core/src/openai_tools.rs merges built-in tools with the MCP list.
  • Each MCP entry flows through mcp_tool_to_openai_tool, which sanitizes and deserializes the input_schema.
  • sanitize_json_schema recursively massages the schema before deserializing into the JsonSchema enum.

Key locations:

| File | Lines | Notes |
| --- | --- | --- |
| codex-rs/core/src/openai_tools.rs | 330-362 | mcp_tool_to_openai_tool and sanitizing logic |
| codex-rs/core/src/openai_tools.rs | 125-158 | JsonSchema::Object allows additional_properties: Option<bool> only |
| codex-rs/core/src/openai_tools.rs | 457-463 | sanitize_json_schema recurses into object additionalProperties, but never changes the type |
| codex-rs/core/src/openai_tools.rs | 535-538 | Logging site for failed conversions |

---

Root Cause Details

Firecrawl’s Schema Shape

The Firecrawl MCP server reports an input schema similar to:

{
  "type": "object",
  "properties": {
    "url": { "type": "string" },
    "format": { "type": "string" },
    "onlyMainContent": { "type": "boolean" },
    "options": {
      "type": "object",
      "additionalProperties": {
        "type": "object",
        "properties": {
          "selector": { "type": "string" },
          "action": { "type": "string" }
        },
        "required": ["selector", "action"],
        "additionalProperties": false
      }
    }
  },
  "additionalProperties": false
}

The innermost additionalProperties is itself an object schema.

Codex Representation Gap

JsonSchema::Object in Codex models the limited subset { properties: BTreeMap<...>, required: Option<Vec<_>>, additional_properties: Option<bool> }. It cannot hold a schema value for additionalProperties even though JSON Schema allows it.

When sanitize_json_schema hits the Firecrawl schema, it recurses into the nested additionalProperties object but leaves it as a JSON object. After sanitation we call serde_json::from_value::<JsonSchema>(serialized_input_schema). Serde expects a boolean but encounters a map, raising invalid type: map, expected a boolean. That bubbles back to get_openai_tools, which logs and skips the tool.

---

Fix Options

Option 1: Extend JsonSchema::Object to Support Schema-Valued additionalProperties

Summary: Teach our internal schema to represent additionalProperties as either a boolean or another schema. This aligns with JSON Schema and ensures future MCP servers with similar constructs work.

Implementation Outline:

  1. Update JsonSchema::Object definition in codex-rs/core/src/openai_tools.rs:

```rust
Object {
properties: BTreeMap<String, JsonSchema>,
required: Option<Vec<String>>,
#[serde(rename = "additionalProperties", skip_serializing_if = "Option::is_none")]
additional_properties: Option<Box<AdditionalProperties>>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(untagged)]
enum AdditionalProperties {
Bool(bool),
Schema(JsonSchema),
}
```

  1. Adjust all constructors/tests that assume additional_properties: Option<bool>.
  2. Update sanitize_json_schema so that:
  • Boolean values are left untouched.
  • Object values are recursively sanitized (existing behavior) and left as JSON objects so JsonSchema can pick them up as Schema.
  1. Ensure serialization back to OpenAI matches the expected schema. The Responses API tolerates JSON Schema objects in additionalProperties as long as they conform to the standard.
  2. Add unit tests verifying conversion succeeds for a schema with nested additionalProperties, ideally modeled after Firecrawl.

Trade-offs: Slightly increases complexity of the schema model but is the most correct general solution.

Option 2: Coerce Unsupported additionalProperties to false

Summary: Drop schema-valued additionalProperties entirely and substitute false. This keeps the schema within our current enum without broad refactors.

Implementation Outline:

  1. In sanitize_json_schema, when encountering additionalProperties with a non-boolean value, replace it with false and optionally log a warning to inform the user that extra properties will be rejected.

``rust
if let Some(ap) = map.get_mut("additionalProperties") {
match ap {
JsonValue::Bool(_) => {}
_ => {
sanitize_json_schema(ap); // optional: sanitize to avoid surprises
*ap = JsonValue::Bool(false);
tracing::warn!("Coercing schema-valued additionalProperties to false for tool {name}");
}
}
}
``

  1. Add targeted tests verifying the coercion prevents runtime errors and leaves the rest of the schema unchanged.

Trade-offs: Quick to implement but lossy—agents lose hints about allowable dynamic keys. This may not be acceptable if Firecrawl relies on arbitrary option names.

Option 3: Reject Tool with Actionable Error Message

Summary: If we prefer not to support advanced schemas, fail fast with an explicit message instructing users to adjust their MCP server configuration.

Implementation Outline:

  1. Detect object-valued additionalProperties and return an error variant that includes the offending tool name and a human-readable explanation.
  2. Surface that message all the way to the CLI, so users learn how to fix their MCP configuration.

Trade-offs: Preserves current behavior (tool unavailable) but improves UX. Does not unlock the Firecrawl tool.

---

Recommended Fix

Adopt Option 1. Supporting schema-valued additionalProperties aligns with the JSON Schema spec, reduces future surprises, and preserves Firecrawl’s richer interface. The implementation is contained within openai_tools.rs and associated tests. Once complete, Codex will stop logging the conversion error and the Firecrawl tool will become available to the model.

---

Testing Checklist

After implementing any option, run:

  1. just fmt
  2. just fix -p codex-core (adjust crate name if necessary)
  3. cargo test -p codex-core

Add a unit test under #[cfg(test)] in openai_tools.rs that constructs a mock tool mimicking Firecrawl’s schema and asserts mcp_tool_to_openai_tool succeeds.

---

Follow-Up Considerations

  • Audit other MCP servers for similar schema patterns (e.g., union types, array tuples). Extend JsonSchema incrementally as needed.
  • When adding deeper schema support, ensure the serialized output still complies with OpenAI’s tool schema requirements (particularly around strict mode and top-level type).
  • Improve logging to include guidance when schemas are coerced or rejected.

View original on GitHub ↗

This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗