Smuggling WebMCP into ChatGPT
ChatGPT can't run a web page's tools yet. An iframe, a fake browser and some creative use of chat messages change that.

WebMCP is a pretty great idea: a web page offers a few tools ("add to cart", "search flights", "check out"), an AI agent in your browser calls them, and instead of the agent guessing at buttons, the page says what it can do, and the agent does it.
Almost nothing can use it yet, though, because it's behind a flag in Chrome, and the agents most people actually talk to have no idea it exists.
I wanted to know whether I could get ChatGPT to use a web page's tools today, without waiting for anyone.
As it turns out, yes, more or less, and it was an interesting hack. It took an iframe, a fake browser, and bending chat messages into something they probably weren't meant for.
Wait, what's WebMCP?
Most agents use websites the hard way today: they take screenshots and click on things, or they read the page's HTML and guess which button does what, which is slow and can break whenever the page changes.
The other option is an MCP server, the standard way apps like ChatGPT call outside tools. The site builds a seperate server on the internet, and ChatGPT connects to it, logs in as you, and calls its tools over HTTP.
WebMCP puts the tools in the page instead. The page calls registerTool() with a name, a description, an input schema and a function, and an agent working in your browser can call it. That has some nice properties:
- You're already logged in. The tool runs in your tab, with your session, so there's no OAuth and no API keys.
- The page knows things the server doesn't, like what's on screen, the half-filled form and the filters you just picked.
- You can see what's going on, because the agent works in the page you're looking at, and the page can ask you before doing anything risky.
- There's no new backend. For a lot of sites the front end already is the product, so it's a few functions instead of a new server to build and secure.
Chrome has an early version behind a flag.
What's stopping us?
WebMCP lives in your browser tab, and ChatGPT lives on OpenAI's servers, with nothing connecting the two.
ChatGPT only talks to servers. In developer mode you can add your own MCP server to ChatGPT as a plugin, and ChatGPT calls it over HTTPS from OpenAI's side. A server on the internet can't see your browser, so it can't see a page's tools either. WebMCP, meanwhile, only exists in the browser: only code running in the same browser as the page can find and call its tools, and even that is behind a flag.
So yeah, on one side there's a model that can only call servers, and on the other, tools that only exist inside a tab. To connect them, I need something that runs in my browser, inside ChatGPT, and that the model can talk to. While ChatGPT doesn't offer that as one feature, it has a few pieces that add up to it.
The players
There are four of them, two belonging to ChatGPT and two making up the plugin I built.
The first is ChatGPT itself: the chat page, running in my browser, and the model, running on OpenAI's servers. The second is the page, meaning whatever site I want ChatGPT to use, which here is a demo shop that offers a few tools.
Then there's the plugin's server, a small MCP server on the internet that gives ChatGPT two tools (which I'll get to), and which, like any server, can't see my browser.
And then there's the panel, the other half of the plugin. ChatGPT apps can show a bit of UI in the chat, as an iframe running in my browser. Mine is a little web browser with an address bar and an iframe of its own, which is where the page loads. The panel can also post messages into the chat, as if I'd typed them, and that turns out to be the important bit.
The model can't reach the page, and the page can't reach the model, so the panel carries everything between them, mostly as chat messages, and that's the smuggling route.
What it looks like
I mention the plugin and ask it to open the browser, and ChatGPT shows a small browser in the chat, with a button for a demo shop:
I click it, and the shop loads inside the panel and, before anything else happens, asks whether ChatGPT may use my cart. That question comes from the shop itself, so only my click can answer it:
I click Allow, the panel's status bar lists the four tools the shop offers, and ChatGPT tells me what it can do:
From there I just ask for things:
The mug really is in the cart, because the shop's own add_to_cart function ran in my browser, and ChatGPT answered from its actual result.
The hack, step by step
There are four problems to solve: show a page, tell the model about its tools, let the model call one, and get the result back.
Step 1: put a browser in the chat
The plugin's server offers ChatGPT a tool called open_browser. I ask for the browser, the model calls the tool, the server says "opened", and ChatGPT shows the panel in the chat: an address bar, back and forward buttons, a status bar, and an iframe for the page. I pick a page (here, the demo shop), it loads in my browser like any page I'd visit, the model confirms, and that turn is done.
So far so good, but the model still has no idea what the page can do.
Step 2: tell the model what the page can do
The panel connects to the page and gets its list of tools (more on how in a minute), and now they need to reach the model. Normally a plugin's tools come from its server, but the server never sees the page, so that's out, and the only route left is a chat message.
So the panel posts one. It's a single line for me ✓ Opened Kiln & Cotton: 4 tools, with the tools attached as JSON for the model, and ChatGPT shows it for a moment, tucks it away, then gives the model a new turn. With the tools in front of it, the model says in plain english what it can do on this page. I never asked the model to list anything, the instructions attached to the panel's message did that.
Step 3: call a tool the server has never heard of
The plugin's second tool is call_site_tool, and it's completely generic: a page, a tool name and some arguments. When I say "add the blue mug to my cart", the model calls it with add_to_cart and the blue mug. But that call goes to the plugin's server, and the server can't reach my browser, so all it can do is hand back a ticket number and tell the model to wait for the result.
So how does the call get to my browser? This is the part I like most: ChatGPT opens a fresh copy of the panel for every tool call, right there in the chat, and hands it the call's arguments and the ticket number, so ChatGPT is doing the delivery for me. The new panel loads the same shop (the cart lives in the URL, so it finds the same cart), connects, and runs add_to_cart. The blue mug lands in the cart while the older panel folds itself away, so it still looks like one browser.
Yes, this means the server is basically a receptionist that takes a message and points at the door. But it works, and it also means the server never sees the page, the tools or the results.
Step 4: get the result back
The panel posts the result as another chat message, ✓ add_to_cart ran on Kiln & Cotton. Result attached carrying the same ticket number so the model knows which call it answers. It's the same trick as step 2, except this time the model is usually still busy with its reply, and this is where it got kinda finicky.
A message from the panel can either start a new turn, or be slipped into a reply that's still being written, and which one works depends on whether the model is busy at that moment. None of it is documented, and ChatGPT shows a red "Steering requires an active compatible turn" when you guess wrong, so the panel tries one, then the other, and waits a couple of seconds if both say no. Then the model reads the result and answers from what actually happened on the page.
One last bit of polish: at first the model narrated the wait, with "I've sent the request to add 1 blue mug ($12) to your cart. Awaiting confirmation from the shop." Thats accurate, but it gets old fast, so now it's told to say nothing until the result arrives, and the whole reply is the one line you see in the screenshot above.
The whole picture
This is everything at once, what runs where and what flows between the pieces:
The main thing to notice is that everything real happens on the left, in your browser, and the only thing on the right that knows about the shop is the model, which only knows what the page chose to tell it.
Why hacks like this work
When a normal program calls a function, a few things are always true: the function was declared up front, the answer comes straight back from the call, and a message from the user really came from the user. Break any of those and a normal program falls over; and this plugin breaks all three:
- The shop's tools are never declared to ChatGPT, they just turn up in a chat message.
- When the model calls one, it gets a ticket number back instead of an answer.
- The real answer arrives later, as a chat message that looks like it came from me.
And the model doesn't mind, because a model doesn't see calls, return values or senders the way a program does. It sees one long stretch of text, so a message that says add_to_cart ran, here's the result reads to the model just like a proper tool result would. As long as the information lands in the conversation and it's clear what it is, the model picks it up.
I think that's worth remembering when you build for LLMs. We're used to strict systems, where every message has to arrive in the right shape, through the right pipe, but models are a lot more forgiving, and that leaves plenty of room for hacks like this one.
Talking to the page: glottis
I skipped one thing in step 2, which is how the panel gets the page's tools in the first place.
The panel and the page are on different origins, so they can't touch each other's code, and all they can do is send each other messages with postMessage. As I've said before, messaging is a pain: you need a bunch of stuff on top, like requests and responses, errors, timeouts, and some way to know the other side is even there.
Native WebMCP would solve this, but it isn't on by default anywhere yet, so for now the page serves its tools with glottis, a small library that makes a page's functions callable from another window. Here's a page offering a tool:
import { z } from 'zod';
import { serve, tool } from 'glottis';
serve({
clients: {
remoteWindow: { allowed: true, domain: '*' }
},
exports: {
say_hello: tool({
description: 'Writes a greeting on the page for the given name.',
input: z.object({
name: z.string()
}),
handler: ({ name }) => {
document.body.append(`Hello, ${ name }!`);
return { greeted: name };
}
})
}
});
The panel connects, gets say_hello back as an ordinary async function, and calls it. glottis also turns the zod schema into the JSON Schema the model sees and checks every call against it before the handler runs, and whatever the handler returns goes back to ChatGPT, while whatever it throws reaches ChatGPT as the error.
There's a lot more to glottis than that, and it has its own post:
The shop asks first
Letting a chatbot drive your cart should probably involve asking you first, and here it's the page that asks, not ChatGPT and not the plugin. glottis lets exports be an async function, and holds the connection open until it returns:
serve({
clients: {
remoteWindow: { allowed: true, domain: '*' }
},
exports: async ({ client }) => {
if (!await askTheUser(`Let ${ client.origin } use your cart?`)) {
throw new Error('The user declined.');
}
return tools;
}
});
The prompt is drawn inside the the shop's own frame, so neither ChatGPT nor the plugin can click it, only me, and the demo shop remembers the answer per cart, so the panels that come later (one per call, remember) don't ask again.
A quick note on security, since this is the part people ask about: the plugin's server never sees a page, a tool or a result; the page decides who gets in and what they can do once they're in; and the model can only call the tools the page offers, so there's no "click anything anywhere" mode.
The real thing, behind a flag
So that's glottis, but what about actual WebMCP? The plugin does that too, and the page barely has to change, because glottis can register the same tools with the browser's own WebMCP when it has it. It's one more line in serve():
serve({
clients: {
remoteWindow: { allowed: true, domain: '*' },
// Also register the tools with native WebMCP, for the page that
// frames this one (the plugin's panel)
webMcp: { allowed: true, domain: location.ancestorOrigins[0] }
},
exports: { say_hello }
});
Under the hood that's WebMCP's registerTool(), with the tools exposed only to the panel's origin (and you can call that yourself, if you'd rather not use glottis). The panel then asks the browser for the shop's tools with getTools() and runs them with executeTool(), with no connection needed. If both routes are available the panel uses glottis, with the status bar saying which one it's on.
Except that WebMCP is guarded by a browser permission called tools, which every frame between ChatGPT's page and the shop needs, and ChatGPT's frames don't have it. So, just to see what would happen, I added it myself by injecting allow="tools *" into ChatGPT's iframes before they loaded, and it turns out that worked: ChatGPT's panel could see the shop's native tools and run them.
That experiment is now a launcher in the repo:
npx chatgpt-webmcp-bridge chrome
It opens your own Chrome in a profile of its own, with the flag on and every iframe given the permission.
Where the seams show
This works, and I use it, but it's a hack, and there are rough edges:
- Results are chat messages, not tool results. The model has to wait for a message it was told is coming, and it nearly always does, but occasionally you'll see that red "Steering requires an active compatible turn" line while the panel works out which way in is open.
- A panel per call. Each call reloads the page in a fresh panel, which is fine for a shop whose state lives in the URL and in storage, and less fine for a page that keeps everything in memory.
- Only pages you've listed. ChatGPT only lets the panel show pages from origins the plugin's server lists up front (they become the panel's
frame-src), as exact origins with no wildcards, so "any page" really means any page whose origin you've listed on a server you run. - The page has to allow framing, with no
X-Frame-Optionsheader that blocks it and aframe-ancestorsthat allows ChatGPT's sandbox. - Developer mode only. OpenAI's app directory isn't going to approve an app that frames arbitrary pages, so for now this is a tool for people who build things.
- Native WebMCP needs a flag and a launcher, until browsers ship WebMCP and ChatGPT grants the permission.
I think most of these go away the day ChatGPT can talk to a page's WebMCP tools directly, and my bet is that it will, eventually. This is just a version you can use today, while the real thing gets built.
Try it
You need ChatGPT with developer mode on, and then it's three steps:
In ChatGPT, open Plugins › Add › Add custom MCP server, choose no authentication, and use this server URL:
https://webmcp.danielbrain.ai/chatgpt-webmcp-bridge/mcpStart a chat, mention the plugin with
@, and ask it to open the browser.Click Try the demo shop, click Allow, and ask it to add something to your cart.
To point it at your own pages, run your own server (one command on Cloudflare Workers, or the package's serve command behind a tunnel) and list your page's origin. Everything is on GitHub, and it's on npm as chatgpt-webmcp-bridge, MIT licensed and very much 0.1.
Give it a try, and please steal any of these ideas. It's very much an experiment, so if you're building pages with tools, or agents that use them, I'd love to hear what you'd do with it, and what I got wrong.
Comments
Join the discussionNo comments yet. Be the first to comment.

