According to spec, mimeType should be present in both the resource declaration and the resource content. This adds the missing mimeType to the registerResource() declaration (third parameter) in all example servers and the quickstart documentation. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: ochafik <olivier.chafik@gmail.com>
7.7 KiB
| 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.
Prerequisites
- Node.js 18+
- Familiarity with the MCP TypeScript SDK
1. Project Setup
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"
}
}
Note
Full files:
package.json,tsconfig.json,vite.config.ts
2. Create the Server
MCP Apps use a two-part registration:
- A tool that the LLM/host calls
- 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 * as 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,
{ mimeType: "text/html;profile=mcp-app" },
async () => {
const html = await fs.readFile(
path.join(import.meta.dirname, "dist", "mcp-app.html"),
"utf-8",
);
return {
contents: [
{ uri: resourceUri, mimeType: "text/html;profile=mcp-app", 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, (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 server.ts
No output means success. If you see errors, check for typos in server.ts.
3. Build the UI
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
Note
Full files:
mcp-app.html,src/mcp-app.ts
This produces dist/mcp-app.html which contains your bundled app:
$ ls dist/mcp-app.html
dist/mcp-app.html
4. Test It
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:
- Select get-time from the "Tool Name" dropdown
- Click Call Tool
- Your UI renders in the sandbox below
- Click Get Server Time — the current time appears!
Next Steps
- Host communication: Add
sendMessage(),sendLog(), andsendOpenLink()to interact with the host — seesrc/mcp-app.ts - React version: Compare with
basic-server-reactfor a React-based UI - API reference: See the full API documentation