@modelcontextprotocol/ext-apps - v0.0.1
    Preparing search index...

    Build Your First MCP App

    This tutorial walks you through building an MCP App—a tool with an interactive UI that renders inside MCP hosts like Claude Desktop.

    A simple app that fetches the current server time and displays it in a clickable UI. You'll learn the core pattern: MCP Apps = Tool + UI Resource.

    Create a new directory and initialize:

    mkdir my-mcp-app && cd my-mcp-app
    npm init -y

    Install dependencies:

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

    Create tsconfig.json:

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

    Create vite.config.ts — this bundles your UI into a single HTML file:

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

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

    Add to your package.json:

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

    Full files: package.json, tsconfig.json, vite.config.ts

    MCP Apps use a two-part registration:

    1. A tool that the LLM/host calls
    2. A resource that serves the UI HTML

    The tool's _meta field links them together.

    Create server.ts:

    import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
    import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
    import { RESOURCE_URI_META_KEY } from "@modelcontextprotocol/ext-apps";
    import cors from "cors";
    import express from "express";
    import fs from "node:fs/promises";
    import path from "node:path";
    import { z } from "zod";

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

    // Two-part registration: tool + resource
    const resourceUri = "ui://get-time/mcp-app.html";

    server.registerTool(
    "get-time",
    {
    title: "Get Time",
    description: "Returns the current server time.",
    inputSchema: {},
    outputSchema: { time: z.string() },
    _meta: { [RESOURCE_URI_META_KEY]: resourceUri }, // Links tool to UI
    },
    async () => {
    const time = new Date().toISOString();
    return {
    content: [{ type: "text", text: time }],
    structuredContent: { time },
    };
    },
    );

    server.registerResource(resourceUri, resourceUri, {}, async () => {
    const html = await fs.readFile(
    path.join(import.meta.dirname, "dist", "mcp-app.html"),
    "utf-8",
    );
    return {
    contents: [{ uri: resourceUri, mimeType: "text/html+mcp", text: html }],
    };
    });

    // Express server for MCP endpoint
    const app = express();
    app.use(cors());
    app.use(express.json());

    app.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);
    });

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

    Full file: server.ts

    Create 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>

    Create src/mcp-app.ts:

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

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

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

    // Register handlers BEFORE connecting
    app.ontoolresult = (result) => {
    const { time } = (result.structuredContent as { time?: string }) ?? {};
    serverTimeEl.textContent = time ?? "[ERROR]";
    };

    // Wire up button click
    getTimeBtn.addEventListener("click", async () => {
    const result = await app.callServerTool({ name: "get-time", arguments: {} });
    const { time } = (result.structuredContent as { time?: string }) ?? {};
    serverTimeEl.textContent = time ?? "[ERROR]";
    });

    // Connect to host
    app.connect(new PostMessageTransport(window.parent));

    Build the UI:

    npm run build
    

    This produces dist/mcp-app.html containing your bundled app.

    Full files: mcp-app.html, src/mcp-app.ts

    You'll need two terminals.

    Terminal 1 — Build and start your server:

    npm run build && npm run serve
    

    Terminal 2 — Run the test host (from the ext-apps repo):

    git clone https://github.com/modelcontextprotocol/ext-apps.git
    cd ext-apps/examples/basic-host
    npm install
    npm run start

    Open http://localhost:8080 in your browser:

    1. Select get-time from the "Tool Name" dropdown
    2. Click Call Tool
    3. Your UI renders in the sandbox below
    4. Click Get Server Time — the current time appears!