Skip to content

ChatGPT MCP Apps host causes React removeChild errors by calling render() multiple times rapidly #559

@danielvoigt

Description

@danielvoigt

Summary

When using MCP Apps with ChatGPT as the host, React 18 throws Failed to execute 'removeChild' on 'Node': The node to be removed is not a child of this node errors during rendering. This does not happen with Claude as the host. The same MCP App HTML works correctly in Claude.

Environment

  • MCP Apps spec version: 2026-01-26
  • Host: ChatGPT (openai-mcp v1.0.0)
  • Protocol version: 2025-11-25
  • React 18.3.1 (createRoot API)
  • Renderer uses flushSync for synchronous commits

ChatGPT's initialize capabilities

{
  "experimental": {"openai/visibility": {"enabled": true}},
  "extensions": {
    "io.modelcontextprotocol/ui": {
      "mimeTypes": ["text/html;profile=mcp-app"]
    }
  }
}

Observed behavior

  1. ChatGPT connects, initializes, and calls tools/list successfully
  2. On tools/call, the MCP App iframe is loaded
  3. ChatGPT sends 3-4 ui/notifications/tool-input (final) events in rapid succession for a single tool call
  4. Each event triggers the renderer's render() function
  5. The first render crashes with removeChild error
  6. ChatGPT's host shows SHOW_ERR error UI
  7. The second render "succeeds" but produces children=0 html=0chars
  8. The third render crashes again
  9. This alternating crash/success pattern repeats, causing visible flashing

Console output from ChatGPT's host

Calling renderer.render()...
RENDER_CRASH: Failed to execute 'removeChild' on 'Node': The node to be removed is not a child of this node.
stack: NotFoundError: ... at pl (<anonymous>:19:94645) | at dl (<anonymous>:19:94349)
SHOW_ERR: Renderer crashed: Failed to execute 'removeChild' on 'Node'...
THEME_CHANGED: undefined
THEME_CHANGED: undefined
TOOL_INPUT (final) after 0 partials
Content keys: content
...
Calling renderer.render()...
Render OK. children=0 html=0chars
THEME_CHANGED: undefined
TOOL_INPUT (final) after 0 partials
...
Calling renderer.render()...
RENDER_CRASH: Failed to execute 'removeChild'...
SHOW_ERR: Renderer crashed...

The log messages (RENDER_CRASH, SHOW_ERR, THEME_CHANGED, Calling renderer.render()..., Render OK. children=0 html=0chars) are from ChatGPT's MCP Apps host wrapper, not from the app code.

Root cause analysis

  1. Multiple rapid TOOL_INPUT events: ChatGPT sends 3-4 tool-input (final) events per tool call. In Claude, only one is sent.

  2. ChatGPT appears to call the renderer's render() function directly rather than loading the MCP App HTML in an iframe and letting the app handle the MCP Apps postMessage protocol. The app's debouncing, render serialization, and error handling in the HTML shell are bypassed.

  3. React 18 concurrent mode: When render() is called multiple times rapidly on the same root, the second call can interrupt the first mid-commit, corrupting React's fiber tree. The crash occurs at React's internal removeChild call during DOM reconciliation.

  4. Error propagation: Even when the app's render() function uses flushSync and wraps in try/catch to swallow errors, ChatGPT's host detects the error independently (likely from the parent frame monitoring the iframe, or by wrapping the render call). The SHOW_ERR error UI is shown regardless of error handling inside the app.

  5. THEME_CHANGED: undefined: ChatGPT sends theme change notifications with undefined theme between renders, which may contribute to DOM interference.

What we've tried (none worked)

  • flushSync to force synchronous React commits
  • container.textContent = '' before createRoot to clear existing content
  • Render serialization guard (if (rendering) return) inside render()
  • Nested wrapper div (#react-root inside #app) for DOM isolation
  • window.addEventListener('error', handler, true) with stopImmediatePropagation in capture phase
  • window.onerror returning true to suppress error propagation
  • renderToString (SSR) as alternative to client-side React
  • Debouncing tool-input events in the HTML shell
  • Never re-throwing from catch blocks

None of these work because ChatGPT's host bypasses the app's HTML shell code and calls the renderer directly.

Expected behavior

  • The host should send a single tool-input (final) event per tool call, not 3-4
  • The host should load the MCP App HTML in an iframe and communicate via the standard ui/* postMessage protocol, not call render() directly
  • If the host does call render() directly, it should serialize calls (one at a time)
  • Error detection should not flash error UI for transient rendering errors that the app handles internally

Comparison with Claude

Claude's MCP Apps host:

  • Sends a single tool-input event per tool call
  • Loads the HTML in a sandboxed iframe
  • Communicates via the standard ui/* postMessage protocol
  • Does not call render() directly
  • No removeChild errors occur
  • Rendering works correctly on every call

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions