MCP Apps (SEP-1865): how an MCP server can ship a real UI

16:9 · 7:04
20 / 20
Transcript1139 words · click to read

Ask an AI assistant to book you a seat, and for years you got back the same thing. A wall of text describing seats. Row 12, window, available. Row 12, middle, taken. You're reading a spreadsheet out loud. Now open chat GPT or Claude and ask the same question, and a real seat map appears. Clickable. You pick 12A with your finger. Same protocol underneath. So what actually changed? And more importantly, whose code is drawing those pixels? To see what changed, look at what a tool was allowed to send back. In the Model Context Protocol. MCP, the standard wiring AI clients to outside servers. A server exposes tools. The model calls one, the tool returns content. Text blocks. Maybe an image. Maybe a JSON object. That's the whole vocabulary. So if your server knows about seats, or a stock chart, or a calendar with 40 meetings on it, you get one move. Describe it in words. host invented its own workaround. Nothing was portable. The fix has a name, mcpapps. And it starts somewhere you might not expect, not in the tool result but in the server's resource list. Your server publishes a resource whose address begins with u, uh, i, colon, slash, slash. Inside it, plain html. The type is spelled out precisely. Next, slash HTML profile equals mcpapp. Then your tool gets a small piece of metadata pointing at it. Underscore meta, ooh, resource eerie. Now the tool and its interface are formally married. Call the tool and the client already knows which page to draw. Notice what's strange there. The HTML doesn't ride along with the result. It's declared upfront before anyone calls anything. That was a real argument during design. C-P-U-I. The community project that pioneered this ships the interface inside the tool result, which is honestly easier to write. The spec went the other way on purpose. Declared up front means the client can fetch it early, cache it, and the part that matters. Read the HTML before it ever runs. You can't review code you haven't seen. None of this switches on by itself. Before anything renders, the client and server shake hands. The client announces an extension it supports, spelled io.modelcontext protocol slash ui, and lists which content types it can render. A server that doesn't see that announcement should keep its UI tools to itself and fall back to plain text. It's a bouncer at the door checking whether the venue is even licensed for live music. No nod, no show. So the handshake works and the client has your HTML. Now it has to run a stranger's web page inside an app holding the user's conversations, files, and keys. Every alarm should be going off. The answer is an iframe, a page inside a page, on a very short leash. The sandbox permits exactly two things. Run scripts, keep its own origin. And on the web, one iframe isn't enough. The client wraps your page in a second iframe on a different origin, which sits in the middle passing notes. Your code never touches the client's origin. There's a second lock, and it's stricter than most production websites. If your resource says nothing about what it needs, the client enforces this. Defaults are seen none. Connects are seen none. No fetching, no sockets, no phoning home. Your page renders, and that's it. Want an image from your CDN or a web socket? You declare those domains in the resources metadata. Connect domains, resource domains, frame domains. When the client builds the content security policy from your declaration, you ask for reach in writing in advance. Locked in a box with no network, your page would be a very pretty poster. So how does it get data? One channel. The oldest, dullest browser API there is, window.postmessage. The interface posts a message up to the client. The client posts one back down. What's inside those messages is the interesting part. It isn't a custom format. J-S-O-N-R-P-C. The exact same message shape MCP already uses everywhere else. Which means the tool calls, the type definitions, the error handling, the timeouts. All of it just works. Unchanged. One layer down. Watch one call go through. Your page loads and sends we slash initialize. I'm awake. Tell me where I am. The client replies with a host context. Light or dark theme. The size of the box you're sitting in. the user's locale and time zone, whether the device has a mouse or a thumb. Then a notification arrives carrying tool input. The arguments the model chose. Then the answer arrives as tool result. And that result is deliberately split in two. A text part written for the model to read and a structured content part written for your HTML to render. One payload, two audiences. This is the moment it stops being a display and becomes an app. The user clicks refresh prices. Your page sends tools slash call, a real MCP tool call. The client proxies it to the server. The answer comes back. The interface can drop a message into the conversation with ui slash message, so the model sees what the user just did. It can ask to go fullscreen, and a tool can be marked visibility app only, callable by the interface, invisible to the model. Buttons the AI is never allowed to press. One more thing separates this from an iframe bolted onto a page. The client hands your interface, its own design tokens, roughly 90 reserved CSS variables. Background colors, text colors, order radii, font stacks, shadows. Read them and your seat map inherits the client's theme. When the user flips to dark mode, a host context change notification tells you, live. Ignore them, and your app looks like a ransom note stapled to the conversation. When the conversation moves on, the client sends a tear down request. A request, not a notification, so your page gets a moment to answer before it's destroyed. Which brings us back to the seat map. What changed, isn't that HTML reached a chat window? Anyone could do that. What changed is the trust boundary got written down, declared up front so it can be reviewed. Sandbox twice. No network by default. Every message logable J-S-O-N, R-P-C, and tool calls the user can be asked to approve. The interface is powerful because it's provably contained. This isn't one company's format, either. SCP-1865 was filed on November 21, 2025, authored by the MCP UI team, Eidosalamin and Lead Yosef. Alongside engineers from Anthropik and OpenAI, folding MCP, UI and chat GPTs apps SDK into a single standard. It's now marked final. Which leaves the interesting question in your hands. Your server already returns a table nobody enjoys reading. What would it feel like if the user could just click it?

Machine-transcribed from the published render, so acronyms may be cased as ordinary words.