Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 30 additions & 1 deletion build-all.mts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = `<!doctype 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 = `<!doctype html>
<html>
<head>
<style>${cssContent}</style>
</head>
<body>
<div id="${name}-root"></div>
<script type="module">${jsContent}</script>
</body>
</html>
`;
} else {
html = `<!doctype html>
<html>
<head>
<script type="module" src="${normalizedBaseUrl}/${name}-${h}.js"></script>
Expand All @@ -188,6 +216,7 @@ for (const name of builtNames) {
</body>
</html>
`;
}
fs.writeFileSync(hashedHtmlPath, html, { encoding: "utf8" });
fs.writeFileSync(liveHtmlPath, html, { encoding: "utf8" });
console.log(`${liveHtmlPath}`);
Expand Down
62 changes: 62 additions & 0 deletions kitchen_sink_server_node/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string> = {
".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";

Expand Down Expand Up @@ -298,6 +311,45 @@ const sessions = new Map<string, SessionRecord>();
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();
Expand Down Expand Up @@ -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");
}
);
Expand All @@ -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://<your-ngrok-url>.ngrok-free.app pnpm run build`);
console.log(` 3. Add the ngrok URL to ChatGPT Settings > Connectors (e.g., https://<your-ngrok-url>.ngrok-free.app/mcp)`);
});