Concepts

What are MCP Apps?

MCP Apps is the official MCP extension that lets a tool answer with an interactive interface instead of a wall of text. This guide explains how it works, end to end, and what you need to build one.

11 min read

Where are we with the beta launch? Show me the board before the stand-up.

Used show_kanban

A live MCP Apps widget, rendered the way a host renders it. Made with Widgetry.

What are MCP Apps, in one paragraph

MCP Apps are interactive user interfaces that an MCP server ships with its tools, and that the AI host (ChatGPT, Claude, VS Code, Microsoft 365 Copilot, Cursor, Goose and others) draws inside the conversation when the model calls those tools. The server registers an HTML document as a resource with a ui:// address, the tool points at it, and the host renders that document in a sandboxed iframe and feeds it the tool's data over a small JSON-RPC protocol. The result is a board, a chart, a map or a form that the user can click, right where the answer would have been. MCP Apps is the first official extension of the Model Context Protocol: it was proposed as SEP-1865 in November 2025 and became stable on 26 January 2026 (MCP blog).

The board above is an MCP App. In a real conversation, a model would call a show_board tool with the columns and cards as arguments, and the host would render this view with that data.

Why MCP tools needed a UI

A plain MCP tool returns text, or structured JSON that the model turns into text. That works for a single fact. It works badly for anything a person wants to look at or act on: forty rows of sales data, the state of a project, a set of flights to compare, a configuration with fields that depend on each other.

In those cases the conversation turns into a slow loop. The model describes the data, the user asks for a change in words, the model calls the tool again and describes the new data. The MCP blog lists the cases where this hurts most: dashboards for exploring data, configuration wizards with dependent fields, document review with highlights, and monitoring views that update live (proposal post).

MCP Apps fixes this without leaving the conversation. The tool still returns data for the model, and the host also shows a view of that data that the user can sort, filter, open and click. When the user does something that needs language (a question, a change of plan), the view hands it back to the chat.

How MCP Apps work

An MCP App has two halves: what the server declares, and what happens inside the iframe at runtime.

What the server declares

The server registers a UI resource: an HTML document with a ui:// URI and the MIME type text/html;profile=mcp-app. Then it links a tool to that resource with _meta.ui.resourceUri. This is what a host sees in tools/list:

json
{
  "name": "show_board",
  "description": "Shows a project board with columns and cards in the conversation.",
  "inputSchema": {
    "type": "object",
    "properties": {
      "title": { "type": "string" },
      "columns": { "type": "array" }
    }
  },
  "_meta": { "ui": { "resourceUri": "ui://widgets/board.html" } }
}

And this is what it gets back when it reads the resource with resources/read:

json
{
  "contents": [
    {
      "uri": "ui://widgets/board.html",
      "mimeType": "text/html;profile=mcp-app",
      "text": "<!doctype html><html>...</html>",
      "_meta": { "ui": { "csp": { "resourceDomains": ["https://cdn.jsdelivr.net"] } } }
    }
  ]
}

Hosts that support the extension say so when they connect, under capabilities.extensions["io.modelcontextprotocol/ui"], listing text/html;profile=mcp-app as a MIME type they render (specification). An older flat key, _meta["ui/resourceUri"], is deprecated; the official SDK still writes both so older hosts keep working.

A tool can also set _meta.ui.visibility. The default is ["model", "app"]. With ["app"] the model does not see the tool, but the view can still call it: useful for a "refresh" or "load more" button that the model should never call on its own.

What happens at runtime

When the model calls the tool, the host loads the HTML into a sandboxed iframe. From then on, the view and the host talk JSON-RPC 2.0 over postMessage:

  1. Handshake. The view sends ui/initialize. The host answers with its host context, and the view confirms with ui/notifications/initialized. The host must not send anything else before that confirmation.
  2. Tool input. The host sends ui/notifications/tool-input with the complete arguments the model wrote. While the model is still streaming them, it can send ui/notifications/tool-input-partial first, so the view can show a skeleton.
  3. Tool result. When the server answers, the host sends ui/notifications/tool-result. The view usually draws from the result's structuredContent. If the call is cancelled, it gets ui/notifications/tool-cancelled instead.
  4. Teardown. When the view goes away, the host sends ui/resource-teardown so it can clean up.

The host context tells the view where it lives: theme (light or dark), styles.variables (the host's CSS variables for colors, fonts and radii), displayMode and availableDisplayModes (inline, fullscreen or picture in picture), containerDimensions, locale, timeZone, platform, safeAreaInsets and more. When something changes, for example the user switches to dark mode, the host sends ui/notifications/host-context-changed. That is how a well-built MCP App looks native in every host: it reads the host's variables instead of hardcoding a palette. The theming guide covers this in detail.

Actions back to the host

The view is not a passive picture. It can ask the host to do things:

  • ui/message: post a message to the chat as if the user wrote it ("Tell me more about the card Launch video"). This is how a click turns into a new turn for the model.
  • ui/open-link: ask the host to open a URL. The host decides whether and how to open it.
  • tools/call: call a tool on the same MCP server, with a standard MCP request. The result comes back to the view, not to the chat.
  • ui/update-model-context: update what the model knows about the view's state, without posting a visible message.
  • ui/request-display-mode: ask to go fullscreen or picture in picture.
  • ui/notifications/size-changed: tell the host the content height changed, so an inline card can grow or shrink.

On the view side, the official SDK (@modelcontextprotocol/ext-apps) wraps all of this in an App class. The core of a view is a few lines. Set the handlers before connect(), or you can miss the first notifications:

ts
import { App } from '@modelcontextprotocol/ext-apps'

const app = new App({ name: 'Board', version: '1.0.0' })
app.ontoolresult = (result) => render(result.structuredContent)
await app.connect()

render is your own function. Calls such as app.callServerTool({ name, arguments }), app.sendMessage and app.openLink map to the actions above.

Is it safe? The MCP Apps security model

Running third-party HTML inside a chat sounds risky, so the extension is built in layers (launch post):

  • Sandboxed iframe. The view runs in an iframe with restricted permissions, isolated from the host page, its cookies and its DOM. Web hosts add a sandbox proxy (an extra frame on a separate origin) between the two. The view cannot read the conversation; it only gets what the host sends.
  • A strict default CSP. If the resource declares nothing, the host applies a policy that allows inline scripts and styles, images from data:, and no network at all (connect-src 'none'). To load a script from a CDN, show images from your storage or call your API, the resource must list those origins in _meta.ui.csp (resourceDomains, connectDomains, frameDomains, baseUriDomains). Hosts must not allow anything undeclared. This is the most common reason a widget shows up blank; the CSP guide explains each field and how to debug it.
  • Declared ahead of time. The UI is a resource the server registers in advance, not HTML generated inside a tool result, so a host can fetch and review it before rendering.
  • Auditable messages. Everything between the view and the host is JSON-RPC, so a host can log it, and a host can ask the user to approve tool calls that the view starts.
  • Explicit permissions. Camera, microphone, geolocation and clipboard writes have to be requested in the resource's _meta.ui.permissions, and hosts may still refuse them.

Hosts also place each app on its own origin. The resource can suggest one with _meta.ui.domain, whose format is host specific: Claude, for example, uses a subdomain of claudemcpcontent.com, and ChatGPT one of oaiusercontent.com.

Where MCP Apps came from

MCP Apps did not start from zero. It merged two lines of work.

MCP-UI. A community project created by Ido Salomon and Liad Yosef that showed interactive UI could travel over MCP as resources. It was adopted by Postman, Shopify, Hugging Face, Goose and ElevenLabs, among others (proposal post).

The OpenAI Apps SDK. OpenAI built apps for ChatGPT on top of MCP, with its own conventions: a text/html+skybridge resource, an openai/outputTemplate key on the tool and a window.openai object inside the iframe.

Both worked, but in different ways, and a server had to choose. So Anthropic, OpenAI and the MCP-UI maintainers wrote SEP-1865 together. It was proposed on 21 November 2025, with text/html+mcp as the MIME type, and declared stable on 26 January 2026 as the first official MCP extension, with the final MIME type text/html;profile=mcp-app. Since then:

  • ChatGPT declared itself "fully compatible with the MCP Apps spec" on 22 February 2026 (OpenAI changelog). OpenAI now recommends the shared fields and bridge for new UI, and window.openai only for what the standard does not cover.
  • MCP-UI describes itself as "standardized into MCP Apps", and its SDK now emits the standard MIME type (mcpui.dev).
  • @modelcontextprotocol/ext-apps 2.0 shipped on 8 September 2026 on top of the split MCP SDK v2 packages, with the same wire protocol as 1.x.

The comparison guide covers what still differs between MCP Apps, MCP-UI and the ChatGPT extensions.

Which hosts render MCP Apps

As of October 2026, the community-maintained client matrix lists Claude (web and desktop), ChatGPT, VS Code with GitHub Copilot, Microsoft 365 Copilot, Cursor, Goose, Postman, MCPJam, Archestra.AI and PostHog Code. Claude also renders them on mobile, with some limits.

Support is not uniform. Each host decides how big an inline card can be, which display modes it offers, which CSP fields it honors (Claude restricts frameDomains, for example) and which CSS variables it sends. The full list, with dates, sources and each host's limits, is in Which clients support MCP Apps.

What happens in hosts that don't support MCP Apps

Nothing breaks. The specification says that if a host does not support MCP Apps, the tool behaves as a standard tool with a text-only fallback. The host ignores _meta.ui, calls the tool, and gives the model the result.

This is the case for Claude Code and other terminal agents: they call the tool and work with its text, but draw nothing. So a good MCP App tool returns two things:

  • structuredContent: the data the view draws.
  • content: a short text summary for the model and for text-only hosts. "Board Beta launch: 2 cards to do, 2 in progress, 1 done" is useful. "Showing widget" is not.

The clients guide has more on writing a good text fallback.

How to build an MCP App

There are three routes, from most code to least.

Build it from scratch. Use the official SDK: registerAppResource and registerAppTool from @modelcontextprotocol/ext-apps/server on the server, and the App class in the view. The official repository has starter templates for React, Vue, Svelte, Preact, Solid and plain JavaScript, plus examples such as maps, 3D scenes, PDFs and heatmaps (ext-apps on GitHub). The view must end up as a single HTML document, so most projects bundle it (the official quickstart uses Vite with a single-file plugin). Follow Build an MCP App in TypeScript for a full walkthrough, including the differences between ext-apps 1.x and 2.x.

Add a UI to a server you already have. If you have an MCP server with tools that return data, you only need the resource, the _meta.ui.resourceUri link and structuredContent in the result. Add an interactive UI to your MCP server shows the shortest path.

Start from a ready-made widget. Widgetry is a builder for MCP Apps widgets that skips the HTML and the protocol. You pick one of 13 templates (board, KPIs, charts, a table, a countdown, a 3D globe and more), change the sample data, the data schema, the Liquid markup and the styles, and choose one of 20 designs or make your own. The editor preview is a real MCP Apps host, built on the official SDK's host bridge, in light and dark. Then you take it to your server: either link it, so your server reads the published widget from Widgetry with a read-only key and gets every change you publish without a redeploy, or download it as an HTML document plus a manifest and register it with a few lines of the official TypeScript SDK. If you don't have a server yet, published widgets are also served from Widgetry's own MCP endpoint. And if you prefer to ask your agent, it can create the widgets through MCP.

Whatever the route, test it in more than one host: the same widget can be fine in one and blank in another because of a CSP or a size limit.

FAQ about MCP Apps

Are MCP Apps the same as ChatGPT apps?

Not exactly. ChatGPT implements the open MCP Apps standard, so an MCP App works in ChatGPT. ChatGPT also offers extra capabilities through window.openai (checkout, file uploads, modals, saved widget state) that other hosts don't have. Since July 2026 OpenAI distributes apps through plugins; ChatGPT plugin UI with MCP Apps covers that change and how to migrate from the older Apps SDK fields.

Do I need React to build an MCP App?

No. The host only receives an HTML document. You can write it by hand, use any framework that bundles to a single file, or use a builder. React is a common choice and the SDK has hooks for it (useApp, useHostStyles), but nothing in the protocol depends on it.

Can an MCP App fetch data from my API?

Yes, if the resource declares the API's origin in connectDomains. The simpler pattern is to let the tool fetch the data on the server and pass it to the view through the tool result: the model sees the same data, nothing extra is opened in the CSP, and the view works the same in every host. When the view needs fresh data later, it can call a server tool with tools/call.

Does Claude Code render MCP Apps?

No. As of October 2026, Claude Code calls the tool and works with its text, without rendering the UI (Claude docs). Claude on the web, on desktop and on mobile does render it. That is why the tool's text output matters.

Your first widget, in the chat in minutes

Pick a template, drop in your data and see it as ChatGPT or Claude will show it. Then link it from your MCP server, or let your agent build the next one.