← Back
[W] Writing

Three ways an AI draws an interface

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.

Simulated, with made-up airlines. In the second tab, make the model ask for something outside the catalogue. In the third, hold a fare and watch the message log.

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.

1. App's component
  1. Model calls a tool
  2. Tool returns JSON
  3. App maps it to a component
2. Model's UI spec
  1. Model writes a UI spec
  2. Client validates it
  3. Catalogue renders it
3. Tool's own UI
  1. Model calls a tool
  2. Tool ships HTML
  3. Host sandboxes it
The red box is where the interface is decided. It moves from your code, to the model, to a third party.

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 type text/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 sends ui/initialize, the host replies with context such as the theme and CSS variables, and then pushes ui/notifications/tool-result with the data. The app can call tools back with tools/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

Move the slider. Each row moves to the right at a different point, and the look and the code are the last to go.
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 free color string 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. FlightRow tells the model when to use it. CardWithIconAndPrice doesn'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.

Next