MCP Apps in 6 steps: ship a server-rendered UI on the 2026-07-28 spec
Summary. MCP Apps went live on 26 January 2026 as the first official Model Context Protocol extension, and it lets a tool return an interactive HTML interface instead of a wall of JSON. Four hosts shipped support at announcement: Claude on web and desktop, Goose, Visual Studio Code Insiders and ChatGPT. The extension is identified as io.modelcontextprotocol/ui, serves HTML over a ui:// URI with the mime type text/html;profile=mcp-app, and runs it in a sandboxed iframe that talks back over JSON-RPC. Underneath, the protocol itself is moving: the 2026-07-28 release candidate was locked on 21 May 2026 and the final specification publishes on 28 July 2026, removing the initialize handshake and the Mcp-Session-Id session that every 2025-11-25 server relies on today. Three core features, Roots, Sampling and Logging, are deprecated in that release with at least 12 months before any removal. For Indian engineering teams the timing matters commercially: Gartner forecast on 1 June 2026 that end-user public cloud spending in India would grow 28.1% to $17.5 billion in 2026, up from $13.7 billion in 2025, and agent platforms are a growing share of that bill.
This is a build guide, not a summary of the announcement. It covers the six things you actually have to get right: the resource, the tool link, the View code, the content security policy, sizing, and the lifecycle. It also flags what breaks when the stateless core lands.
What actually shipped on 26 January 2026
MCP Apps was proposed on 21 November 2025 and became the first official extension roughly nine weeks later. It merges two prior efforts. MCP-UI, created by Ido Salomon and Liad Yosef, developed the bidirectional communication model and the HTML, external URL and remote DOM content types, with adopters including Postman, HuggingFace, Shopify, Goose and ElevenLabs. OpenAI's Apps SDK, launched in November 2025, proved the same demand inside ChatGPT. The SEP-1865 specification states plainly why the merge happened: without standardisation, "servers cannot reliably expect UI support via MCP", each host implements slightly different behaviours, and developers maintain separate adapters for MCP-UI and the Apps SDK.
David Soria Parra, Co-Creator of MCP and Member of Technical Staff at Anthropic, put the ambition in one line in the launch post: "I am excited about the possibilities that MCP Apps opens up. Having seen a glimpse of what is possible, I cannot wait to see what the community will build."
The engineering framing from Microsoft is more useful for planning. Harald Kirschner, Principal Product Manager for VS Code, described the gap the extension closes: "With MCP Apps, that contract finally includes the missing human step: when the workflow needs a decision, a selection, or exploration, the client can give you the right interaction without turning the conversation into a choose-your-own-adventure prompt."
| Host | Support at 26 Jan 2026 announcement | Practical note for server authors |
|---|---|---|
| Claude (web and desktop) | Available | Report implementation bugs at github.com/anthropics/claude-ai-mcp |
| Goose | Available | Reference implementation for MCP; has its own MCP Apps tutorial |
| Visual Studio Code | Insiders channel | Stable channel followed the Insiders rollout |
| ChatGPT | Starting that week | Feedback routed through the OpenAI Community Apps SDK category |
| JetBrains IDEs | Exploring | Denis Shiryaev, Head of AI DevTools Ecosystem, stated JetBrains is "excited to explore bringing this extension to our IDEs" |
| Kiro (AWS) | Exploring | Clare Liguori, Senior Principal Engineer at AWS, cited rendering dynamic UIs "with security built in from day one" |
The honest read on that table: two hosts were production-ready on day one, two were rolling out, and two were public statements of intent. If your server needs a UI on every client, you still need the text fallback path, which the extension gives you for free because hosts that do not support MCP Apps see the ordinary text tool result.
The architecture in one pass
Four pieces do all the work.
-
UI resources. Predeclared resources on the
ui://scheme holding bundled HTML and JavaScript. -
Tool-UI linkage. A tool points at its resource through
_meta.ui.resourceUri. -
Bidirectional communication. The iframe talks to the host with the same JSON-RPC base protocol MCP uses everywhere else, carried over
postMessage. - Security model. Mandatory iframe sandboxing plus an auditable message path.
The extension is optional and must be negotiated explicitly through the extension capabilities mechanism. That is the part teams skip. If you do not check negotiation, your UI silently never renders on a host that has not enabled the extension, and you will spend an afternoon reading postMessage logs to discover it.
Step 1: declare the ui:// resource
The resource is an ordinary MCP resource with three hard constraints from the specification: the URI must start with ui://, the mime type must be exactly text/html;profile=mcp-app, and the content must be a valid HTML5 document supplied either as text or as base64 in blob.
{
"uri": "ui://weather-server/dashboard-template",
"name": "weather_dashboard",
"description": "Interactive weather dashboard view",
"mimeType": "text/html;profile=mcp-app"
}
The content itself comes back through a normal resources/read:
{
"contents": [{
"uri": "ui://weather-server/dashboard-template",
"mimeType": "text/html;profile=mcp-app",
"text": "<!DOCTYPE html><html>...</html>",
"_meta": {
"ui": {
"csp": {
"connectDomains": ["https://api.openweathermap.org"],
"resourceDomains": ["https://cdn.jsdelivr.net"]
},
"prefersBorder": true
}
}
}]
}
Predeclaring the template is a security feature, not a packaging detail. Because the HTML exists as a resource before any tool runs, hosts can prefetch it, cache it, and security-review it before anything renders. A host that wants to run a static scan over every UI template in a corporate MCP registry can do so without executing a single tool call.
Step 2: link the tool to the resource
The tool declares its UI in _meta:
{
"name": "visualize_data",
"description": "Visualize data as an interactive chart",
"inputSchema": { "type": "object" },
"_meta": {
"ui": {
"resourceUri": "ui://charts/interactive"
}
}
}
One migration trap sits here. The flat form, _meta["ui/resourceUri"], is deprecated in the 2026-01-26 extension specification and the spec states it will be removed before GA. Code generated from mid-2025 MCP-UI examples still emits the flat key. Grep for it now. The nested _meta.ui.resourceUri is the form to ship.
Step 3: build the View against the ext-apps SDK
The View is a plain HTML document. The @modelcontextprotocol/ext-apps package on npm gives it an App class for host communication, so you are not hand-rolling postMessage handlers.
import { App } from "@modelcontextprotocol/ext-apps";
const app = new App();
await app.connect();
// Receive tool results from the host
app.ontoolresult = (result) => {
renderChart(result.data);
};
// Call server tools from the UI
const response = await app.callServerTool({
name: "fetch_details",
arguments: { id: "123" },
});
// Update model context
await app.updateModelContext({
content: [{ type: "text", text: "User selected option B" }],
});
Three capabilities in that snippet are worth calling out because they are what separate an MCP App from an ordinary embedded iframe. callServerTool lets the interface invoke a tool without a new user prompt. updateModelContext writes what the user did back into the model's context, so the assistant knows a selection happened. And the host can be asked to open links in the user's real browser rather than inside the frame. All of it travels over standard postMessage, so no framework lock-in is introduced.
If you want a starting point rather than a blank file, the ext-apps repository ships working example servers: threejs-server for 3D visualisation, map-server for interactive maps, pdf-server for document viewing, system-monitor-server for real-time dashboards and sheet-music-server for music notation. Pick the one whose shape matches yours and delete what you do not need.
Step 4: get the content security policy right
This is where most reviews fail. If you omit ui.csp entirely, the host must apply this default:
default-src 'none';
script-src 'self' 'unsafe-inline';
style-src 'self' 'unsafe-inline';
img-src 'self' data:;
media-src 'self' data:;
connect-src 'none';
Read connect-src 'none' carefully. A View that ships with no declared CSP cannot make a single network request. Every dashboard that fetches live data from an API will render as an empty shell until the domain is declared. That is the single most common first-run failure, and it looks like a bug in the host rather than a policy default.
| CSP field | CSP directive it feeds | What you put in it |
|---|---|---|
connectDomains |
connect-src | Origins for fetch, XHR and WebSocket calls |
resourceDomains |
script-src, img-src, style-src, font-src | Origins for scripts, images, styles and fonts |
frameDomains |
frame-src | Origins for nested iframes inside your View |
baseUriDomains |
base-uri | Allowed base URIs for the document |
permissions.camera |
iframe allow attribute | Requests camera access |
permissions.microphone |
iframe allow attribute | Requests microphone access |
permissions.geolocation |
iframe allow attribute | Requests geolocation access |
permissions.clipboardWrite |
iframe allow attribute | Requests clipboard write access |
Two host rules constrain what you can negotiate. The host must construct the CSP from your declared domains, and it may restrict further but must never allow an undeclared domain. The host should also log CSP configurations for security review. In practice that means the widest policy you will ever get is the one you declared, and an enterprise host is free to narrow it. Design for the narrow case: bundle your JavaScript into the template rather than pulling it from a CDN, and you remove an entire class of "works on my host" defects.
The four permissions above are requests, not grants. Ask for camera when you need camera. A View that requests microphone, geolocation and clipboard write "just in case" is the kind of thing a security team blocks outright, and the launch post is explicit that hosts can block a UI before it ever renders.
Step 5: handle sizing and display modes
The host tells the View how much room it has through containerDimensions in the host context. Each of height and width behaves independently.
| Mode | Field present | Who controls the size |
|---|---|---|
| Fixed |
height or width
|
Host controls it; the View fills the available space |
| Flexible |
maxHeight or maxWidth
|
View controls it, up to the stated maximum |
| Unbounded | Field omitted | View controls it with no limit |
| Mixed | e.g. width plus maxHeight
|
Host fixes one axis, the View grows on the other |
| Unknown |
containerDimensions absent |
Treat as unbounded and send size changes anyway |
The View reads the configuration and sets CSS accordingly:
const containerDimensions = hostContext.containerDimensions;
if (containerDimensions) {
if ("height" in containerDimensions) {
document.documentElement.style.height = "100vh";
} else if ("maxHeight" in containerDimensions && containerDimensions.maxHeight) {
document.documentElement.style.maxHeight = `${containerDimensions.maxHeight}px`;
}
if ("width" in containerDimensions) {
document.documentElement.style.width = "100vw";
} else if ("maxWidth" in containerDimensions && containerDimensions.maxWidth) {
document.documentElement.style.maxWidth = `${containerDimensions.maxWidth}px`;
}
}
When dimensions are flexible, hosts must listen for ui/notifications/size-changed and resize the iframe to match. Views built on the SDK send those notifications automatically through a ResizeObserver when autoResize is enabled, which is the default, and the notifications are debounced so they only fire when the dimensions actually change. Leave autoResize on unless you have a specific reason to drive sizing yourself.
Display modes work the same way: the View declares which modes it supports in appCapabilities.availableDisplayModes on the ui/initialize request, so the host only offers options the View can render. A View can request a change with ui/request-display-mode, and the host must return the resulting mode in the response whether or not it honoured the request. Do not assume your request succeeded.
Step 6: implement the lifecycle in the right order
The message flow is strict, and getting it wrong produces a View that renders but never receives data.
| Message | Direction | When it fires |
|---|---|---|
ui/notifications/sandbox-proxy-ready |
Sandbox to host | Sandbox is ready to accept the HTML resource |
ui/notifications/sandbox-resource-ready |
Host to sandbox | Host sends the raw HTML to load |
ui/initialize |
View to host | MCP-style handshake, carries appCapabilities
|
ui/notifications/initialized |
View to host | Handshake complete; host must not send anything before this |
ui/notifications/tool-input-partial |
Host to View | Optional, zero or more times while arguments stream |
ui/notifications/tool-input |
Host to View | Complete tool arguments, sent at most once |
ui/notifications/tool-result |
Host to View | Tool execution result |
ui/notifications/tool-cancelled |
Host to View | Tool execution was cancelled |
ui/resource-teardown |
Host to View | Warning before the View is torn down |
The ordering rule that catches people: the host must not send any request or notification to the View before it receives the initialized notification, and ui/notifications/tool-input is required before ui/notifications/tool-result. If your View subscribes to results but never signals initialisation, you will sit on an empty dashboard with no error anywhere.
Message routing has one more rule worth internalising. The sandbox forwards messages in both directions for any method that does not start with ui/notifications/sandbox-, and the host may forward View messages onward to the server for any method that does not start with ui/. The host may also block messages or subject them to further user approval. Assume your UI-initiated tool call can be refused, and handle the refusal in the interface rather than hanging.
The remaining UI-specific requests are small and useful: ui/open-link asks the host to open an external URL, ui/message sends content into the host's chat interface, and ui/update-model-context quietly writes state the model should know about. The specification notes that hosts may defer sending that context to the model until the next user message and may dedupe identical calls, so treat ui/update-model-context as eventually consistent, not as a synchronous write.
What the 2026-07-28 specification changes underneath you
MCP Apps sits on top of the base protocol, and the base protocol changes on 28 July 2026. The release candidate was locked on 21 May 2026, giving SDK maintainers a ten-week validation window in which Tier 1 SDKs were expected to ship support. The maintainers describe it as the largest revision since launch, and it contains breaking changes.
Change in 2026-07-28
|
What it replaces from 2025-11-25
|
What you have to do |
|---|---|---|
initialize / initialized handshake removed (SEP-2575) |
One-time capability exchange | Send protocol version, client info and capabilities in _meta on every request; use server/discover when you need capabilities up front |
Mcp-Session-Id removed (SEP-2567) |
Protocol-level session | Drop sticky routing and shared session stores; mint explicit handles such as a basket_id from a tool and pass them as ordinary arguments |
Mcp-Method and Mcp-Name headers required (SEP-2243) |
Body inspection at the gateway | Set both headers; servers reject requests where the headers and body disagree |
ttlMs and cacheScope on list and read results (SEP-2549) |
Long-lived SSE stream to learn a list changed | Set a sane TTL and decide whether a tools/list response is safe to share across users |
| Roots, Sampling and Logging deprecated (SEP-2577) | Core features | Move to tool parameters or resource URIs, direct LLM provider APIs, and stderr or OpenTelemetry; the methods still work for at least 12 months |
Missing-resource error becomes -32602 (SEP-2164) |
MCP-custom -32002
|
Update any client that matches on the literal -32002
|
| Tools use full JSON Schema 2020-12 (SEP-2106) | Restricted schema subset | You may now use oneOf, anyOf, allOf, conditionals and $ref; do not auto-dereference external $ref URIs |
For an MCP App specifically, the stateless rework is mostly good news. Your View already receives everything it needs through explicit notifications rather than a held-open connection, and multi round-trip requests (SEP-2322) replace the pattern of holding a Server-Sent Events stream open for an elicitation. The server returns an InputRequiredResult with an opaque requestState, the client collects the answers, and the original call is re-issued with inputResponses and the echoed state. Any server instance can pick that retry up, which is exactly what you want when your MCP server runs behind a round-robin load balancer with three replicas.
The item to schedule work for is Tasks. It shipped as an experimental core feature in 2025-11-25, and in this release it graduates to an extension with a reshaped lifecycle: the server answers tools/call with a task handle, the client drives it with tasks/get, tasks/update and tasks/cancel, and tasks/list is removed because it cannot be scoped safely without sessions. Anyone who shipped against the experimental Tasks API has to migrate. If your MCP App renders progress for a long-running job, that is your code.
Governance changed too, and it is the reason to build now rather than wait. Every feature gets an Active, Deprecated and Removed lifecycle with at least twelve months between deprecation and the earliest possible removal, and a Standards Track proposal cannot reach Final status until a matching scenario lands in the conformance suite. The maintainers' stated expectation is that implementers targeting 2026-07-28 will adopt future revisions without rewriting their transport or lifecycle code. The real cost of this migration is the transport rewrite, not the UI.
The security review your enterprise host will run
Running server-supplied HTML inside a client is, as the launch post says, running code you did not write within your MCP host. The extension answers that with four layers: sandboxed iframes with restricted permissions, pre-declared templates a host can review before rendering, auditable JSON-RPC for every UI-to-host message, and optional explicit user consent for UI-initiated tool calls.
Treat these as the checklist a reviewer will apply to your server:
- Does every UI resource declare the narrowest
ui.cspthat still works, with bundled assets instead of CDN pulls where possible? - Does the View request only the permissions it uses?
- Is every UI-initiated
callServerToolsafe to refuse, and does the interface degrade cleanly when it is? - Does the server behave correctly when the host renders the text fallback instead of the UI?
- Are the templates stable enough to be cached and reviewed, or do they change on every deploy?
The last one is underrated. A template that is regenerated per request defeats prefetching and review, and it turns a one-time security sign-off into a recurring one. If you are also hardening the server side of this, our notes on MCP server hardening, CVEs and configs cover the transport and credential issues that sit outside this extension, and the 2026-07-28 stateless spec migration guide covers the base-protocol work in detail. Teams isolating untrusted execution more aggressively will find the sandboxing patterns for AI coding agents useful as a comparison point.
India-specific considerations
Two things shape this work for teams building from India.
The first is spend. Gartner forecast on 1 June 2026 that end-user public cloud spending in India would reach $17.5 billion in 2026, a 28.1% rise from $13.7 billion in 2025, with PaaS the largest single category at a forecast $6.4 billion and IaaS the fastest-growing at 40%. Agent infrastructure rides on that PaaS and IaaS line. A stateless MCP server that runs behind a plain round-robin load balancer is materially cheaper to operate than one that needs sticky sessions and a shared session store, and after 28 July 2026 that is the default rather than an optimisation.
The second is data protection. An MCP App renders inside the host's client, but the data it displays comes from your server, and the ui/update-model-context call pushes user actions back into a model's context. Under India's Digital Personal Data Protection Act 2023, that is still processing personal data if the payload contains it. Two design rules follow. Keep personal data out of updateModelContext payloads unless the model genuinely needs it, because that content is intended for the model and may be deferred or deduped rather than deleted on request. And declare connectDomains narrowly so a View cannot quietly send user data to an origin outside your processing boundary. Teams working through the wider obligations will find our DPDP Act engineering playbook for Indian startups a better starting point than a generic compliance checklist.
Where an MCP App is the wrong answer
Three cases, based on what the extension is designed to do.
Use it when the interaction is genuinely interactive: filtering a dataset, stepping through a configuration wizard with dependent fields, reviewing a document with clickable approvals, or watching live metrics change without re-running the tool. Those are the four scenarios the maintainers themselves list, and they share a property: every equivalent text interaction costs another prompt and another round trip.
Do not use it to render a result the model should be reasoning over. If the assistant needs to summarise, compare or act on the data, structured content in the tool result is what it needs, and a chart the model cannot read adds nothing. Do not use it as a way to smuggle a full web application into a chat client either. The sandbox, the CSP defaults and the pre-declared template model are all built to stop exactly that, and a host is entitled to block a UI before it renders.
And do not build it as your only path. Hosts without MCP Apps support fall back to the ordinary text tool result, which is the graceful degradation the design gives you. Ship the text path first, make it good, then add the UI for the hosts that render it.
FAQ
What is MCP Apps?
MCP Apps is the first official Model Context Protocol extension, announced live on 26 January 2026. It lets a server return an interactive HTML interface from a tool call. The host renders that HTML in a sandboxed iframe, and the interface talks back to the host using MCP's JSON-RPC base protocol over postMessage.
Which clients support MCP Apps today?
At the 26 January 2026 announcement, Claude supported it on web and desktop, Goose supported it, Visual Studio Code supported it in the Insiders channel, and ChatGPT support started that week. JetBrains and AWS both said publicly they were exploring support. Hosts without support fall back to the plain text tool result.
What mime type does a UI resource use?
The specification requires the exact mime type text/html;profile=mcp-app. The resource URI must start with the ui:// scheme, and the content must be a valid HTML5 document supplied either as a text string or as base64 in blob. Other mime types are reserved for future extensions to the standard.
Why is my MCP App unable to fetch data?
Almost always the content security policy. If you omit ui.csp, the host must apply a restrictive default that includes connect-src 'none', which blocks every fetch, XHR and WebSocket call the View makes. Declare each API origin in connectDomains, and declare script, image and font origins in resourceDomains.
How does a tool point at its UI resource?
Through the _meta.ui.resourceUri field on the tool definition, holding the ui:// URI of the resource. The older flat form, _meta["ui/resourceUri"], is deprecated in the 2026-01-26 extension specification and will be removed before general availability, so migrate any generated code that still emits it.
What breaks in the 2026-07-28 specification?
The initialize handshake and the Mcp-Session-Id header are removed, the Mcp-Method and Mcp-Name headers become required, the missing-resource error changes from -32002 to -32602, and Tasks moves from an experimental core feature to an extension with tasks/list removed. Roots, Sampling and Logging are deprecated.
How long before deprecated MCP features are removed?
The feature lifecycle policy introduced in the 2026-07-28 release gives every feature Active, Deprecated and Removed states with at least twelve months between deprecation and the earliest possible removal. Roots, Sampling and Logging keep working in this release and in every specification version published within a year of it.
Can a host refuse a tool call started from the UI?
Yes. Hosts can require explicit user approval for UI-initiated tool calls, may block messages outright, and can decline to render a UI at all after reviewing the pre-declared template. Design the interface so a refused or blocked call degrades visibly rather than leaving the user staring at a spinner.
How eCorpIT can help
eCorpIT is a Gurugram-based technology consultancy founded in 2021, assessed at CMMI Level 5 and MSME certified, with senior-led engineering teams building agent infrastructure for Indian and global clients. We design and ship MCP servers, including the transport migration to the stateless 2026-07-28 core and MCP Apps interfaces that pass an enterprise host's security review rather than failing it on content security policy. Our work covers the parts teams usually underestimate: capability negotiation, the text fallback path, CSP scoping, and data-handling design aligned with DPDP Act requirements. If you are planning MCP server development and integration or broader enterprise AI agent development, talk to us through /contact-us/.
References
- MCP Apps: Bringing UI capabilities to MCP clients, Model Context Protocol Blog, 26 January 2026
- The 2026-07-28 MCP Specification Release Candidate, Model Context Protocol Blog, 21 May 2026
- MCP Apps extension specification, 2026-01-26, modelcontextprotocol/ext-apps on GitHub
- SEP-1865: MCP Apps, interactive user interfaces for MCP
- MCP Apps proposal, Model Context Protocol Blog, 21 November 2025
- ext-apps repository with example servers, GitHub
- @modelcontextprotocol/ext-apps package on npm
- MCP Apps documentation guide, modelcontextprotocol.io
- Getting started with MCP Apps, quickstart
- MCP-UI project site
- OpenAI Apps SDK developer documentation
- Building MCP Apps with Goose, Block
- Gartner forecasts end-user public cloud spending in India to surpass $17 billion in 2026, 1 June 2026
- India's public cloud spending to grow 28% to $17.5 billion in 2026, Business Standard
Last updated: 22 July 2026.
Top comments (0)