From f89456d0db9688178c40c175b2f7fb1ffd0eca83 Mon Sep 17 00:00:00 2001 From: Andrew Khadder <54488379+khandrew1@users.noreply.github.com> Date: Sat, 21 Feb 2026 16:48:35 -0800 Subject: [PATCH] Auto-allow localhost in sandbox CSP for MCP Apps In srcdoc iframes, CSP 'self' resolves to about:srcdoc which is useless for allowing local dev server connections. This caused MCP app developers to declare localhost domains in their ui.csp metadata just to avoid CSP violations from Inspector infrastructure (Vite HMR, API server, etc). - Auto-include localhost/127.0.0.1 wildcard origins in connect-src (HTTP, HTTPS, WS, WSS) and resource directives - Add 'unsafe-eval' to script-src for Vite dev mode compatibility - Add worker-src directive for Web Worker support - Remove debug console.log statements from buildCSP() Co-Authored-By: Claude --- .../routes/apps/mcp-apps/sandbox-proxy.html | 81 +++++++++---------- 1 file changed, 38 insertions(+), 43 deletions(-) diff --git a/mcpjam-inspector/server/routes/apps/mcp-apps/sandbox-proxy.html b/mcpjam-inspector/server/routes/apps/mcp-apps/sandbox-proxy.html index 29f9243c5..67ef89677 100644 --- a/mcpjam-inspector/server/routes/apps/mcp-apps/sandbox-proxy.html +++ b/mcpjam-inspector/server/routes/apps/mcp-apps/sandbox-proxy.html @@ -91,18 +91,33 @@ * @returns {string} CSP policy string */ function buildCSP(csp) { + // Inspector is a dev tool — auto-allow localhost connections. + // In srcdoc iframes, 'self' resolves to about:srcdoc (useless), + // so we must explicitly allow localhost/127.0.0.1 for dev server + // connections (Vite HMR, API server, local MCP servers, etc.) + var localhostSrc = [ + "http://localhost:*", + "https://localhost:*", + "http://127.0.0.1:*", + "https://127.0.0.1:*", + ].join(" "); + var localhostConnectSrc = + localhostSrc + + " ws://localhost:* wss://localhost:* ws://127.0.0.1:* wss://127.0.0.1:*"; + // Per SEP-1865: If no CSP declared, use restrictive defaults // Note: 'self' doesn't work in srcdoc iframes (refers to about:srcdoc) // So we use 'unsafe-inline' for scripts/styles since all widget code is inline if (!csp) { return [ "default-src 'none'", - "script-src 'unsafe-inline'", + "script-src 'unsafe-inline' 'unsafe-eval'", "style-src 'unsafe-inline'", - "img-src data:", - "font-src data:", - "media-src data:", - "connect-src 'none'", + "img-src data: " + localhostSrc, + "font-src data: " + localhostSrc, + "media-src data: " + localhostSrc, + "worker-src blob: " + localhostSrc, + "connect-src " + localhostConnectSrc, "frame-src 'none'", "object-src 'none'", "base-uri 'none'", @@ -111,68 +126,48 @@ // Build CSP from declared domains (SEP-1865) // Per spec: "Host MAY further restrict but MUST NOT allow undeclared domains" - const connectDomains = (csp.connectDomains || []) + var connectDomains = (csp.connectDomains || []) .map(sanitizeDomain) .filter(Boolean); - const resourceDomains = (csp.resourceDomains || []) + var resourceDomains = (csp.resourceDomains || []) .map(sanitizeDomain) .filter(Boolean); - const frameDomains = (csp.frameDomains || []) + var frameDomains = (csp.frameDomains || []) .map(sanitizeDomain) .filter(Boolean); - const baseUriDomains = (csp.baseUriDomains || []) + var baseUriDomains = (csp.baseUriDomains || []) .map(sanitizeDomain) .filter(Boolean); - // connect-src: Only allow declared domains, or 'none' if empty - const connectSrc = - connectDomains.length > 0 ? connectDomains.join(" ") : "'none'"; + // connect-src: declared domains + localhost (dev tool) + var connectSrc = + connectDomains.length > 0 + ? localhostConnectSrc + " " + connectDomains.join(" ") + : localhostConnectSrc; - // Resource sources: data: and blob: are always allowed for inline content - // Only add declared resourceDomains - no forced CDNs per SEP-1865 - const resourceSrc = + // Resource sources: data:, blob:, and localhost always allowed + // Plus declared resourceDomains + var resourceSrc = resourceDomains.length > 0 - ? ["data:", "blob:", ...resourceDomains].join(" ") - : "data: blob:"; + ? ["data:", "blob:", localhostSrc, ...resourceDomains].join(" ") + : "data: blob: " + localhostSrc; // frame-src: Only allow declared frame domains, or 'none' if empty - const frameSrc = + var frameSrc = frameDomains.length > 0 ? frameDomains.join(" ") : "'none'"; // base-uri: Only allow declared base URI domains, or 'none' if empty - const baseUri = + var baseUri = baseUriDomains.length > 0 ? baseUriDomains.join(" ") : "'none'"; - console.log("[buildCSP] Processing domains:", { - frameDomains, - frameSrc, - baseUriDomains, - baseUri, - }); - - console.log( - "[buildCSP] Built CSP string:", - [ - "default-src 'none'", - "script-src 'unsafe-inline' " + resourceSrc, - "style-src 'unsafe-inline' " + resourceSrc, - "img-src " + resourceSrc, - "font-src " + resourceSrc, - "media-src " + resourceSrc, - "connect-src " + connectSrc, - "frame-src " + frameSrc, - "object-src 'none'", - "base-uri " + baseUri, - ].join("; "), - ); - return [ "default-src 'none'", - "script-src 'unsafe-inline' " + resourceSrc, + "script-src 'unsafe-inline' 'unsafe-eval' " + resourceSrc, "style-src 'unsafe-inline' " + resourceSrc, "img-src " + resourceSrc, "font-src " + resourceSrc, "media-src " + resourceSrc, + "worker-src blob: " + resourceSrc, "connect-src " + connectSrc, "frame-src " + frameSrc, "object-src 'none'",