An agent can reach your data. It cannot interact with the interface that makes that data explorable. The answer comes back as text, or a screenshot. The interactive chart stays behind a login.
Every MCP tool we built for our own agents is now available to any agent the user runs: Claude, Codex, Cursor, and whatever the team picks next. They query the same AI-native lakehouse, the same semantic layer.
None of them can hand you the live interface: the chart you hover, the query you re-run, the cohort you re-bucket. So the conversation ends in text, or "open the platform to see more."
MCP Apps close that gap. The platform doesn't move. Its reach does.
Instead of building adapters for each host, we expose the same charts, queries, and interactive components through MCP. They render inside Claude today, and in any other host that adopts MCP Apps.
Some answers need pixels
MCP tools return text, images, or structured JSON. That's enough for "what was our MRR last month." It isn't enough for "compare retention across last quarter's cohorts and let me re-bucket by plan." A chart you can hover, a form you can configure, a query you can iterate on.
In Under the Hood: Agents, we wrote that anything a human can do in the UI, an agent can do through MCP. MCP Apps let us run that the other way.
Anything an agent surfaces through MCP, a human can now touch, inside the chat, without switching tabs.
What MCP Apps actually are
MCP Apps is an extension to the Model Context Protocol, formally SEP-1865, drafted in November 2025 by Ido Salomon, Liad Yosef, and Olivier Chafik, now at Final status. It unifies two parallel efforts that had been prototyping interactive UIs on top of MCP, MCP-UI and OpenAI's Apps SDK, into one open standard.
The protocol is small. Four primitives do the work:
ui://URIs: Every MCP App UI is aui://resource. Hosts can discover, prefetch, and audit the HTML before it renders.- MIME type profile: Content is served as
text/html;profile=mcp-app. Theprofile=suffix is how hosts recognize the bytes as a renderable app, not arbitrary HTML. - Tool-to-UI binding: A tool points to its UI via
_meta.ui.resourceUri. That one field is what separates a regular MCP tool from an MCP App tool. postMessagetransport: The app runs inside a sandboxed iframe and talks back to the host overpostMessageusing JSON-RPC, the same wire format as the rest of MCP. Shared methods where they fit (tools/call),ui/-prefixed methods where they don't (ui/initialize).
The security model is layered. The app runs inside a sandboxed iframe, isolated from the host's DOM and cookies. Templates are predeclared and reviewable before they render. Every message between UI and host travels over postMessage and is auditable. And the host mediates any tool call the app initiates.
One protocol, every compliant host, no special deal to sign.
Unlike a URL, an MCP App can talk back: variable changes, fresh agent results, and tool re-runs all happen through the host.
One renderer, two venues
The design decision worth naming up front: we didn't build a second renderer. The chart that renders inside Claude when an agent calls view_insight is built from the same chart components that render on altertable.ai. Same code, same series builders, same legends. The MCP App is how those components travel. It isn't a parallel implementation we have to keep in sync.
That symmetry is the whole point. We build one component library for our own product, and MCP Apps make it available to any AI agent, internal or external, without a bespoke integration per host. One library, two surfaces. Nothing to keep in sync, nothing to re-secure.
Take the SQL Playground. The component used for query_lakehouse results renders inside Claude and is the same one our in-product chat uses. Same SQL editor, same result table, same formatter. The MCP App is how it travels outside the platform. The chat is how it travels inside. Two surfaces. One experience.
The lakehouse matters here specifically. Rendering inside the agent puts agent and human on the same source of truth, at the same instant. Not a sample. Not a stale snapshot.
Insight Viewer
The Insight Viewer renders the chart view_insight returns (segmentation, funnel, or retention), with a variable editor above it when the insight exposes parameters, so the user can tweak inputs and watch the chart re-run in place.
Insight Viewer running inside Claude Code
SQL Playground
The SQL Playground shows the SQL query the agent ran and its results side by side. The statement is pre-loaded into a lightweight SQL editor, and the rows render through the same QueryResultTable that ships in altertable.ai. The user can read, tweak, or re-run the query against the lakehouse, right there in the chat.
SQL Playground running inside Claude Code
How it ships
Backend
The whole MCP Apps surface on our Rails side, built on RubyLLM, lives in a single module plus one resource class per app. A tool becomes an MCP App tool by adding one line:
class ViewInsight < ApplicationTooltool_name 'view_insight'meta ui: MCPAppUi.meta(MCPAppUi::INSIGHT_VIEWER_URI)# [...]end
The matching resource class declares its URI and MIME type, then reads the app's HTML straight from /mcp-apps/* at request time rather than baking it into the Rails asset pipeline. The UI ships without a backend redeploy.
That request-time read is a small choice with a big quality-of-life win during iteration. The MCP mount itself auto-discovers every registered resource and wires a single handler that injects the _meta.ui payload the spec requires on every app response, leaving no registry or dispatch to maintain by hand.
Frontend
Every MCP App in mcp-apps/ is a single self-contained HTML file. The build inlines CSS, JS, and assets so the ui:// resource fetches one response and renders. No chunked loading from a sandboxed iframe. It's a deliberate shortcut: simple to ship and audit today, but heavy enough that we won't keep it as is.
A shared McpAppShell component handles the protocol boilerplate: the handshake (via @modelcontextprotocol/ext-apps, the reference SDK), result parsing, theme, and the frame-check guard that surfaces a clear error if the app gets loaded standalone. Each app lives above the shell and imports straight from the product tree. The Insight Viewer calls view_insight again when a user tweaks a variable. The SQL Playground does the same with its SQL editor and query_lakehouse. The production chart and table components don't know or care that they're running inside a sandboxed iframe, inside someone else's chat.
One app or many?
The spec lets you fuse everything into one app, or split by UI shape. We split by shape, not by tool: view_insight and preview_insight share the Insight Viewer, query_lakehouse has the SQL Playground.
Each ui:// is a self-contained bundle, and bundles pay for what they include. The Insight Viewer ships at 479 KB gzipped, the SQL Playground at 296 KB. A fused bundle would ship Recharts on every SQL call and the SQL editor on every chart call.
A new UI is a new directory under mcp-apps/. Vite picks it up. Tools that share a UI share its ui://. Nothing gets everything.
No adapters, no winners
The other way to do this would have been to build a Claude integration. Then a Codex integration. Then a Cursor one, then a Gemini one, and whatever ships next week. We would have picked winners, written adapters, and spent the next year chasing SDKs.
MCP is the bet that we don't have to do any of that. It's the neutral surface between the lakehouse and whatever agent the user prefers using. External agents come in through the same OAuth flow we built for our own. Their tokens hit the same scopes, their tool calls hit the same handlers. Internal, external, vendor, or the agent your platform team built last quarter: they're all MCP clients, and none of them is a special case.
Alpic AI is building Skybridge on the same standard, a likely fit as we move off our single-HTML shortcut.
Each step pushed the boundary further out. Local first. Then remote. Now the interface.
The lakehouse stops being a destination and becomes portable.






