openai / openai/codex

Windows Desktop: MCP form parser rejects root title generated by Python SDK and returns cancel

Open
#46,003 3 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

app enhancement mcp sandbox windows-os
Dominant language
Rust
Stars
125k
Forks
19.4k
PR merge metrics
PR metrics pending

Description

Update: concrete cause identified under Ask for approval

The operator also switched the task to Ask for approval. The active Codex log explicitly reported approval_policy=OnRequest. A fresh form request still immediately returned action=cancel.

The local application log now provides the actionable cause (server name and request identifier omitted):

target=codex_app_server::bespoke_event_handling
failed to parse typed MCP elicitation schema
error=unknown field `title`, expected one of `$schema`, `type`, `properties`, `required`

This means the Full access / never explanation was incomplete. There are two distinct observations:

  1. Full access suppresses forms as documented.
  2. In an interactive OnRequest task, Codex's typed parser rejects the optional root title produced by Pydantic through Python MCP SDK Context.elicit, and the server receives only cancel.

The SDK's elicit_with_validation calls schema.model_json_schema() and passes the resulting schema to session.elicit_form. The SDK's own elicitation documentation also shows a root-level title in the generated schema.

A narrow local compatibility change removes only the optional root title while retaining property labels, the required boolean, response validation, explicit consent, and one-time plan-token checks:

from pydantic import BaseModel, ConfigDict, Field

def omit_root_title(schema: dict) -> None:
    schema.pop("title", None)

class Consent(BaseModel):
    model_config = ConfigDict(json_schema_extra=omit_root_title)
    approve: bool = Field(description="Confirm this specific operation")

An actual SDK stdio regression now checks the emitted wire schema against the root keys accepted by the Codex parser. It failed on the old form with extra key title and passes after the change. All 16 targeted protocol/consent tests pass, including decline/cancel, replay protection, hash/login mismatch, and expiration. The patched server has not yet been reloaded into the desktop task, so successful live UI rendering is not claimed. No real workflow writes were performed.

Requested fix: interoperate with the standard Python SDK's generated form schema, or at least return a clear schema-compatibility error and visible remediation instead of a cancellation that looks like a user decision. No automatic acceptance or weakening of consent is requested.


Original observations and reproduction

Environment
  • Codex Desktop for Windows (Microsoft Store/AppX): OpenAI.Codex 26.908.9136.0.
  • Bundled Codex core and negotiated MCP client version: 0.154.0-alpha.6.2.
  • Windows: Microsoft Windows NT 10.0.26200.0.
  • Local stdio MCP server using Python mcp==1.29.1 / FastMCP.
  • The server reads the connected client's capabilities and reports supports_form=true.
  • Observed on 2026-09-16.
Problem

A user-authorized workflow requires an ordinary MCP form with one required boolean field before a consequential operation. In the desktop app's Full access mode, the MCP client promptly returns action=decline, without providing a usable confirmation path. Restarting the app did not resolve it.

After the user changed the task to Approve for me, a fresh invocation returned action=cancel in approximately five seconds. Whether the second response was caused by the UI, reviewer, or another client-side path has not been established; it should not be diagnosed as the same implementation defect without further evidence.

The server correctly refused both responses and performed no write. The issue is the interaction between the permissions preset, MCP form availability, and the absence of an actionable explanation.

Observed sequence
  1. An existing desktop task used Full access.
  2. A normal MCP preview completed.
  3. Applying the unchanged preview requested Context.elicit(..., schema=Consent).
  4. The connected client returned decline; the operation stopped before external writes.
  5. A trusted project configuration was prepared to allow MCP forms:
approval_policy = { granular = { sandbox_approval = false, rules = false, mcp_elicitations = true, request_permissions = false, skill_approval = false } }
approvals_reviewer = "user"
  1. The user restarted Codex but retained the task's Full access preset. The active task still exposed approval_policy=never; the client again returned decline. The updated server's capability and response diagnostics confirmed that the new code had loaded.
  2. The user switched the task to Approve for me. The active task reported workspace-write and approvals_reviewer=auto_review. A newly issued preview and apply returned cancel, with authorized=false and executed=false.

Sanitized diagnostics for Full access:

{
  "approval": {
    "mechanism": "mcp_elicitation",
    "action": "decline",
    "client": {
      "name": "codex-mcp-client",
      "version": "0.154.0-alpha.6.2",
      "supports_form": true
    },
    "authorized": false
  },
  "executed": false
}

The follow-up under Approve for me had the same client metadata and action: "cancel".

Minimal independent reproduction

This is a reduced reproduction derived from the live server's form call. It has not yet been independently connected to the affected desktop UI. It performs no external requests, file writes, or real workflow actions.

from mcp.server.fastmcp import Context, FastMCP
from pydantic import BaseModel, Field

server = FastMCP("form-permissions-repro")

class Consent(BaseModel):
    approve: bool = Field(description="Confirm this harmless form test")

@server.tool(annotations={"readOnlyHint": True, "idempotentHint": True})
async def probe_form(ctx: Context) -> dict:
    result = await ctx.elicit(
        message="Harmless MCP form test. No real operation will execute.",
        schema=Consent,
    )
    return {
        "action": result.action,
        "data": result.data.model_dump() if result.data is not None else None,
        "executed": False,
    }

if __name__ == "__main__":
    server.run(transport="stdio")

Register this as a local stdio MCP server, then compare Full access, Ask for approval, Custom with MCP elicitations enabled, and Approve for me.

Expected behavior / requested improvement

I understand that current documentation explicitly defines Full access as danger-full-access plus approval_policy=never, and describes granular MCP settings as allowing forms instead of automatically rejecting them. Therefore, the Full access result may be intentional policy behavior rather than a regression.

Nevertheless, the current UX is counterintuitive: choosing the apparently more permissive mode makes a governed MCP workflow impossible. Please:

  • Make the Full access preset's effect on MCP forms visible in the permissions UI.
  • Surface a clear, actionable explanation when policy suppresses a form, including which task-level override wins over project configuration.
  • Distinguish policy suppression, automatic reviewer decisions, unsupported UI/schema, user decline, and user cancellation in client diagnostics.
  • Provide a supported way to allow genuine MCP form responses independently of the filesystem/network preset.
  • Clarify or investigate the cancel result under Approve for me.

Please preserve the actual authorization boundary; silently accepting forms or synthesizing required boolean values would not be an appropriate fix.

Related issue and scope

Related: #45621, now closed as not planned. That report concerns a Linux headless app-server with streamable HTTP. This report concerns the Windows desktop UI, local stdio, permissions-preset discoverability, and a second result under Approve for me. I have not established that they share a root cause.

Documentation:

Only sanitized versions, policy settings, and protocol outcomes are included. No credentials, client identifiers, account data, private project paths, or session transcripts are attached.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with the minimal Python stdio reproduction and the MCP elicitation schema parsing path, then compare Full access, Ask for approval, Custom, and Approve for me. Use the targeted protocol and consent tests to verify root-title compatibility, policy outcomes, cancellation diagnostics, and preservation of the authorization boundary; successful live desktop rendering remains unverified.

Written by the indexing model from the issue text.

Assessment

Tech stack
python, rust
Domain
api, desktop, security
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
52/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.