Learn/Module 02

The protocol stack

AG-UI, A2UI, MCP Apps, the OpenAI Apps SDK, json-render, OpenUI, WebMCP — most confusion disappears once you see they answer different questions on different layers. This module maps the stack as of November 2026, with the details that matter in production.

11 min read3 hours with labsupdated 2026-11-08
By the end you can
  1. 01Place any generative UI spec on the right layer by asking three questions
  2. 02Describe AG-UI 1.0's event families, state model and how interactivity returns
  3. 03Describe A2UI 1.0's message envelopes, flat component model and data binding
  4. 04Describe how an MCP App is linked, hosted, sandboxed and bridged
  5. 05Explain where each spec draws its security boundary, and where all of them are weak

Three questions, six layers

"AG-UI or A2UI?" is the most common question in this field, and it's malformed. Ask any generative UI system three separate questions:

  1. How do the bytes move? — transport.
  2. What does the model actually emit? — format.
  3. Who executes the UI code, and under what trust? — hosting.

Each of the main specs answers exactly one of them. As of November 2026 the stack looks like this:

Layer Question it answers Specs
L0 agent ↔ agent how agents delegate to agents A2A 1.0
L1 agent ↔ tools how agents reach tools and data; structured input from the user MCP (2026-07-28), MCP elicitation
L2 agent ↔ frontend how events, state and tool calls stream to an app AG-UI 1.0, AI SDK UI message stream, ACP (editors), ChatKit protocol
L3 UI description what the model emits when it describes UI as data A2UI 1.0, json-render, OpenUI Lang, ChatKit widgets, Adaptive Cards, Block Kit
L4 hosting & sandbox how developer-shipped UI runs inside an AI host MCP Apps, OpenAI Apps SDK
L5 website ↔ agent how a page exposes tools to a browser agent WebMCP

And the specs now wrap each other. AG-UI carries A2UI surfaces and MCP Apps as activity events. A2UI has a catalog with an McpApp component. json-render ships an MCP Apps server. OpenUI Lang has an A2UI profile. A single product can — and often should — use three of them at once.

A useful prior: three trust models

Before the details, one classification that changes how you test everything. "Generative UI" names three different trust models:

  1. The model selects pre-built components. Tool calls render your components (AG-UI frontend tools, AI SDK tool parts). Safe like data.
  2. The model composes from a catalog. A2UI, json-render, OpenUI Lang, ChatKit widgets. Safe like data, if the catalog is enforced where it's rendered.
  3. A third party ships code. MCP Apps and the Apps SDK: a developer wrote the HTML; the model only picks the tool. This is code, and needs a sandbox.

AG-UI: the event stream

What it is. An event protocol between an agent backend and a user-facing app: one RunAgentInput goes in, one ordered stream of typed events comes out. Started by CopilotKit, MIT-licensed. Protocol 1.0 shipped in September 2026 with a normative spec, a JSON Schema the SDKs are generated from, and a conformance corpus of test streams.

The events. 31 types in eight families:

Family Events
Lifecycle RUN_STARTED, RUN_FINISHED, RUN_ERROR, STEP_STARTED, STEP_FINISHED
Text TEXT_MESSAGE_START, _CONTENT, _END, _CHUNK
Tool calls TOOL_CALL_START, _ARGS, _END, _CHUNK, _RESULT
State STATE_SNAPSHOT, STATE_DELTA (RFC 6902), MESSAGES_SNAPSHOT
Activity ACTIVITY_SNAPSHOT, ACTIVITY_DELTA
Reasoning REASONING_START … REASONING_ENCRYPTED_VALUE (replaced the old THINKING_*)
Subagents SUBAGENT_STARTED, _FINISHED, _ERROR (new in 1.0)
Passthrough RAW, CUSTOM

Transport. HTTP POST of RunAgentInput with server-sent events back is required; protobuf framing is optional; custom bindings (WebSockets, in-process) must preserve order and a distinct termination signal. Authentication is explicitly out of scope.

How interactivity returns. This is the part people get wrong: AG-UI has no mid-run channel from the client. Everything comes back on the next run, by one of three routes:

  1. Frontend tools. The client lists tools it can execute; the agent calls one; the run finishes as success with pendingToolCallIds; the client runs the tool and sends a tool message in the next run.
  2. Interrupts (normative in 1.0). The run finishes with an interrupt outcome listing what it needs (id, reason, message, toolCallId, responseSchema, expiresAt). The next run carries resume: [{ interruptId, status, payload }]. The client must answer every interrupt, and the agent must not perform an interrupted action on the strength of a missing answer.
  3. State writes. The client edits state, which travels back with the next run.

Two rules worth memorising. Activity messages (rendered A2UI surfaces, MCP App activities, progress) are human-only: they must be stripped from what the agent sees next. So AG-UI never shows the model its own UI — if the model needs to know what the person saw or did, route it through a tool result, state or a user message. And the spec's safety section says apps should obtain consent before side-effectful tool calls and must not represent an action as user-approved when it wasn't.

Lab 02 is a scripted AG-UI 1.0 run you can scrub.

A2UI: UI as data

What it is. Google's declarative, streaming UI format. The agent sends component blueprints and a data model; the renderer maps them onto its own native components from a trusted catalog — "safe like data, but expressive like code". Apache-2.0, now at a2ui-project/a2ui. v1.0 became current on 2026-11-06 (it was previously called v0.10).

Messages. Every message carries "version": "1.0" and exactly one of:

Agent → renderer Renderer → agent
createSurface (surfaceId, catalogId, optional components and dataModel) action (name, sourceComponentId, resolved context)
updateComponents (upsert by id) error (code, message, path)
updateDataModel (JSON Pointer path + value) callAgentFunction, rendererFunctionResponse
deleteSurface, callRendererFunction

A flat component list. Each component has an id, a component type and props; containers reference children by ID; the root has id: "root". This is a streaming decision, not a style choice: a nested tree can't safely render until its brackets close, while a flat list renders progressively and patches surgically.

Binding. A value is a literal, a binding {"@path": "/expense/amount"}, or a function call {"@call": "formatCurrency", "args": {…}}. Inputs two-way bind to paths in the data model; nothing reaches the agent until an action fires.

What changed recently, and why it matters. v1.0 renamed client/server to renderer/agent, moved bindings to @path (a plain {"path": …} is now literal data — don't find-and-replace your way through migration), removed theme from surfaces ("separate layout from branding"), made catalogs mixable per component, added bidirectional function calls with allowedCallers and requiresUserActivation, and made accessibility attributes normative for renderers. It's the only spec in this module with normative accessibility.

Where it runs. Stable renderers for web core, Lit, Angular and React; alpha for Flutter, SwiftUI and Jetpack Compose in AndroidX; many community renderers. Transport bindings for A2A, AG-UI, REST, WebSockets and MCP. In production in Google Opal; being integrated into Gemini Enterprise.

The catch: version skew. A2UI changed its envelope several times in a year, and integrations lag — in November 2026 some first-party middleware still prompts v0.9 shapes. Always send and check the in-band version, and reject what you don't speak.

MCP Apps: UI inside AI hosts

What it is. The official MCP extension (io.modelcontextprotocol/ui) that lets an MCP server ship an interactive HTML view for a tool. Proposed as SEP-1865 on 2025-11-21 by MCP maintainers at Anthropic and OpenAI together with the creators of the community MCP-UI project; stable since 2026-01-26. It now renders in Claude, ChatGPT, VS Code Copilot, Microsoft 365 Copilot, Cursor, Goose, Glean and others, and in Gemini and Search since late October 2026.

The pieces.

// the tool links to a UI resource
{ "name": "show_expense", "inputSchema": { … },
  "_meta": { "ui": { "resourceUri": "ui://expenses/approval", "visibility": ["model", "app"] } } }

// the resource is HTML with a declared profile and policy
{ "uri": "ui://expenses/approval", "mimeType": "text/html;profile=mcp-app",
  "_meta": { "ui": { "csp": { "connectDomains": [] }, "prefersBorder": true } } }
  • visibility: ["app"] hides a tool from the model but lets the view call it — the cleanest way to give the UI capabilities the model shouldn't have.
  • Tools must still return meaningful text content, because not every host renders UI.

The bridge. The view and host talk JSON-RPC over postMessage. The view sends ui/initialize; the host answers with its capabilities and a hostContext (theme, standard CSS variables, display mode, locale, safe areas); the view confirms with ui/notifications/initialized. Then the host sends tool-input (and optionally streaming tool-input-partial), tool-result and tool-cancelled; the view can call tools/call, ui/message (post a user turn), and ui/update-model-context (silently tell the model something).

The sandbox. A web host must render the view in an iframe on a different, host-controlled origin, with a CSP built from the declared domains on a default-src 'none' baseline. Hosts may prefetch and hash ui:// templates at connect time, which makes the URI a review unit: ship breaking HTML under a new URI, and don't generate per-call HTML that defeats that review.

Three subtleties experts know.

  1. A button's tools/call goes to the server, not the model. The model only learns about the click if the view also sends ui/message or ui/update-model-context. Many apps perform the effect and never tell the model — an audit gap.
  2. ui/message posts with role: "user". A malicious view can speak as the person. The spec only says hosts may ask for consent.
  3. Consent for UI-initiated tool calls is optional in the spec. Sandboxing is mandatory; asking the person before a UI triggers a destructive tool is a "MAY". This is the most important gap in the stack, and it's yours to close. (Module 06)

The OpenAI Apps SDK: MCP Apps plus extensions

The Apps SDK launched in October 2025, before MCP Apps existed. OpenAI's current docs say: use the MCP Apps spec for shared fields and bridge methods; use window.openai only for ChatGPT-specific capabilities or compatibility. So _meta.ui.resourceUri is primary and openai/outputTemplate is an alias; tools/call is primary and window.openai.callTool is the compatibility path.

What's ChatGPT-only: persisted widgetState, file upload and download, checkout, modals and display-mode controls. One data-visibility rule matters for cost and privacy: content and structuredContent reach the model and the widget (the model reads tool output verbatim, so keep it small), while a result's _meta reaches only the widget.

json-render: a catalog compiled into the prompt

What it is. Vercel Labs' generative UI framework (Apache-2.0, 0.22 in November 2026, pre-1.0). You define a catalog of Zod-typed components and actions; the catalog compiles into a system prompt; the model emits a flat JSON spec { root, elements }, streamed as JSON Patch lines; a registry renders it — in React, Vue, Svelte, React Native, PDF, email, video, terminal, chat cards and MCP Apps.

const catalog = defineCatalog(schema, {
  components: { Card: { props: z.object({ title: z.string() }), description: "A titled card" } },
  actions: { approve_expense: { params: z.object({ expenseId: z.string() }), description: "Approve" } },
});
catalog.prompt();        // the system prompt
catalog.validate(spec);  // validation against the same source of truth

How it differs from A2UI. json-render is a library-defined shape, not a versioned protocol. The catalog lives in the prompt, so the client must re-validate everything; in A2UI the renderer advertises its catalogs and is authoritative. json-render has built-in confirm on action bindings; A2UI has no built-in confirmation. And json-render defines no wire message for sending actions back to the agent — that's your app's job.

Two measured gotchas (version 0.22): catalog.validate() rejects unknown components but did not reject an unknown action name — allow-list actions in your handler too. And the generated prompt for a four-component, two-action catalog was about 15,800 characters: catalog-in-prompt has a real token cost.

OpenUI: four different things

The name is overloaded; be precise.

Name What it is Relevant?
Thesys OpenUI an open framework around OpenUI Lang, a compact line-oriented DSL for generative UI (Lang 1.0 in October 2026), with React, Vue, Svelte and Angular runtimes yes — an L3 format
Thesys C1 the original 2025 "generative UI API", now repositioned as OpenUI Gateway historical and commercial
wandb/openui a describe-a-UI → live HTML prototyping tool no — a tool, not a protocol
W3C Open UI Community Group incubates native HTML controls: popover, invoker commands, customizable <select> indirectly — better declarative primitives for renderers and agents

And one more label to retire: "Open-JSON-UI" appears in some framework docs as an OpenAI standard, but no OpenAI-published spec exists. The closest real artifact is ChatKit's widget JSON.

WebMCP: the page talks to the agent

What it is. A W3C community-group draft, edited by people from Microsoft and Google, that lets a web page register tools for the browser's agent via document.modelContext (it moved from navigator in May 2026 — older guides are stale). registerTool({ name, description, inputSchema, execute, annotations }), plus a declarative form API (<form toolname tooldescription>). In origin trials in Chrome 149 and Edge 150; ChatGPT Desktop supports it.

The weakness it admits. Annotations like readOnlyHint are self-asserted — the spec itself says a malicious site can omit or misrepresent them. Declared intent isn't verified behaviour. Module 05 covers designing for it.

Where every spec is weak

Read the specs side by side and the same gaps appear in all of them:

  • Consent for UI-initiated effects is optional or host-specific everywhere, and untestable across hosts.
  • Effect-safety vocabulary is fragmented: MCP has readOnlyHint/destructiveHint, WebMCP has its own hints, A2UI has requiresUserActivation, json-render has confirm, AG-UI has interrupts, the AI SDK has approval requests. No spec defines reversibility, or what "confirmation before effect" means operationally.
  • Accessibility is normative only in A2UI 1.0. Nothing covers focus across iframe boundaries or announcing streamed updates.
  • Design-system conformance is untested by any spec: catalogs allow component types, not tokens, spacing or brand rules.
  • Version skew is the dominant real-world failure mode of 2026.
  • There's no cross-protocol trace linking rendered element → gesture → bridge message → tool call → model turn.

These gaps are where an independent practitioner adds the most value — and where RUI was built to sit.

Choosing, in one table

If you need… Reach for
your agent's events, state and approvals in your own React app AG-UI (via CopilotKit or a framework with an AG-UI adapter), or the AI SDK's UI stream
the model to compose screens from your components, safely, across platforms A2UI (protocol, renderer-authoritative) or json-render (library, many targets)
your product to appear inside ChatGPT, Claude, Copilot or Gemini MCP Apps (plus window.openai extensions for ChatGPT-only features)
a page that browser agents can operate reliably WebMCP on top of a solid accessibility tree
a simple structured question to the user from an MCP server MCP elicitation

Check yourself

  1. Place AG-UI, A2UI and MCP Apps on the stack and explain, in two sentences, why they don't compete.
  2. In AG-UI 1.0, what's the difference between a run that stops on a frontend tool and a run that ends with an interrupt?
  3. Why does A2UI use a flat list of components with ID references?
  4. An MCP App's "Approve" button calls tools/call. Does the model know the person approved? What would make it know?
  5. Where does the catalog live in A2UI versus json-render, and what does that imply for validation?
  6. Name three things no current spec standardises that a production team still needs.

Go deeper