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)`);
});