MCP Apps

MCP Apps enable servers to return interactive HTML interfaces (data visualizations, forms, dashboards) that render directly within AI conversations.

Why MCP Apps

Traditional text responses have limitations. Sometimes users need to interact with data, not just read about it. MCP Apps offer key advantages over standalone web applications:

Context Preservation

The app lives inside the conversation. Users don’t switch tabs, lose their place, or wonder which chat thread had that dashboard. The UI is right there, alongside the discussion that led to it.

Bidirectional Data Flow

Your app can call any tool on the MCP server, and the host can push fresh results to your app. This is achieved through existing MCP patterns without requiring separate APIs, authentication, or state management.

Host Integration

The app can delegate actions to the host, which then invokes the capabilities and tools the user has already connected. Instead of every app implementing direct integrations, the app requests an outcome (like “schedule this meeting”), and the host routes it through the user’s existing connected capabilities.

Security Guarantees

MCP Apps run in a sandboxed iframe controlled by the host. They can’t access the parent page, steal cookies, or escape their container. This means hosts can safely render third-party apps without trusting the server author completely.

  sequenceDiagram
    participant User
    participant Agent
    participant App as MCP App iframe
    participant Server as MCP Server

    User->>Agent: "show me analytics"
    Note over User,App: Interactive app rendered in chat
    Agent->>Server: tools/call
    Server-->>Agent: tool input/result
    Agent-->>App: tool result pushed to app
    User->>App: user interacts
    App->>Agent: tools/call request
    Agent->>Server: tools/call (forwarded)
    Server-->>Agent: fresh data
    Agent-->>App: fresh data
    Note over User,App: App updates with new data
    App-->>Agent: context update

When to Use MCP Apps

Use CaseDescription
Complex Data ExplorationInteractive maps, drill-down charts, toggleable metrics
Multi-Option ConfigurationForms with validation, defaults, interdependent choices
Rich Media ViewingPDF review, 3D models, image previews with pan/zoom
Real-Time MonitoringLive dashboards, metrics, logs, system status
Multi-Step WorkflowsExpense reports, code reviews, issue triage

How MCP Apps Work

Traditional MCP tools return text, images, or structured data. MCP Apps extend this pattern by allowing tools to declare a reference to an interactive UI in their description that the host renders in place.

The core pattern combines two MCP primitives:

  1. A tool that declares a UI resource via _meta.ui.resourceUri
  2. A UI resource that renders data as an interactive HTML interface

Workflow

  1. UI Preloading: The tool description includes a _meta.ui.resourceUri field pointing to a ui:// resource. The host can preload this resource before the tool is called.

  2. Resource Fetch: The host fetches the UI resource from the server. This resource contains an HTML page, often bundled with its JavaScript and CSS.

  3. Sandboxed Rendering: Web hosts render the HTML inside a sandboxed iframe within the conversation. The sandbox restricts the app’s access to the parent page.

  4. Bidirectional Communication: The app and host communicate through a JSON-RPC protocol. The app can request tool calls, send messages, update the model’s context, and receive data from the host.

Quick Start

Prerequisites

Using an AI Coding Agent

The fastest way to create an MCP App is using an AI coding agent with the MCP Apps skill.

Install the Skill

For Claude Code:

/plugin marketplace add modelcontextprotocol/ext-apps
/plugin install mcp-apps@modelcontextprotocol-ext-apps

Or use the Vercel Skills CLI:

npx skills add modelcontextprotocol/ext-apps

Create Your App

Ask your AI coding agent:

Create an MCP App that displays a color picker

The agent will scaffold a complete project with server, UI, and configuration files.

Run Your App

npm install && npm run build && npm run serve

Manual Setup

If you prefer manual setup, follow these steps:

Project Structure

my-mcp-app/
├── package.json
├── tsconfig.json
├── vite.config.ts
├── server.ts          # MCP server with tool + resource
├── mcp-app.html       # UI entry point
└── src/
    └── mcp-app.ts     # UI logic

Install Dependencies

npm install @modelcontextprotocol/ext-apps @modelcontextprotocol/sdk
npm install -D typescript vite vite-plugin-singlefile express cors @types/express @types/cors tsx

Configure the Project

package.json:

{
  "type": "module",
  "scripts": {
    "build": "INPUT=mcp-app.html vite build",
    "serve": "npx tsx server.ts"
  }
}

tsconfig.json:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "strict": true,
    "outDir": "dist"
  },
  "include": ["*.ts", "src/**/*.ts"]
}

vite.config.ts:

import { defineConfig } from "vite";
import { viteSingleFile } from "vite-plugin-singlefile";

export default defineConfig({
  plugins: [viteSingleFile()],
  build: {
    outDir: "dist",
    rollupOptions: {
      input: process.env.INPUT,
    },
  },
});

Building an MCP App

Let’s build a simple app that displays the current server time.

Server Implementation

The server needs to register a tool with _meta.ui.resourceUri and serve the bundled HTML as a resource.

// server.ts
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import {
  registerAppTool,
  registerAppResource,
  RESOURCE_MIME_TYPE,
} from "@modelcontextprotocol/ext-apps/server";
import cors from "cors";
import express from "express";
import fs from "node:fs/promises";
import path from "node:path";

const server = new McpServer({
  name: "My MCP App Server",
  version: "1.0.0",
});

// ui:// scheme tells hosts this is an MCP App resource
const resourceUri = "ui://get-time/mcp-app.html";

// Register tool with UI metadata
registerAppTool(
  server,
  "get-time",
  {
    title: "Get Time",
    description: "Returns the current server time.",
    inputSchema: {},
    _meta: { ui: { resourceUri } },
  },
  async () => {
    const time = new Date().toISOString();
    return {
      content: [{ type: "text", text: time }],
    };
  },
);

// Register resource to serve bundled HTML
registerAppResource(
  server,
  resourceUri,
  resourceUri,
  { mimeType: RESOURCE_MIME_TYPE },
  async () => {
    const html = await fs.readFile(
      path.join(import.meta.dirname, "dist", "mcp-app.html"),
      "utf-8",
    );
    return {
      contents: [
        { uri: resourceUri, mimeType: RESOURCE_MIME_TYPE, text: html },
      ],
    };
  },
);

// Expose MCP server over HTTP
const expressApp = express();
expressApp.use(cors());
expressApp.use(express.json());

expressApp.post("/mcp", async (req, res) => {
  const transport = new StreamableHTTPServerTransport({
    sessionIdGenerator: undefined,
    enableJsonResponse: true,
  });
  res.on("close", () => transport.close());
  await server.connect(transport);
  await transport.handleRequest(req, res, req.body);
});

expressApp.listen(3001, () => {
  console.log("Server listening on http://localhost:3001/mcp");
});

UI Implementation

mcp-app.html:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>Get Time App</title>
  </head>
  <body>
    <p>
      <strong>Server Time:</strong>
      <code id="server-time">Loading...</code>
    </p>
    <button id="get-time-btn">Get Server Time</button>
    <script type="module" src="/src/mcp-app.ts"></script>
  </body>
</html>

src/mcp-app.ts:

import { App } from "@modelcontextprotocol/ext-apps";

const serverTimeEl = document.getElementById("server-time")!;
const getTimeBtn = document.getElementById("get-time-btn")!;

const app = new App({ name: "Get Time App", version: "1.0.0" });

// Establish communication with the host
app.connect();

// Handle initial tool result
app.ontoolresult = (result) => {
  const time = result.content?.find((c) => c.type === "text")?.text;
  serverTimeEl.textContent = time ?? "[ERROR]";
};

// Call tool on button click
getTimeBtn.addEventListener("click", async () => {
  const result = await app.callServerTool({
    name: "get-time",
    arguments: {},
  });
  const time = result.content?.find((c) => c.type === "text")?.text;
  serverTimeEl.textContent = time ?? "[ERROR]";
});

Testing Your App

Build and Run

npm run build && npm run serve

Your server will be available at http://localhost:3001/mcp.

Testing with Claude

For local development, expose your server to the internet using cloudflared:

npx cloudflared tunnel --url http://localhost:3001

Copy the generated URL and add it as a custom connector in Claude:

  1. Click your profile → Settings → Connectors
  2. Click “Add custom connector”
  3. Paste the URL

Testing with Basic Host

The ext-apps repository includes a test host:

git clone https://github.com/modelcontextprotocol/ext-apps.git
cd ext-apps/examples/basic-host
npm install
SERVERS='["http://localhost:3001/mcp"]' npm start

Navigate to http://localhost:8080 to test your app.

Security Model

MCP Apps run in a sandboxed iframe, providing strong isolation from the host application. The sandbox prevents:

  • Access to the parent window’s DOM
  • Reading the host’s cookies or local storage
  • Navigating the parent page
  • Executing scripts in the parent context

All communication goes through the postMessage API, which the App class abstracts. The host controls which capabilities your app can access.

Framework Support

MCP Apps use standard web primitives (postMessage, iframe). You can use any framework:

  • React, Vue, Svelte, Preact, Solid
  • Vanilla JavaScript

The App class is a convenience wrapper, not a requirement. You can implement the postMessage protocol directly if preferred.

Client Support

MCP Apps is an extension to the core MCP specification. Host support varies by client.

Currently supported by:

  • Claude (web and Desktop)
  • Visual Studio Code (Insiders)
  • Goose, Postman, MCPJam

For building MCP client support:

  1. Use @mcp-ui/client for React components
  2. Build on App Bridge from the SDK for iframe rendering and message passing

Related Resources