Make vite work out-of-the-box for simple-host (#33)

This commit is contained in:
Jan Olaf Martin 2025-12-01 13:57:46 -08:00 • committed by GitHub
commit a7d73c72e0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 33 additions and 104 deletions

View file

@ -0,0 +1 @@
<meta http-equiv="refresh" content="0; url=/example-host-react.html">

View file

@ -4,9 +4,9 @@
"version": "1.0.0",
"type": "module",
"scripts": {
"start": "NODE_ENV=development npm run build && concurrently 'npm run start:server'",
"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'"
"start": "vite dev",
"start:server": "vite preview",
"build": "vite build"
},
"dependencies": {
"@modelcontextprotocol/ext-apps": "../..",

View file

@ -1,80 +0,0 @@
#!/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");
});

View file

@ -8,7 +8,11 @@ import { Tool } from "@modelcontextprotocol/sdk/types.js";
import { AppRenderer, AppRendererProps } from "../src/AppRenderer";
import { AppBridge } from "../../../dist/src/app-bridge";
const SANDBOX_PROXY_URL = new URL("http://localhost:8081/sandbox.html");
// We use '[::1]' for the sandbox to ensure it's a different origin from 'localhost'.
const SANDBOX_PROXY_URL = new URL(
"/sandbox.html",
location.href.replace("localhost:", "[::1]:"),
);
/**
* Example React application demonstrating the AppRenderer component.

View file

@ -18,7 +18,11 @@ import {
McpUiSizeChangeNotificationSchema,
} from "@modelcontextprotocol/ext-apps";
const SANDBOX_PROXY_URL = new URL("http://localhost:8081/sandbox.html");
// We use '[::1]' for the sandbox to ensure it's a different origin from 'localhost'.
const SANDBOX_PROXY_URL = new URL(
"/sandbox.html",
location.href.replace("localhost:", "[::1]:"),
);
window.addEventListener("load", async () => {
const client = new Client({

View file

@ -1,24 +1,24 @@
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import { viteSingleFile } from "vite-plugin-singlefile";
const INPUT = process.env.INPUT;
if (!INPUT) {
throw new Error("INPUT environment variable is not set");
}
const isDevelopment = process.env.NODE_ENV === "development";
export default defineConfig({
plugins: [react(), viteSingleFile()],
build: {
sourcemap: isDevelopment ? "inline" : undefined,
cssMinify: !isDevelopment,
minify: !isDevelopment,
rollupOptions: {
input: INPUT,
export default defineConfig(({ mode }) => {
const isDevelopment = mode === "development";
return {
plugins: [react()],
build: {
sourcemap: isDevelopment ? "inline" : undefined,
cssMinify: !isDevelopment,
minify: !isDevelopment,
rollupOptions: {
input: [
"index.html",
"example-host-vanilla.html",
"example-host-react.html",
"sandbox.html",
],
},
outDir: `dist`,
emptyOutDir: false,
},
outDir: `dist`,
emptyOutDir: false,
},
};
});