* docs: add tool visibility and restructure _meta.ui - Restructure tool metadata: `_meta["ui/resourceUri"]` → `_meta.ui.resourceUri` - Add `visibility` array field: ["model"], ["apps"], or ["model", "apps"] - Default ["model"] preserves standard MCP behavior - ["apps"] enables widget-only tools hidden from agent - Add McpUiToolMeta interface for type safety - Add Design Decision #4 explaining approach vs OpenAI's two-field model 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * address PR review comments - Change default visibility to ["model", "apps"] - Add deprecation notice for flat ui/resourceUri format - Add McpUiToolMeta and McpUiToolVisibility to spec.types.ts - Improve tools/list and tools/call behavior wording 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * chore: regenerate schemas for McpUiToolMeta types 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * feat: tool visibility and nested _meta.ui format - Rename visibility "apps" → "app" in McpUiToolVisibility type - Add _meta.ui.resourceUri nested format (deprecate flat format) - Add getToolUiResourceUri() utility with backward compatibility - Add visibility demo to system-monitor-server: - get-system-stats: visibility ["model"] with resourceUri - refresh-stats: visibility ["app"] (app-only polling) - Update all example servers to use new _meta.ui format - Add 11 unit tests for getToolUiResourceUri() 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * chore: regenerate schemas with updated dependencies * feat: export McpUiToolMeta type and add type annotations - Export McpUiToolMeta and McpUiToolVisibility types from types.ts - Export corresponding Zod schemas (McpUiToolMetaSchema, McpUiToolVisibilitySchema) - Add `as McpUiToolMeta` type annotations to all example servers - Update docs/quickstart.md with proper typing Ensures type safety for `_meta.ui` tool metadata across the codebase. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * feat: add server helpers and optional connect() transport Add `src/server/` with convenience functions for registering MCP App tools and resources: - `registerAppTool(server, name, config, handler)` - `registerAppResource(server, name, uri, config, callback)` The `transport` parameter in `App.connect()` is now optional, defaulting to `PostMessageTransport(window.parent)`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * Update src/server/index.ts Co-authored-by: Jonathan Hefner <jonathan@hefner.pro> * feat: update McpUiAppToolConfig to support both _meta.ui and flat formats * refactor: simplify server helper imports * docs: add deprecation notice and improve resourceUri documentation * feat: add backward compat normalization in registerAppTool - If _meta.ui.resourceUri is set, also set legacy flat key - If legacy flat key is set, also set _meta.ui.resourceUri - Preserves existing visibility when merging - Does not overwrite if both formats already set * refactor: avoid mutating config arg in registerAppTool * style: format with prettier --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Olivier Chafik <ochafik@anthropic.com> Co-authored-by: Jonathan Hefner <jonathan@hefner.pro>
284 lines
7.7 KiB
Markdown
284 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_MIME_TYPE,
|
|
type McpUiToolMeta,
|
|
} 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: { ui: { resourceUri } as McpUiToolMeta }, // 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/)
|