Back to Compendiums

MCP Apps

From MCP-UI to an official extension: how interactive tool UIs work.

by matsjfunke

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:

json
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}
13

The 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.

json
1"_meta": {
2  "ui": { "resourceUri": "ui://tickets/editor", "visibility": ["app"] }
3}
4

Sandbox 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.

text
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:

json
1{
2  "jsonrpc": "2.0",
3  "id": 7,
4  "method": "ui/request-display-mode",
5  "params": { "mode": "fullscreen" }
6}
7

The 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

ElementCommunicationDescriptionKey Purpose
ui/initializeApp → Hostrequest that exchanges protocol version, capabilities, and host contextLearn what the host supports before using it
ui/messageApp → Hostrequest that sends a prompt to the host, added to the chat as a user messagePrompt the host model from the app UI, triggering a reply
ui/update-model-contextApp → Hostrequest that shares the latest app state for later turns, replacing the previous snapshotGive the host model context without triggering a reply
tools/callApp → Host → Serverrequest the host forwards to the server to run a permitted toolUse an MCP tool as a backend endpoint to update the app UI
resources/readApp → Host → Serverrequest to read a resource by URI, including the HTML the host uses to load the appUse an MCP resource as a backend source for files and documents in the app UI
ui/open-linkApp → Hostrequest to open an external URLRedirect the user to a website in a new browser tab
ui/download-fileApp → Hostrequest to download a file, if the host advertises downloadFileExport files from the app, since the sandbox blocks direct downloads
ui/request-display-modeApp → Hostrequest for inline, fullscreen, or pip displayExpand the app to fullscreen or pop it out as picture-in-picture
ui/notifications/size-changedApp → Hostnotification of a new width or heightGrow or shrink the iframe to fit the app's content
ui/notifications/tool-inputHost → Appnotification carrying the complete tool argumentsPre-fill the app UI with the arguments the model chose
ui/notifications/tool-resultHost → Appnotification carrying the tool output, including structured data and errorsRender the tool's output in the app UI
ui/notifications/host-context-changedHost → Appnotification of theme, display mode, or dimension changesMatch the host's theme (e.g. dark mode) and layout
notifications/messageApp → Hostlog with a level such as info, warning, or errorDebug the app without changing the conversation
ui/notifications/tool-input-partialHost → Appnotification of provisional arguments while the model is still generatingStream a live preview while the model is still writing the arguments
ui/notifications/tool-cancelledHost → Appnotification that tool execution was cancelled, optionally with a reasonStop loading and mark the view incomplete
ui/resource-teardownHost → Apprequest to release subscriptions and timers before the app is removedClean 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.

json
1{
2  "mcpServers": {
3    "matsjfunke": { "url": "https://mcp.matsjfunke.com/mcp" }
4  }
5}
6