Skip to main content
Glama
webServer.ts6.37 kB
import express, { Request, Response } from "express"; import getPort from "get-port"; import path from "path"; import fs from "fs"; import fsPromises from "fs/promises"; import { fileURLToPath } from "url"; import { getDataDir, getTasksFilePath, getWebGuiFilePath, } from "../utils/paths.js"; export async function createWebServer() { // 創建 Express 應用 // Create Express application const app = express(); // 儲存 SSE 客戶端的列表 // Store list of SSE clients let sseClients: Response[] = []; // 發送 SSE 事件的輔助函數 // Helper function to send SSE events function sendSseUpdate() { sseClients.forEach((client) => { // 檢查客戶端是否仍然連接 // Check if client is still connected if (!client.writableEnded) { client.write( `event: update\ndata: ${JSON.stringify({ timestamp: Date.now(), })}\n\n` ); } }); // 清理已斷開的客戶端 (可選,但建議) // Clean up disconnected clients (optional, but recommended) sseClients = sseClients.filter((client) => !client.writableEnded); } // 設置靜態文件目錄 // Set up static file directory const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const publicPath = path.join(__dirname, "..", "..", "src", "public"); const TASKS_FILE_PATH = await getTasksFilePath(); // 使用工具函數取得檔案路徑 // Use utility function to get file path app.use(express.static(publicPath)); // 設置 API 路由 // Set up API routes app.get("/api/tasks", async (req: Request, res: Response) => { try { // 使用 fsPromises 保持異步讀取 // Use fsPromises to maintain async reading const tasksData = await fsPromises.readFile(TASKS_FILE_PATH, "utf-8"); res.json(JSON.parse(tasksData)); } catch (error) { // 確保檔案不存在時返回空任務列表 // Ensure empty task list is returned when file doesn't exist if ((error as NodeJS.ErrnoException).code === "ENOENT") { res.json({ tasks: [] }); } else { res.status(500).json({ error: "Failed to read tasks data" }); } } }); // 新增:SSE 端點 // Add: SSE endpoint app.get("/api/tasks/stream", (req: Request, res: Response) => { res.writeHead(200, { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", Connection: "keep-alive", // 可選: CORS 頭,如果前端和後端不在同一個 origin // Optional: CORS headers if frontend and backend are not on the same origin // "Access-Control-Allow-Origin": "*", }); // 發送一個初始事件或保持連接 // Send an initial event or maintain connection res.write("data: connected\n\n"); // 將客戶端添加到列表 // Add client to the list sseClients.push(res); // 當客戶端斷開連接時,將其從列表中移除 // When client disconnects, remove it from the list req.on("close", () => { sseClients = sseClients.filter((client) => client !== res); }); }); // 定義 writeWebGuiFile 函數 // Define writeWebGuiFile function async function writeWebGuiFile(port: number | string) { try { // 讀取 TEMPLATES_USE 環境變數並轉換為語言代碼 // Read TEMPLATES_USE environment variable and convert to language code const templatesUse = process.env.TEMPLATES_USE || "en"; const getLanguageFromTemplate = (template: string): string => { if (template === "zh") return "zh-TW"; if (template === "en") return "en"; // 自訂範本預設使用英文 // Custom templates default to English return "en"; }; const language = getLanguageFromTemplate(templatesUse); const websiteUrl = `[Task Manager UI](http://localhost:${port}?lang=${language})`; const websiteFilePath = await getWebGuiFilePath(); const DATA_DIR = await getDataDir(); try { await fsPromises.access(DATA_DIR); } catch (error) { await fsPromises.mkdir(DATA_DIR, { recursive: true }); } await fsPromises.writeFile(websiteFilePath, websiteUrl, "utf-8"); } catch (error) { // Silently handle error - console not supported in MCP } } return { app, sendSseUpdate, async startServer() { // 獲取可用埠 // Get available port const port = process.env.WEB_PORT || (await getPort()); // 啟動 HTTP 伺服器 // Start HTTP server const httpServer = app.listen(port, () => { // 在伺服器啟動後開始監聽檔案變化 // Start monitoring file changes after server starts try { // 檢查檔案是否存在,如果不存在則不監聽 (避免 watch 報錯) // Check if file exists, don't monitor if it doesn't exist (to avoid watch errors) if (fs.existsSync(TASKS_FILE_PATH)) { fs.watch(TASKS_FILE_PATH, (eventType, filename) => { if ( filename && (eventType === "change" || eventType === "rename") ) { // 稍微延遲發送,以防短時間內多次觸發 (例如編輯器保存) // Slightly delay sending to prevent multiple triggers in a short time (e.g., editor saves) // debounce sendSseUpdate if needed // Debounce sendSseUpdate if needed sendSseUpdate(); } }); } } catch (watchError) {} // 將 URL 寫入 WebGUI.md // Write URL to WebGUI.md writeWebGuiFile(port).catch((error) => {}); }); // 設置進程終止事件處理 (確保移除 watcher) // Set up process termination event handling (ensure watcher removal) const shutdownHandler = async () => { // 關閉所有 SSE 連接 // Close all SSE connections sseClients.forEach((client) => client.end()); sseClients = []; // 關閉 HTTP 伺服器 // Close HTTP server await new Promise<void>((resolve) => httpServer.close(() => resolve())); }; process.on("SIGINT", shutdownHandler); process.on("SIGTERM", shutdownHandler); return httpServer; }, }; }

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/cjo4m06/mcp-shrimp-task-manager'

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