diff --git a/build-all.mts b/build-all.mts index 3b3a9711..845a353a 100644 --- a/build-all.mts +++ b/build-all.mts @@ -171,13 +171,41 @@ const defaultBaseUrl = "http://localhost:4444"; const baseUrlCandidate = process.env.BASE_URL?.trim() ?? ""; const baseUrlRaw = baseUrlCandidate.length > 0 ? baseUrlCandidate : defaultBaseUrl; const normalizedBaseUrl = baseUrlRaw.replace(/\/+$/, "") || defaultBaseUrl; + +// Check if we should inline assets (useful for ngrok free tier which shows interstitial pages) +const inlineAssets = process.env.INLINE_ASSETS === "true"; console.log(`Using BASE_URL ${normalizedBaseUrl} for generated HTML`); +if (inlineAssets) { + console.log("INLINE_ASSETS=true: Inlining JS and CSS into HTML files"); +} for (const name of builtNames) { const dir = outDir; const hashedHtmlPath = path.join(dir, `${name}-${h}.html`); const liveHtmlPath = path.join(dir, `${name}.html`); - const html = ` + + let html: string; + + if (inlineAssets) { + // Read the JS and CSS files and inline them + const jsPath = path.join(dir, `${name}-${h}.js`); + const cssPath = path.join(dir, `${name}-${h}.css`); + const jsContent = fs.existsSync(jsPath) ? fs.readFileSync(jsPath, "utf8") : ""; + const cssContent = fs.existsSync(cssPath) ? fs.readFileSync(cssPath, "utf8") : ""; + + html = ` + + + + + +
+ + + +`; + } else { + html = ` @@ -188,6 +216,7 @@ for (const name of builtNames) { `; + } fs.writeFileSync(hashedHtmlPath, html, { encoding: "utf8" }); fs.writeFileSync(liveHtmlPath, html, { encoding: "utf8" }); console.log(`${liveHtmlPath}`); diff --git a/kitchen_sink_server_node/src/server.ts b/kitchen_sink_server_node/src/server.ts index c97e2138..83008fcf 100644 --- a/kitchen_sink_server_node/src/server.ts +++ b/kitchen_sink_server_node/src/server.ts @@ -47,6 +47,19 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url)); const ROOT_DIR = path.resolve(__dirname, "..", ".."); const ASSETS_DIR = path.resolve(ROOT_DIR, "assets"); +// MIME types for serving static files +const MIME_TYPES: Record = { + ".html": "text/html", + ".js": "application/javascript", + ".css": "text/css", + ".json": "application/json", + ".png": "image/png", + ".jpg": "image/jpeg", + ".gif": "image/gif", + ".svg": "image/svg+xml", + ".map": "application/json", +}; + const TEMPLATE_URI = "ui://widget/kitchen-sink-lite.html"; const MIME_TYPE = "text/html+skybridge"; @@ -298,6 +311,45 @@ const sessions = new Map(); const ssePath = "/mcp"; const postPath = "/mcp/messages"; +/** + * Serve static files from the assets directory. + * Returns true if a file was served, false otherwise. + */ +function serveStaticFile( + req: IncomingMessage, + res: ServerResponse, + pathname: string +): boolean { + // Remove leading slash and resolve path + const filename = pathname.replace(/^\//, ""); + const filePath = path.join(ASSETS_DIR, filename); + + // Security: prevent directory traversal + if (!filePath.startsWith(ASSETS_DIR)) { + return false; + } + + if (!fs.existsSync(filePath) || fs.statSync(filePath).isDirectory()) { + return false; + } + + const ext = path.extname(filePath).toLowerCase(); + const mimeType = MIME_TYPES[ext] || "application/octet-stream"; + + try { + const content = fs.readFileSync(filePath); + res.writeHead(200, { + "Content-Type": mimeType, + "Access-Control-Allow-Origin": "*", + "Cache-Control": "public, max-age=3600", + }); + res.end(content); + return true; + } catch { + return false; + } +} + async function handleSseRequest(res: ServerResponse) { res.setHeader("Access-Control-Allow-Origin", "*"); const server = createKitchenSinkServer(); @@ -392,6 +444,11 @@ const httpServer = createServer( return; } + // Serve static files from assets directory + if (req.method === "GET" && serveStaticFile(req, res, url.pathname)) { + return; + } + res.writeHead(404).end("Not Found"); } ); @@ -407,4 +464,9 @@ httpServer.listen(port, () => { console.log( ` Message post endpoint: POST http://localhost:${port}${postPath}?sessionId=...` ); + console.log(` Static assets served from: ${ASSETS_DIR}`); + console.log(`\nTo use with ngrok:`); + console.log(` 1. Run: ngrok http ${port}`); + console.log(` 2. Rebuild assets: BASE_URL=https://.ngrok-free.app pnpm run build`); + console.log(` 3. Add the ngrok URL to ChatGPT Settings > Connectors (e.g., https://.ngrok-free.app/mcp)`); });