ext-apps/docs/quickstart.md
Anton Pidkuiko 8550545ccb
fix: add mimeType to resource declarations in examples (#149)
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>
2025-12-15 12:09:47 +00:00

281 lines
7.7 KiB
Markdown

---
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
- Node.js 18+
- Familiarity with the [MCP TypeScript SDK](https://github.com/modelcontextprotocol/typescript-sdk)
## 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 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`:
```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 { 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`](https://github.com/modelcontextprotocol/ext-apps/blob/main/examples/basic-server-vanillajs/server.ts)
Then, verify your server compiles:
```bash
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`:
```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`:
```typescript
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:
```bash
npm run build
```
> [!NOTE]
> **Full files:** [`mcp-app.html`](https://github.com/modelcontextprotocol/ext-apps/blob/main/examples/basic-server-vanillajs/mcp-app.html), [`src/mcp-app.ts`](https://github.com/modelcontextprotocol/ext-apps/blob/main/examples/basic-server-vanillajs/src/mcp-app.ts)
This produces `dist/mcp-app.html` which contains your bundled app:
```console
$ ls dist/mcp-app.html
dist/mcp-app.html
```
## 4. Test It
You'll need two terminals.
**Terminal 1** — Build and start your server:
```bash
npm run build && npm run serve
```
**Terminal 2** — Run the test host (from the [ext-apps repo](https://github.com/modelcontextprotocol/ext-apps)):
```bash
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!
## Next Steps
- **Host communication**: Add [`sendMessage()`](https://modelcontextprotocol.github.io/ext-apps/api/classes/app.App.html#sendmessage), [`sendLog()`](https://modelcontextprotocol.github.io/ext-apps/api/classes/app.App.html#sendlog), and [`sendOpenLink()`](https://modelcontextprotocol.github.io/ext-apps/api/classes/app.App.html#sendopenlink) to interact with the host — see [`src/mcp-app.ts`](https://github.com/modelcontextprotocol/ext-apps/blob/main/examples/basic-server-vanillajs/src/mcp-app.ts)
- **React version**: Compare with [`basic-server-react`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-server-react) for a React-based UI
- **API reference**: See the full [API documentation](https://modelcontextprotocol.github.io/ext-apps/api/)