On July 28, 2026, the Model Context Protocol ships its biggest update yet, and
the part most people are sleeping on is MCP Apps. The 2026-07-28 release moves
MCP under the Linux Foundation, drops session IDs, and ships an Extensions
framework. MCP Apps is the first official extension out of that framework, and
its design choice is the one that changes what a chat can host.
The part most people miss is what the HTML an MCP App returns actually is at
the protocol level. It is a full MCP client running in a sandboxed iframe,
talking to the host over JSON-RPC. Every click, every keystroke, every form
submission is a structured protocol call that flows through the same audit
trail as a model-driven tool call. That single architectural decision is why
this spec is the one that finally settles how agents and UIs talk.
This post is a deep dive on the interactivity surface. Not a tutorial. Not a
"how to build your first app" walkthrough. A spec-level look at what the
protocol actually gives you, and why it matters.
What the protocol actually gives the iframe
MCP Apps is a named extension: io.modelcontextprotocol/ui. The spec is
SEP-1865,
stable as of 2026-01-26, shipping as official in the 2026-07-28 release. It was
co-developed with the MCP-UI maintainers at OpenAI and Anthropic, which is why
the resulting spec feels finished in a way that prior attempts at "MCP + UI"
did not.
The transport is the part that matters. From the
spec:
MCP Apps uses JSON-RPC 2.0 over postMessage for iframe-host communication.
UI capabilities (e.g., tool call, prompt, etc.) will reuse MCP's existing
protocol. In the future, we may choose to enrich the communication protocol
with additional MCP capabilities.
In practice, the rendered iframe initializes a JSON-RPC client over the
browser's postMessage channel. The app advertises its own capabilities,
negotiates with the host, and gets a stable message pipe. Once that handshake
completes, the iframe is a peer MCP client. It can call the same tools the
model calls. It can use the same capabilities. Every action it takes flows
through the same audit trail.
Here is the conceptual shape, in code:
// The iframe is an MCP client. The postMessage transport is its wire.
const transport = new MessageTransport(window.parent);
const client = new Client({ name: "ui-view", version: "1.0.0" });
await client.connect(transport);
That is the whole trick. The iframe is a peer MCP client. The sandboxed
rendering is what it looks like from the outside. The protocol is what it is
from the inside, and all the implications follow from there.
The seven interactivity primitives
What does the spec actually let the UI do? Here is the surface, in plain
terms.
1. The UI can call tools on the server
The most direct interactive capability. A chart, a map, a dashboard can refresh
its own data without the user saying a word to the model.
const result = await app.callServerTool({
name: "get_weather",
arguments: { location: "Paris" },
});
The user clicks a region. The UI calls a tool. Fresh data renders. The model is
not in the loop. The result goes through the same MCP plumbing the model would
have used, including the same consent and logging path. Sort, filter, paginate
all happen on the client. None of it costs a tool call or a round trip to the
model.
2. Apps can have tools the model cannot see
A tool can declare visibility: ["app"] in its _meta.ui field. The host
hides it from the model's tools/list. The agent does not know the tool
exists. Only the rendered UI can call it.
{
"name": "refresh_dashboard",
"description": "Refresh dashboard data",
"inputSchema": { "type": "object" },
"_meta": {
"ui": {
"resourceUri": "ui://weather-server/dashboard-template",
"visibility": ["app"]
}
}
}
This is the cleanest way to wire a frontend to a private backend action. The
model never sees a refresh_dashboard or apply_filter tool in its list. The
UI calls it directly. Cross-server calls for app-only tools are blocked by
spec. Visibility defaults to ["model", "app"] if omitted, so opting into the
hidden-tool pattern is a deliberate choice.
3. Hosts can stream tool inputs to the app before the tool finishes
The _meta.ui.resourceUri field lets the host preload the UI resource ahead
of the tool call. When the model invokes a tool with a long-running argument
set, the host can stream those arguments into the rendered UI in real time.
The user sees a form filling itself in. The UI prepares its layout. The tool
result lands, and the app is already in the right state to display it. The
"loading spinner" pattern is replaced by a "watch the data arrive" pattern.
The spec is explicit that hosts MAY prefetch and cache UI resource content for
performance. Resource templates are static. Tool data is dynamic. The split is
the design.
4. Apps can update the model's context
A user makes a selection in the UI. The app sends a structured message back to
the host. The host can choose to inject that selection into the model's
context.
A chart lets the user mark a date range. The model now knows the date range.
The next reply uses it. A form returns a configuration object. The model
inherits it. The app and the model share state through the host. Hidden DOM events are
out of scope. The conversation stays the source of truth.
This is the bridge that makes the iframe part of the conversation. The chat
stays the source of truth, and the UI inherits it.
5. Capabilities are declared, not assumed
An app that needs the camera, microphone, geolocation, or clipboard-write
declares the request in _meta.ui.permissions. The host sets the iframe's
Permission Policy accordingly, or denies.
"_meta": {
"ui": {
"permissions": {
"camera": {},
"microphone": {},
"geolocation": {},
"clipboardWrite": {}
}
}
}
Apps must feature-detect at runtime, because the host can refuse. The
principle is the same as the rest of MCP: capability before access, never the
other way around.
6. External origins are declared, not guessed
An app that needs to call an external API declares connectDomains in
_meta.ui.csp. The host writes a Content Security Policy header based on the
declaration. The default is default-src 'none'.
"_meta": {
"ui": {
"csp": {
"connectDomains": ["https://api.openweathermap.org"],
"resourceDomains": ["https://cdn.jsdelivr.net"],
"frameDomains": ["https://www.youtube.com"]
}
}
}
-
connectDomainsmaps toconnect-src(fetch, XHR, WebSocket) -
resourceDomainsmaps toscript-src,style-src,img-src,font-src,media-src(wildcard subdomains supported) -
frameDomainsmaps toframe-src(nested iframes) -
baseUriDomainsmaps tobase-uri
The host enforces, the server declares, and the gap between the two is the
audit trail. If a domain is not declared, the request is blocked. There is no
implicit trust.
7. Hosts can give an app a stable origin
Some apps need a predictable origin for OAuth callbacks, CORS allowlists, or
third-party API keys. The _meta.ui.domain field lets the server request a
dedicated origin.
"_meta": {
"ui": {
"domain": "a904794854a047f6.claudemcpcontent.com"
}
}
Claude uses hash-based subdomains. ChatGPT uses URL-derived subdomains like
www-example-com.oaiusercontent.com. The format is host-specific, but the
principle is portable. An app that needs to be a first-class OAuth client can
be one, without the host exposing its own cookies or storage to the iframe.
The security model, end to end
The sandboxing is the part that makes third-party apps deployable in
production. Hosts render MCP Apps in a sandboxed iframe with a deny-by-default
CSP. The app cannot:
- Access the parent window's DOM
- Read the host's cookies or localStorage
- Navigate the parent page
- Execute scripts in the parent context
All communication between the app and the host goes through postMessage. The
host controls which capabilities the app can access. A host might restrict
which tools an app can call, disable the sendOpenLink capability, or refuse
to honor a permission request. The iframe cannot escalate.
The default CSP, when no declarations are made, is:
default-src 'none';
script-src 'self' 'unsafe-inline';
style-src 'self' 'unsafe-inline';
img-src 'self' data:;
media-src 'self' data:;
connect-src 'none';
The app is, by default, an island. Every external connection has to be
declared, every capability has to be requested, every tool call goes through
JSON-RPC. The audit trail is the protocol.
Why this is the spec that settled it
There have been several attempts at "agents plus UI" in the last two years.
Most failed at one of two points: fragmentation or trust.
MCP-UI and OpenAI's Apps SDK both proved the demand. MCP-UI validated the
HTML/external URL/remote DOM content types and the bidirectional communication
model. OpenAI's Apps SDK, launched November 2025, validated that developers
would build rich interactive apps inside ChatGPT using MCP as the backbone.
Both shipped to real users. Neither became the standard on its own.
The 2026-01-26 spec is the merger. MCP Apps is the unified pattern, and the
contributor list reads like a roll call of who actually shipped:
Authors: Ido Salomon, Liad Yosef, Olivier Chafik, Jerome Swannack, Jonathan
Hefner, Anton Pidkuiko, Nick Cooper, Bryan Ashley, Alexi Christakis
Ido Salomon is the creator of MCP-UI. Olivier Chafik is on the MCP core team.
The rest come from the OpenAI Apps SDK side. That is what "cross-vendor
agreement on UI transport" looks like in practice: the people who would
otherwise build competing standards agreed on one spec, one content type
(text/html;profile=mcp-app), one URI scheme (ui://), and one security
model.
The first content type is HTML. Other types are explicitly reserved for future
extensions. The spec is a foundation, not a ceiling.
What this actually changes
Three things shift, in order of how much they matter for builders.
The chat window becomes a real client surface. The pattern used to be: a
chat tool with a "click here to open the dashboard" link. The dashboard is a
separate web app, with its own auth, its own state, its own audit trail. MCP
Apps collapses that into the conversation. The UI is right there. The model is
in the loop or not, depending on the design. The audit trail is one.
Frontend-to-backend wiring becomes standard. A form in an MCP App can call
a tool that the model does not see. The server can do authorization, validation,
and persistence on those calls, the same way it does for model-driven calls.
The pattern that took web teams a decade to converge on (a frontend that calls
a backend with auth, validation, and observability) is now the default in the
chat.
The audit and consent story closes. Every UI action is a JSON-RPC message.
The host sees it. The user can consent to it. The logs record it. The
Permissions are declared upfront. The CSP is enforced at the wire. The audit
trail records every action. For teams that had to write a custom audit layer
for every agent-plus-UI deployment, the spec does that work now.
The bottom line
MCP Apps is the part of the 2026-07-28 release that will quietly matter most
over the next 18 months. The Linux Foundation move earns trust. The stateless
core earns scale. The Extensions framework earns stability. MCP Apps earns
distribution, because every chat client that adopts the spec is a new place
where your server can run.
The pattern is the same one OpenAI's Apps SDK and MCP-UI both used, now
standardized. The iframe is the agent. JSON-RPC is the wiring. The host is
the consent layer. The result is a chat window that can host anything a web
app can host, with the agent's audit trail preserved.
For teams choosing between a custom UI and an MCP App, the real question is
whether you want the model in the loop on every click. The protocol is ready
for either answer.
Sources
- MCP Apps specification (SEP-1865, stable 2026-01-26)
- MCP Apps overview
- The 2026-07-28 release-candidate post
- MCP Apps quickstart
- Client support matrix
Filed in content/mcp-apps-interactive-html.md. The deep-dive research that
grounded this article is in content/mcp-apps-explainer.md.
Top comments (0)