--- title: Quickstart --- # 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. ## What You'll Build 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`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-server-vanillajs). ## Prerequisites - Familiarity with MCP concepts, especially [Tools](https://modelcontextprotocol.io/docs/learn/server-concepts#tools) and [Resources](https://modelcontextprotocol.io/docs/learn/server-concepts#resources) - Familiarity with the [MCP TypeScript SDK](https://github.com/modelcontextprotocol/typescript-sdk) - Node.js 18+ > [!TIP] > New to building MCP servers? Start with the [official MCP quickstart guide](https://modelcontextprotocol.io/docs/develop/build-server) to learn the core concepts first. ## 1. Project Setup Create a new directory and initialize: ```bash mkdir my-mcp-app && cd my-mcp-app npm init -y ``` Install dependencies: ```bash 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`: ```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: ```typescript 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`: ```json { "type": "module", "scripts": { "build": "INPUT=mcp-app.html vite build", "serve": "npx tsx server.ts" } } ``` > [!NOTE] > **Full files:** [`package.json`](https://github.com/modelcontextprotocol/ext-apps/blob/main/examples/basic-server-vanillajs/package.json), [`tsconfig.json`](https://github.com/modelcontextprotocol/ext-apps/blob/main/examples/basic-server-vanillajs/tsconfig.json), [`vite.config.ts`](https://github.com/modelcontextprotocol/ext-apps/blob/main/examples/basic-server-vanillajs/vite.config.ts) ## 2. Create the Server 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`: ```typescript 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`](https://github.com/modelcontextprotocol/ext-apps/blob/main/examples/basic-server-vanillajs/server.ts) Then, verify your server compiles: ```bash npx tsc --noEmit ``` No output means success. If you see errors, check for typos in `server.ts`. ## 3. Build the UI Create `mcp-app.html`: ```html
Server Time: Loading...