MCP elicitation: deny_unknown_fields on McpElicitationSchema rejects top-level 'title', breaking FastMCP/Pydantic servers
What happened
Codex rejects every MCP elicitation/create request from a FastMCP (Python) server with:
unknown field 'title', expected one of '$schema', 'type', 'properties', 'required'
The elicitation never reaches the user; the tool call fails. Observed on codex-cli 0.142.4 (stdio transport); the relevant code is unchanged on main as of 2026-07-05, so current alphas (0.143.0-alpha.36) should reproduce.
Root cause
McpElicitationSchema in codex-rs/app-server-protocol/src/protocol/v2/mcp.rs is declared with #[serde(rename_all = "camelCase", deny_unknown_fields)] and only models $schema / type / properties / required — no top-level title.
FastMCP (and any Pydantic-based server) emits the model's auto-generated title at the top level of requestedSchema. Verified against fastmcp 3.4.2 — this is the wire schema for a plain ctx.elicit(..., response_type=str):
{
"properties": {
"value": {"title": "Value", "type": "string"}
},
"required": ["value"],
"title": "ScalarElicitationType",
"type": "object"
}
Serde's deny_unknown_fields hard-fails on "title" before the form is ever shown.
Why this is a Codex bug rather than a FastMCP one
- The normative MCP schema (2025-11-25
schema.json) does not setadditionalProperties: falseonElicitRequestFormParams.requestedSchema, so extra members liketitlevalidate against the spec. - The official Rust SDK models it: rmcp's
elicitation_schema.rshas a top-leveltitlefield and does not usedeny_unknown_fields. - Codex itself is inconsistent about it: the property-level schemas in the same file (
McpElicitationStringSchema,McpElicitationNumberSchema, ...) all accepttitle— only the object level rejects it.
Impact
Any FastMCP-based MCP server using elicitation is unusable with Codex, despite Codex advertising elicitation: {form, url} in the initialize handshake and having a working form UI (#17043). FastMCP puts title there by default with no supported way to suppress it, so this likely affects a large share of Python MCP servers in the wild.
Suggested fix
Either add the field:
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
or drop deny_unknown_fields from McpElicitationSchema (matching rmcp and the spec's permissive stance). Either is a one-line change.
Repro
pip install fastmcp(3.4.2), minimal server:
```python
from fastmcp import FastMCP, Context
mcp = FastMCP("probe")
@mcp.tool
async def try_elicitation(ctx: Context) -> str:
result = await ctx.elicit("Reply with any short string.", response_type=str)
return f"ELICIT_{result.action}"
mcp.run()
```
- Register it as an MCP server in Codex, call
try_elicitation. - Codex logs the
unknown field 'title'error; the elicitation fails instead of rendering the form.
5 Comments
Confirmed the root cause and put together a tested fix on a branch (happy to open a PR if the team would like one, per the invitation-only policy).
Root cause recap:
McpElicitationSchemaincodex-rs/app-server-protocol/src/protocol/v2/mcp.rsis#[serde(deny_unknown_fields)]but only models$schema/type/properties/required, so FastMCP's top-leveltitletriggersunknown field 'title'before the form renders.One thing worth flagging: a
title-only fix is incomplete. Pydantic also emits a top-leveldescriptionwhenever the elicitation type has a docstring, which hits the identicaldeny_unknown_fieldsfailure. For example,ctx.elicit("...", response_type=SomeModel)whereSomeModelhas a docstring produces:So
descriptionshould be accepted alongsidetitle. Both are already accepted on the per-property schemas in this same file (McpElicitationStringSchema,NumberSchema,BooleanSchema, …) — only the object level rejects them, which is the inconsistency. Both are also spec-valid (the MCP 2025-11-25requestedSchemadoes not setadditionalProperties: false) andrmcpmodels a top-leveltitle.Proposed fix: add optional top-level
titleanddescriptiontoMcpElicitationSchema, keepingdeny_unknown_fieldsfor genuinely unexpected members. Regenerate the TS/JSON schema fixtures and add a regression test using the FastMCP wire shape (fails before, passes after).I have this implemented on a branch with
cargo test -p codex-app-server-protocol(incl. the new regression test),cargo check -p codex-tui,just fmt, and clippy all green: https://github.com/Pranav188/codex/tree/fix/mcp-elicitation-top-level-title-descriptionOnly the object-level fix is needed; the per-property schemas already handle these keywords.
_(Analysis and patch prepared with AI assistance.)_
Confirmed your
descriptionextension firsthand (fastmcp 3.4.2): a Pydantic elicitation model with a docstring emits a top-level"description"alongside"title"—— so a
title-only fix would indeed keep failing for any model with a docstring. Accepting both optional top-level fields while keepingdeny_unknown_fieldsmatches the per-property schemas in the same file and stays within the normativerequestedSchema(noadditionalProperties: false). Your proposed scope looks exactly right from the reporter's side; a regression test on the FastMCP wire shape covers the real-world case that hit this.Unable to create PR - I can't create PR to repo... Can someone assist?
@chethanuk the contributions are invite only. i fixed it too, but can't create PR
Some additional analysis for whoever lands the invited fix — there's a second breakage shape hiding behind the reported one.
What's already fine (from source): the per-property structs (
McpElicitationStringSchemaetc.) do modeltitleanddescription, so the property-level extras in the wire examples above parse today. The reported failure is top-level only (title, and per @fugu-ai alsodescription).The next shape:
defaulton non-boolean properties. Pydantic emitsdefaultinside the property schema for any field with a default value (verified with pydantic 2.xmodel_json_schema()):McpElicitationBooleanSchemamodelsdefault(faithful to the spec, which only defines it for booleans), but the String/Number/Enum variants don't — and they're alldeny_unknown_fieldsinside the#[serde(untagged)]McpElicitationPrimitiveSchema. So a string property with a default fails every variant and surfaces as the opaquedata did not match any variant of untagged enum McpElicitationPrimitiveSchema— after the top-leveltitle/descriptionleniency is added. A fix scoped to the reported error message will look merged-and-done, then break again on the firstctx.elicitmodel with a field default.Fix-shape implication: since the normative MCP schema doesn't set
additionalProperties: falseanywhere underrequestedSchema(and rmcp models it leniently), droppingdeny_unknown_fieldson the elicitation structs aligns with the spec and closes the whole class — as opposed to adding fields one wire-quirk at a time. If stricter parsing is preferred, the minimum set for Pydantic-based servers is: top-leveltitle+description, plusdefaulton the non-boolean primitive variants.(Caveat:
defaultemission is verified at the Pydantic layer; FastMCP passestitle/descriptionthrough unmodified per this thread, and its schemas come straight from Pydantic, so pass-through ofdefaultis expected but I haven't captured it on the wire.)