fix(server): MCP App forms now show when a write tool defers to them#23850

open1
main
GitHub fork: fix/mcp-app-deferred-forms
opened1d agobySshivam
View on GitHubTwo-way sync enabledPR last imported 3h ago

Problem

GitHub's MCP server (and any server built the same way) hands write tools like create_pull_request off to an MCP App form. It answers the first call with an isError result whose status is awaiting_user_submission, and expects the host to show the form. Codex reports an isError result as a failed call, and T3 only captured and showed MCP apps for completed calls. So the form never appeared, and the write could never happen.

Change

  • The Codex adapter captures the app whenever the call has a result, not only when it completed, and passes isError: true on to the app for a failed call.
  • Web and mobile show a captured app whatever the call's status. Only the adapter attaches an app, and only once the call has a result.

Clicking submit in the form goes through the existing confirmation prompt for app tool calls, then the write runs.

Only Codex is affected. GitHub's server defers only when the client's initialize carries the io.modelcontextprotocol/ui extension (shouldDeferToForm). Claude Code 2.1.308 doesn't send it, so writes run directly there. What the test server logged from each client's initialize:

1claude-code ui extension: no
2codex (via T3) ui extension: yes

Scope and approval

A small fix for an obvious bug reported in #23825. MCP Apps already render inline in threads (#16236). This only stops a Codex isError result from hiding an app that the server asked the host to show. No new settings or workflows, and the existing confirm prompt still guards app-initiated writes.

Verification

Before / after (Linux, web UI, Codex)

I reproduced this with a small stdio MCP server that defers the same way GitHub's does (isError + awaiting_user_submission unless the call carries _ui_submitted: true).

Before (main): the call shows as a plain tool row. No form.

before

After: the form shows up with the title the agent passed in.

after: form shown

Clicking "Create note" asks for confirmation. After confirming, the server gets the _ui_submitted call and creates the note. Server log for the turn:

1tools/call create_note {"title":"Ship the fix"} -> isError, awaiting_user_submission
2tools/call create_note {"title":"Ship the fix","_ui_submitted":true} -> Created note "Ship the fix".

after: submitted

Tests

  • CodexAdapterV2.test.ts: a failed call with a result still gets its app captured, and the app sees isError: true. Fails on main.
  • session-logic.test.ts: the web timeline shows the app for a failed call.

With GitHub's real MCP server (web, Codex)

github-mcp-server v2.0.3 (stdio, --toolsets=issues) against a throwaway private repo. The first issue_write call came back as GitHub's deferral (isError: true, "An interactive form has been shown to the user… The issue has NOT been created yet") and created nothing.

Before (fix reverted): no form, and no issue gets created.

real server before

After: GitHub's "Create issue" form shows in the thread.

real server form

Clicking "Create issue" asks for confirmation with the _ui_submitted: true call, and after confirming GitHub created the issue.

real server submitted

Mobile (Android, POCO F4, Codex, GitHub's real server)

A new thread started from the phone: the form shows, "Create issue" asks to allow the call, and GitHub created the issue. With only the mobile line reverted (server fix kept), the same thread shows no form, so the mobile change is needed.

FormSubmittedMobile line reverted
mobile formmobile submittedmobile reverted

Not covered

After a page reload, a submitted thread shows the empty form again, because the app keeps its "submitted" state only in memory and T3 doesn't save app state. The confirm prompt still guards a second submit. Fixed in the stacked #24208.

Closes #23825

Opus 5.5, Claude Code in T3 Code.

🤖 Generated with Claude Code

Checks

All checks passed

Added 1 commit

  • fix(server): show MCP app forms when a write tool defers to them335e7e2
Synced from GitHub

Approvability

Verdict: Approved at 335e7e2

Macroscope's review found this PR approvable — This is a focused MCP App rendering fix: Codex-deferred write calls marked failed can now retain their app form, while unrelated failed tools remain ordinary tool rows. The server and web behavior are covered by targeted tests, with the mobile change being the same narrow presentation condition.

You can add or adjust custom eligibility rules. Learn more.

Synced from GitHub

Review in Change Stack →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
📥 Commits

Reviewing files that changed from the base of the PR and between 2c8bdf6177159d8fcd12a897d7c9311f6eb93237 and 335e7e2fdfc0daa8dfee7f6814c06f99678c25be.

📒 Files selected for processing: 5
  • apps/mobile/src/lib/threadActivity.ts
  • apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts
  • apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts
  • apps/web/src/session-logic.test.ts
  • apps/web/src/session-logic.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 9 remain after this review.

📝 Walkthrough

The Codex adapter now captures MCP App resources for eligible failed results. The web and mobile clients now check dynamic-tool items for MCP Apps regardless of status.

Changes: Failed MCP App results

Layer / File(s)Summary
Capture resources on failed results
apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts, apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts
The adapter captures MCP App resources for results without errors, including failed items. It marks attached results as errors for failed items. A replay test checks the resource URI, error flag, and pending-work state.
Extract MCP Apps in client timelines
apps/web/src/session-logic.ts, apps/mobile/src/lib/threadActivity.ts, apps/web/src/session-logic.test.ts
The web and mobile clients attempt MCP App extraction for dynamic-tool items regardless of status. A web test checks the MCP App entry for a failed GitHub tool call.

Priority: ➖ Normal

Estimated code review effort: 2 (Simple) | ~10 minutes

Change: Bug fix · Severity of issue fixed: Medium

Suggested reviewers: juliusmarminge, maria-rcks

Merge Risk: ⚪ Minimal · up to 335e7

The change enables deferred MCP App forms to appear after failed tool calls. No actionable merge-blocking risk is identified beyond normal validation.

Pre-merge checks: Passed 4
Check nameStatusExplanation
Title checkPassedThe title is concise, specific, and accurately identifies the fix for MCP App forms deferred by write tools.
Linked Issues checkPassedIssue #23825 asks that a deferred MCP App result either shows the form or shows an understandable deferred state with an escape hatch that keeps the tool result accessible. The PR takes the first opti…
Out of Scope Changes checkPassedAll changed files serve the objective of showing deferred MCP App forms for failed-status tool calls. The CodexAdapterV2.ts change widens the app-capture condition and adds the isError flag. The ses…
Description checkPassedThe description includes all required sections: Problem, Change, Scope and approval, and Verification. It explains the deferred MCP App failure, identifies the affected components, links issue #23825,…
Full details: Linked Issues check

Explanation

Issue #23825 asks that a deferred MCP App result either shows the form or shows an understandable deferred state with an escape hatch that keeps the tool result accessible. The PR takes the first option. Verified at head: in CodexAdapterV2.ts, the app reference is now built whenever item.result != null && item.error == null, not only when item.status === "completed". For status === "failed" the captured output sets result: { ...item.result, isError: true }, so the app receives the isError flag. In session-logic.ts, deriveTimelineEntriesFromVisibleTurnItems calls mcpAppFromToolItem for every dynamic_tool item, so the web timeline creates the app entry for a failed call. The mobile change in threadActivity.ts applies the same status-independent extraction. The added tests in CodexAdapterV2.test.ts and session-logic.test.ts cover a failed call with a deferred-form result. The author's before/after screenshots show the form rendering, the confirmation prompt, and the submitted write. The screenshots are not independently verifiable from the repository, so the PASS relies on the diff and the described tests. The mobile path has no device run; the issue marks mobile as unverified, so this does not block the verdict.

Full details: Out of Scope Changes check

Explanation

All changed files serve the objective of showing deferred MCP App forms for failed-status tool calls. The CodexAdapterV2.ts change widens the app-capture condition and adds the isError flag. The session-logic.ts and threadActivity.ts changes remove the completed-only status gate in the web and mobile timelines. The CodexAdapterV2.test.ts and session-logic.test.ts additions test this behavior. No unrelated refactoring, feature, or configuration change appears in the reviewed diffs. The Codex-only scope matches the linked issue and the PR description.

Full details: Description check

Explanation

The description includes all required sections: Problem, Change, Scope and approval, and Verification. It explains the deferred MCP App failure, identifies the affected components, links issue #23825, documents focused automated tests, manual web and mobile verification, screenshots, limitations, and agent details.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create a new PR

Comment @coderabbitai help to get the list of available commands.