Build an MCP App ↗
yesEditorial Notes
MCP Apps extend the protocol beyond tool calls and resource access into interactive user interfaces, making this guide relevant if you want to build rich client experiences on top of MCP servers. The tutorial walks through the full build process from project scaffolding to rendering UI components driven by server-provided data. Start here if you have already built basic MCP servers and want to understand how the Apps extension layer adds frontend capabilities. Be aware that MCP Apps is a newer extension and the API surface may evolve faster than the core protocol.
Original Documentation
Documentation Index#
Fetch the complete documentation index at: https://modelcontextprotocol.io/llms.txt Use this file to discover all available pages before exploring further.
Getting started guide for building interactive UI applications with MCP Apps
Prerequisites#
You’ll need Node.js 18 or higher. Familiarity with MCP tools and resources is recommended since MCP Apps combine both primitives. Experience with the MCP TypeScript SDK will help you better understand the server-side patterns.
Getting started#
The fastest way to create an MCP App is using an AI coding agent with the MCP Apps skill. If you prefer to set up a project manually, skip to Manual setup.
Using an AI coding agent#
AI coding agents with Skills support can scaffold a complete MCP App project for you. Skills are folders of instructions and resources that your agent loads when relevant. They teach the AI how to perform specialized tasks like creating MCP Apps.
The create-mcp-app skill includes architecture guidance, best practices, and
working examples that the agent uses to generate your project.
If you are using Claude Code, you can install the skill directly with:
/plugin marketplace add modelcontextprotocol/ext-apps
/plugin install mcp-apps@modelcontextprotocol-ext-apps
```
You can also use the [Vercel Skills CLI](https://skills.sh/) to install skills across different AI coding agents:
```bash
npx skills add modelcontextprotocol/ext-apps
```
Alternatively, you can install the skill manually by cloning the ext-apps repository:
```bash
git clone https://github.com/modelcontextprotocol/ext-apps.git
```
And then copying the skill to the appropriate location for your agent:
| Agent | Skills directory (macOS/Linux) | Skills directory (Windows) |
| ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------ | ------------------------------------- |
| [Claude Code](https://docs.anthropic.com/en/docs/claude-code/skills) | `~/.claude/skills/` | `%USERPROFILE%\.claude\skills\` |
| [VS Code](https://code.visualstudio.com/docs/copilot/customization/agent-skills) and [GitHub Copilot](https://docs.github.com/en/copilot/concepts/agents/about-agent-skills) | `~/.copilot/skills/` | `%USERPROFILE%\.copilot\skills\` |
| [Gemini CLI](https://geminicli.com/docs/cli/skills/) | `~/.gemini/skills/` | `%USERPROFILE%\.gemini\skills\` |
| [Cline](https://cline.bot/blog/cline-3-48-0-skills-and-websearch-make-cline-smarter) | `~/.cline/skills/` | `%USERPROFILE%\.cline\skills\` |
| [Goose](https://block.github.io/goose/docs/guides/context-engineering/using-skills/) | `~/.config/goose/skills/` | `%USERPROFILE%\.config\goose\skills\` |
| [Codex](https://developers.openai.com/codex/skills/) | `~/.codex/skills/` | `%USERPROFILE%\.codex\skills\` |
<span class="callout-start" data-callout-type="note"></span>
This list is not comprehensive. Other agents may support skills in different locations; check your agent's documentation.
<span class="callout-end"></span>
For example, with Claude Code you can install the skill globally (available in all projects):
```bash
cp -r ext-apps/plugins/mcp-apps/skills/create-mcp-app ~/.claude/skills/create-mcp-app
```
```powershell
Copy-Item -Recurse ext-apps\plugins\mcp-apps\skills\create-mcp-app $env:USERPROFILE\.claude\skills\create-mcp-app
```
Or install it for a single project only by copying to `.claude/skills/` in your project directory:
```bash
mkdir -p .claude/skills && cp -r ext-apps/plugins/mcp-apps/skills/create-mcp-app .claude/skills/create-mcp-app
```
```powershell
New-Item -ItemType Directory -Force -Path .claude\skills | Out-Null; Copy-Item -Recurse ext-apps\plugins\mcp-apps\skills\create-mcp-app .claude\skills\create-mcp-app
```
To verify the skill is installed, ask your agent "What skills do you have access to?" — you should see `create-mcp-app` as one of the available skills.
<span class="step-end"></span>
<span class="step-marker" data-step-title="Create your app"></span>
Ask your AI coding agent to build it:Create an MCP App that displays a color picker
```
The agent will recognize the create-mcp-app skill is relevant, load its instructions, then scaffold a complete project with server, UI, and configuration files.

npm install && npm run build && npm run serve
```
```powershell
npm install; npm run build; npm run serve
```
<span class="callout-start" data-callout-type="tip"></span>
You might need to make sure that you are first in the **app folder** before running the commands above.
<span class="callout-end"></span>
<span class="step-end"></span>
<span class="step-marker" data-step-title="Test your app"></span>
Follow the instructions in [Testing your app](#testing-your-app) below. For the color picker example, start a new chat and ask Claude to provide you a color picker.
<Frame caption="Testing the color picker in Claude">
<img src="https://mintcdn.com/mcp/GU_E-622SLWFdCrP/images/quickstart-apps/test-color-picker.gif?s=09413b99bc31d7edc7f9aa22df4faa6a" alt="Testing the color picker in Claude" data-og-width="800" width="800" data-og-height="544" height="544" data-path="images/quickstart-apps/test-color-picker.gif" data-optimize="true" data-opv="3" />
<span class="step-end"></span>
<span class="steps-end"></span>
### Manual setup
If you're not using an AI coding agent, or prefer to understand the setup
process, follow these steps.
<span class="steps-start"></span>
<span class="step-marker" data-step-title="Create the project structure"></span>
A typical MCP App project separates the server code from the UI code:
<Tree>
<Tree.Folder name="my-mcp-app" defaultOpen>
<Tree.File name="package.json" />
<Tree.File name="tsconfig.json" />
<Tree.File name="vite.config.ts" />
<Tree.File name="server.ts" comment="MCP server with tool + resource" />
<Tree.File name="mcp-app.html" comment="UI entry point" />
<Tree.Folder name="src" defaultOpen>
<Tree.File name="mcp-app.ts" comment="UI logic" />
</Tree.Folder>
</Tree.Folder>
</Tree>
The server registers the tool and serves the UI resource. The UI resource will eventually be rendered in a secure iframe with deny-by-default CSP configuration. If your app has CSS and JS assets, you will need to [configure CSP](https://apps.extensions.modelcontextprotocol.io/api/documents/Patterns.html#configuring-csp-and-cors), or you can bundle your assets into the HTML with a tool like `vite-plugin-singlefile`, which is what we will do in this tutorial.
<span class="step-end"></span>
<span class="step-marker" data-step-title="Install dependencies"></span>
```bash
npm install @modelcontextprotocol/ext-apps @modelcontextprotocol/sdk
npm install -D typescript vite vite-plugin-singlefile express cors @types/express @types/cors tsx
```
The `ext-apps` package provides helpers for both the server side (registering tools and resources) and the client side (the `App` class for UI-to-host communication). Vite with the `vite-plugin-singlefile` plugin is used here to bundle your UI and assets into a single HTML file for convenience, but this is optional — you can use any bundler or serve unbundled files if you [configure CSP](https://apps.extensions.modelcontextprotocol.io/api/documents/Patterns.html#configuring-csp-and-cors).
<span class="step-end"></span>
<span class="step-marker" data-step-title="Configure the project"></span>
<span class="tab-group-start"></span>
<span class="tab-start" data-tab-title="package.json"></span>
The `"type": "module"` setting enables ES module syntax. The `build` script uses the `INPUT` environment variable to tell Vite which HTML file to bundle. The `serve` script runs your server using `tsx` for TypeScript execution.
```json
{
"type": "module",
"scripts": {
"build": "INPUT=mcp-app.html vite build",
"serve": "npx tsx server.ts"
}
}
```
<span class="tab-end"></span>
<span class="tab-start" data-tab-title="tsconfig.json"></span>
The TypeScript configuration targets modern JavaScript (`ES2022`) and uses ESNext modules with bundler resolution, which works well with Vite. The `include` array covers both the server code in the root and UI code in `src/`.
```json
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"outDir": "dist"
},
"include": ["*.ts", "src/**/*.ts"]
}
```
<span class="tab-end"></span>
<span class="tab-start" data-tab-title="vite.config.ts"></span>
```typescript
import { defineConfig } from "vite";
import { viteSingleFile } from "vite-plugin-singlefile";
export default defineConfig({
plugins: [viteSingleFile()],
build: {
outDir: "dist",
rollupOptions: {
input: process.env.INPUT,
},
},
});
```
<span class="tab-end"></span>
<span class="tab-group-end"></span>
<span class="step-end"></span>
<span class="step-marker" data-step-title="Build the project"></span>
With the project structure and configuration in place, continue to [Building an MCP App](#building-an-mcp-app) below to implement the server and UI.
<span class="step-end"></span>
<span class="steps-end"></span>
## Building an MCP App
Let's build a simple app that displays the current server time. This example
demonstrates the full pattern: registering a tool with UI metadata, serving the
bundled HTML as a resource, and building a UI that communicates with the server.
### Server implementation
The server needs to do two things: register a tool that includes the
`_meta.ui.resourceUri` field, and register a resource handler that serves the
bundled HTML. Here's the complete server file:
```typescript
// server.ts
console.log("Starting MCP App server...");
const server = new McpServer({
name: "My MCP App Server",
version: "1.0.0",
});
// The ui:// scheme tells hosts this is an MCP App resource.
// The path structure is arbitrary; organize it however makes sense for your app.
const resourceUri = "ui://get-time/mcp-app.html";
// Register the tool that returns the current time
registerAppTool(
server,
"get-time",
{
title: "Get Time",
description: "Returns the current server time.",
inputSchema: {},
_meta: { ui: { resourceUri } },
},
async () => {
const time = new Date().toISOString();
return {
content: [{ type: "text", text: time }],
};
},
);
// Register the resource that serves the bundled HTML
registerAppResource(
server,
resourceUri,
resourceUri,
{ mimeType: RESOURCE_MIME_TYPE },
async () => {
const html = await fs.readFile(
path.join(import.meta.dirname, "dist", "mcp-app.html"),
"utf-8",
);
return {
contents: [
{ uri: resourceUri, mimeType: RESOURCE_MIME_TYPE, text: html },
],
};
},
);
// Expose the MCP server over HTTP
const expressApp = express();
expressApp.use(cors());
expressApp.use(express.json());
expressApp.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);
});
expressApp.listen(3001, (err) => {
if (err) {
console.error("Error starting server:", err);
process.exit(1);
}
console.log("Server listening on http://localhost:3001/mcp");
});Let’s break down the key parts:
resourceUri: Theui://scheme tells hosts this is an MCP App resource. The path structure is arbitrary.registerAppTool: Registers a tool with the_meta.ui.resourceUrifield. When the host calls this tool, the UI is fetched and rendered, and the tool result is passed to it upon arrival.registerAppResource: Serves the bundled HTML when the host requests the UI resource.- Express server: Exposes the MCP server over HTTP on port 3001.
UI implementation#
The UI consists of an HTML page and a TypeScript module that uses the App
class to communicate with the host. Here’s the HTML:
<!-- 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>
</body>
</html>And the TypeScript module:
// src/mcp-app.ts
const serverTimeEl = document.getElementById("server-time")!;
const getTimeBtn = document.getElementById("get-time-btn")!;
const app = new App({ name: "Get Time App", version: "1.0.0" });
// Establish communication with the host
app.connect();
// Handle the initial tool result pushed by the host
app.ontoolresult = (result) => {
const time = result.content?.find((c) => c.type === "text")?.text;
serverTimeEl.textContent = time ?? "[ERROR]";
};
// Proactively call tools when users interact with the UI
getTimeBtn.addEventListener("click", async () => {
const result = await app.callServerTool({
name: "get-time",
arguments: {},
});
const time = result.content?.find((c) => c.type === "text")?.text;
serverTimeEl.textContent = time ?? "[ERROR]";
});The key parts:
app.connect(): Establishes communication with the host. Call this once when your app initializes.app.ontoolresult: A callback that fires when the host pushes a tool result to your app (e.g., when the tool is first called and the UI renders).app.callServerTool(): Lets your app proactively call tools on the server. Keep in mind that each call involves a round-trip to the server, so design your UI to handle latency gracefully.
The App class provides additional methods for logging, opening URLs, and
updating the model’s context with structured data from your app. See the full
API documentation.
Testing your app#
To test your MCP App, build the UI and start your local server:
npm run build && npm run servenpm run build; npm run serveIn the default configuration, your server will be available at
http://localhost:3001/mcp. However, to see your app render, you need an MCP
host that supports MCP Apps. You have several options.
Testing with Claude#
Claude (web) and Claude Desktop
support MCP Apps. For local development, you’ll need to expose your server to
the internet. You can run an MCP server locally and use tools like cloudflared
to tunnel traffic through.
In a separate terminal, run:
npx cloudflared tunnel --url http://localhost:3001Copy the generated URL (e.g., https://random-name.trycloudflare.com) and add it
as a custom connector
in Claude - click on your profile, go to Settings, Connectors, and
finally Add custom connector.
Custom connectors are available on paid Claude plans (Pro, Max, or Team).

Testing with the basic-host#
The ext-apps repository includes a test host for development. Clone the repo and
install dependencies:
git clone https://github.com/modelcontextprotocol/ext-apps.git
cd ext-apps/examples/basic-host
npm installgit clone https://github.com/modelcontextprotocol/ext-apps.git
cd ext-apps\examples\basic-host
npm installRunning npm start from ext-apps/examples/basic-host/ will start the basic-host
test interface. To connect it to a specific server (e.g., one you’re developing),
pass the SERVERS environment variable inline:
SERVERS='["http://localhost:3001/mcp"]' npm start$env:SERVERS='["http://localhost:3001/mcp"]'; npm startNavigate to http://localhost:8080. You’ll see a simple interface where you can
select a tool and call it. When you call your tool, the host fetches the UI
resource and renders it in a sandboxed iframe. You can then interact with your
app and verify that tool calls work correctly.

Learn more#
Full SDK reference and API details
Source code, examples, and issue tracker
Technical specification for implementers
Feedback#
MCP Apps is under active development. If you encounter issues or have ideas for improvements, open an issue on the GitHub repository. For broader discussions about the extension’s direction, join the conversation in GitHub Discussions.