Labs/Protocol Rosetta stone
07

One card, six protocols

The same expense-approval card expressed in AG-UI, A2UI, MCP Apps, the OpenAI Apps SDK, json-render and an Adaptive Card.

GoalLearn the protocols by comparing them on one concrete example instead of six abstract specs.

AG-UIA2UIMCP AppsApps SDKjson-renderAdaptive Cards
Layer
Transport: agent ↔ frontend event stream
The model emits
A tool call. Your frontend owns the component it renders.
The click gets back via
Tool message in the next RunAgentInput (or an interrupt resume)
Trust model
No UI code crosses the wire; only args. You render your own component.
Backed by
CopilotKit + open community
What travels · AG-UIAG-UI core events (docs.ag-ui.com, Nov 2026)
Frontend tool the client offers (RunAgentInput.tools)
{
"name": "show_expense_approval",
"description": "Ask the person to approve or reject an expense.",
"parameters": {
"type": "object",
"properties": {
"expenseId": {
"type": "string"
},
"title": {
"type": "string"
},
"amount": {
"type": "number"
},
"submitter": {
"type": "string"
}
},
"required": [
"expenseId",
"title",
"amount",
"submitter"
]
}
}
Events the agent streams (SSE, one per line)
[
{
"type": "RUN_STARTED",
"threadId": "thr_42",
"runId": "run_7"
},
{
"type": "TOOL_CALL_START",
"toolCallId": "tc_1",
"toolCallName": "show_expense_approval"
},
{
"type": "TOOL_CALL_ARGS",
"toolCallId": "tc_1",
"delta": "{\"expenseId\":\"exp_8812\",\"title\":\"Team offsite\","
},
{
"type": "TOOL_CALL_ARGS",
"toolCallId": "tc_1",
"delta": "\"amount\":1240,\"submitter\":\"Priya\"}"
},
{
"type": "TOOL_CALL_END",
"toolCallId": "tc_1"
},
{
"type": "RUN_FINISHED",
"threadId": "thr_42",
"runId": "run_7"
}
]
Client side: map the tool name to your own component
// Any AG-UI client works the same way: fold TOOL_CALL_* events into args,
// then render a component you registered for that tool name.
const renderers = {
  show_expense_approval: (args, respond) => (
    <ExpenseCard {...args}
      onApprove={() => respond({ decision: "approve" })}
      onReject={() => respond({ decision: "reject" })} />
  ),
};
Live render
Team offsite
$1,240.00
Submitted by Priya

The agent never sent UI. It sent arguments to a tool your frontend registered; this card is your own React component.

What goes back to the agentclick Approve or Reject

Nothing yet. Press a button in the render above.

Checked against: AG-UI events · AG-UI tools · AG-UI interrupts · as of 2026-11-08

The six side by side

Click a row to load it above.

ProtocolKindModel emitsWho draws itClick returns asStreaming
AG-UITransportEvents + tool callsYour componentsTool message / resumeStreaming-first: text, args, state deltas
A2UIUI as dataComponent list + data modelClient renderer + catalogaction messageIncremental by design (flat ids)
MCP AppsUI hostingNothing UI-specificServer-shipped HTML in sandboxJSON-RPC tools/call, ui/messagetool-input-partial lets the view preview args
OpenAI Apps SDKUI hostingNothing UI-specificServer-shipped HTML in ChatGPTwindow.openai.callToolSame as MCP Apps
json-renderUI as dataFlat spec within a catalogYour components via registryYour action handlerSpecStream compiler patches the spec
Adaptive CardsUI as data(Usually code, not models)Host rendereradaptiveCard/action invokeWhole card; refresh replaces it

They mostly don't compete

The six answer three different questions. A real stack often uses one from each row: A2UI messages carried over AG-UI, rendered next to an MCP App in the same host.

01
Moves events
AG-UI

How the agent's run reaches the browser: text, tool calls, state. Says nothing about what the UI looks like.

02
Describes UI as data
A2UIjson-renderAdaptive Cards

What to draw, as JSON the client interprets with components it trusts. Safe by construction, limited by the catalog.

03
Hosts UI code
MCP AppsApps SDK

Ship real HTML from a server and run it in a sandboxed iframe inside a host such as ChatGPT or Claude. Unlimited UI, paid for with a sandbox.

How it worksclick to expand

Three renders are real interpreters.A2UI, json-render and the Adaptive Card are drawn by code in this page that reads the exact JSON shown under “What travels”. Anything outside their small allowlists is refused instead of drawn.

Two renders are real sandboxes. The MCP Apps and Apps SDK views run in an iframe with sandbox="allow-scripts". This page plays the host: it answers ui/initialize, pushes the tool result, and receives tools/call. The View uses raw postMessage where a real app would use the ext-apps SDK.

Simplified on purpose. The handshake carries only a few fields, there is no MCP server behind the host, and AG-UI runs are shown as static events (Lab 02 plays one live).

// The host side of the MCP Apps bridge, in miniature
window.addEventListener("message", (e) => {
  if (e.source !== iframe.contentWindow) return   // only our View
  const m = e.data
  if (m.method === "ui/initialize")
    reply(m.id, { protocolVersion, hostInfo, hostContext })
  if (m.method === "ui/notifications/initialized")
    notify("ui/notifications/tool-result", toolResult)
  if (m.method === "tools/call")
    reply(m.id, await mcpServer.callTool(m.params))  // proxied
})