MCP.so
Sign In
Back to blog

MCP Apps: How Interactive UI in MCP Works (2026 Guide)

Oct 15, 2026MCP.so TeamMCP.so Team

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)

ItemCurrent state
SpecSEP-1865 "MCP Apps: Interactive User Interfaces for MCP", Extensions track
StatusStable, version 2026-01-26. A draft version is in development
AnnouncedJanuary 26, 2026, as the first official MCP extension
Repomodelcontextprotocol/ext-apps
SDK@modelcontextprotocol/ext-apps (subpaths /server, /react, /app-bridge), v2.x
MIME typetext/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:

  1. Tool definition. Your tool declares a ui:// resource that holds its HTML interface.
  2. Tool call. The model calls the tool on your server as usual.
  3. Host renders. The host fetches the resource and shows it in a sandboxed iframe.
  4. 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 type text/html;profile=mcp-app. Its _meta.ui can declare csp (allowed origins), permissions, domain and prefersBorder.
  • Tool link. The tool's _meta.ui.resourceUri names the resource to render. _meta.ui.visibility can be ["model", "app"] (the default), or ["app"] for helper tools the UI can call but the model never sees.
  • Handshake. The View sends ui/initialize and receives hostContext (theme, display mode, locale and so on), then sends ui/notifications/initialized.
  • Data in. ui/notifications/tool-input and ui/notifications/tool-result deliver the call's arguments and results.
  • Actions out. The View can send tools/call, ui/message (post a message into the chat), ui/open-link and ui/update-model-context.
  • Display modes. inline (default), fullscreen and pip (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:

HostSupportNotes
Claude (web and desktop)Yes, since launchClaude 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
ChatGPTYesOpenAI'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)YesAnnounced 2026-01-26, Insiders first, then Stable
GooseYesOne of the launch hosts in the official announcement
Postman, MCPJam, mcp-use Inspector, Alpic PlaygroundListedNamed 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, run npm install && npm start and open http://localhost:8080. The reference host connects to http://localhost:3001/mcp by 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 _meta and 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-UIOpenAI Apps SDK (ChatGPT)MCP Apps
What it isCommunity project that pioneered UI over MCPOpenAI's way to build apps for ChatGPT, now documented under PluginsOfficial, vendor-neutral MCP extension
Status today@mcp-ui/server and @mcp-ui/client now implement the MCP Apps standardChatGPT implements MCP Apps. window.openai adds ChatGPT-only extrasStable spec 2026-01-26
Tool to UI linkEmbedded UI resources_meta["openai/outputTemplate"] (kept as an alias)_meta.ui.resourceUri
Where it runsMCP-UI hosts, plus MCP Apps hostsChatGPTClaude, ChatGPT, VS Code, Goose and more

What this means in practice:

  • New project: use MCP Apps (ui.resourceUri, text/html;profile=mcp-app, the App class). OpenAI's docs tell developers to use the shared names, and to use window.openai only for things the spec doesn't cover. Those are requestCheckout, file upload and selection, requestModal and widgetState. Feature-detect them, don't branch on the host name.
  • Existing ChatGPT app: the old window.openai.toolInput, toolOutput, callTool and sendFollowUpMessage still work as aliases. The migrate-oai-app skill and the migration guide map them to ontoolinput, ontoolresult, callServerTool and ui/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 MIME text/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 resourceDomains or connectDomains, including localhost in development. Missing origins fail silently. The browser console shows the violations.
  • First result never shows. Set ontoolresult and ontoolinput before app.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.