# Files changed · PR \#23850 · pingdotgg/t3code

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

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

Visibility: public

[fix\(server\): MCP App forms now show when a write tool defers to them](https://git.cafe/pingdotgg/t3code/pull/23850.md)

Version: 7

Head commit: 335e7e2fdfc0daa8dfee7f6814c06f99678c25be

Base commit: 2c8bdf6177159d8fcd12a897d7c9311f6eb93237

Comparison base: ef92441313da327139df75daa8f723c0f42348f6

Showing up to 20 entries from this revision. If the pull request changes, reopen the first page before continuing.

## apps/mobile/src/lib/threadActivity.ts

Status: modified; +1; −4

```diff
@@ -2114,10 +2114,7 @@
       entryRows.push(index);
       continue;
     }
-    const app =
+    const app = item.type === "dynamic_tool" ? mcpAppFromToolItem(item) : undefined;
-      item.type === "dynamic_tool" && item.status === "completed"
-        ? mcpAppFromToolItem(item)
-        : undefined;
     if (app) {
       const entry: RawThreadFeedEntry = {
         type: "mcp-app",
```

## apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts

Status: modified; +69; −0

```diff
@@ -4500,6 +4500,75 @@
     ),
   );
 
+  const MCP_APP_DEFERRED_RESULT = {
+    content: [{ type: "text", text: "An interactive form is being shown to the user." }],
+    structuredContent: { status: "awaiting_user_submission" },
+  };
+  // Codex reports a tool's `isError` result, which a write that defers to its
+  // form returns, as a failed call.
+  const mcpAppDeferredTranscript = makeCodexReplayTranscript({
+    scenario: MCP_APP_SCENARIO,
+    entries: mcpAppTranscript.entries.map((entry) =>
+      entry.type === "emit_inbound" && entry.label === "item/completed/app-tool"
+        ? {
+            ...entry,
+            frame: {
+              method: "item/completed",
+              params: {
+                item: {
+                  ...mcpAppToolItem("inProgress"),
+                  status: "failed",
+                  result: MCP_APP_DEFERRED_RESULT,
+                },
+                threadId: MCP_APP_NATIVE_THREAD,
+                turnId: MCP_APP_NATIVE_TURN,
+                completedAtMs: 1782622441500,
+              },
+            },
+          }
+        : entry,
+    ),
+  });
+
+  it.effect("captures the app of a tool call that defers to its form", () =>
+    Effect.scoped(
+      Effect.gen(function* () {
+        const appCaptured = yield* Deferred.make<OrchestrationV2TurnItem>();
+        const harness = yield* makeCodexReplayHarness(mcpAppDeferredTranscript, (event) =>
+          event.type === "turn_item.updated" &&
+          event.turnItem.type === "dynamic_tool" &&
+          event.turnItem.status === "failed"
+            ? Deferred.succeed(appCaptured, event.turnItem)
+            : Effect.void,
+        );
+        const now = yield* DateTime.now;
+
+        yield* harness.runtime.startTurn(
+          makeCodexTestTurnInput({
+            threadId: harness.threadId,
+            providerThread: harness.providerThread,
+            now,
+            attemptId: RunAttemptId.make("attempt-codex-mcp-app-deferred"),
+            text: MCP_APP_PROMPT,
+          }),
+        );
+        const item = yield* Deferred.await(appCaptured);
+
+        const output = item.type === "dynamic_tool" ? (item.output as Record<string, unknown>) : {};
+        assert.equal(
+          readMcpAppReference(output[MCP_APP_OUTPUT_KEY])?.resourceUri,
+          MCP_APP_RESOURCE,
+        );
+        assert.deepEqual(output.result, { ...MCP_APP_DEFERRED_RESULT, isError: true });
+        assert.isFalse(yield* harness.hasPendingBackgroundWork);
+      }).pipe(
+        Effect.provide(
+          Layer.mergeAll(IdAllocator.layer, McpProviderSessions.layer, NodeServices.layer),
+        ),
+      ),
+    ),
+  );
+
   const ORPHAN_WAIT_SCENARIO = "codex-orphaned-dynamic-tool";
   const ORPHAN_WAIT_NATIVE_THREAD = "native-codex-orphan-wait-thread";
   const ORPHAN_WAIT_NATIVE_TURN = "native-codex-orphan-wait-turn";
```

## apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts

Status: modified; +10; −2

```diff
@@ -3816,8 +3816,10 @@
           // item, removes it, so no stored app goes unreferenced.
           Effect.uninterruptibleMask((restore) =>
             Effect.gen(function* () {
+              // Codex reports an `isError` result as failed. Apps still render
+              // for it: a write tool that defers to its form returns one.
               const reference =
-                item.status === "completed" && item.result != null && item.error == null
+                item.result != null && item.error == null
                   ? yield* restore(
                       getNativeThreadId(context.providerThread).pipe(
                         Effect.flatMap((threadId) =>
@@ -3875,7 +3877,13 @@
                   ? artifacts.turnItem
                   : {
                       ...artifacts.turnItem,
+                      output: {
-                      output: { [MCP_APP_OUTPUT_KEY]: reference, result: item.result },
+                        [MCP_APP_OUTPUT_KEY]: reference,
+                        result:
+                          item.status === "failed"
+                            ? { ...item.result, isError: true }
+                            : item.result,
+                      },
                     };
               yield* emitProviderEvent({
                 type: "node.updated",
```

## apps/web/src/session-logic.test.ts

Status: modified; +17; −0

```diff
@@ -1487,6 +1487,23 @@
     expect(rowsFor(entries, false)).toEqual(["message", "turn-fold", "mcp-app", "message"]);
   });
 
+  it("hosts an MCP app whose call failed, as a form-deferring write does", () => {
+    const mcpApp = {
+      attachmentId: "render-thread-app-html",
+      server: "github",
+      tool: "create_pull_request",
+      resourceUri: "ui://github-mcp-server/pr-create",
+    };
+    const call = renderCall("failed", {
+      t3McpApp: mcpApp,
+      result: { content: [], isError: true },
+    });
+    const entries = turn(
+      call.type === "dynamic_tool" ? { ...call, toolName: "github.create_pull_request" } : call,
+    );
+    expect(entries.find((entry) => entry.kind === "mcp-app")).toMatchObject({ mcpApp });
+  });
+
   it("keeps a render visible when its superseded attempt folds", () => {
     const attempt: OrchestrationV2RunAttempt = {
       id: RunAttemptId.make("attempt-superseded"),
```

## apps/web/src/session-logic.ts

Status: modified; +1; −4

```diff
@@ -771,10 +771,7 @@
       continue;
     }
 
-    const mcpApp =
+    const mcpApp = item.type === "dynamic_tool" ? mcpAppFromToolItem(item) : undefined;
-      item.type === "dynamic_tool" && item.status === "completed"
-        ? mcpAppFromToolItem(item)
-        : undefined;
     if (mcpApp !== undefined) {
       entries.push({
         id: item.id,
```
