Choosing a protocol
Five options, three genuinely different problems. Most bad choices come from
answering the wrong question.
The three problems
- "A tool result should render as a widget inside someone else's chat host."
→ MCP Apps family (MCP Apps, mcp-ui, OpenAI Apps SDK).
- "An agent should describe UI that my client renders with its own native
components." → A2UI.
- "My frontend needs to show what a long-running agent is doing, share state
with it, and let the user interrupt." → AG-UI.
If the answer is more than one, you need more than one. They compose cleanly.
Decision tree
Do you own the frontend that renders the UI?
├─ NO — a chat host renders it (Claude, ChatGPT, M365 Copilot, Goose)
│ └─ Start with MCP Apps. Then:
│ ├─ Need externalUrl or remoteDom delivery, or a pre-MCP-Apps host?
│ │ └─ mcp-ui (it implements MCP Apps and keeps its own action protocol)
│ └─ Need ChatGPT-only capability — checkout, uploadFile, requestModal?
│ └─ MCP Apps + the OpenAI Apps SDK extensions on top
│
└─ YES — you own the client
├─ The client must render with its OWN native widgets
│ (Flutter/SwiftUI/design-system React), or executing model-authored
│ markup is unacceptable
│ └─ A2UI
└─ The agent is long-running and the UI must show progress, shared state,
thinking steps, and support interrupts
└─ AG-UI (and render individual results with A2UI or your own components)
The axes that discriminate
| Axis |
MCP Apps |
mcp-ui |
Apps SDK |
A2UI |
AG-UI |
| Who renders |
Chat host, sandboxed iframe |
Chat host, sandboxed iframe |
ChatGPT, sandboxed iframe |
Your client, native widgets |
Your frontend |
| UI is |
HTML/JS you author |
HTML/JS/URL/DOM script |
HTML/JS you author |
JSON data |
Events → your components |
| Portability |
High — any compliant host |
High + legacy hosts |
ChatGPT-first |
Very high — any renderer |
You own it |
| Design control |
Total |
Total |
Total, within ChatGPT rules |
The client's, not yours |
Total |
| Injection surface |
Sandboxed script |
Sandboxed script |
Sandboxed script |
None — data only |
None |
| Non-web surfaces |
No |
No |
No |
Yes |
Depends on frontend |
| Streaming granularity |
Tool input → result |
Tool input → result |
Tool input → result |
Per component/data path |
Per token/event |
| Long-running agents |
Poor fit |
Poor fit |
Poor fit |
Partial |
Purpose-built |
| Human-in-the-loop |
Via tool calls |
Via actions |
Via tool calls |
Via actions |
First-class interrupts |
Valid combinations
- MCP Apps + Apps SDK extensions — the standard recommendation for anything
shipping to ChatGPT. Portable core, ChatGPT-only affordances feature-detected.
- MCP Apps + mcp-ui — one server, both resource shapes, when you must reach
hosts on either side of the extension's adoption.
- AG-UI + A2UI — AG-UI carries the event stream; A2UI payloads ride inside
it as the UI description. This is the strongest fully-owned-frontend stack.
- AG-UI + MCP Apps — an MCP Apps View that opens an AG-UI stream for a
long-running sub-task inside the widget.
Anti-patterns
Reaching for window.openai first. It ties you to one host for capability
that MCP Apps already covers. Use the standard field; layer extensions only for
checkout, files, and modals.
A2UI for a ChatGPT widget. ChatGPT renders MCP Apps resources, not A2UI
surfaces. You would be writing a renderer nobody asked for.
AG-UI to render a single tool result. The event machinery buys you nothing
if the interaction is one request and one card. Use MCP Apps.
MCP Apps for a 20-minute research agent. You get one tool-result and no
vocabulary for progress, reasoning steps, or interrupts. Use AG-UI, or split:
AG-UI for the run, MCP Apps for the final artifact.
externalUrl as the default. It costs an origin in CSP, a network hop, and
an independent deploy to keep in sync. Take it when you genuinely have an
existing app; not to avoid learning the bundler.
Attaching a UI resource to every tool. See the decoupled data/render pattern
in openai-apps-sdk. This is the most common performance defect in shipped apps.
Keeping the migration path open
Whatever you choose, these keep the exit cheap:
- Tools work without UI. Always return real
content and
structuredContent. This is what lets the same server serve a text-only
host, a different protocol, or an API consumer.
- Keep rendering logic in one module. The bridge (
app.* / window.openai.*
/ postMessage) should be behind a thin adapter, not sprayed through
components. Porting then means rewriting one file.
- Feature-detect, never host-detect. A host-name branch is a permanent fork.
- Version resource URIs.
ui://x/v1.html → ui://x/v2.html makes a
protocol change a deploy, not a migration.
/ui:port automates the mechanical part of moving between protocols once these
hold.
Quick answers
| Requirement |
Choice |
| "Show a chart when my MCP tool returns data" |
MCP Apps |
| "Ship an app in the ChatGPT directory" |
MCP Apps + Apps SDK |
| "Add a widget to a Microsoft 365 Copilot declarative agent" |
MCP Apps (check the Copilot matrix) |
| "Render agent UI in our Flutter app" |
A2UI |
| "Our design system must own every pixel" |
A2UI |
| "Show the agent's progress and let users approve steps" |
AG-UI |
| "We already host the dashboard as a web app" |
mcp-ui externalUrl |
| "The host should render it with its own components" |
mcp-ui remoteDom or A2UI |
| "Model output must never become executable markup" |
A2UI |
Related
mcp-apps-protocol, mcp-ui-sdk, openai-apps-sdk, a2ui-protocol,
ag-ui-protocol — the specs behind each column.
host-capability-matrix — what the target host actually implements.
1---
2name: protocol-selection
3description: This skill should be used when choosing between MCP Apps, mcp-ui, the OpenAI Apps SDK, A2UI, and AG-UI for a given requirement — the decision tree, the axes that actually discriminate, valid combinations, anti-patterns, and how to keep a migration path open.
4version: 1.0.0
5trigger_phrases: [which protocol, mcp apps vs mcp-ui, a2ui vs mcp apps, ag-ui vs mcp apps, should i use apps sdk, agent ui protocol choice, generative ui options]
6categories: [architecture, decision, ui]
7author: mcp-apps-studio
8created: 2026-08-13
9updated: 2026-08-13
10---
11
12# Choosing a protocol
13
14Five options, three genuinely different problems. Most bad choices come from
15answering the wrong question.
16
17## The three problems
18
191. **"A tool result should render as a widget inside someone else's chat host."**
20 → MCP Apps family (MCP Apps, mcp-ui, OpenAI Apps SDK).
212. **"An agent should describe UI that my client renders with its own native
22 components."** → A2UI.
233. **"My frontend needs to show what a long-running agent is doing, share state
24 with it, and let the user interrupt."** → AG-UI.
25
26If the answer is more than one, you need more than one. They compose cleanly.
27
28## Decision tree
29
30```
31Do you own the frontend that renders the UI?
32├─ NO — a chat host renders it (Claude, ChatGPT, M365 Copilot, Goose)
33│ └─ Start with MCP Apps. Then:
34│ ├─ Need externalUrl or remoteDom delivery, or a pre-MCP-Apps host?
35│ │ └─ mcp-ui (it implements MCP Apps and keeps its own action protocol)
36│ └─ Need ChatGPT-only capability — checkout, uploadFile, requestModal?
37│ └─ MCP Apps + the OpenAI Apps SDK extensions on top
38│
39└─ YES — you own the client
40 ├─ The client must render with its OWN native widgets
41 │ (Flutter/SwiftUI/design-system React), or executing model-authored
42 │ markup is unacceptable
43 │ └─ A2UI
44 └─ The agent is long-running and the UI must show progress, shared state,
45 thinking steps, and support interrupts
46 └─ AG-UI (and render individual results with A2UI or your own components)
47```
48
49## The axes that discriminate
50
51| Axis | MCP Apps | mcp-ui | Apps SDK | A2UI | AG-UI |
52|---|---|---|---|---|---|
53| Who renders | Chat host, sandboxed iframe | Chat host, sandboxed iframe | ChatGPT, sandboxed iframe | Your client, native widgets | Your frontend |
54| UI is | HTML/JS you author | HTML/JS/URL/DOM script | HTML/JS you author | JSON data | Events → your components |
55| Portability | High — any compliant host | High + legacy hosts | ChatGPT-first | Very high — any renderer | You own it |
56| Design control | Total | Total | Total, within ChatGPT rules | The client's, not yours | Total |
57| Injection surface | Sandboxed script | Sandboxed script | Sandboxed script | **None — data only** | None |
58| Non-web surfaces | No | No | No | **Yes** | Depends on frontend |
59| Streaming granularity | Tool input → result | Tool input → result | Tool input → result | Per component/data path | Per token/event |
60| Long-running agents | Poor fit | Poor fit | Poor fit | Partial | **Purpose-built** |
61| Human-in-the-loop | Via tool calls | Via actions | Via tool calls | Via actions | **First-class interrupts** |
62
63## Valid combinations
64
65- **MCP Apps + Apps SDK extensions** — the standard recommendation for anything
66 shipping to ChatGPT. Portable core, ChatGPT-only affordances feature-detected.
67- **MCP Apps + mcp-ui** — one server, both resource shapes, when you must reach
68 hosts on either side of the extension's adoption.
69- **AG-UI + A2UI** — AG-UI carries the event stream; A2UI payloads ride inside
70 it as the UI description. This is the strongest fully-owned-frontend stack.
71- **AG-UI + MCP Apps** — an MCP Apps View that opens an AG-UI stream for a
72 long-running sub-task inside the widget.
73
74## Anti-patterns
75
76**Reaching for `window.openai` first.** It ties you to one host for capability
77that MCP Apps already covers. Use the standard field; layer extensions only for
78checkout, files, and modals.
79
80**A2UI for a ChatGPT widget.** ChatGPT renders MCP Apps resources, not A2UI
81surfaces. You would be writing a renderer nobody asked for.
82
83**AG-UI to render a single tool result.** The event machinery buys you nothing
84if the interaction is one request and one card. Use MCP Apps.
85
86**MCP Apps for a 20-minute research agent.** You get one `tool-result` and no
87vocabulary for progress, reasoning steps, or interrupts. Use AG-UI, or split:
88AG-UI for the run, MCP Apps for the final artifact.
89
90**`externalUrl` as the default.** It costs an origin in CSP, a network hop, and
91an independent deploy to keep in sync. Take it when you genuinely have an
92existing app; not to avoid learning the bundler.
93
94**Attaching a UI resource to every tool.** See the decoupled data/render pattern
95in `openai-apps-sdk`. This is the most common performance defect in shipped apps.
96
97## Keeping the migration path open
98
99Whatever you choose, these keep the exit cheap:
100
1011. **Tools work without UI.** Always return real `content` and
102 `structuredContent`. This is what lets the same server serve a text-only
103 host, a different protocol, or an API consumer.
1042. **Keep rendering logic in one module.** The bridge (`app.*` / `window.openai.*`
105 / postMessage) should be behind a thin adapter, not sprayed through
106 components. Porting then means rewriting one file.
1073. **Feature-detect, never host-detect.** A host-name branch is a permanent fork.
1084. **Version resource URIs.** `ui://x/v1.html` → `ui://x/v2.html` makes a
109 protocol change a deploy, not a migration.
110
111`/ui:port` automates the mechanical part of moving between protocols once these
112hold.
113
114## Quick answers
115
116| Requirement | Choice |
117|---|---|
118| "Show a chart when my MCP tool returns data" | MCP Apps |
119| "Ship an app in the ChatGPT directory" | MCP Apps + Apps SDK |
120| "Add a widget to a Microsoft 365 Copilot declarative agent" | MCP Apps (check the Copilot matrix) |
121| "Render agent UI in our Flutter app" | A2UI |
122| "Our design system must own every pixel" | A2UI |
123| "Show the agent's progress and let users approve steps" | AG-UI |
124| "We already host the dashboard as a web app" | mcp-ui `externalUrl` |
125| "The host should render it with its own components" | mcp-ui `remoteDom` or A2UI |
126| "Model output must never become executable markup" | A2UI |
127
128## Related
129
130- `mcp-apps-protocol`, `mcp-ui-sdk`, `openai-apps-sdk`, `a2ui-protocol`,
131 `ag-ui-protocol` — the specs behind each column.
132- `host-capability-matrix` — what the target host actually implements.