[BUG] ChatGPT web remounts an MCP App widget without the second tool result on consecutive turns

I found a reproducible issue in ChatGPT web where an MCP App widget works on the first tool call but
does not receive the tool result when the same widget-linked tool is called again in the next
conversation turn.

The second turn causes ChatGPT to remove the first iframe and create a new one. However, the newly
created iframe never receives the second result through window.openai.toolOutput. The user
therefore sees an incomplete or blank widget even though the MCP tool implementation returns valid,
distinct data for each invocation.

Environment

Component Version or configuration
Host ChatGPT web, Developer mode, custom MCP connector
Date observed July 31, 2026
Operating system Windows 11 Pro 25H2
Browser Chrome 150.0.7871.127
Runtime Node.js 20 or later
MCP transport Stateless Streamable HTTP, exposed through ngrok
@modelcontextprotocol/ext-apps 1.7.5
@modelcontextprotocol/sdk 1.30.0
zod 4.3.6
Observed MCP protocol version 2025-11-25

Minimal reproduction

The reproduction server contains only:

  • One input-free tool named show-demo-map
  • One static UI resource at ui://renderer-turn-repro/map-v4.html
  • One inline HTML widget
  • No external API, database, authentication, frontend framework, or build step
  • No tool call initiated from inside the widget

The complete result-generation logic is:

let renderCallNumber = 0;

function renderStaticResult() {
  renderCallNumber += 1;
  return {
    content: [{ type: "text", text: "Displaying the static demo result." }],
    structuredContent: {
      itemCount: 3,
      callNumber: renderCallNumber,
    },
  };
}

The server returns a monotonically increasing callNumber:

{
  "itemCount": 3,
  "callNumber": 1
}

The next invocation returns:

{
  "itemCount": 3,
  "callNumber": 2
}

An integration test calls the same tool twice against the same server instance and confirms that
the two results contain callNumber: 1 and callNumber: 2.

Steps to reproduce

  1. Start a server with the minimal configuration and result-generation logic shown above, then
    expose its /mcp endpoint over HTTPS.
  2. Register the endpoint as a custom connector in ChatGPT Developer mode.
  3. Start a new conversation.
  4. Enter @<connector-name> Display the map.
  5. Wait for the first widget to render.
  6. Enter @<connector-name> Display the map again.

Expected behavior

The conversation should contain two rendered widgets:

  • The first widget displays callNumber: 1.
  • The second widget displays callNumber: 2.

Alternatively, if ChatGPT intentionally reuses or replaces the existing widget, the active iframe
should still receive the most recent tool result.

Actual behavior

The first call renders correctly and the widget receives itemCount and callNumber.

On the second turn:

  1. ChatGPT invokes the same widget-linked tool again.
  2. The first iframe receives a pagehide event.
  3. ChatGPT creates a new iframe.
  4. The new iframe starts with empty toolInput and toolOutput objects.
  5. The new iframe never receives the second tool result.
  6. The visible widget displays callNumber: missing instead of callNumber: 2.

Browser console evidence

The first rendered iframe receives the expected result:

[renderer-turn-repro] app-created {
  "toolInputKeys": [],
  "toolOutputKeys": ["itemCount", "callNumber"],
  "itemCount": 3,
  "callNumber": 1
}

When the same tool is requested in the next turn, the first iframe is removed:

[renderer-turn-repro] pagehide

The replacement iframe is then created without any tool result:

[renderer-turn-repro] app-created {
  "toolInputKeys": [],
  "toolOutputKeys": [],
  "itemCount": null,
  "callNumber": null
}

No later global update delivers callNumber: 2 to that iframe.

Why this appears to be a ChatGPT host issue

  • The resource is valid because the first invocation renders successfully.
  • The tool has no input, so the failure is not caused by argument changes between turns.
  • The result is generated locally and synchronously, with no external dependency.
  • The same server instance returns distinct, schema-valid results for two consecutive calls in the
    integration test.
  • The browser log shows that ChatGPT explicitly tears down the working iframe and creates another
    iframe, but the replacement is not populated with the new result.
  • The failure occurs at the boundary between the tool result and the ChatGPT-provided
    window.openai.toolOutput state.

This suggests that the result of the second invocation is not being associated with, or delivered
to, the replacement iframe.

Impact

Any MCP App that uses repeated tool calls across conversation turns can become blank, stale, or
incomplete. This affects common interactions such as refreshing a search, changing filters,
requesting another result, or reopening the same type of visualization.

From the user’s perspective, the tool appears to stop working after the first successful response,
and the widget cannot show a useful error because the host does not provide the second result.

Related report

This may be related to an existing community topic titled:

ChatGPT Apps SDK widget becomes blank after the second question

The reproduction described here additionally isolates the iframe replacement and shows that the
new iframe is created with an empty toolOutput object.

Request

Could the ChatGPT Apps team investigate how a second tools/call result is associated with the
newly mounted iframe when the same widget-linked tool is called in consecutive conversation turns?

Please also let us know whether there is a supported workaround that allows the replacement iframe
to receive the latest tool result reliably.