@modelcontextprotocol/ext-apps - v0.4.0
    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.

    Note

    The complete example is available at examples/basic-server-vanillajs.

    Tip

    New to building MCP servers? Start with the official MCP quickstart guide to learn the core concepts first.

    Create a new directory and initialize:

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

    Install dependencies:

    npm install @modelcontextprotocol/ext-apps @modelcontextprotocol/sdk
    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"
    }
    }

    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 {
    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",
    });

    // Two-part registration: tool + resource, tied together by the resource URI.
    const resourceUri = "ui://get-time/mcp-app.html";

    // Register a tool with UI metadata. When the host calls this tool, it reads
    // `_meta.ui.resourceUri` to know which resource to fetch and render as an
    // interactive UI.
    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 the resource, which returns the bundled HTML/JavaScript for the UI.
    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 },
    ],
    };
    },
    );

    // Start an Express server that exposes the MCP endpoint.
    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, (err) => {
    if (err) {
    console.error("Error starting server:", err);
    process.exit(1);
    }
    console.log("Server listening on http://localhost:3001/mcp");
    });
    Note

    Full file: server.ts

    Then, verify your server compiles:

    npx tsc --noEmit
    

    No output means success. If you see errors, check for typos in 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 } 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.content?.find((c) => c.type === "text")?.text;
    serverTimeEl.textContent = time ?? "[ERROR]";
    };

    // Wire up 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]";
    });

    // Connect to host
    app.connect();
    Note

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

    Build the UI:

    npm run build
    

    This produces dist/mcp-app.html which contains your bundled app:

    $ ls dist/mcp-app.html
    dist/mcp-app.html

    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!