Skip to main content
Glama
masx200

Persistent Terminal MCP Server

by masx200
web-ui-server.ts10.2 kB
import express, { Request, Response } from "express"; import { WebSocketServer, WebSocket } from "ws"; import { createServer, Server as HttpServer } from "http"; import path from "path"; import { fileURLToPath } from "url"; import { TerminalManager } from "./terminal-manager.js"; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); /** * Web UI 服务器 * 提供静态文件服务、REST API 和 WebSocket 实时推送 */ export class WebUIServer { private app: express.Application; private httpServer: HttpServer | null = null; private wss: WebSocketServer | null = null; private terminalManager: TerminalManager; private clients: Set<WebSocket> = new Set(); constructor(terminalManager: TerminalManager) { this.terminalManager = terminalManager; this.app = express(); this.setupMiddleware(); this.setupRoutes(); } /** * 设置中间件 */ private setupMiddleware(): void { // JSON 解析 this.app.use(express.json()); this.app.use(express.urlencoded({ extended: true })); // 静态文件服务 const publicPath = path.join(__dirname, "../public"); this.app.use(express.static(publicPath)); // 请求日志 this.app.use((req, res, next) => { if (process.env.MCP_DEBUG === "true") { process.stderr.write(`[WEB-UI] ${req.method} ${req.path}\n`); } next(); }); } /** * 设置路由 */ private setupRoutes(): void { // 主页 this.app.get("/", (req: Request, res: Response) => { res.sendFile(path.join(__dirname, "../public/index.html")); }); // 终端详情页 this.app.get("/terminal/:id", (req: Request, res: Response) => { res.sendFile(path.join(__dirname, "../public/terminal.html")); }); // REST API 端点 this.setupApiRoutes(); } /** * 设置 API 路由 */ private setupApiRoutes(): void { // 获取所有终端 this.app.get("/api/terminals", async (req: Request, res: Response) => { try { const result = await this.terminalManager.listTerminals(); res.json(result); } catch (error) { res.status(500).json({ error: "Failed to list terminals", message: error instanceof Error ? error.message : String(error), }); } }); // 获取终端详情 this.app.get("/api/terminals/:id", async (req: Request, res: Response) => { try { const { id } = req.params; if (!id) { res.status(400).json({ error: "Terminal ID is required" }); return; } const session = this.terminalManager.getTerminalInfo(id); if (!session) { res.status(404).json({ error: "Terminal not found" }); return; } res.json({ id: session.id, pid: session.pid, shell: session.shell, cwd: session.cwd, created: session.created.toISOString(), lastActivity: session.lastActivity.toISOString(), status: session.status, }); } catch (error) { res.status(500).json({ error: "Failed to get terminal info", message: error instanceof Error ? error.message : String(error), }); } }); // 创建终端 this.app.post("/api/terminals", async (req: Request, res: Response) => { try { const { shell, cwd, env } = req.body; const terminalId = await this.terminalManager.createTerminal({ shell, cwd, env, }); const session = this.terminalManager.getTerminalInfo(terminalId); res.status(201).json({ terminalId, status: session?.status, pid: session?.pid, shell: session?.shell, cwd: session?.cwd, }); // 广播新终端创建事件 this.broadcast({ type: "terminal_created", terminalId, }); } catch (error) { res.status(400).json({ error: "Failed to create terminal", message: error instanceof Error ? error.message : String(error), }); } }); // 读取终端输出 this.app.get( "/api/terminals/:id/output", async (req: Request, res: Response) => { try { const { id } = req.params; if (!id) { res.status(400).json({ error: "Terminal ID is required" }); return; } const { since, maxLines, mode } = req.query; const result = await this.terminalManager.readFromTerminal({ terminalId: id, since: since ? parseInt(since as string) : undefined, maxLines: maxLines ? parseInt(maxLines as string) : undefined, mode: mode as any, }); res.json(result); } catch (error) { res.status(400).json({ error: "Failed to read terminal output", message: error instanceof Error ? error.message : String(error), }); } }, ); // 写入终端输入 this.app.post( "/api/terminals/:id/input", async (req: Request, res: Response) => { try { const { id } = req.params; if (!id) { res.status(400).json({ error: "Terminal ID is required" }); return; } const { input, appendNewline } = req.body; await this.terminalManager.writeToTerminal({ terminalId: id, input, appendNewline, }); res.json({ success: true }); } catch (error) { res.status(400).json({ error: "Failed to write to terminal", message: error instanceof Error ? error.message : String(error), }); } }, ); // 终止终端 this.app.delete( "/api/terminals/:id", async (req: Request, res: Response) => { try { const { id } = req.params; if (!id) { res.status(400).json({ error: "Terminal ID is required" }); return; } const { signal } = req.query; await this.terminalManager.killTerminal(id, signal as string); res.json({ success: true }); // 广播终端终止事件 this.broadcast({ type: "terminal_killed", terminalId: id, }); } catch (error) { res.status(400).json({ error: "Failed to kill terminal", message: error instanceof Error ? error.message : String(error), }); } }, ); // 获取终端统计 this.app.get( "/api/terminals/:id/stats", async (req: Request, res: Response) => { try { const { id } = req.params; if (!id) { res.status(400).json({ error: "Terminal ID is required" }); return; } const result = await this.terminalManager.getTerminalStats(id); res.json(result); } catch (error) { res.status(400).json({ error: "Failed to get terminal stats", message: error instanceof Error ? error.message : String(error), }); } }, ); } /** * 设置 WebSocket */ private setupWebSocket(): void { if (!this.httpServer) return; this.wss = new WebSocketServer({ server: this.httpServer }); this.wss.on("connection", (ws: WebSocket) => { this.clients.add(ws); if (process.env.MCP_DEBUG === "true") { process.stderr.write("[WEB-UI] WebSocket client connected\n"); } ws.on("close", () => { this.clients.delete(ws); if (process.env.MCP_DEBUG === "true") { process.stderr.write("[WEB-UI] WebSocket client disconnected\n"); } }); ws.on("error", (error) => { if (process.env.MCP_DEBUG === "true") { process.stderr.write(`[WEB-UI] WebSocket error: ${error}\n`); } }); }); // 监听终端事件并广播 this.terminalManager.on( "terminalOutput", (terminalId: string, data: string) => { this.broadcast({ type: "output", terminalId, data, }); }, ); this.terminalManager.on("terminalExit", (terminalId: string) => { this.broadcast({ type: "exit", terminalId, }); }); } /** * 广播消息给所有客户端 */ private broadcast(message: any): void { const payload = JSON.stringify(message); this.clients.forEach((client) => { if (client.readyState === WebSocket.OPEN) { client.send(payload); } }); } /** * 启动服务器 */ async start(port: number, host?: string): Promise<void> { return new Promise((resolve, reject) => { this.httpServer = createServer(this.app); const serverHost = host || process.env.WEB_UI_HOST || "127.0.0.1"; this.httpServer.listen(port, serverHost, () => { if (process.env.MCP_DEBUG === "true") { const displayHost = serverHost === "0.0.0.0" ? "localhost" : serverHost; process.stderr.write( `[WEB-UI] Server started on http://${displayHost}:${port}\n`, ); } // 启动 WebSocket this.setupWebSocket(); resolve(); }); this.httpServer.on("error", (error) => { reject(error); }); }); } /** * 停止服务器 */ async stop(): Promise<void> { return new Promise((resolve) => { // 关闭所有 WebSocket 连接 this.clients.forEach((client) => { client.close(); }); this.clients.clear(); // 关闭 WebSocket 服务器 if (this.wss) { this.wss.close(); this.wss = null; } // 关闭 HTTP 服务器 if (this.httpServer) { this.httpServer.close(() => { if (process.env.MCP_DEBUG === "true") { process.stderr.write("[WEB-UI] Server stopped\n"); } resolve(); }); this.httpServer = null; } else { resolve(); } }); } }

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/masx200/persistent-terminal-mcp'

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