MCP Apps: How Interactive UI in MCP Works (2026 Guide)
MCP Apps let MCP servers ship interactive UI as ui:// resources rendered in sandboxed iframes in Claude, ChatGPT, VS Code and Goose. Spec, SDK and an example.
MCP Apps is the official Model Context Protocol extension that lets an MCP server return interactive UI (charts, forms, maps, dashboards) instead of plain text. A tool points to a ui:// resource that holds an HTML page. The host (Claude, ChatGPT, VS Code, Goose and others) renders that page in a sandboxed iframe, and the page talks back to the host over JSON-RPC. You write it once and it runs in every host that supports the extension.
This guide covers where the spec stands today, which clients render MCP Apps, a minimal working example from the official SDK, and how MCP Apps relates to the OpenAI Apps SDK and MCP-UI.
Status at a Glance (October 2026)
| Item | Current state |
|---|---|
| Spec | SEP-1865 "MCP Apps: Interactive User Interfaces for MCP", Extensions track |
| Status | Stable, version 2026-01-26. A draft version is in development |
| Announced | January 26, 2026, as the first official MCP extension |
| Repo | modelcontextprotocol/ext-apps |
| SDK | @modelcontextprotocol/ext-apps (subpaths /server, /react, /app-bridge), v2.x |
| MIME type | text/html;profile=mcp-app |
| Link key | _meta.ui.resourceUri on the tool |
The SEP was first published on November 21, 2025. Its authors include the creators of MCP-UI and people from Anthropic and OpenAI.
How MCP Apps Work
The ext-apps README describes four steps:
- Tool definition. Your tool declares a
ui://resource that holds its HTML interface. - Tool call. The model calls the tool on your server as usual.
- Host renders. The host fetches the resource and shows it in a sandboxed iframe.
- Two-way messages. The host passes tool input and results to the UI with notifications, and the UI can call other tools through the host.
So the UI is just another MCP resource, which is why MCP Apps fit the existing protocol. For a refresher on how tools and resources differ, see MCP resources vs tools.
The pieces of the protocol
- UI resource. A resource with a
ui://URI and the MIME typetext/html;profile=mcp-app. Its_meta.uican declarecsp(allowed origins),permissions,domainandprefersBorder. - Tool link. The tool's
_meta.ui.resourceUrinames the resource to render._meta.ui.visibilitycan be["model", "app"](the default), or["app"]for helper tools the UI can call but the model never sees. - Handshake. The View sends
ui/initializeand receiveshostContext(theme, display mode, locale and so on), then sendsui/notifications/initialized. - Data in.
ui/notifications/tool-inputandui/notifications/tool-resultdeliver the call's arguments and results. - Actions out. The View can send
tools/call,ui/message(post a message into the chat),ui/open-linkandui/update-model-context. - Display modes.
inline(default),fullscreenandpip(picture-in-picture). Hosts don't have to support all three.
The security model
Everything runs in a sandboxed iframe, and the CSP denies by default. If a resource declares no ui.csp, the spec makes the host use connect-src 'none', so the UI can't call any external API. To allow external origins you list them in connectDomains (fetch/WebSocket), resourceDomains (scripts, images, fonts, styles), frameDomains (nested iframes) and baseUriDomains. Hosts can make these rules stricter but must never allow a domain you didn't declare. The launch post adds that all UI-to-host messages are auditable JSON-RPC, and that hosts can ask the user to approve tool calls the UI starts. For the wider threat model, read MCP security best practices.
Which Clients Support MCP Apps
Only hosts we could verify against first-party docs:
| Host | Support | Notes |
|---|---|---|
| Claude (web and desktop) | Yes, since launch | Claude docs: Claude asks permission before showing an app (Allow / Always allow). Remote apps that set ui.domain must use Claude's hashed *.claudemcpcontent.com format |
| ChatGPT | Yes | OpenAI's changelog says ChatGPT has been "fully compatible with the MCP Apps spec" since 2026-02-22. See Add UI to your MCP server |
| VS Code (GitHub Copilot agent) | Yes | Announced 2026-01-26, Insiders first, then Stable |
| Goose | Yes | One of the launch hosts in the official announcement |
| Postman, MCPJam, mcp-use Inspector, Alpic Playground | Listed | Named as supporting hosts in the ext-apps README |
Support for individual features still differs between hosts, especially display modes and host-specific extras. Build the tool so it still returns useful text, so that clients without MCP Apps support can finish the job. The full matrix is on modelcontextprotocol.io/clients, and our /clients directory lists MCP clients.
Build a Minimal MCP App
The code below comes straight from the official quickstart example (ext-apps 2.x, Node.js 20+). It's a get-time tool whose UI shows the server time and has a refresh button. If you've never built a server, start with how to build an MCP server.
1. Install the packages
npm install -S @modelcontextprotocol/ext-apps \
@modelcontextprotocol/client@^2.0.0 \
@modelcontextprotocol/server@^2.0.0 \
@modelcontextprotocol/node@^2.0.0 \
@modelcontextprotocol/express@^2.0.0 \
zod@^4.2.0
npm install -D typescript vite vite-plugin-singlefile tsx
Hosts load the UI as a single HTML resource, so the quickstart uses Vite plus vite-plugin-singlefile to bundle the JavaScript inline.
2. Register the tool and the UI resource (server.ts)
import fs from 'node:fs/promises';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import {
registerAppResource,
registerAppTool,
RESOURCE_MIME_TYPE,
} from '@modelcontextprotocol/ext-apps/server';
import { McpServer } from '@modelcontextprotocol/server';
import { z } from 'zod';
const DIST_DIR = path.join(
path.dirname(fileURLToPath(import.meta.url)),
'dist'
);
export function createServer(): McpServer {
const server = new McpServer({
name: 'Quickstart MCP App Server',
version: '1.0.0',
});
// Tool + resource, tied together by the resource URI.
const resourceUri = 'ui://get-time/mcp-app.html';
registerAppTool(
server,
'get-time',
{
title: 'Get Time',
description: 'Returns the current server time.',
inputSchema: z.object({}),
_meta: { ui: { resourceUri } }, // links this tool to its UI
},
async () => {
const time = new Date().toISOString();
return { content: [{ type: 'text', text: time }] };
}
);
registerAppResource(
server,
resourceUri,
resourceUri,
{ mimeType: RESOURCE_MIME_TYPE },
async () => {
const html = await fs.readFile(
path.join(DIST_DIR, 'mcp-app.html'),
'utf-8'
);
return {
contents: [
{ uri: resourceUri, mimeType: RESOURCE_MIME_TYPE, text: html },
],
};
}
);
return server;
}
RESOURCE_MIME_TYPE is the constant for text/html;profile=mcp-app. Use it instead of typing the string. The helpers registerAppTool and registerAppResource also write the host-specific metadata for you, which is why Claude's docs recommend them.
3. Build the View (mcp-app.html + src/mcp-app.ts)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Get Time App</title>
</head>
<body>
<p>
<strong>Server Time:</strong> <code id="server-time">Loading...</code>
</p>
<button id="get-time-btn">Get Server Time</button>
<script type="module" src="/src/mcp-app.ts"></script>
</body>
</html>
import { App } from '@modelcontextprotocol/ext-apps';
const serverTimeEl = document.getElementById('server-time')!;
const getTimeBtn = document.getElementById('get-time-btn')!;
const app = new App({ name: 'Get Time App', version: '1.0.0' });
// Register handlers BEFORE connect() so you don't miss the first result.
app.ontoolresult = (result) => {
const time = result.content?.find((c) => c.type === 'text')?.text;
serverTimeEl.textContent = time ?? '[ERROR]';
};
getTimeBtn.addEventListener('click', async () => {
const result = await app.callServerTool({ name: 'get-time', arguments: {} });
const time = result.content?.find((c) => c.type === 'text')?.text;
serverTimeEl.textContent = time ?? '[ERROR]';
});
app.connect();
Copy main.ts (Streamable HTTP on port 3001 at /mcp, or stdio with --stdio), vite.config.ts and the package.json scripts from the quickstart folder. React users get useApp and useHostStyles from @modelcontextprotocol/ext-apps/react.
4. Test it
- basic-host. Clone
ext-apps, runnpm install && npm startand openhttp://localhost:8080. The reference host connects tohttp://localhost:3001/mcpby default. - Claude Desktop. Add a stdio entry under Settings, Developer, Edit Config. To try a published example first:
{
"mcpServers": {
"map": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-map", "--stdio"]
}
}
}
- ChatGPT. It connects only to remote HTTPS servers. Deploy the server or tunnel it, then add it as a custom MCP server. See our ChatGPT MCP setup guide, and our guide to deploying an MCP server.
- Debugging. MCP Inspector helps you check that the tool's
_metaand the resource come back correctly before you look into rendering.
Using an AI coding agent? The repo ships Agent Skills (create-mcp-app, add-app-to-server, migrate-oai-app, convert-web-app). In Claude Code, install them with /plugin marketplace add modelcontextprotocol/ext-apps and then /plugin install mcp-apps@mcp-apps.
MCP Apps vs OpenAI Apps SDK vs MCP-UI
All three live in the same family tree. The launch post says MCP Apps builds on MCP-UI and the OpenAI Apps SDK.
| MCP-UI | OpenAI Apps SDK (ChatGPT) | MCP Apps | |
|---|---|---|---|
| What it is | Community project that pioneered UI over MCP | OpenAI's way to build apps for ChatGPT, now documented under Plugins | Official, vendor-neutral MCP extension |
| Status today | @mcp-ui/server and @mcp-ui/client now implement the MCP Apps standard | ChatGPT implements MCP Apps. window.openai adds ChatGPT-only extras | Stable spec 2026-01-26 |
| Tool to UI link | Embedded UI resources | _meta["openai/outputTemplate"] (kept as an alias) | _meta.ui.resourceUri |
| Where it runs | MCP-UI hosts, plus MCP Apps hosts | ChatGPT | Claude, ChatGPT, VS Code, Goose and more |
What this means in practice:
- New project: use MCP Apps (
ui.resourceUri,text/html;profile=mcp-app, theAppclass). OpenAI's docs tell developers to use the shared names, and to usewindow.openaionly for things the spec doesn't cover. Those arerequestCheckout, file upload and selection,requestModalandwidgetState. Feature-detect them, don't branch on the host name. - Existing ChatGPT app: the old
window.openai.toolInput,toolOutput,callToolandsendFollowUpMessagestill work as aliases. Themigrate-oai-appskill and the migration guide map them toontoolinput,ontoolresult,callServerToolandui/message. - Building a host: the MCP-UI client SDK (
AppRenderer) is a complete MCP Apps renderer. You can also embed Views with@modelcontextprotocol/ext-apps/app-bridge.
Common Problems
- Text appears but no UI. Check that the tool has
_meta.ui.resourceUri, that the resource is registered with MIMEtext/html;profile=mcp-app, and that the host supports MCP Apps. - Blank iframe or failed requests. It's almost always CSP. Every origin your bundle loads from or calls must be listed in
resourceDomainsorconnectDomains, including localhost in development. Missing origins fail silently. The browser console shows the violations. - First result never shows. Set
ontoolresultandontoolinputbeforeapp.connect(). - Stale UI after a deploy. OpenAI's docs say to treat the resource URI as a cache key. When you make a breaking change, publish a new URI and update the tools that use it.
- Claude rejects
ui.domain. For Claude the value is the first 32 hex characters of the SHA-256 of your server URL plus.claudemcpcontent.com, as described in Claude's docs.
Next Steps
MCP Apps make a tool result something people can click on, but the tool behind it still has to be good. Browse the MCP server directory to see how existing servers design their tools, use best MCP servers for inspiration, and when your app is live, submit it to mcp.so so others can find it.
Frequently asked questions
What are MCP Apps?
- MCP Apps is the official Model Context Protocol extension (SEP-1865) that lets an MCP server ship interactive UI. A tool points to a ui:// HTML resource, and the host renders it in a sandboxed iframe that talks to the host over JSON-RPC.
Is the MCP Apps spec stable?
- Yes. The MCP Apps specification reached stable status with version 2026-01-26, announced on January 26, 2026 as the first official MCP extension. A draft version keeps evolving in the modelcontextprotocol/ext-apps repository.
Which clients support MCP Apps?
- Claude (web and desktop), ChatGPT, VS Code with GitHub Copilot, and Goose support MCP Apps, and tools like Postman and MCPJam can render them too. Feature support such as fullscreen or picture-in-picture still varies by host.
What is the difference between MCP Apps and the OpenAI Apps SDK?
- The OpenAI Apps SDK was ChatGPT's own way to render app UI, and MCP Apps is the open standard built partly on it. ChatGPT now implements MCP Apps, keeps window.openai names as aliases, and adds ChatGPT-only extras like checkout and file uploads.