Add Quickstart guide for building MCP Apps
Introduces a step-by-step tutorial covering: - Project setup with Vite and TypeScript - Server creation with tool + resource registration - UI development using the App SDK - Testing with basic-host Also updates README to link to the guide and configures typedoc to include it in generated documentation. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
308537daac
commit
0571b56fbb
3 changed files with 253 additions and 0 deletions
|
|
@ -69,6 +69,7 @@ Then open http://localhost:8080/
|
|||
|
||||
## Resources
|
||||
|
||||
- [Quickstart Guide](https://modelcontextprotocol.github.io/ext-apps/api/documents/Quickstart.html)
|
||||
- [API Documentation](https://modelcontextprotocol.github.io/ext-apps/api/)
|
||||
- [Draft Specification](https://github.com/modelcontextprotocol/ext-apps/blob/main/specification/draft/apps.mdx)
|
||||
- [SEP-1865 Discussion](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/1865)
|
||||
|
|
|
|||
251
docs/quickstart.md
Normal file
251
docs/quickstart.md
Normal file
|
|
@ -0,0 +1,251 @@
|
|||
---
|
||||
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**.
|
||||
|
||||
## 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"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**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 { 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, {}, async () => {
|
||||
const html = await fs.readFile(
|
||||
path.join(import.meta.dirname, "dist", "mcp-app.html"),
|
||||
"utf-8",
|
||||
);
|
||||
return {
|
||||
contents: [{ uri: resourceUri, mimeType: "text/html+mcp", 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, () => {
|
||||
console.log("Server listening on http://localhost:3001/mcp");
|
||||
});
|
||||
```
|
||||
|
||||
**Full file:** [`server.ts`](https://github.com/modelcontextprotocol/ext-apps/blob/main/examples/basic-server-vanillajs/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
|
||||
```
|
||||
|
||||
This produces `dist/mcp-app.html` containing your bundled app.
|
||||
|
||||
**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)
|
||||
|
||||
## 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/)
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
{
|
||||
"$schema": "https://typedoc.org/schema.json",
|
||||
"projectDocuments": ["docs/quickstart.md"],
|
||||
"entryPoints": [
|
||||
"src/app.ts",
|
||||
"src/app-bridge.ts",
|
||||
|
|
|
|||
Loading…
Reference in a new issue