Skip to main content
Glama
http-server.ts4.12 kB
import * as http from 'node:http'; import * as fs from 'node:fs'; import * as path from 'node:path'; import * as os from 'node:os'; // MIME types for common file extensions const MIME_TYPES: Record<string, string> = { '.html': 'text/html', '.js': 'application/javascript', '.css': 'text/css', '.json': 'application/json', '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.gif': 'image/gif', '.svg': 'image/svg+xml', '.webp': 'image/webp', '.ico': 'image/x-icon', '.woff': 'font/woff', '.woff2': 'font/woff2', '.ttf': 'font/ttf', }; // Image storage directory const IMAGES_DIR = path.join(os.homedir(), '.pane', 'images'); let httpServer: http.Server | null = null; /** * Start an HTTP server to serve static files from a directory */ export function startHTTPServer(port: number, staticDir: string): void { const absoluteDir = path.resolve(staticDir); if (!fs.existsSync(absoluteDir)) { console.error(`[HTTP] Static directory not found: ${absoluteDir}`); console.error('[HTTP] Skipping HTTP server (dev mode?)'); return; } httpServer = http.createServer((req, res) => { // Handle /images/* route from ~/.pane/images/ if (req.url?.startsWith('/images/')) { const imagePath = req.url.slice(8); // Remove '/images/' const fullImagePath = path.join(IMAGES_DIR, imagePath); // Security: prevent directory traversal if (!fullImagePath.startsWith(IMAGES_DIR)) { res.writeHead(403); res.end('Forbidden'); return; } // Check if file exists if (!fs.existsSync(fullImagePath)) { res.writeHead(404); res.end('Image not found'); return; } // Read and serve image fs.readFile(fullImagePath, (err, data) => { if (err) { res.writeHead(500); res.end('Error reading image'); return; } const ext = path.extname(fullImagePath).toLowerCase(); const mimeType = MIME_TYPES[ext] || 'application/octet-stream'; res.writeHead(200, { 'Content-Type': mimeType, 'Cache-Control': 'public, max-age=31536000', // 1 year cache (immutable UUIDs) }); res.end(data); }); return; } // Default to index.html for root or SPA routes let filePath = req.url === '/' ? '/index.html' : req.url || '/index.html'; // Remove query strings filePath = filePath.split('?')[0]; // Construct full path let fullPath = path.join(absoluteDir, filePath); // Security: prevent directory traversal if (!fullPath.startsWith(absoluteDir)) { res.writeHead(403); res.end('Forbidden'); return; } // Check if file exists, fallback to index.html for SPA if (!fs.existsSync(fullPath)) { fullPath = path.join(absoluteDir, 'index.html'); } // Read and serve file fs.readFile(fullPath, (err, data) => { if (err) { res.writeHead(404); res.end('Not Found'); return; } const ext = path.extname(fullPath).toLowerCase(); const mimeType = MIME_TYPES[ext] || 'application/octet-stream'; res.writeHead(200, { 'Content-Type': mimeType }); res.end(data); }); }); httpServer.listen(port, () => { console.error(`[HTTP] UI available at http://localhost:${port}`); }); httpServer.on('error', (err: NodeJS.ErrnoException) => { if (err.code === 'EADDRINUSE') { console.error(`[HTTP] Port ${port} already in use, skipping HTTP server`); } else { console.error('[HTTP] Server error:', err); } }); } /** * Close the HTTP server */ export function closeHTTPServer(): Promise<void> { return new Promise((resolve) => { if (httpServer) { httpServer.close(() => { console.error('[HTTP] Server closed'); httpServer = null; resolve(); }); } else { resolve(); } }); } /** * Check if we're in production mode (dist folder exists) */ export function isProductionMode(distPath: string): boolean { return fs.existsSync(path.resolve(distPath)); }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/uptownhr/pane'

If you have feedback or need assistance with the MCP directory API, please join our Discord server