# fix\(server\): MCP App forms now show when a write tool defers to them · PR \#23850 · pingdotgg/t3code

[View on GitCafe](https://git.cafe/pingdotgg/t3code/pull/23850)

Repository: [pingdotgg/t3code](https://git.cafe/pingdotgg/t3code)

Visibility: public

State: open

Source repository: Unavailable fork

Source branch: fix/mcp-app-deferred-forms

Target branch: main

Head commit: 335e7e2fdfc0daa8dfee7f6814c06f99678c25be

[Discussion and reviews](https://git.cafe/pingdotgg/t3code/pull/23850)

[Commits](https://git.cafe/pingdotgg/t3code/pull/23850/commits?format=markdown)

[Files changed](https://git.cafe/pingdotgg/t3code/pull/23850/diffs?format=markdown)

## Description

---

## 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`](https://github.com/github/github-mcp-server/blob/main/pkg/github/ui_capability.go)). Claude Code 2.1.308 doesn't send it, so writes run directly there. What the test server logged from each client's `initialize`:

```
claude-code       ui extension: no
codex (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](https://github.com/user-attachments/assets/3edae043-db76-47c4-9017-451ac3a47748)

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

![after: form shown](https://github.com/user-attachments/assets/23550d7c-8905-49b4-9581-3b905bbba984)

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

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

![after: submitted](https://github.com/user-attachments/assets/82d5499c-e4f1-4d62-822d-4bbab0f944dc)

### 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](https://github.com/user-attachments/assets/04317221-b26e-48c6-aab2-d40292f5620b)

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

![real server form](https://github.com/user-attachments/assets/3848cf8b-0647-42de-b702-0c74fc45f21c)

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

![real server submitted](https://github.com/user-attachments/assets/fc617162-c024-4e6f-9422-46604c6790b1)

### 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.

| Form | Submitted | Mobile line reverted |
| --- | --- | --- |
| ![mobile form](https://github.com/user-attachments/assets/37954382-88a7-47a5-88db-71e44fb4806d) | ![mobile submitted](https://github.com/user-attachments/assets/4c19671a-2db4-4e84-b868-15c5a91998fa) | ![mobile reverted](https://github.com/user-attachments/assets/b7a705be-ab27-474f-9163-8045fffcd308) |

### 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](https://claude.com/claude-code)





