Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 1 addition & 13 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,18 +25,6 @@ jobs:

- run: npm install

- run: npm run build

- name: Build simple-host example
working-directory: examples/simple-host
run: |
npm install
npm run build

- name: Build simple-server example
working-directory: examples/simple-server
run: |
npm install
npm run build
- run: npm run build:all

- run: npm run prettier
4 changes: 1 addition & 3 deletions .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -1,4 +1,2 @@
npm run build
npm run build:all
npm run prettier:fix
( cd examples/simple-host && npm run build )
( cd examples/simple-server && npm run build )
6 changes: 6 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
examples/basic-host/**/*.ts
examples/basic-host/**/*.tsx
examples/basic-server-react/**/*.ts
examples/basic-server-react/**/*.tsx
examples/basic-server-vanillajs/**/*.ts
examples/basic-server-vanillajs/**/*.tsx
10 changes: 6 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ Embed and communicate with MCP Apps in your chat application.

- **SDK for Hosts**: `@modelcontextprotocol/ext-apps/app-bridge` — [API Docs](https://modelcontextprotocol.github.io/ext-apps/api/modules/app-bridge.html)

There's no _supported_ host implementation in this repo (beyond the [examples/simple-host](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/simple-host) example).
There's no _supported_ host implementation in this repo (beyond the [examples/basic-host](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-host) example).

We have [contributed a tentative implementation](https://github.com/MCP-UI-Org/mcp-ui/pull/147) of hosting / iframing / sandboxing logic to the [MCP-UI](https://github.com/idosal/mcp-ui) repository, and expect OSS clients may use it, while other clients might roll their own hosting logic.

Expand Down Expand Up @@ -54,20 +54,22 @@ Your `package.json` will then look like:

## Examples

- [examples/simple-server](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/simple-server) — Example MCP server with tools that return UI Apps
- [examples/simple-host](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/simple-host) — Bare-bones example of hosting MCP Apps
- [`examples/basic-server-react`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-server-react) — Example MCP server with tools that return UI Apps (React)
- [`examples/basic-server-vanillajs`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-server-vanillajs) — Example MCP server with tools that return UI Apps (vanilla JS)
- [`examples/basic-host`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-host) — Bare-bones example of hosting MCP Apps

To run the examples end-to-end:

```
npm i
npm start
npm run examples:start
```

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 changes: 251 additions & 0 deletions docs/quickstart.md
Original file line number Diff line number Diff line change
@@ -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/)
37 changes: 37 additions & 0 deletions examples/basic-host/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Example: Basic Host

A reference implementation showing how to build an MCP Host application that connects to MCP servers and renders tool UIs in a secure sandbox.

This basic host can also be used to test MCP Apps during local development.

## Key Files

- [`index.html`](index.html) / [`src/index.tsx`](src/index.tsx) - React UI host with tool selection, parameter input, and iframe management
- [`sandbox.html`](sandbox.html) / [`src/sandbox.ts`](src/sandbox.ts) - Outer iframe proxy with security validation and bidirectional message relay
- [`src/implementation.ts`](src/implementation.ts) - Core logic: server connection, tool calling, and AppBridge setup

## Getting Started

```bash
npm install
npm run dev
# Open http://localhost:8080
```

## Architecture

This example uses a double-iframe sandbox pattern for secure UI isolation:

```
Host (port 8080)
└── Outer iframe (port 8081) - sandbox proxy
└── Inner iframe (srcdoc) - untrusted tool UI
```

**Why two iframes?**

- The outer iframe runs on a separate origin (port 8081) preventing direct access to the host
- The inner iframe receives HTML via `srcdoc` and is restricted by sandbox attributes
- Messages flow through the outer iframe which validates and relays them bidirectionally

This architecture ensures that even if tool UI code is malicious, it cannot access the host application's DOM, cookies, or JavaScript context.
13 changes: 13 additions & 0 deletions examples/basic-host/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>MCP Apps Host</title>
<link rel="stylesheet" href="/src/global.css">
</head>
<body>
<div id="root"></div>
<script src="/src/index.tsx" type="module"></script>
</body>
</html>
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
{
"homepage": "https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/simple-host",
"name": "@modelcontextprotocol/ext-apps-host",
"homepage": "https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-host",
"name": "@modelcontextprotocol/ext-apps-basic-host",
"version": "1.0.0",
"type": "module",
"scripts": {
"start": "vite dev",
"start:server": "vite preview",
"build": "vite build"
"build": "concurrently 'INPUT=index.html vite build' 'INPUT=sandbox.html vite build'",
"watch": "concurrently 'INPUT=index.html vite build --watch' 'INPUT=sandbox.html vite build --watch'",
"serve": "bun serve.ts",
"start": "NODE_ENV=development npm run build && npm run serve",
"dev": "NODE_ENV=development concurrently 'npm run watch' 'npm run serve'"
},
"dependencies": {
"@modelcontextprotocol/ext-apps": "../..",
Expand All @@ -28,6 +30,7 @@
"prettier": "^3.6.2",
"vite": "^6.0.0",
"vite-plugin-singlefile": "^2.3.0",
"typescript": "^5.9.3",
"vitest": "^3.2.4"
}
}
File renamed without changes.
Loading