Lessons from building 20 MCP Apps in two days
Teal Larson shares hands-on lessons from building around twenty MCP Apps in two days, covering architecture patterns, sandboxing caveats, cross-host rendering inconsistencies, and why the text response must always remain the primary contract.
Score breakdown
MCP Apps introduce real UI surfaces into chat-based tool responses, but the silent degradation behavior and host-visible iframe content mean teams that ignore the text-response contract or put secrets in forms risk tools that break invisibly or expose sensitive data.
- 01MCP Apps are the first official extension to the MCP spec, letting a tool return a UI resource rendered as a sandboxed iframe alongside its result.
- 02UI code is served via the `ui://` resource scheme and ships bundled inside the MCP server — it is fetched via MCP, not HTTP.
- 03Hosts that don't support MCP Apps silently ignore the `_meta.ui` property, so the text response must always contain the full answer.
Teal Larson's team spent two days building around twenty MCP Apps and documented the practical realities of the spec in its current state. MCP Apps are the first official extension to the MCP spec, allowing a tool to return a UI resource alongside its result via the `ui://` resource scheme — the UI is fetched via MCP, not HTTP, and ships bundled inside the MCP server itself. The team settled on a single Vite project under a `/ui` directory, configured to output one HTML file per TSX file at build time, which let them import their existing internal design system components directly.
A central lesson is that the text response remains the true contract.
A central lesson is that the text response remains the true contract. If a host does not support MCP Apps, the `_meta.ui` property is silently ignored and only the text response is shown. Larson warns that stuffing the answer into the UI while leaving the text response empty produces a tool that silently breaks in non-supporting clients — including Claude Code and most terminal-based clients. The recommended pattern is to treat the UI as a deterministic render of props passed from the tool, keeping the actual answer in the text response and the UI as enrichment on top.
Cross-host rendering is inconsistent and there is no standardized testing harness yet, making the iteration loop a manual install-and-eyeball process across multiple clients. Larson recommends designing for narrow widths from the start to handle the variation. On security: because the sandboxed iframe's content is visible to the host, secrets such as API keys and OAuth tokens should never be collected inside an MCP App — URL elicitation or a separate secure form outside the app is the recommended alternative. Larson notes that MCP Tasks are in an experimental phase and may eventually enable gathering tool inputs via UI before a tool call, a pattern that is not yet possible.
Key facts
- 01MCP Apps are the first official extension to the MCP spec, letting a tool return a UI resource rendered as a sandboxed iframe alongside its result.
- 02UI code is served via the `ui://` resource scheme and ships bundled inside the MCP server — it is fetched via MCP, not HTTP.
- 03Hosts that don't support MCP Apps silently ignore the `_meta.ui` property, so the text response must always contain the full answer.
- 04The team used a single Vite project under a `/ui` directory, outputting one HTML file per TSX file at build time, importing their existing design system.
- 05Container width, typography, and dark/light handling vary across hosts (ChatGPT, Claude, VS Code, mobile) with no standardized testing harness yet.
- 06Secrets like API keys and OAuth tokens should never be collected inside an MCP App because the iframe content is visible to the host.
- 07MCP Tasks are in an experimental phase and may eventually allow gathering tool inputs via UI before a tool call.
Topics
Summary and scoring are generated automatically from the original article. We always link back to the publisher and never republish images or paywalled content. Last processed Jun 20, 2026 · 08:55 UTC. How this works →