No. 15 · MAY 2026 · 6 Min Read

MCP Apps: When Your Server Needs a Face

Abstract

MCP Apps let servers render interactive UI directly in conversations. Use one when people need to manipulate state that text handles badly.

I wrote about designing MCP servers that models actually use well a couple weeks ago. The premise was that your server is a developer experience product and the developer is an LLM. Clear tool names, errors that guide recovery, and a small surface area. The whole server is a prompt.

MCP Apps flip the audience. Your server can talk directly to the user.

MCP Apps are not part of MCP core. They are an opt-in extension for interactive UI. SEP-1865 is the final design record, not the living protocol. The current MCP Apps specification is still Draft, and it calls out protocol details that will change before GA. The current core specification requires both client and server to advertise an extension before either relies on it.

MCP Apps are less settled than the early announcement suggested. The idea is still useful. Most MCP servers still return text and let the model figure out presentation. That’s right for most tools. But there is an entire class of interaction where it is actively bad.

How It Works

There are two pieces. Then comes negotiation.

First: a tool can declare a UI resource in _meta.ui.resourceUri, pointing to a ui:// URI. The current Draft spec deprecates the old flat _meta["ui/resourceUri"] form, so use the nested field.

Second: the server serves a valid HTML document from that URI through resources/read, with the MIME type text/html;profile=mcp-app. The resource, not the tool, carries UI security metadata such as _meta.ui.csp and requested permissions.

On a host that negotiated the extension, the host renders that resource in a sandboxed iframe. It owns the postMessage boundary. The view starts with ui/initialize; the host replies with its context and capabilities. The host then sends the tool input and result through ui/notifications/tool-input and ui/notifications/tool-result. The view can request server tool calls only when the host advertises that proxy capability. It can request a model-context update only when the host advertises that capability.

The sandbox is part of the design, not window dressing. The Draft spec requires restricted iframe sandboxing, auditable JSON-RPC messages, and CSP enforcement from resource metadata. A server supplies HTML. The host decides how and whether to render it safely.

Inline, fullscreen, and picture-in-picture are the defined display modes. A view declares appCapabilities.availableDisplayModes; the host declares its own available modes and can decline a requested change. An app cannot assume a particular display mode.

The Display Problem

This exists because models are bad at being dashboards.

When a tool returns structured data, the model serializes it into prose or a markdown table. Fine for five rows. Unusable for fifty. And every time the user asks a follow-up (“sort by date,” “show me just the failures,” “what about Q3”), the model re-fetches, re-serializes, and re-renders the entire state. No filters. No drill-down. No persistence. Each turn is a from-scratch reconstruction of something that should be interactive.

Maps, charts, multi-step configuration forms. Anything where the user needs to explore rather than read. These are visual, stateful interactions that text handles poorly. The model becomes a bottleneck between the user and the data. Every interaction costs a round trip through inference.

MCP Apps can remove the bottleneck. The server supplies the interface. The user interacts directly. The model can stay informed without mediating every click. A dashboard can stay a dashboard across turns instead of being regenerated from scratch each time.

When Not To Build One

The same instinct from minimizing the tool surface applies here. Every MCP app you build is complexity you maintain. It is HTML, JavaScript, and CSS bundled into your server. It is a second interface to test. It is a visual design problem added to an API design problem.

If the model can describe the result in a paragraph, a widget is overhead. If your tool returns a status and a message, that is text. Let the model present it. It is good at that.

An app earns its cost when:

  • Users need to explore data. Filtering, sorting, drilling into detail. The model re-describing a table on every turn is a symptom that you need direct manipulation.
  • The interaction is spatial. Maps. Diagrams. Anything where position carries meaning that prose destroys.
  • Configuration has dependencies. A form where selecting option A changes what options B and C show. The model can walk through this, but it takes three turns and the user hates it.
  • State persists across turns. If the user is building something up over multiple interactions, a persistent UI holds that state without the model reconstructing it.

Text is the right interface when it works. Most tools return text results, and most of them are fine as text. A dedicated UI earns its place when direct manipulation is the point of the work.

The Model Stays in the Loop

Without model-context updates, the UI becomes a side channel.

The host does not inspect the iframe’s DOM, and the model does not receive DOM events. A user can click all afternoon while the conversation context stays frozen.

ui/update-model-context is the path back. A view can send content or structuredContent to the host, but only after hostCapabilities.updateModelContext says the host accepts it. The host should make that context available in future turns. It may defer it until the next user message, deduplicate it, or keep only the most recent update.

Model-context updates carry facts the model will need later: “User selected items A, B, and C.” “User set the date range to Q1 2026.” “User approved 3 of 5 line items.” The model can then reason about the interaction without pretending it saw the interface.

The inverse matters too. When a tool call finishes while the view is displayed, the host sends ui/notifications/tool-result. The model acts, the UI receives current data. The view and conversation stay aligned without pretending the model saw the interface.

Graceful Degradation

A host’s name, product page, or six-month-old release note cannot establish support. Support claims need current host documentation. In the protocol, capability negotiation is the source of truth.

In the 2026-07-28 core protocol, there is no initialization negotiation handshake. A client advertises its extensions in _meta["io.modelcontextprotocol/clientCapabilities"] on each request. A server advertises its extensions in server/discover. For MCP Apps, the client declaration looks like this:

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "get_weather",
    "arguments": { "location": "Chicago" },
    "_meta": {
      "io.modelcontextprotocol/protocolVersion": "2026-07-28",
      "io.modelcontextprotocol/clientCapabilities": {
        "extensions": {
          "io.modelcontextprotocol/ui": {
            "mimeTypes": ["text/html;profile=mcp-app"]
          }
        }
      }
    }
  }
}

Both sides need the extension. If either does not advertise it, the server falls back to core behavior or rejects a request that truly requires the extension. For an interactive tool, fall back to core behavior.

The text response is primary. The app enhances a client that has negotiated it. The Draft specification also requires meaningful content from UI-enabled tools when UI is available.

The UI Is Part of the Prompt Surface

The argument from the MCP server article was that every design decision in your server is context engineering. Tool names shape reasoning. Error messages guide recovery. Documentation endpoints provide on-demand context.

MCP Apps extend that surface when the client and server agree to use them. The UI communicates with users; model-context updates communicate with the model. Both shape the information available on the next turn.

The extension is still maturing. Most MCP servers are text-only, and most should be. But when a user needs direct manipulation, repeated model serialization has a real cost.