AI chat assistants are great at talking about your data. They’re less great at showing it to you - a wall of text is a poor substitute for a live chart, an editable list, or a client record you can actually interact with. The Model Context Protocol (MCP) Apps extension closes that gap. MCP Apps is an open, opt-in extension to the MCP Standard (SEP-1865, capability io.modelcontextprotocol/ui), that lets any MCP tool return a small, interactive web app or alongside the usual plain text, rendered in the conversation.
io.Intelligence, interop.io’s framework for bringing AI assistants and agents into io.Connect, ships full support for MCP Apps today, across both io.Assist (our ready-made chat assistant) and ai-web (the toolkit you can wire into your own chat UI). In this post I’ll walk through a an advisor workflow, and dig into how the pieces fit together: enabling MCP Apps, turning existing functionality into an app with almost no extra code, and making the apps feel native.
A day in the life of a financial advisor
Wealth advisors already use AI tools to work faster, but often need to copy and paste details from another app. It’s much easier if they can instead ask the assistant to pull the data securely and visibly. In io.Assist it looks like this:
Advisor: “Pull up Jane Smith’s profile.”
The assistant calls a tool and instead of printing a paragraph of client details, it renders a small Client Profile card right in the chat: name, segment, assets under management, risk profile, notes. The advisor can be sure that the data is accurate and not semi-hallucinated.
Advisor: “What’s on her watchlist?”
A second app appears: a Client Watchlist card listing her tracked instruments, each row with a little chart icon and a trash icon.
The advisor decides one of the names isn’t relevant anymore and clicks the trash icon next to it. The row disappears immediately, and because the app persists its state through the host, that row stays gone even if the advisor navigates away and reopens the same tool later.
Curious about one of the remaining names, the advisor clicks the chart icon next to it. That click doesn’t just update the current app - it sends a new message into the conversation (“Show me the chart for AAPL”), which the assistant picks up and answers by invoking a third tool. No need to lose time typing - just a click is enough to send the next relevant prompt.
An Instrument Chart app opens, drawn straight onto a canvas, no image round-trip needed.
When ready, the advisor clicks the close button on the chart, which tells the host that the app (and, by extension, the tool call behind it) is no longer needed. No dangling card left behind.
Now the advisor wants a fuller picture. They ask the assistant to lay out the client profile, the watchlist, and a market overview side by side.
Advisor: “Build me a workspace with Jane’s profile, her watchlist, and the market overview.”
Here, io.Intelligence’s built-in Workspace Widget kicks in. The assistant calls a tool that previews the proposed layout - a small thumbnail of how the apps would be arranged. And since building the layout is a meaningful action, the assistant pauses and asks for confirmation (an elicitation prompt) before doing anything.
If one of the requested apps is already open elsewhere, the advisor is offered a choice: replace the existing instance, replace all duplicates, or open a fresh one.
All of this: three purpose-built apps and a full workspace came from a handful of ordinary tool calls. That’s the point: once a tool exposes an MCP App, everything above comes for free from the host.
How io.Assist and ai-web host MCP Apps in io.Connect
Through io.Assist. If you’re using io.Assist (our drop-in AI chat assistant), MCP Apps support is a configuration flag, not a coding project. Enable the io.modelcontextprotocol/ui capability extension and point at a sandbox proxy, and any tool that returns an app-shaped resource renders inline automatically - in a floating card, inline in the chat, or as part of an io.Connect Workspace, depending on the display mode you choose.
aiWebConfig: {
mcp: {
clientsConfig: {
capabilities: {
extensions: {
"io.modelcontextprotocol/ui": {
mimeTypes: ["text/html;profile=mcp-app"],
},
},
},
},
mcpApps: {
sandboxProxyUrl: sandboxProxyUrl, // a page built on @interopio/mcp-apps-proxy
displayMode: "workspace", // or "inline"
},
},
},
That’s the whole integration surface: no custom rendering code, no manual iframe management. Every tool that comes back with an app-shaped resource is picked up automatically from here on.
Through ai-web, in your own chat UI. If you’re building a custom chat interface, @interopio/ai-web gives you the same MCP Apps machinery as a library: a bridge that manages the sandboxed iframe, forwards tool input/results to the app, and exposes maximize(), minimize(), close(), and status/message events so your UI can react to what the app is doing. You own the surrounding chrome; ai-web owns the protocol plumbing.
Either way, the apps themselves run in a sandboxed iframe, communicating with the host purely through postMessage - no app ever gets direct access to your page.
Exposing existing tools as MCP Apps
If you already have a working tool it’s very straightforward to enable MCP Apps. io.Intelligence’s MCP server recognizes two flavors of “method tools” (tools backed by an interop method rather than hand-written MCP server code) and both can return an app.
Static method tools are declared once, centrally, in configuration: a name, an input/output schema, and which interop method backs it. They can be constant (always listed, regardless of whether anything is currently serving that method) or variable (listed only while a matching method is actually registered somewhere on the network). If you already expose functionality this way, adding an MCP App to it is just adding a resource to that same config entry - no new code path.
Dynamic method tools need no central configuration at all. Any connected application can register an interop method at runtime with a special flag, and io.Intelligence’s dynamic tool tracker picks it up automatically - tool appears, tool disappears, entirely driven by whether the method is currently registered:
await io.interop.register(
{
name: "advisor_get_client_watchlist",
description: "Get the instrument watchlist of an advisory client by name.",
flags: {
ioIntelMCPTool: {
name: "advisor_get_client_watchlist",
title: "Client Watchlist",
inputSchema: JSON.stringify({
type: "object",
properties: { clientName: { type: "string" } },
required: ["clientName"],
}),
resource: clientWatchlistAppHtml, // a self-contained HTML document
},
},
},
async (args) => lookupWatchlist(args.clientName),
);
That’s everything you need to “turn a function into an MCP App”: register the method, attach the ioIntelMCPTool flag with a schema and a resource, and return normal data from the handler. The three apps in the demo above are built exactly this way.
On the app side, guest apps use the official @modelcontextprotocol/ext-apps SDK to speak the standard protocol - a short App/PostMessageTransport handshake, then event listeners for receiving tool input and tool results:
The SDK is a convenience, not a requirement: the protocol itself is plain JSON-RPC 2.0 over postMessage, so an app can speak it directly with window.postMessage/window.addEventListener(“message”, …) and no dependency at all - that’s exactly how our own built-in Workspace Widget is implemented.
<script type="module">
import { App, PostMessageTransport } from "https://esm.sh/@modelcontextprotocol/ext-apps/app-with-deps";
const app = new App({ name: "client-watchlist-app", version: "1.0.0" }, {});
app.addEventListener("toolresult", (params) => render(params.structuredContent));
await app.connect(new PostMessageTransport(window.parent, window.parent));
</script>
Custom notifications: maximize, minimize, close, and state that survives
The standard MCP Apps protocol gives you the handshake, tool input/result delivery, and a request to change display mode (inline vs. fullscreen vs. picture-in-picture). io.Intelligence layers a small set of additional notifications on top, aimed at one problem in particular: making an app feel like a first-class part of the host, not an embedded guest.
ui/notifications/close-ws-app- sent app → host, asking the host to tear the app down. In the demo, closing the Instrument Chart marks that tool call as “done” from the host’s perspective.ui/notifications/save-state- sent app → host to persist the app’s state (the watchlist’s removed rows, for instance), keyed by the tool call that produced it. The host answers withui/notifications/save-state-responseso the app knows whether the save succeeded.ui/notifications/load-state- sent host → app to hand the saved state back when the app is shown again.ui/notifications/response-generation-status- sent host → app; tells the app whether the assistant is still generating a response, useful for apps that want to show a pending indicator while they wait.
These are plain JSON-RPC notifications sent over the same postMessage channel the standard protocol already uses - no proprietary SDK required to send or receive them:
window.parent.postMessage(
{ jsonrpc: "2.0", method: "ui/notifications/close-ws-app", params: {} },
"*",
);
Maximize and minimize, meanwhile, use the standard protocol’s display-mode request - the app asks to switch to fullscreen or back to inline, and the host decides whether that’s allowed:
Note: The spec also defines a third display mode, pip (picture-in-picture, a floating overlay). io.Intelligence does not support pip today - io.Assist and ai-web only ever advertise inline and fullscreen as available display modes, so a spec-compliant app will never be offered pip as an option to request.
const result = await app.requestDisplayMode({ mode: "fullscreen" });
Put together, these notifications are what let an MCP App open docked in the chat, pop out to fullscreen, remember what the user did to it, and close itself cleanly when its job is finished — all without the host needing to know anything specific about that particular app.
The ai-web APIs, briefly
If you’re not using io.Assist and want to host MCP Apps in your own chat surface, @interopio/ai-web is the layer to reach for. In short:
- Configuration - enable the
io.modelcontextprotocol/uicapability extension and pointmcpApps.sandboxProxyUrlat the sandbox proxy that will host the iframes; optionally set a defaultdisplayMode. - Bridge - ai-web owns the connection between your chat UI and each app instance: it forwards tool input/result notifications into the iframe and relays the app’s requests (display mode, messages, open-link, read-resource) back out.
- Instance API - each running app exposes a small surface to your UI code:
sendMessage,onMessage,onStatusChange,onDisplayModeChange,close(),maximize(),minimize(), and anisMaximizedflag — everything you need to build your own chrome around an app without reimplementing the protocol.
When MCP Apps are worth using
A few places this pays for itself quickly:
- Turning read-only answers into actionable UI. A tool that used to return “here are 4 stocks” can instead return a watchlist you can trim, chart, and act on - without leaving the conversation.
- Reusing what you’ve already built. If your application already exposes functionality over interop, making it available as an MCP App is a config change plus a small self-contained HTML file, not a new integration project.
- Composable workspaces, not isolated widgets. Because MCP Apps run inside io.Connect, a handful of simple apps can be arranged by the assistant into a full multi-app workspace on request - something a single chat response could never do on its own.
MCP Apps don’t replace the conversation - they give it something to point at.
Learn more
- MCP Apps overview: display modes, lifecycle and the custom notifications
- Workspace Widget: the built-in app behind the workspace demo
- Add support for MCP Apps: step-by-step chapter in the io.Assist Anywhere guide
- MCP Core tool types: static and dynamic method tools and their
resourcefield - AI Web configuration:
mcpAppsand capability settings - SEP-1865: MCP Apps and the
@modelcontextprotocol/ext-appsSDK - Understanding io.Intelligence’s building blocks






