Skip to content

Commit 925904c

Browse files
Add basic-host and basic-server-react examples
Introduce new example implementations to replace simple-host and simple-server: - basic-host: Reference host implementation with double-iframe sandbox pattern for secure UI isolation (outer iframe validates and relays messages, inner iframe renders untrusted tool UI via srcdoc) - basic-server-react: MCP server example demonstrating tool registration with linked UI resources and React UI using the useApp() hook Build infrastructure updates: - Add build:all script and simplify CI/pre-commit to use it - Add examples:dev for watch-mode development workflow Also change PostMessageTransport logging from info to debug level. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 1a6f237 commit 925904c

29 files changed

+1398
-25
lines changed

.github/workflows/ci.yml

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -25,18 +25,6 @@ jobs:
2525

2626
- run: npm install
2727

28-
- run: npm run build
29-
30-
- name: Build simple-host example
31-
working-directory: examples/simple-host
32-
run: |
33-
npm install
34-
npm run build
35-
36-
- name: Build simple-server example
37-
working-directory: examples/simple-server
38-
run: |
39-
npm install
40-
npm run build
28+
- run: npm run build:all
4129

4230
- run: npm run prettier

.husky/pre-commit

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,2 @@
1-
npm run build
1+
npm run build:all
22
npm run prettier:fix
3-
( cd examples/simple-host && npm run build )
4-
( cd examples/simple-server && npm run build )

.prettierignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
examples/basic-host/**/*.ts
2+
examples/basic-host/**/*.tsx
3+
examples/basic-server-react/**/*.ts
4+
examples/basic-server-react/**/*.tsx

README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ Embed and communicate with MCP Apps in your chat application.
2323

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

26-
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).
26+
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).
2727

2828
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.
2929

@@ -54,14 +54,14 @@ Your `package.json` will then look like:
5454
5555
## Examples
5656

57-
- [examples/simple-server](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/simple-server) — Example MCP server with tools that return UI Apps
58-
- [examples/simple-host](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/simple-host) — Bare-bones example of hosting MCP Apps
57+
- [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
58+
- [examples/basic-host](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-host) — Bare-bones example of hosting MCP Apps
5959

6060
To run the examples end-to-end:
6161

6262
```
6363
npm i
64-
npm start
64+
npm run examples:start
6565
```
6666

6767
Then open http://localhost:8080/

examples/basic-host/README.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# Example: Basic Host
2+
3+
A reference implementation showing how to build an MCP Host application that connects to MCP servers and renders tool UIs in a secure sandbox.
4+
5+
## Key Files
6+
7+
- [`index.html`](index.html) / [`src/index.tsx`](src/index.tsx) - React UI host with tool selection, parameter input, and iframe management
8+
- [`sandbox.html`](sandbox.html) / [`src/sandbox.ts`](src/sandbox.ts) - Outer iframe proxy with security validation and bidirectional message relay
9+
- [`src/implementation.ts`](src/implementation.ts) - Core logic: server connection, tool calling, and AppBridge setup
10+
11+
## Getting Started
12+
13+
```bash
14+
npm install
15+
npm run dev
16+
# Open http://localhost:8080
17+
```
18+
19+
## Architecture
20+
21+
This example uses a double-iframe sandbox pattern for secure UI isolation:
22+
23+
```
24+
Host (port 8080)
25+
└── Outer iframe (port 8081) - sandbox proxy
26+
└── Inner iframe (srcdoc) - untrusted tool UI
27+
```
28+
29+
**Why two iframes?**
30+
31+
- The outer iframe runs on a separate origin (port 8081) preventing direct access to the host
32+
- The inner iframe receives HTML via `srcdoc` and is restricted by sandbox attributes
33+
- Messages flow through the outer iframe which validates and relays them bidirectionally
34+
35+
This architecture ensures that even if tool UI code is malicious, it cannot access the host application's DOM, cookies, or JavaScript context.

examples/basic-host/index.html

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6+
<title>MCP Apps Host</title>
7+
<link rel="stylesheet" href="/src/global.css">
8+
</head>
9+
<body>
10+
<div id="root"></div>
11+
<script src="/src/index.tsx" type="module"></script>
12+
</body>
13+
</html>

examples/basic-host/package.json

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
{
2+
"homepage": "https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-host",
3+
"name": "@modelcontextprotocol/ext-apps-basic-host",
4+
"version": "1.0.0",
5+
"type": "module",
6+
"scripts": {
7+
"build": "concurrently 'INPUT=index.html vite build' 'INPUT=sandbox.html vite build'",
8+
"watch": "concurrently 'INPUT=index.html vite build --watch' 'INPUT=sandbox.html vite build --watch'",
9+
"serve": "bun serve.ts",
10+
"start": "NODE_ENV=development npm run build && npm run serve",
11+
"dev": "NODE_ENV=development concurrently 'npm run watch' 'npm run serve'"
12+
},
13+
"dependencies": {
14+
"@modelcontextprotocol/ext-apps": "../..",
15+
"@modelcontextprotocol/sdk": "^1.22.0",
16+
"react": "^19.2.0",
17+
"react-dom": "^19.2.0",
18+
"zod": "^3.25.0"
19+
},
20+
"devDependencies": {
21+
"@types/express": "^5.0.0",
22+
"@types/node": "^22.0.0",
23+
"@types/react": "^19.2.2",
24+
"@types/react-dom": "^19.2.2",
25+
"@vitejs/plugin-react": "^4.3.4",
26+
"bun": "^1.3.2",
27+
"concurrently": "^9.2.1",
28+
"cors": "^2.8.5",
29+
"express": "^5.1.0",
30+
"prettier": "^3.6.2",
31+
"vite": "^6.0.0",
32+
"vite-plugin-singlefile": "^2.3.0",
33+
"typescript": "^5.9.3",
34+
"vitest": "^3.2.4"
35+
}
36+
}

examples/basic-host/sandbox.html

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<!doctype html>
2+
<html>
3+
<head>
4+
<meta charset="utf-8" />
5+
<!-- Permissive CSP so nested content is not constrained by host CSP -->
6+
<meta http-equiv="Content-Security-Policy" content="
7+
default-src 'self';
8+
img-src * data: blob: 'unsafe-inline';
9+
media-src * blob: data:;
10+
font-src * blob: data:;
11+
script-src 'self'
12+
'wasm-unsafe-eval'
13+
'unsafe-inline'
14+
'unsafe-eval'
15+
blob: data: http://localhost:* https://localhost:*;
16+
style-src * blob: data: 'unsafe-inline';
17+
connect-src *;
18+
frame-src * blob: data: http://localhost:* https://localhost:*;
19+
base-uri 'self';
20+
" />
21+
<title>MCP-UI Proxy</title>
22+
<style>
23+
html,
24+
body {
25+
margin: 0;
26+
height: 100vh;
27+
width: 100vw;
28+
}
29+
body {
30+
display: flex;
31+
flex-direction: column;
32+
}
33+
* {
34+
box-sizing: border-box;
35+
}
36+
iframe {
37+
background-color: transparent;
38+
border: 0px none transparent;
39+
padding: 0px;
40+
overflow: hidden;
41+
flex-grow: 1;
42+
}
43+
</style>
44+
</head>
45+
<body>
46+
<script type="module" src="/src/sandbox.ts"></script>
47+
</body>
48+
</html>

examples/basic-host/serve.ts

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
#!/usr/bin/env npx tsx
2+
/**
3+
* HTTP servers for the MCP UI example:
4+
* - Host server (port 8080): serves host HTML files (React and Vanilla examples)
5+
* - Sandbox server (port 8081): serves sandbox.html with permissive CSP
6+
*
7+
* Running on separate ports ensures proper origin isolation for security.
8+
*/
9+
10+
import express from "express";
11+
import cors from "cors";
12+
import { fileURLToPath } from "url";
13+
import { dirname, join } from "path";
14+
15+
const __filename = fileURLToPath(import.meta.url);
16+
const __dirname = dirname(__filename);
17+
18+
const HOST_PORT = parseInt(process.env.HOST_PORT || "8080", 10);
19+
const SANDBOX_PORT = parseInt(process.env.SANDBOX_PORT || "8081", 10);
20+
const DIRECTORY = join(__dirname, "dist");
21+
22+
// ============ Host Server (port 8080) ============
23+
const hostApp = express();
24+
hostApp.use(cors());
25+
26+
// Exclude sandbox.html from host server
27+
hostApp.use((req, res, next) => {
28+
if (req.path === "/sandbox.html") {
29+
res.status(404).send("Sandbox is served on a different port");
30+
return;
31+
}
32+
next();
33+
});
34+
35+
hostApp.use(express.static(DIRECTORY));
36+
37+
hostApp.get("/", (_req, res) => {
38+
res.redirect("/index.html");
39+
});
40+
41+
// ============ Sandbox Server (port 8081) ============
42+
const sandboxApp = express();
43+
sandboxApp.use(cors());
44+
45+
// Permissive CSP for sandbox content
46+
sandboxApp.use((_req, res, next) => {
47+
const csp = [
48+
"default-src 'self'",
49+
"img-src * data: blob: 'unsafe-inline'",
50+
"style-src * blob: data: 'unsafe-inline'",
51+
"script-src * blob: data: 'unsafe-inline' 'unsafe-eval'",
52+
"connect-src *",
53+
"font-src * blob: data:",
54+
"media-src * blob: data:",
55+
"frame-src * blob: data:",
56+
].join("; ");
57+
res.setHeader("Content-Security-Policy", csp);
58+
res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
59+
res.setHeader("Pragma", "no-cache");
60+
res.setHeader("Expires", "0");
61+
next();
62+
});
63+
64+
sandboxApp.get(["/", "/sandbox.html"], (_req, res) => {
65+
res.sendFile(join(DIRECTORY, "sandbox.html"));
66+
});
67+
68+
sandboxApp.use((_req, res) => {
69+
res.status(404).send("Only sandbox.html is served on this port");
70+
});
71+
72+
// ============ Start both servers ============
73+
hostApp.listen(HOST_PORT, () => {
74+
console.log(`Host server: http://localhost:${HOST_PORT}`);
75+
});
76+
77+
sandboxApp.listen(SANDBOX_PORT, () => {
78+
console.log(`Sandbox server: http://localhost:${SANDBOX_PORT}`);
79+
console.log("\nPress Ctrl+C to stop\n");
80+
});

examples/basic-host/src/global.css

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
* {
2+
box-sizing: border-box;
3+
}
4+
5+
html, body {
6+
font-family: system-ui, -apple-system, sans-serif;
7+
font-size: 1rem;
8+
}
9+
10+
code {
11+
font-size: 1em;
12+
}

0 commit comments

Comments
 (0)