Skip to content

Commit 123b740

Browse files
authored
Merge pull request #15 from modelcontextprotocol/ochafik/simple-host-example
Add simple host example
2 parents 39d84a9 + c800b34 commit 123b740

18 files changed

+1248
-24
lines changed

README.md

Lines changed: 39 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ This repo provides:
1616
- [ui-vanilla.tsx](./examples/simple-server/src/ui-vanilla.ts): vanilla App returned by the `create-ui-vanilla`
1717
- [ui-raw.tsx](./examples/simple-server/src/ui-raw.ts): same as vanilla App but doesn't use the SDK runtime (just its types)
1818

19+
- [examples/simple-host](./examples/simple-host): bare-bone examples on how to host MCP Apps (both use the [AppBridge](./src/app-bridge.ts) class to talk to a hosted App)
20+
- [example-host-react.tsx](./examples/simple-host/src/example-host-react.tsx) uses React (esp. [AppRenderer.tsx](./examples/simple-host/src/AppRenderer.tsx))
21+
- [example-host-vanilla.tsx](./examples/simple-host/src/example-host-vanilla.tsx) doesn't use React
22+
1923
- [message-transport](./src/message-transport.ts): `PostMessageTransport` class that uses `postMessage` to exchange JSON-RPC messages between windows / iframes
2024

2125
- [app.ts](./src/app.ts): `App` class used by an App to talk to its host
@@ -26,22 +30,47 @@ This repo provides:
2630

2731
What this repo does NOT provide:
2832

29-
- There's no host implementation here (beyond the `AppBridge` just used for communications).
33+
- There's no _supported_ host implementation in this repo (beyond the [examples/simple-host](./examples/simple-host) example)
3034
- 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.
31-
- A prior iteration of an e2e prototype w/ client, server and hosting parts is available [in this gist](https://gist.github.com/ochafik/a9603ba2d6757d6038ce066eded4c354)
3235

33-
## Installation
36+
## Using the SDK
3437

35-
This repo is in flux and isn't published to npm (when it is, it will use the `@modelcontextprotocol/ext-apps` package). Please install it from git for now:
38+
### Run examples
39+
40+
Run the examples in this repo end-to-end:
3641

37-
```bash
38-
npm install git+https://github.com/modelcontextprotocol/ext-apps.git
3942
```
43+
npm i
44+
npm start
45+
open http://localhost:8080/
46+
```
47+
48+
> [!NOTE]
49+
> Please bear with us while we add more examples!
50+
51+
### Using the SDK in your project
4052

41-
## Development Notes
53+
This repo is in flux and isn't published to npm yet: when it is, it will use the `@modelcontextprotocol/ext-apps` package.
4254

43-
### Build tools in dependencies
55+
In the meantime you can depend on the SDK library in a Node.js project by installing it w/ its git URL:
4456

45-
The build tools (`esbuild`, `tsx`, `typescript`) are in `dependencies` rather than `devDependencies`. This is intentional: it allows the `prepare` script to run when the package is installed from git, since npm doesn't install devDependencies for git dependencies.
57+
```bash
58+
npm install -S git+https://github.com/modelcontextprotocol/ext-apps.git
59+
```
60+
61+
Your `package.json` will then look like:
62+
63+
```json
64+
{
65+
...
66+
"dependencies": {
67+
...
68+
"@modelcontextprotocol/ext-apps": "git+https://github.com/modelcontextprotocol/ext-apps.git"
69+
}
70+
}
71+
```
4672

47-
Once the package is published to npm with pre-built `dist/`, these can be moved back to `devDependencies`.
73+
> [!NOTE]
74+
> The build tools (`esbuild`, `tsx`, `typescript`) are in `dependencies` rather than `devDependencies`. This is intentional: it allows the `prepare` script to run when the package is installed from git, since npm doesn't install devDependencies for git dependencies.
75+
>
76+
> Once the package is published to npm with pre-built `dist/`, these can be moved back to `devDependencies`.
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
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+
<style>
7+
* {
8+
box-sizing: border-box;
9+
}
10+
body {
11+
margin: 0;
12+
font-family: system-ui, -apple-system, sans-serif;
13+
}
14+
</style>
15+
<title>Example MCP-UI React Host</title>
16+
</head>
17+
<body>
18+
<div id="root"></div>
19+
<script src="/src/example-host-react.tsx" type="module"></script>
20+
</body>
21+
</html>
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
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+
<style>
7+
#chat-root {
8+
display: flex;
9+
flex-direction: column;
10+
}
11+
</style>
12+
<script src="/src/example-host-vanilla.ts" type="module"></script>
13+
<title>Example MCP View Host</title>
14+
</head>
15+
<body>
16+
<h1>Example MCP View Host</h1>
17+
18+
<div id="controls"></div>
19+
<div id="chat-root"></div>
20+
</body>
21+
</html>

examples/simple-host/package.json

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
{
2+
"homepage": "https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/simple-host",
3+
"name": "@modelcontextprotocol/ext-apps-host",
4+
"version": "1.0.0",
5+
"type": "module",
6+
"scripts": {
7+
"start:server": "tsx server.ts",
8+
"start:mcp-server": "cd ../simple-server && npm install && npm run start",
9+
"build": "concurrently 'INPUT=example-host-vanilla.html vite build' 'INPUT=example-host-react.html vite build' 'INPUT=sandbox.html vite build'",
10+
"server": "bun server.ts",
11+
"start": "NODE_ENV=development npm run build && concurrently 'npm run start:server' 'npm run start:mcp-server'"
12+
},
13+
"dependencies": {
14+
"@modelcontextprotocol/ext-apps": "../..",
15+
"@modelcontextprotocol/sdk": "^1.22.0",
16+
"react-dom": "^19.2.0",
17+
"react": "^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-dom": "^19.2.2",
24+
"@types/react": "^19.2.2",
25+
"@vitejs/plugin-react": "^4.3.4",
26+
"concurrently": "^9.2.1",
27+
"cors": "^2.8.5",
28+
"esbuild": "~0.19.10",
29+
"express": "^5.1.0",
30+
"prettier": "^3.6.2",
31+
"tsx": "^4.20.6",
32+
"typescript": "^5.9.3",
33+
"vite-plugin-singlefile": "^2.3.0",
34+
"vite": "^6.0.0",
35+
"vitest": "^3.2.4"
36+
}
37+
}

examples/simple-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/simple-host/server.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
#!/usr/bin/env npx tsx
2+
/**
3+
* Simple HTTP server to serve the host and sandbox html files with appropriate
4+
* Content Security Policy (CSP) headers.
5+
*/
6+
7+
import express from "express";
8+
import cors from "cors";
9+
import { fileURLToPath } from "url";
10+
import { dirname, join } from "path";
11+
12+
const __filename = fileURLToPath(import.meta.url);
13+
const __dirname = dirname(__filename);
14+
15+
const PORT = parseInt(process.env.PORT || "8080", 10);
16+
const DIRECTORY = join(__dirname, "dist");
17+
18+
const app = express();
19+
20+
// CORS middleware for all routes
21+
app.use(cors());
22+
23+
// Custom middleware for sandbox.html and root
24+
app.use((req, res, next) => {
25+
if (req.path === "/sandbox.html" || req.path === "/") {
26+
// Permissive CSP to allow external resources (images, styles, scripts)
27+
const csp = [
28+
"default-src 'self'",
29+
"img-src * data: blob: 'unsafe-inline'",
30+
"style-src * blob: data: 'unsafe-inline'",
31+
"script-src * blob: data: 'unsafe-inline' 'unsafe-eval'",
32+
"connect-src *",
33+
"font-src * blob: data:",
34+
"media-src * blob: data:",
35+
"frame-src * blob: data:",
36+
].join("; ");
37+
res.setHeader("Content-Security-Policy", csp);
38+
39+
// Disable caching to ensure fresh content on every request
40+
res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
41+
res.setHeader("Pragma", "no-cache");
42+
res.setHeader("Expires", "0");
43+
}
44+
next();
45+
});
46+
47+
// Serve static files from dist directory
48+
app.use(express.static(DIRECTORY));
49+
50+
// Redirect root to example-host.html
51+
app.get("/", (_req, res) => {
52+
res.redirect("/example-host-react.html");
53+
});
54+
55+
app.listen(PORT, () => {
56+
console.log(`Server running on: http://localhost:${PORT}`);
57+
console.log("Press Ctrl+C to stop the server\n");
58+
});

0 commit comments

Comments
 (0)