This tutorial walks you through building an MCP App—a tool with an interactive View (a UI that renders inside an iframe) that displays in MCP hosts like Claude Desktop.
Feel like vibe coding instead? Try the MCP Apps agent skills.
A simple app that fetches the current server time and displays it in an interactive View. You'll learn the core pattern: MCP Apps = Tool + UI Resource. The complete example is available in examples/quickstart.
This tutorial assumes you've built an MCP server before and are comfortable with Tools and Resources. If not, the official MCP quickstart is a good place to start.
We'll use the MCP TypeScript SDK to build the server.
You'll also need Node.js 18+.
We'll set up a minimal TypeScript project with Vite for bundling.
Start by creating a project directory:
mkdir my-mcp-app && cd my-mcp-app
Install the dependencies you'll need:
npm init -y
npm install @modelcontextprotocol/ext-apps @modelcontextprotocol/sdk express cors
npm install -D typescript vite vite-plugin-singlefile @types/express @types/cors @types/node tsx concurrently cross-env
Configure your package.json:
npm pkg set type=module
npm pkg set scripts.build="tsc --noEmit && tsc -p tsconfig.server.json && cross-env INPUT=mcp-app.html vite build"
npm pkg set scripts.start='concurrently "cross-env NODE_ENV=development INPUT=mcp-app.html vite build --watch" "tsx watch main.ts"'
Windows cmd.exe users will need to convert quotes in the above command: npm pkg set scripts.start="concurrently ""cross-env NODE_ENV=development INPUT=mcp-app.html vite build --watch"" ""tsx watch main.ts""".
tsconfig.json:{
"compilerOptions": {
"target": "ESNext",
"lib": ["ESNext", "DOM", "DOM.Iterable"],
"module": "ESNext",
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"verbatimModuleSyntax": true,
"noEmit": true,
"strict": true,
"skipLibCheck": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src", "server.ts", "main.ts"]
}
tsconfig.server.json — for compiling server-side code:{
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2022"],
"module": "NodeNext",
"moduleResolution": "NodeNext",
"declaration": true,
"emitDeclarationOnly": true,
"outDir": "./dist",
"rootDir": ".",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["server.ts", "main.ts"]
}
vite.config.ts — bundles UI into a single HTML file:import { defineConfig } from "vite";
import { viteSingleFile } from "vite-plugin-singlefile";
const INPUT = process