Ask an AI for flights and you might get a card with times, prices and a button, not a paragraph.
Someone decided what that card looks like. It was your app, the model, or whoever wrote the tool. Which one it was is the most important decision in generative UI, and it decides almost everything else.
Here's the same request, built all three ways.
All three show roughly the same thing. What changes is who wrote each part, what the model is allowed to say, and what happens when it says something wrong.
- Model calls a tool
- Tool returns JSON
- App maps it to a component
- Model writes a UI spec
- Client validates it
- Catalogue renders it
- Model calls a tool
- Tool ships HTML
- Host sandboxes it
1. The app owns every pixel
This is the oldest pattern, and it's plain tool calling. You describe some tools to the model, such as search_flights with its arguments. The model doesn't run anything. It replies with the name of a tool and the arguments it wants, your code runs it, and gets data back.
Then your app looks at which tool was called and renders the matching component. search_flights always becomes <FlightResults>.
The model's only creative choice is which tool to call and with what arguments. Everything you see after that was designed, built and tested before the conversation started. It's on brand, it's accessible because you made it so, and when it breaks, the bug is in your code where you can find it.
The cost is that the model can't show anything you didn't anticipate. Every new kind of answer needs a new component and a release.
2. The model writes a description
In the second pattern, the model writes a description of a UI as data, usually JSON, and the client renders it using its own components.
Google's A2UI is the clearest example. Google introduced it in December 2025 as an open project, and it's now an Apache 2.0 project at a2ui.org. The announcement puts the whole idea in one line: "A2UI is a declarative data format, not executable code." The client keeps "a 'catalog' of trusted, pre-approved UI components", and the agent can only ask for those.
That catalogue is the safety boundary. The demo's spec is a simplified version of the idea, not A2UI's actual wire format, but the renderer works the same way:
function node(c) {
const def = catalogue[c.type];
if (!def) return fallbackText(c); // unknown component
const props = pick(c, def.props); // unknown props are dropped
return def.render(props);
}
Turn on the rogue request in the second tab. The model asks for a PriceChart the app doesn't have, and tries to restyle the heading with a style prop. Neither gets through. The chart becomes its fallback sentence, the style is dropped, and nothing the model wrote ever runs as code.
The structure matters for streaming too. A2UI describes components as a flat list with ID references, rather than one deeply nested tree. The announcement says that lets models generate it incrementally, "allowing for progressive rendering". The client can draw the parts it has while the rest is still being written.
What you give up is predictability. The model now decides the layout, so the same request can produce a different screen next time. Your components are still yours, but how they're put together isn't.
3. The tool ships its own UI
The third pattern gives the screen to the tool's author. The standard for this is MCP Apps, an extension to the Model Context Protocol. It was proposed as SEP-1865 in November 2025, written by maintainers at OpenAI and Anthropic with the creators of MCP-UI. On 26 January 2026 it went live as the first official MCP extension.
Here's how it works, from the spec:
- The UI is a resource. A server declares HTML at a
ui://address, with the typetext/html;profile=mcp-app. A tool points to it in its metadata, so the host can fetch and review it before the tool is even called. - It runs in a sandboxed iframe. The iframe must be on a different origin from the host, and the host serves the document with a content security policy built from the domains the app declared.
- It talks over
postMessage. The messages are MCP's own JSON-RPC. The app sendsui/initialize, the host replies with context such as the theme and CSS variables, and then pushesui/notifications/tool-resultwith the data. The app can call tools back withtools/call, and the host can ask the person first.
The third tab runs a cut-down version of that handshake. Look at it in dark mode: the app stays white, because using the theme the host sends is the app author's choice, not yours. Press Hold fare and the app asks to call a tool. The host asks you, in its own UI and outside the iframe, where the app can't fake the dialog.
As of November 2026, the MCP project's support matrix lists MCP Apps in Claude on the web and desktop, ChatGPT, VS Code with GitHub Copilot, Microsoft 365 Copilot, Cursor, Goose and Postman, among others. The list is community-maintained, so check it before relying on a specific host.
This is the only pattern where the model can show something nobody at your company designed, like a seat map or an interactive chart. It's also the only one where somebody else's code runs inside your product.
Who's responsible when it breaks
| App's component | Model's UI spec | Tool's own UI | |
|---|---|---|---|
| Brand | Yours, exactly | Yours, but the composition varies | The tool author's |
| Safety | Only your code runs | Only data, checked against a schema | Third-party code, sandboxed |
| What it can show | What you built | What your catalogue can combine | Anything HTML can do |
| Streaming | After the tool returns | Part by part, as the spec arrives | The app can preload, and arguments can stream in |
| Accessibility | You control it | Your components, the model's order | Whatever the author built |
| When it breaks | Your bug | Your renderer or the model's output | Their bug, in your product |
The last row is the one people skip. When a third-party app inside your chat is broken or inaccessible, the person using it doesn't blame the MCP server. They blame you.
Your design system becomes a vocabulary
With the second pattern especially, your component library stops being only for designers and engineers. Its catalogue and schema become the words the model is allowed to use. That changes how you design it.
- Constrain the props. An enum like
variant: "heading" | "body"is something the model can't get wrong. A freecolorstring is an invitation. Leave out anything that lets the model restyle, and let your theme decide. - Name things for what they mean, not how they look.
FlightRowtells the model when to use it.CardWithIconAndPricedoesn't. - Give every component a fallback. The model will ask for things that don't exist. Decide in advance what that looks like, and make it a sentence, not an error.
- Keep side effects out of the model's hands. A button in a spec should name an action, and the app decides what that action does. Anything that books, pays, sends or deletes needs a confirmation the host draws, in all three patterns.
I'd start with the first pattern and earn your way along the spectrum. Move to a catalogue when you keep shipping one-off components. Allow a tool's own UI when what it needs to show really can't be built from your parts. Each step to the right gives the model, or someone else, more of your screen.