Skip to content
Merged
49 changes: 39 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ This repo provides:
- [ui-vanilla.tsx](./examples/simple-server/src/ui-vanilla.ts): vanilla App returned by the `create-ui-vanilla`
- [ui-raw.tsx](./examples/simple-server/src/ui-raw.ts): same as vanilla App but doesn't use the SDK runtime (just its types)

- [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)
- [example-host-react.tsx](./examples/simple-host/src/example-host-react.tsx) uses React (esp. [AppRenderer.tsx](./examples/simple-host/src/AppRenderer.tsx))
- [example-host-vanilla.tsx](./examples/simple-host/src/example-host-vanilla.tsx) doesn't use React

- [message-transport](./src/message-transport.ts): `PostMessageTransport` class that uses `postMessage` to exchange JSON-RPC messages between windows / iframes

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

What this repo does NOT provide:

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

## Installation
## Using the SDK

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:
### Run examples

Run the examples in this repo end-to-end:

```bash
npm install git+https://github.com/modelcontextprotocol/ext-apps.git
```
npm i
npm start
open http://localhost:8080/
```

> [!NOTE]
> Please bear with us while we add more examples!

### Using the SDK in your project

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

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

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.
```bash
npm install -S git+https://github.com/modelcontextprotocol/ext-apps.git
```

Your `package.json` will then look like:

```json
{
...
"dependencies": {
...
"@modelcontextprotocol/ext-apps": "git+https://github.com/modelcontextprotocol/ext-apps.git"
}
}
```

Once the package is published to npm with pre-built `dist/`, these can be moved back to `devDependencies`.
> [!NOTE]
> 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.
>
> Once the package is published to npm with pre-built `dist/`, these can be moved back to `devDependencies`.
21 changes: 21 additions & 0 deletions examples/simple-host/example-host-react.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
* {
box-sizing: border-box;
}
body {
margin: 0;
font-family: system-ui, -apple-system, sans-serif;
}
</style>
<title>Example MCP-UI React Host</title>
</head>
<body>
<div id="root"></div>
<script src="/src/example-host-react.tsx" type="module"></script>
</body>
</html>
21 changes: 21 additions & 0 deletions examples/simple-host/example-host-vanilla.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
#chat-root {
display: flex;
flex-direction: column;
}
</style>
<script src="/src/example-host-vanilla.ts" type="module"></script>
<title>Example MCP View Host</title>
</head>
<body>
<h1>Example MCP View Host</h1>

<div id="controls"></div>
<div id="chat-root"></div>
</body>
</html>
37 changes: 37 additions & 0 deletions examples/simple-host/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
{
"homepage": "https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/simple-host",
"name": "@modelcontextprotocol/ext-apps-host",
"version": "1.0.0",
"type": "module",
"scripts": {
"start:server": "tsx server.ts",
"start:mcp-server": "cd ../simple-server && npm install && npm run start",
"build": "concurrently 'INPUT=example-host-vanilla.html vite build' 'INPUT=example-host-react.html vite build' 'INPUT=sandbox.html vite build'",
"server": "bun server.ts",
"start": "NODE_ENV=development npm run build && concurrently 'npm run start:server' 'npm run start:mcp-server'"
},
"dependencies": {
"@modelcontextprotocol/ext-apps": "../..",
"@modelcontextprotocol/sdk": "^1.22.0",
"react-dom": "^19.2.0",
"react": "^19.2.0",
"zod": "^3.25.0"
},
"devDependencies": {
"@types/express": "^5.0.0",
"@types/node": "^22.0.0",
"@types/react-dom": "^19.2.2",
"@types/react": "^19.2.2",
"@vitejs/plugin-react": "^4.3.4",
"concurrently": "^9.2.1",
"cors": "^2.8.5",
"esbuild": "~0.19.10",
"express": "^5.1.0",
"prettier": "^3.6.2",
"tsx": "^4.20.6",
"typescript": "^5.9.3",
"vite-plugin-singlefile": "^2.3.0",
"vite": "^6.0.0",
"vitest": "^3.2.4"
}
}
48 changes: 48 additions & 0 deletions examples/simple-host/sandbox.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<!-- Permissive CSP so nested content is not constrained by host CSP -->
<meta http-equiv="Content-Security-Policy" content="
default-src 'self';
img-src * data: blob: 'unsafe-inline';
media-src * blob: data:;
font-src * blob: data:;
script-src 'self'
'wasm-unsafe-eval'
'unsafe-inline'
'unsafe-eval'
blob: data: http://localhost:* https://localhost:*;
style-src * blob: data: 'unsafe-inline';
connect-src *;
frame-src * blob: data: http://localhost:* https://localhost:*;
base-uri 'self';
" />
<title>MCP-UI Proxy</title>
<style>
html,
body {
margin: 0;
height: 100vh;
width: 100vw;
}
body {
display: flex;
flex-direction: column;
}
* {
box-sizing: border-box;
}
iframe {
background-color: transparent;
border: 0px none transparent;
padding: 0px;
overflow: hidden;
flex-grow: 1;
}
</style>
</head>
<body>
<script type="module" src="/src/sandbox.ts"></script>
</body>
</html>
58 changes: 58 additions & 0 deletions examples/simple-host/server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
#!/usr/bin/env npx tsx
/**
* Simple HTTP server to serve the host and sandbox html files with appropriate
* Content Security Policy (CSP) headers.
*/

import express from "express";
import cors from "cors";
import { fileURLToPath } from "url";
import { dirname, join } from "path";

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

const PORT = parseInt(process.env.PORT || "8080", 10);
const DIRECTORY = join(__dirname, "dist");

const app = express();

// CORS middleware for all routes
app.use(cors());

// Custom middleware for sandbox.html and root
app.use((req, res, next) => {
if (req.path === "/sandbox.html" || req.path === "/") {
// Permissive CSP to allow external resources (images, styles, scripts)
const csp = [
"default-src 'self'",
"img-src * data: blob: 'unsafe-inline'",
"style-src * blob: data: 'unsafe-inline'",
"script-src * blob: data: 'unsafe-inline' 'unsafe-eval'",
"connect-src *",
"font-src * blob: data:",
"media-src * blob: data:",
"frame-src * blob: data:",
].join("; ");
res.setHeader("Content-Security-Policy", csp);

// Disable caching to ensure fresh content on every request
res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
res.setHeader("Pragma", "no-cache");
res.setHeader("Expires", "0");
}
next();
});

// Serve static files from dist directory
app.use(express.static(DIRECTORY));

// Redirect root to example-host.html
app.get("/", (_req, res) => {
res.redirect("/example-host-react.html");
});

app.listen(PORT, () => {
console.log(`Server running on: http://localhost:${PORT}`);
console.log("Press Ctrl+C to stop the server\n");
});
Loading