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
2 changes: 1 addition & 1 deletion examples/simple-host/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"type": "module",
"scripts": {
"start": "NODE_ENV=development npm run build && concurrently 'npm run start:server'",
"start:server": "bun server.ts",
"start:server": "bun serve.ts",
"build": "concurrently 'INPUT=example-host-vanilla.html vite build' 'INPUT=example-host-react.html vite build' 'INPUT=sandbox.html vite build'"
},
"dependencies": {
Expand Down
80 changes: 80 additions & 0 deletions examples/simple-host/serve.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
#!/usr/bin/env npx tsx
/**
* HTTP servers for the MCP UI example:
* - Host server (port 8080): serves host HTML files (React and Vanilla examples)
* - Sandbox server (port 8081): serves sandbox.html with permissive CSP
*
* Running on separate ports ensures proper origin isolation for security.
*/

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 HOST_PORT = parseInt(process.env.HOST_PORT || "8080", 10);
const SANDBOX_PORT = parseInt(process.env.SANDBOX_PORT || "8081", 10);
const DIRECTORY = join(__dirname, "dist");

// ============ Host Server (port 8080) ============
const hostApp = express();
hostApp.use(cors());

// Exclude sandbox.html from host server
hostApp.use((req, res, next) => {
if (req.path === "/sandbox.html") {
res.status(404).send("Sandbox is served on a different port");
return;
}
next();
});

hostApp.use(express.static(DIRECTORY));

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

// ============ Sandbox Server (port 8081) ============
const sandboxApp = express();
sandboxApp.use(cors());

// Permissive CSP for sandbox content
sandboxApp.use((_req, res, next) => {
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);
res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
res.setHeader("Pragma", "no-cache");
res.setHeader("Expires", "0");
next();
});

sandboxApp.get(["/", "/sandbox.html"], (_req, res) => {
res.sendFile(join(DIRECTORY, "sandbox.html"));
});

sandboxApp.use((_req, res) => {
res.status(404).send("Only sandbox.html is served on this port");
});

// ============ Start both servers ============
hostApp.listen(HOST_PORT, () => {
console.log(`Host server: http://localhost:${HOST_PORT}`);
});

sandboxApp.listen(SANDBOX_PORT, () => {
console.log(`Sandbox server: http://localhost:${SANDBOX_PORT}`);
console.log("\nPress Ctrl+C to stop\n");
});
58 changes: 0 additions & 58 deletions examples/simple-host/server.ts

This file was deleted.

2 changes: 1 addition & 1 deletion examples/simple-host/src/example-host-react.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { Tool } from "@modelcontextprotocol/sdk/types.js";
import { AppRenderer, AppRendererProps } from "../src/AppRenderer";
import { AppBridge } from "../../../dist/src/app-bridge";

const SANDBOX_PROXY_URL = URL.parse("/sandbox.html", location.href)!;
const SANDBOX_PROXY_URL = new URL("http://localhost:8081/sandbox.html");

/**
* Example React application demonstrating the AppRenderer component.
Expand Down
2 changes: 1 addition & 1 deletion examples/simple-host/src/example-host-vanilla.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import {
McpUiSizeChangeNotificationSchema,
} from "@modelcontextprotocol/ext-apps";

const SANDBOX_PROXY_URL = URL.parse("/sandbox.html", location.href)!;
const SANDBOX_PROXY_URL = new URL("http://localhost:8081/sandbox.html");

window.addEventListener("load", async () => {
const client = new Client({
Expand Down
38 changes: 33 additions & 5 deletions examples/simple-host/src/sandbox.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,43 @@
// Double-iframe raw HTML mode (HTML sent via postMessage)
import type {
McpUiSandboxProxyReadyNotification,
McpUiSandboxResourceReadyNotification,
} from "../../../dist/src/types";

if (window.self === window.top) {
throw new Error("This file is only to be used in an iframe sandbox.");
}
if (!document.referrer) {
throw new Error("No referrer, cannot validate embedding site.");
}
if (!document.referrer.match(/^http:\/\/(localhost|127\.0\.0\.1)(:|\/|$)/)) {
throw new Error(
`Embedding domain not allowed in referrer ${document.referrer} (update the validation logic to allow your domain)`,
);
}

// Try and break out of this iframe
try {
window.top!.alert("If you see this, the sandbox is not setup securely.");

throw new Error(
"Managed to break out of iframe, the sandbox is not setup securely.",
);
} catch (e) {
// Ignore
}

const inner = document.createElement("iframe");
inner.style = "width:100%; height:100%; border:none;";
// sandbox will be set from postMessage payload; default minimal before html arrives
inner.setAttribute("sandbox", "allow-scripts allow-same-origin allow-forms");
document.body.appendChild(inner);

// Wait for HTML content from parent
window.addEventListener("message", async (event) => {
// Note: in production you'll also want to validate event.origin against your outer domain.
if (event.source === window.parent) {
if (
event.data &&
event.data.method === "ui/notifications/sandbox-resource-ready"
event.data.method ===
("ui/notifications/sandbox-resource-ready" as McpUiSandboxResourceReadyNotification["method"])
) {
const { html, sandbox } = event.data.params;
if (typeof sandbox === "string") {
Expand All @@ -34,7 +61,8 @@ window.addEventListener("message", async (event) => {
window.parent.postMessage(
{
jsonrpc: "2.0",
method: "ui/notifications/sandbox-proxy-ready",
method:
"ui/notifications/sandbox-proxy-ready" as McpUiSandboxProxyReadyNotification["method"],
params: {},
},
"*",
Expand Down