MCP Apps
From MCP-UI to an official extension: how interactive tool UIs work.
TL;DR: What is an MCP App?
An MCP App is an interactive HTML interface that an MCP server attaches to a tool. When the tool is called, the AI host renders it in a sandboxed iframe inside the conversation.
A Brief History of Putting Buttons in Chat
MCP-UI pioneered interactive interfaces for LLM tools. Building on it and OpenAI's Apps SDK, MCP maintainers and the UI Community Working Group developed MCP Apps, which became the first official MCP extension on January 26, 2026. MCP-UI continues to provide libraries implementing the standard. Release Post
Iframes, postMessage, and JSON-RPC
If tools, resources, and JSON-RPC are new to you, start with my MCP compendium.
A tool points to a UI resource
The server links a tool to its interface through _meta.ui.resourceUri. A simplified tool definition looks like this:
1{
2 "name": "show_ticket",
3 "description": "Open a ticket for viewing and editing",
4 "inputSchema": {
5 "type": "object",
6 "properties": { "id": { "type": "string" } },
7 "required": ["id"]
8 },
9 "_meta": {
10 "ui": { "resourceUri": "ui://tickets/editor" }
11 }
12}
13The ui:// URI identifies a resource on the MCP server. The host reads it to obtain an HTML document with the MIME type text/html;profile=mcp-app. The document contains the UI code; tool arguments and results supply the data it renders. Resource specification
Who can call a tool
_meta.ui.visibility controls who may call a tool and defaults to ["model", "app"]. Setting it to ["app"] hides the tool from the model, so only the app can call it. This suits UI-only actions like a refresh button or form submission. The host rejects tools/call requests from the app for tools without "app" in their visibility.
1"_meta": {
2 "ui": { "resourceUri": "ui://tickets/editor", "visibility": ["app"] }
3}
4Sandbox and message flow
In a web host, the app runs inside a sandboxed iframe. Its JavaScript executes in a separate browsing context, with access constrained by the host's sandbox and security policy. The app can use React, another framework, or plain JavaScript.
Both communication links speak JSON-RPC 2.0; only the transport differs. The host talks to the MCP server over a regular MCP transport (stdio or Streamable HTTP), while the embedded app talks to the host over postMessage. The app has no MCP connection of its own, so the host mediates every MCP request from the iframe.
MCP App (sandboxed iframe)
↕ JSON-RPC over postMessage
Host ↔ Conversation / model
↕ JSON-RPC over stdio / Streamable HTTP (MCP)
MCP server
A JSON-RPC request has an id and receives a response with that same ID. A notification has no ID and expects no response. For example, an app can request more space:
1{
2 "jsonrpc": "2.0",
3 "id": 7,
4 "method": "ui/request-display-mode",
5 "params": { "mode": "fullscreen" }
6}
7The host returns the mode it actually granted. During initialization, ui/initialize exchanges capabilities and host context; the app then sends ui/notifications/initialized. Apps should check what their host supports. The protocol provides the communication contract, while the app still owns its rendering and state updates. Protocol and lifecycle
Capabilities Overview
| Element | Communication | Description | Key Purpose |
|---|---|---|---|
| ui/initialize | App → Host | request that exchanges protocol version, capabilities, and host context | Learn what the host supports before using it |
| ui/message | App → Host | request that sends a prompt to the host, added to the chat as a user message | Prompt the host model from the app UI, triggering a reply |
| ui/update-model-context | App → Host | request that shares the latest app state for later turns, replacing the previous snapshot | Give the host model context without triggering a reply |
| tools/call | App → Host → Server | request the host forwards to the server to run a permitted tool | Use an MCP tool as a backend endpoint to update the app UI |
| resources/read | App → Host → Server | request to read a resource by URI, including the HTML the host uses to load the app | Use an MCP resource as a backend source for files and documents in the app UI |
| ui/open-link | App → Host | request to open an external URL | Redirect the user to a website in a new browser tab |
| ui/download-file | App → Host | request to download a file, if the host advertises downloadFile | Export files from the app, since the sandbox blocks direct downloads |
| ui/request-display-mode | App → Host | request for inline, fullscreen, or pip display | Expand the app to fullscreen or pop it out as picture-in-picture |
| ui/notifications/size-changed | App → Host | notification of a new width or height | Grow or shrink the iframe to fit the app's content |
| ui/notifications/tool-input | Host → App | notification carrying the complete tool arguments | Pre-fill the app UI with the arguments the model chose |
| ui/notifications/tool-result | Host → App | notification carrying the tool output, including structured data and errors | Render the tool's output in the app UI |
| ui/notifications/host-context-changed | Host → App | notification of theme, display mode, or dimension changes | Match the host's theme (e.g. dark mode) and layout |
| notifications/message | App → Host | log with a level such as info, warning, or error | Debug the app without changing the conversation |
| ui/notifications/tool-input-partial | Host → App | notification of provisional arguments while the model is still generating | Stream a live preview while the model is still writing the arguments |
| ui/notifications/tool-cancelled | Host → App | notification that tool execution was cancelled, optionally with a reason | Stop loading and mark the view incomplete |
| ui/resource-teardown | Host → App | request to release subscriptions and timers before the app is removed | Clean up before the host unloads the app |
App = the UI in the iframe, Host = the AI application embedding it (e.g. Langdock, Claude or ChatGPT), Server = the MCP server providing the tools and UI.
MCP Debug App
My MCP server at https://mcp.matsjfunke.com/mcp ships a debug app that exercises every method above except notifications/message and logs each message, showing what a host actually supports. Connect it and ask the assistant to call open_debug_app.
1{
2 "mcpServers": {
3 "matsjfunke": { "url": "https://mcp.matsjfunke.com/mcp" }
4 }
5}
6