AI Engineer Europe 2026
Why MCP and ChatGPT Apps Use Double Iframes — Frédéric Barthelet, Alpic
Read the talk
Why MCP and ChatGPT Apps Use Two Iframes
Embedding an interactive app inside a conversation requires more than rendering HTML: the browser must isolate host data, preserve app storage, and enforce the right security policy.
From a talk by Frédéric Barthelet
Before you start: Familiarity with HTML, JavaScript, browser origins, and the basic idea of an MCP tool will help you follow the walkthrough.
Bringing an app into a conversation
How can a conversational agent display another company’s interactive interface without giving that interface access to the host application? That is the browser-security problem behind this walkthrough by Frédéric Barthelet, who introduces himself as Alpic’s CTO and co-founder. Alpic provides MCP hosting, and its applications need a place to run inside someone else’s product.
MCP and ChatGPT Apps combine two capabilities. Discovery brings products and services into ChatGPT’s app store and Claude’s connectors, both through browsing and through relevant conversations. Interactive UI lets those services display interfaces where previously the agent could only return text. A view might come from an MCP server or be generated dynamically; either way, the host must render it safely.
Barthelet traces this development through Ido and Liad’s MCP UI, OpenAI’s Apps SDK preview, launched on October 6, 2025, and then the MCP Apps extension, which gives multiple clients a shared way to support interactive interfaces.
Suggest correction
This note stays in this page until you copy or download it. Nothing is submitted; reloading clears the draft.
From a tool call to an HTML view
The UI unit is a view: an HTML document that can include JavaScript and CSS. A tool advertises metadata associating it with a particular UI resource. When a supporting host invokes that tool, it uses the associated view to present the interaction. The application remains ordinary web content; the protocol supplies the connection between the tool and its interface.
The lifecycle separates discovery, retrieval, and data delivery:
- During
tools/list, the host discovers which tools advertise UI resources. - The host retrieves the relevant HTML, either ahead of time for caching or when the tool is invoked.
- The host creates the frame for the view and delivers tool data so it can render dynamic content.
The current documented lifecycle also permits preloading and initializing the view while the tool executes; the UI need not wait for a completed result before it exists. Inputs and results can arrive through notifications.
Suggest correction
This note stays in this page until you copy or download it. Nothing is submitted; reloading clears the draft.
The unexpected second iframe
Inspecting ChatGPT’s DOM, Barthelet found an iframe nested inside another iframe. Why would a host need both? He uses ChatGPT to investigate the design and says the same pattern appears in Claude. The first constraint becomes clear before adding any third-party UI: the host already has a security policy for its own page.
Content Security Policy, or CSP, tells the browser which resources a document may load and which scripts it may execute. A server can deliver it in response headers alongside the HTML document. Its directives cover scripts, stylesheets, images, API connections, and other resource types. Two directives drive the initial embedding problem:
| Directive | Relevant restriction |
|---|---|
script-src | Which scripts may execute |
frame-src | Which sources may load in nested frames |
The host cannot simply accept arbitrary app HTML without considering both restrictions.
Suggest correction
This note stays in this page until you copy or download it. Nothing is submitted; reloading clears the draft.
Direct HTML injection inherits the wrong environment
An iframe creates a nested browsing context. It has two useful ways to receive a document:
| Attribute | How the document arrives |
|---|---|
src | The browser loads a document URL |
srcdoc | The caller supplies HTML directly |
Because the MCP server already exposes an HTML resource, srcdoc looks like the shortest route from that resource to a visible view. It avoids a separate browser fetch for the document.
The attempted embedding can be expressed with ordinary browser JavaScript:
javascript
function mountView(container, html) {
const frame = document.createElement("iframe");
frame.srcdoc = html;
container.append(frame);
return frame;
}
Here, html is the resource obtained from the MCP server. In this unsandboxed setup, the srcdoc document shares the parent’s origin and inherits the parent document’s CSP. Barthelet describes ChatGPT’s script-src policy as requiring a nonce generated for each request. Arbitrary JavaScript bundled with an app does not have that authorization, so the interface’s scripts are blocked.
Relaxing the host’s CSP would make another problem visible. This is a thought experiment, not a production fix: once the third-party scripts can run in that same-origin environment, they can access host-origin data such as localStorage and script-accessible cookies. Barthelet’s example is an app reading ChatGPT’s local storage and sending it to the app’s backend. Allowing the code to execute does not establish a safe boundary for it.
Suggest correction
This note stays in this page until you copy or download it. Nothing is submitted; reloading clears the draft.
Sandboxing removes more than host access
Restore the original CSP, then try the iframe’s sandbox attribute. Without allow-same-origin, sandboxing gives the document an opaque origin, commonly represented as null. It no longer has the host’s origin identity, so it cannot use that identity to access the parent DOM.
That isolation also removes capabilities the app may need. In the illustrated setup, localStorage, IndexedDB, and cookies are unavailable because they depend on an origin under which data can be stored and retrieved. The app is isolated, but it has lost ordinary browser persistence.
Adding allow-same-origin preserves the embedded document’s real origin; it does not generally assign the parent’s origin to any iframe. But for this srcdoc example, the real origin is precisely the parent origin that caused the problem. The HTML standard’s sandbox warning applies when scripts are also allowed and the embedded document is same-origin with its parent: the child can remove the sandbox attribute and reload to escape those restrictions. The missing ingredient is a useful origin that belongs neither to the host nor to another app.
Suggest correction
This note stays in this page until you copy or download it. Nothing is submitted; reloading clears the draft.
A separate URL moves the problem
Using src offers an obvious alternative. The app developer could expose the HTML at a normal /view endpoint and let the browser load it from the developer’s server. That supplies a separate origin, but it runs into the host’s frame-src policy. With an explicit allowlist, ChatGPT would have to admit every app domain and update its policy whenever a new app entered the ecosystem. The browser mechanism works; maintaining the host’s policy becomes the scaling problem.
A host-controlled content domain makes the allowlist manageable. Barthelet uses openai-usercontent.com as his example: authorize the controlled domain space, then use an app-specific subdomain as a routing key. A server behind that domain retrieves the appropriate HTML resource from the MCP server and serves it to the browser.
Now the host must operate infrastructure that dynamically serves third-party application code on its own content domain. That creates responsibility for serving code the host did not write, as well as an infrastructure burden. A smaller MCP host may not have OpenAI’s or Anthropic’s resources to build and operate such a service. The next step retains the separate domain but removes the need to serve a different application document for every app.
Suggest correction
This note stays in this page until you copy or download it. Nothing is submitted; reloading clears the draft.
One reusable loader, separate origins for each app
The double-iframe design serves a small, reusable loader in an outer iframe on a domain different from the conversational host. That loader obtains the view HTML and puts it into an inner iframe using srcdoc. The MCP Apps specification recommends this architecture for web hosts and describes the host sending HTML to the outer proxy; the loader does not need to fetch independently from the MCP server. The inner document can share the outer frame’s origin without sharing ChatGPT’s origin.
Serving the loader from one shared origin would still let different apps collide in origin-indexed storage. Instead, serve the same loader content from different app subdomains. Barthelet’s concrete distinction is app ABC123 versus app ABC456: ABC123 must not be able to read ABC456’s localStorage.
| Layer | Responsibility |
|---|---|
| Conversational host | Retain its own origin and security policy |
| Outer iframe | Load the common wrapper on an app-specific origin |
Inner srcdoc iframe | Render the app HTML inside that boundary |
The bytes served for the outer loader can be identical across subdomains while the browser still treats those subdomains as different origins. This separates local storage between apps; cookies additionally require appropriate scoping rather than assuming subdomains alone guarantee isolation.
The app also needs its own CSP. Barthelet describes taking policy declarations from MCP metadata and applying them through a CSP meta tag in the outer frame. Those restrictions govern what the view can load or execute, including scripts and further embedded frames. The extra iframe separates the host’s security environment from the app’s security environment while keeping a stable origin for the app.
Barthelet says he has implemented this design in production and points to Facebook’s App Marketplace as an earlier use of the pattern. For an app developer, the practical question is now which metadata controls the environment inside those nested frames.
Suggest correction
This note stays in this page until you copy or download it. Nothing is submitted; reloading clears the draft.
Declare the domains the view actually uses
The host can construct the app’s policy only if the app declares its dependencies. List every domain needed by the view in the provided MCP Apps metadata:
- Connections: External API requests need connection-domain declarations, which feed
connect-src. - Resources: Script and image sources need resource declarations so the browser can load them.
- Nested frames: A view embedding another frame needs the corresponding frame-domain declaration.
The declaration slide also includes baseUriDomains alongside connectDomains, resourceDomains, and frameDomains. These are categories of policy inputs, not permission to load arbitrary content.
Barthelet compares debugging CSP with his early difficulties getting CORS right around 2016: a browser security restriction can look like a broken application until the developer identifies the missing permission. At the time of the recording, he reports that ChatGPT developer mode had previously removed CSP enforcement, allowing missing domains to remain hidden until production. He also mentions a newly added developer-mode option intended to improve that workflow. The operational requirement is to test with the policy the deployed view will actually face.
Suggest correction
This note stays in this page until you copy or download it. Nothing is submitted; reloading clears the draft.
Catch a missing domain before submission
Alpic’s open-source Skybridge adds development features over the official Apps SDK. Barthelet highlights end-to-end type safety between the MCP server and its widgets or views, polyfills for host-specific Claude and ChatGPT APIs outside the common specification, and local development tools. The feature he demonstrates is the CSP Inspector.
The generated example is a Magic 8 Ball app. Barthelet describes it as choosing from 25 predefined answers: an MCP tool generates an answer, and a view displays the question and response. Starting the Skybridge server opens a browser-based development environment. A tool list on the left lets him invoke the Magic 8 Ball tool directly, render its associated view, and see code changes reflected live without first bringing the app into ChatGPT.
The inspector compares domains declared in metadata with domains the view actually accesses. The demonstration follows a short repair loop:
- Start with a matching policy. The inspector is green because the observed accesses are covered by the declarations.
- Add a dependency. Barthelet adds a fetch to an IP-location API. The component rerenders, and the inspector identifies the called domain as missing from the metadata.
- Repair the declaration. He adds that domain to the application metadata and reloads. The inspector returns to green.
The useful feedback is the exact missing domain, surfaced while changing the view rather than after shipping it.
Barthelet reports seeing ChatGPT App Store submission rejections and production failures caused by missing CSP domains. The inspector closes that specific gap between a working local interface and an interface permitted to make its required requests. He ends with QR codes for the slides and Skybridge, providing a route back to the architecture and the tooling used to exercise it.
Suggest correction
This note stays in this page until you copy or download it. Nothing is submitted; reloading clears the draft.
Resources
From the talk
Alpic's React and TypeScript framework for MCP Apps, with type inference, local development tools and examples.
How tools advertise interactive HTML resources and communicate with sandboxed views.
OpenAI's October 2025 announcement of apps in ChatGPT and the Apps SDK preview.
Further reading
- MCP Apps specification: January 2026 versionDocumentation
Protocol details for sandbox proxies, CSP metadata, view initialization and tool-result delivery.
- HTML iframe sandbox semanticsDocumentation
The browser standard's explanation of iframe origins, sandbox flags and same-origin escape conditions.
Read the transcript and explore slides
Loading transcript and slides…