| | #!/usr/bin/env npx tsx |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| |
|
| | import express from "express"; |
| | import cors from "cors"; |
| | import { fileURLToPath } from "url"; |
| | import { dirname, join } from "path"; |
| | import type { McpUiResourceCsp } from "@modelcontextprotocol/ext-apps"; |
| |
|
| | 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"); |
| | const SERVERS: string[] = process.env.SERVERS |
| | ? JSON.parse(process.env.SERVERS) |
| | : ["http://localhost:3001/mcp"]; |
| |
|
| | |
| | const hostApp = express(); |
| | hostApp.use(cors()); |
| |
|
| | |
| | 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("/api/servers", (_req, res) => { |
| | res.json(SERVERS); |
| | }); |
| |
|
| | hostApp.get("/", (_req, res) => { |
| | res.redirect("/index.html"); |
| | }); |
| |
|
| | |
| | const sandboxApp = express(); |
| | sandboxApp.use(cors()); |
| |
|
| | |
| | |
| | |
| | |
| | |
| | function sanitizeCspDomains(domains?: string[]): string[] { |
| | if (!domains) return []; |
| | return domains.filter((d) => typeof d === "string" && !/[;\r\n'" ]/.test(d)); |
| | } |
| |
|
| | function buildCspHeader(csp?: McpUiResourceCsp): string { |
| | const resourceDomains = sanitizeCspDomains(csp?.resourceDomains).join(" "); |
| | const connectDomains = sanitizeCspDomains(csp?.connectDomains).join(" "); |
| | const frameDomains = sanitizeCspDomains(csp?.frameDomains).join(" ") || null; |
| | const baseUriDomains = |
| | sanitizeCspDomains(csp?.baseUriDomains).join(" ") || null; |
| |
|
| | const directives = [ |
| | |
| | "default-src 'self' 'unsafe-inline'", |
| | |
| | `script-src 'self' 'unsafe-inline' 'unsafe-eval' blob: data: ${resourceDomains}`.trim(), |
| | |
| | `style-src 'self' 'unsafe-inline' blob: data: ${resourceDomains}`.trim(), |
| | |
| | `img-src 'self' data: blob: ${resourceDomains}`.trim(), |
| | |
| | `font-src 'self' data: blob: ${resourceDomains}`.trim(), |
| | |
| | `connect-src 'self' ${connectDomains}`.trim(), |
| | |
| | |
| | |
| | |
| | |
| | `worker-src 'self' blob: ${resourceDomains}`.trim(), |
| | |
| | frameDomains ? `frame-src ${frameDomains}` : "frame-src 'none'", |
| | |
| | "object-src 'none'", |
| | |
| | baseUriDomains ? `base-uri ${baseUriDomains}` : "base-uri 'none'", |
| | ]; |
| |
|
| | return directives.join("; "); |
| | } |
| |
|
| | |
| | sandboxApp.get(["/", "/sandbox.html"], (req, res) => { |
| | |
| | let cspConfig: McpUiResourceCsp | undefined; |
| | if (typeof req.query.csp === "string") { |
| | try { |
| | cspConfig = JSON.parse(req.query.csp); |
| | } catch (e) { |
| | console.warn("[Sandbox] Invalid CSP query param:", e); |
| | } |
| | } |
| |
|
| | |
| | const cspHeader = buildCspHeader(cspConfig); |
| | res.setHeader("Content-Security-Policy", cspHeader); |
| |
|
| | |
| | res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate"); |
| | res.setHeader("Pragma", "no-cache"); |
| | res.setHeader("Expires", "0"); |
| |
|
| | res.sendFile(join(DIRECTORY, "sandbox.html")); |
| | }); |
| |
|
| | sandboxApp.use((_req, res) => { |
| | res.status(404).send("Only sandbox.html is served on this port"); |
| | }); |
| |
|
| | |
| | hostApp.listen(HOST_PORT, (err) => { |
| | if (err) { |
| | console.error("Error starting server:", err); |
| | process.exit(1); |
| | } |
| | console.log(`Host server: http://localhost:${HOST_PORT}`); |
| | }); |
| |
|
| | sandboxApp.listen(SANDBOX_PORT, (err) => { |
| | if (err) { |
| | console.error("Error starting server:", err); |
| | process.exit(1); |
| | } |
| | console.log(`Sandbox server: http://localhost:${SANDBOX_PORT}`); |
| | console.log("\nPress Ctrl+C to stop\n"); |
| | }); |
| |
|