Markdown source

Transcript: Why MCP and ChatGPT Apps Use Double Iframes — Frédéric Barthelet, Alpic

Source Video

Local Cache

Transcript

[music] >> Hi everyone. My name is Fred. I'm the CTO and co-founder of Alpic, the MCP hosting company. And today I would like to share with you an adventure deep diving into the double iFrame mechanism that we have on ChatGPT and MCP app and what it matters when we build apps. First thing first, if you haven't had the chance to listen to either Ido and Leat talks just before about MCP apps, a quick sum up of what those MCP and ChatGPT apps are. That's a new surface area for your business to expose product and services with new acquisition channel that has two main criteria.

First one being discoverability, so you will have ecosystems of connectors and apps available in a consumer generalistic agent like ChatGPT and Claude. So ChatGPT app store and Claude connectors. Those apps browsable inside the store, but they are also discoverable in chat. So if you're having a conversation that's relevant for an app to be brought into to add additional context and feature some nice additional actions, they will be brought into the conversation. And the second part, which is the biggest part and what we will be focusing on in this talk, which is the addition of interactive UI inside those conversational agents where you used to add text only.

Apps add a new layer of UI that could be provided by the MCP server, but could be generated generative UI as well. They were first thought of using MCP UI that was developed by Leat and Ido just before, then released by Open AI with an app SDK back in October last year and standardized across multiple clients on the first official extension of MCP called the app extension. How does it work under the hood? If we take a little bit closer look at how this UI is brought into the conversation, those are brought using views. Views are the name that we use for those small snippet of UI that appears inside the conversation.

Views are always rendered as a result of a tool call. So if your server exposed multiple tool to be used, you can actually add metadata on some of them to say this tool is best used when results will be displayed using a specific UI. And if the host supports MCP apps, it will use the relevant view corresponding to this tool call to display the results. Views are simple HTML document. You can include JS and CSS inside. Nothing new under the sun here.

It's just a way to package those small snippets of application and they are discoverable ahead of time because all views are described on the tool list calls that happens at the beginning of the conversation between the host and your MCP server or MCP app. So each tools that supports UI will advertise the resource that's needed to display the UI. It can be cached ahead of time or it can be served and downloaded and served right away when the tool call that needs UI to be rendered is made.

The conversational agent on the host will create this new iFrame where the view will be displayed and it will inject the tool results inside so that you have dynamic content rendered to the user. If you take a closer look at what is inside the DOM of the host when you take a I was a bit curious and I wanted to know how it was working or how ChatGPT was actually rendering third-party UI inside the conversation. I was a bit surprised and I was met with not so much expectation about having a double iFrame, having an iFrame nested in inside another iFrame. And this gave me the idea for this talk.

I want to bring you today with me deep diving into why the decision was made to do this kind of inception nesting of iFrames and what are the benefits, what was it put in place and what are the implications when you build apps, what should be paying attention to and how to make sure that your experience is very nice. Before we go into that, let's take a close look at what ChatGPT was before MCP app were implemented. We'll be using ChatGPT as the example throughout this deep dive, but the exact same happened on Claude AI if you want to take another look by yourself.

The initial thing to take a look at that is very important is something called content security policy. Those are directives returned by server as response header to document call. So when you load ChatGPT inside your browser, ChatGPT will respond with a document plus security policy directive on what the browser should be allowed to load and execute and what it shouldn't be allowed to load and execute. You've got multiple directives included inside content security policy. Some about which scripts you can run, which CSS style sheet you can download, which image you can download, which API you can connect to and ask question to.

I will not go into the details, but two are very important to remember here. Frame SRC, which basically is a directive to allow specific website to render iFrame inside the document and script SRC, which basically allow specific sites script to be run inside the browser. To be able to run external UI inside ChatGPT, we will use a dedicated HTML element that has been made specifically for this purpose, which is the inline frame element or iFrame that is made to basically spawn up nested browsing context inside your browser window. So those small pieces of views will be rendered as almost separately completely isolated browsing context.

They are very convenient and they have two ways to be used. First one is to provide a source for the iFrame that you want to render. So basically a URL of another page to be loaded by your browser and executed locally and rendered inside the space it's made of it's made for. And the source doc, which is another attribute which allows you to push inside the iFrame content that you want to render as is without having the browser to load another content.

So if you want to build this marketplace of app and have third-party UI rendered inside ChatGPT, why not use straight away source doc as the attribute for as the attribute for uh for injecting context into. And I'm realizing now that it's a little bit small, but I think I can zoom in a bit. No, I cannot. Okay, sorry about that. You'll have to trust me about what is written inside here. So I will just put inside an iFrame injecting context into the iFrame. Context being the content being basically the resource that is being exposed by the MCP server. So pure HTML loading inside.

If I do that, uh it's not going to work mostly because when you load up an iFrame with source doc attribute specified, the iFrame that you are spawning up is sharing the same origin and sharing the same therefore CSP as the host that is responsible for rendering it. So any script that would be part of your application that would be completely blocked by existing ChatGPT CSP on script SRC directive, which basically require every script in ChatGPT to be signed with a specific nonce produced ahead of time at each request, which is a cool security feature to put in, but it prevents any app to be able to execute JS.

So in order to do that, what if we relax a little bit the content security policy of ChatGPT and make it so that it can execute any line of code? I would not suggest doing that into production, just an experimental thought here. But if you do that, you are faced a new problem. Basically, you are sharing same origin as your parent DOM. So the loaded iFrame scripts would be able to access local storage or cookies that are indexed by origin.

So you would be able as an app to for example get the existing local storage of ChatGPT and send it to your back-end server, which if you are Open AI, you would not want people to be able to do. So let's roll back, put back the CSP as it was before and instead sandbox the iFrame. Sandbox is another attribute that you can use on iFrames allowing iFrames to be rendered in what we call the no pack origin. It would mean that basically the iFrame will not share anymore the parent origin.

It would be something equivalent to null making sure that they don't share the same origin and won't have the same problem of script being able to access the parent DOM. However, doing so, you lack any capabilities that are dependent on origin indexing. Because all content, all scripts that are rendered inside your iFrame will now be pointing towards a null origin. You cannot use local storage. You cannot use local index DB. You cannot use cookies because those are indexed by origin.

And the only way to actually provide an origin to a sandboxed iFrame is to put allow same origin, which is an additional attribute that brings back the exact same origin as the parent back into the iFrame and you're back to square one where you have an iFrame with exactly the right condition to escape its sandboxing and access parent DOM, access parent local storage, access parent cookies. Okay, so iFrame source doc is not the way to go forward. Let's move on to the next best solution that we have using the source attribute. Source attribute basically allows me to reference an an endpoint that will be the content loaded by the browser inside this iFrame.

I'm a developer of the ChatGPT app and MCP app. Why not expose my view, my small HTML application as a normal endpoint like on the view endpoint for example of my own server? That would be a nice way to do it. However, it would require open AI to modify the another CSP directive, which is basically the frame source directive listing all domains that are allowed to actually render iframe on chat GPT to include an infinite list of all the MCP application that will be developed by various companies and brought into the store.

So, every time a new app would come out, chat GPT would have to update CSP to include the new domain so that the frame can be rendered on the specific domain. This is not doable full scale. So, what we can do instead is provide kind of a proxy controlled domain, single one that will be owned by chat GPT in that case. For example, open AI open AI user content.com. That's an actual domain that they're using for user content that they want to expose on their own domain. And use this domain as a reference inside the frame source to make sure that the directive does not block rendering any iframes that are loaded on there.

And you need to provide a server on the open AI user content that's able to download the resource content from the MCP server, the HTML, and expose it so that it can be rendered for example on any subdomain and use the first part of the subdomain as the routing key to the right application. Doing so, you effectively needs to put in motion an infrastructure where your domain hosts external third-party UI from all apps that will be submitted on your store. Which is not a very good position to be into because once again, you will be responsible for code that you don't know what it's doing and you will be exposing it on your own domain.

In addition to that, if you're not open AI or if you're not Anthropic, you might not have the resource as a host to put in place the infrastructure required to serve this kind of dynamic serving of content. So, what you can do instead is go through the double iframe mechanism. And what you will do with that is basically load the same script for everybody, which will be a simple script responsible to recover the resources and initiate an iframe with the source doc attribute. So, we put the content inside, but this iframe will not be served at top level because it shares the same origin and it has the escaping problem we are mentioning before.

It will be served inside an iframe with dedicated domain that is different from chat GPT to make sure the isolation stay there. You won't don't want to stop there. Actually, you want to put subdomains for this exact script loader. It will be the exact same content that will be served every time, but you want to put it on various subdomains so that if your app uses any APIs that requires origin indexing like local storage or cookie, you don't have collision in between your app. So, you want app ABC123 not to be able to access local storage from app ABC456 for example.

The infrastructure for that is much less intensive because you're serving the exact same script content of the first iframe for every subdomain that is exist on this specific endpoint. Last but not least, you want to be able to provide the same kind of content security policy definition to the app itself so that it can prevent execution of any such script or rendering of iframe directly inside the view. There is a way to provide this into the MCP spec and the way you will render it is using a specific meta tag inside the first iframe. This is the actual solution that I implemented into production. And it's not a new solution.

It's been around for a long time and actually the first time this solution was implemented was back in Facebook days when they released their app marketplace, which is exactly the same problem that you have to run and render third-party UI inside the context of your own application. What's important for you as an app developer is to make sure what are the specs available for you to be able to control the behavior that results from this double iframe nesting.

And the thing that you will have to make sure to do is every time you build an application, declare all domains your application depends upon inside the provided metadata in the MCP app spec so that you are sure that they will be rewritten correctly inside the nested iframe. For example, if you are from your app connecting to an external API to fetch data, you need to reference this domain inside the connect SRC directive of the metadata. Same thing for the script, image, frame and base UI not so much used, but the two first one are very important.

And it's reminded me of a very old problem that I had when I started my developer's days back in 2016 I think it was. As a new developer in the space, I was experiencing a trouble getting cross-origin resource security right calls. I had trouble getting it right. CSP reminded me of those ugly day of not getting right the first time with calls. So, there has been effort in the ecosystem to make builders' life better, especially for example on open AI side where they activated they added an option in developer mode.

So, if you're a developer and developing app, they have a specific mode which is developer mode which allows you to have access to additional features. Up to today, when you were in developer mode, all CSP were removed. So, you were discovering when you were going into production if some of your servers could not be reached because of missing domains inside your CSP, which was not ideal. They are not the only one doing a bunch of work to make builders' life much easier. At Alpic, we build an open source framework called Skybridge. Skybridge is a super set of feature on top of the official app SDK either and the app we are mentioning.

It brings a few things to the table, end-to-end type safety between your MCP server and your app widgets and views. You have a lot of APIs that provides polyfill for features that are not part of the common specification and specific to some of the hosts, some of code and some of chat GPT APIs. And we provide a bunch of modern development features especially in the on dev environment features. And I wanted to show you one just specifically made for CSP, which we call the CSP inspector. Time for a small demo. I do have a few minutes left. Perfect. Let me quickly switch to my screen. Up. Uh up. Okay.

So, this is Um this is an example code base that will be generated when you create a new Skybridge application. This example Skybridge application comes with a small application. It's a eight ball that you can ask any question to that will respond one of the 25 predefined answer. It has MCP tool that serves as generating the answer and a view that is there to display the question and the answer to the question that you ask. When I start the server of Skybridge, I have access in my browser to a dev tool, which is basically a small app that will give me inspection tooling to work with my app before I bring it to chat GPT.

For example, on the left I have a list of tools that are exposed by my app. I can if I want ask execute any of the tool. So, for example here, I'm executing the magic eight ball tool. And if there are views associated with this tool, they will be rendered inside the inspector for me to have a closer look at it and make changes and see those changes reflected live in the UI.

The neat thing I wanted to demonstrate is the CSP part of the inspector that we built, which basically looks at all the domains that you listed inside your metadata and all the domains that are actually accessed by calls made by your view and compare them to make sure that none of them are not listed yet.

So, for example here, everything's green, but if I go back to my actual code base and for example fetch some API to get info about my IP location, this will be reflected straight away in the inspector because the component has been re-rendered and I know I have the exact domain that I want that I just called listed as missing from the metadata. And I can go back inside my application and add the missing domain and now it should appear green if I reload it. Yeah, everything good. So, neat little tool.

There are a lot of other features that are packed inside Skybridge, but that's one of them that we made sure to be available to builders because we've seen a lot of rejection coming from chat GPT app store submission because of missing CSP and app not working in production because of missing CSP domains. Um Just to finish up quickly if I can. Up. Uh up. This one. Yeah. Um yeah, that's all. Thank you again for your time. If you want to grab the slides and take a look later on, feel free to scan the first QR code. If you want to give a try to Skybridge, feel free to scan the second one.

I will run right now a small lottery. We have ski goggles to win if you star Skybridge repo in the next minute or so. I will draw name at random and you will win a ski ball mask. Thank you very much for your attention. >> [applause] [music]