Skip to main content

MCP Apps: Adding Interactive UIs to the Model Context Protocol

· 7 min read
MCPBundles

The Model Context Protocol just got a major upgrade. MCP Apps (SEP-1865) is a new extension that lets MCP servers deliver interactive user interfaces directly to AI applications like Claude, ChatGPT, or Cursor.

This isn't just about pretty visuals. It's about giving your AI tools the ability to show data in ways that actually make sense—charts, tables, dashboards, forms—while maintaining the security and auditability that MCP was built on.

Developer viewing interactive dashboard with charts and graphs

What's the Problem?

MCP tools today return text or structured data. That works fine for simple responses. But what if your tool needs to:

  • Display a dashboard with real-time metrics
  • Show a chart comparing quarterly sales data
  • Present a form for data entry with validation
  • Render a complex table with sorting and filtering

You can't do that with plain text. You need UI.

Before MCP Apps, every host (Claude Desktop, ChatGPT, Cursor) had to invent their own way of handling rich UIs. MCP-UI and OpenAI's Apps SDK both tackled this problem independently, but without a standard, developers had to build adapters for each platform.

That fragmentation is exactly what MCP Apps solves.

How It Works

The architecture is clean. MCP Apps introduces three key concepts:

1. UI Resources

A new type of resource identified by the ui:// URI scheme. When your server declares a UI resource, it's saying "I have an interactive interface available at this location."

{
uri: "ui://affinity-dashboard",
name: "affinity_dashboard_ui",
description: "Interactive Affinity CRM dashboard",
mimeType: "text/html+mcp"
}

The initial spec focuses on HTML (text/html+mcp), but the architecture leaves room for future formats like declarative UI or remote DOM.

2. Tool Metadata Linking

Tools reference UI resources through a _meta field:

{
name: "affinity_dashboard",
description: "Open interactive Affinity CRM dashboard",
_meta: {
"ui/resourceUri": "ui://affinity-dashboard"
}
}

When an AI calls this tool, the host knows there's a UI available. The host can then fetch the UI resource and render it.

3. Bidirectional Communication

Here's where it gets interesting. The UI runs in a sandboxed iframe, but it can communicate back to the host using standard MCP JSON-RPC messages over postMessage.

From inside the iframe, you can:

// Call another MCP tool
window.parent.postMessage({
jsonrpc: "2.0",
method: "tools/call",
params: {
name: "affinity_list_companies",
arguments: { limit: 100 }
}
}, '*');

// Read a resource
window.parent.postMessage({
jsonrpc: "2.0",
method: "resources/read",
params: { uri: "data://companies" }
}, '*');

The host proxies these requests back to your MCP server and sends results via notifications:

// Host sends result back to iframe
{
method: "ui/notifications/tool-result",
params: {
requestId: "...",
result: { /* tool response */ }
}
}

This creates a full request/response cycle where your UI can stay interactive and dynamic while maintaining MCP's security model.

The Security Model

Everything runs in a sandboxed iframe with strict Content Security Policy controls. The UI can't make arbitrary network requests—it has to go through the host using MCP protocol messages.

Sandboxed iframe showing security isolation

This means:

  • All communication is auditable (it's JSON-RPC messages)
  • The host can log, inspect, or block any request
  • Users can see exactly what data is being accessed
  • No hidden API calls or data exfiltration

What This Means for Developers

If you're building MCP servers, you can now:

  1. Return HTML from a resource endpoint
@mcp.resource("ui://dashboard")
async def get_dashboard():
html = await render_dashboard()
return {
"uri": "ui://dashboard",
"mimeType": "text/html+mcp",
"text": html
}
  1. Link tools to UIs via metadata
@mcp.tool()
async def show_dashboard():
return {
"content": [{"type": "text", "text": "Dashboard loaded"}],
"_meta": {"ui/resourceUri": "ui://dashboard"}
}
  1. Build interactive UIs that call back to your server

Your HTML can use JavaScript to make tool calls and resource reads, creating a fully interactive experience.

The HTML you return is just a string. How you generate it—template engines, component libraries, hand-written code—is up to you. The protocol doesn't care about your implementation details.

Real-World Example: CRM Dashboard

Let's say you're building an Affinity CRM integration. You want a dashboard showing:

  • Recent companies
  • Active opportunities
  • Pipeline status

Interactive CRM dashboard with charts and data visualizations

Step 1: Define the tool

@mcp.tool()
async def affinity_dashboard():
"""Open interactive Affinity CRM dashboard"""
return {
"content": [{"type": "text", "text": "Dashboard opened"}],
"_meta": {"ui/resourceUri": "ui://affinity-dashboard"}
}

Step 2: Create the UI resource

@mcp.resource("ui://affinity-dashboard")
async def get_affinity_dashboard():
# Fetch initial data
companies = await call_tool("affinity_list_companies", {"limit": 10})
opportunities = await call_tool("affinity_list_opportunities", {"limit": 10})

# Generate HTML however you want
html = generate_dashboard_html(companies, opportunities)

return {
"uri": "ui://affinity-dashboard",
"mimeType": "text/html+mcp",
"text": html
}

The HTML you return can include JavaScript that uses window.parent.postMessage() to trigger tool calls for filtering, sorting, or loading more data. The host will proxy those requests back to your server and send results via notifications.

What Hosts Need to Support

For MCP client developers (building Claude Desktop, Cursor, etc.), you need to:

  1. Detect UI resources when tools are called
  2. Fetch the HTML via resources/read
  3. Render in a sandboxed iframe
  4. Listen for postMessage events from the iframe
  5. Proxy MCP requests from the iframe to the server
  6. Send results back via ui/notifications/*

The spec defines all the message types and flows. It's based on JSON-RPC, so if you're already handling MCP, you're 90% there.

Why This Matters

MCP Apps unifies what MCP-UI and Apps SDK pioneered. It means:

  • Developers write once, works in all MCP clients
  • Hosts get a standard to implement against
  • Users get consistency across different AI tools
  • Security stays strong through sandboxing and auditability

And it's backwards compatible. If a host doesn't support MCP Apps, your tools still work—they just won't show UIs.

Getting Started

If you're building MCP servers:

  1. Read SEP-1865 (the spec)
  2. Check out MCP-UI's SDKs for reference implementations
  3. Start simple: return basic HTML, then build up to more complex UIs

If you're building MCP clients:

  1. Implement the io.modelcontextprotocol/ui extension
  2. Add iframe rendering with proper sandboxing
  3. Handle the postMessage bridge for tool calls and notifications

What's Next

The spec currently focuses on HTML, but there's room for:

  • Declarative UI (JSON-based UI descriptions)
  • Remote DOM (Shopify's approach for framework-agnostic UIs)
  • Streaming updates (real-time data pushed to UIs)
  • Persistent state (UIs that remember context across sessions)

MCP Apps is an extension, not a core requirement. But as more hosts adopt it, expect to see richer, more interactive AI tools that go way beyond text responses.

Conclusion

MCP Apps brings interactive UIs to the Model Context Protocol without breaking its security model or auditability. It's an optional extension that unifies existing approaches (MCP-UI, Apps SDK) into an open standard.

For developers, it means you can build tools that show charts, dashboards, and forms—not just text. For users, it means AI interactions can be visual and interactive when that makes sense.

The protocol is live, implementations are starting, and the ecosystem is moving fast. If you're building MCP servers or clients, now's the time to explore what interactive UIs can do for your tools.


Want to try MCP Apps? Check out MCPBundles to see how we're implementing dynamic UI resources across hundreds of provider integrations.

References