-
Notifications
You must be signed in to change notification settings - Fork 227
Description
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
flushSyncfor synchronous commits
ChatGPT's initialize capabilities
{
"experimental": {"openai/visibility": {"enabled": true}},
"extensions": {
"io.modelcontextprotocol/ui": {
"mimeTypes": ["text/html;profile=mcp-app"]
}
}
}Observed behavior
- ChatGPT connects, initializes, and calls
tools/listsuccessfully - On
tools/call, the MCP App iframe is loaded - ChatGPT sends 3-4
ui/notifications/tool-input(final) events in rapid succession for a single tool call - Each event triggers the renderer's
render()function - The first render crashes with
removeChilderror - ChatGPT's host shows
SHOW_ERRerror UI - The second render "succeeds" but produces
children=0 html=0chars - The third render crashes again
- 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
-
Multiple rapid TOOL_INPUT events: ChatGPT sends 3-4
tool-input(final) events per tool call. In Claude, only one is sent. -
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. -
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 internalremoveChildcall during DOM reconciliation. -
Error propagation: Even when the app's
render()function usesflushSyncand 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). TheSHOW_ERRerror UI is shown regardless of error handling inside the app. -
THEME_CHANGED: undefined: ChatGPT sends theme change notifications withundefinedtheme between renders, which may contribute to DOM interference.
What we've tried (none worked)
flushSyncto force synchronous React commitscontainer.textContent = ''beforecreateRootto clear existing content- Render serialization guard (
if (rendering) return) insiderender() - Nested wrapper div (
#react-rootinside#app) for DOM isolation window.addEventListener('error', handler, true)withstopImmediatePropagationin capture phasewindow.onerrorreturningtrueto suppress error propagationrenderToString(SSR) as alternative to client-side React- Debouncing
tool-inputevents 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 callrender()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-inputevent per tool call - Loads the HTML in a sandboxed iframe
- Communicates via the standard
ui/*postMessage protocol - Does not call
render()directly - No
removeChilderrors occur - Rendering works correctly on every call