import http from "node:http"; import fsPromises from "node:fs/promises"; import path from "node:path"; import { fileURLToPath } from "node:url"; import { MONITOR_PORT, MONITOR_DISABLED } from "./config/index.js"; import { sessionCredentials } from "./auth/credentials.js"; import { activeSessions } from "./httpServer.js"; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const monitorHtmlPath = path.join(__dirname, "monitor.html"); /** * Get active sessions with their credentials info */ function getActiveSessions() { const sessions = []; for (const [sessionId, sessionData] of activeSessions.entries()) { const creds = sessionCredentials.get(sessionId); sessions.push({ sessionId, website: creds?.website || null, hasToken: !!creds?.token, hasTokenHash: !!creds?.tokenHash, profileName: creds?.profileName || null, startTime: sessionData.startTime, durationMs: Date.now() - sessionData.startTime }); } return sessions.sort((a, b) => b.startTime - a.startTime); } // SSE clients for real-time updates export const sseClients = new Set(); /** * Get the monitor HTML page */ async function getMonitorHtml() { try { return await fsPromises.readFile(monitorHtmlPath, "utf-8"); } catch (error) { return ` MCP Monitor

MCP Monitor

No se encontrĂ³ el archivo de interfaz en ${monitorHtmlPath}.

`; } } /** * Broadcast an SSE event to all connected clients */ export function broadcastSse(event, payload) { const chunk = `event: ${event}\ndata: ${JSON.stringify(payload)}\n\n`; for (const client of sseClients) { try { client.write(chunk); } catch { sseClients.delete(client); } } } /** * Broadcast sessions update to all connected monitor clients */ export function broadcastSessionsUpdate() { broadcastSse("sessions", { sessions: getActiveSessions() }); } /** * Start the monitor HTTP server */ export function startMonitorServer(requestMonitor, toolHandlers) { if (MONITOR_DISABLED) { console.error("MCP monitor UI deshabilitada (MCP_MONITOR_DISABLED=1)."); return null; } const monitorServer = http.createServer(async (req, res) => { const url = new URL(req.url ?? "/", "http://localhost"); if (req.method === "GET" && (url.pathname === "/" || url.pathname === "/monitor")) { const html = await getMonitorHtml(); res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" }); res.end(html); return; } if (req.method === "GET" && url.pathname === "/requests") { res.writeHead(200, { "Content-Type": "application/json" }); res.end(JSON.stringify({ requests: requestMonitor.getSummaries() })); return; } if (req.method === "GET" && url.pathname.startsWith("/requests/")) { const [, , rawId] = url.pathname.split("/"); const entry = requestMonitor.getEntryById(rawId); if (!entry) { res.writeHead(404, { "Content-Type": "application/json" }); res.end(JSON.stringify({ error: "Request not found" })); return; } res.writeHead(200, { "Content-Type": "application/json" }); res.end(JSON.stringify(entry)); return; } if (req.method === "GET" && url.pathname === "/sessions") { res.writeHead(200, { "Content-Type": "application/json" }); res.end(JSON.stringify({ sessions: getActiveSessions() })); return; } if (req.method === "GET" && url.pathname === "/stats") { res.writeHead(200, { "Content-Type": "application/json" }); res.end(JSON.stringify({ stats: requestMonitor.getSessionStats() })); return; } if (req.method === "GET" && url.pathname === "/events") { res.writeHead(200, { "Content-Type": "text/event-stream", "Cache-Control": "no-cache, no-transform", Connection: "keep-alive", }); res.write("event: bootstrap\n"); res.write(`data: ${JSON.stringify({ requests: requestMonitor.getSummaries(), sessions: getActiveSessions(), stats: requestMonitor.getSessionStats() })}\n\n`); sseClients.add(res); req.on("close", () => { sseClients.delete(res); }); return; } if (req.method === "POST" && url.pathname.startsWith("/retry/")) { const [, , rawId] = url.pathname.split("/"); const entry = requestMonitor.getEntryById(rawId); if (!entry) { res.writeHead(404, { "Content-Type": "application/json" }); res.end(JSON.stringify({ error: "Request not found" })); return; } if (entry.method !== "tools/call") { res.writeHead(400, { "Content-Type": "application/json" }); res.end(JSON.stringify({ error: "Only tool calls can be retried" })); return; } const toolName = entry.request.params.name; const toolHandler = toolHandlers.get(toolName); if (!toolHandler) { res.writeHead(404, { "Content-Type": "application/json" }); res.end(JSON.stringify({ error: `Tool handler for '${toolName}' not found` })); return; } try { const extra = { sessionId: entry.sessionId }; const retryEntry = requestMonitor.start(entry.method, entry.request, extra); const result = await toolHandler.handler(entry.request.params.arguments, extra); requestMonitor.finish(retryEntry, result); res.writeHead(200, { "Content-Type": "application/json" }); res.end(JSON.stringify({ success: true, newRequestId: retryEntry.id, result })); } catch (error) { res.writeHead(500, { "Content-Type": "application/json" }); res.end(JSON.stringify({ error: error.message })); } return; } if (req.method === "GET" && url.pathname === "/health") { res.writeHead(200, { "Content-Type": "application/json" }); res.end(JSON.stringify({ status: "ok" })); return; } res.writeHead(404, { "Content-Type": "application/json" }); res.end(JSON.stringify({ error: "Not found" })); }); monitorServer.on("error", (error) => { console.warn( `[monitor] No se pudo iniciar la UI en el puerto ${MONITOR_PORT}: ${error.message}. Establece MCP_MONITOR_DISABLED=1 para ocultar este aviso.` ); }); monitorServer.listen(MONITOR_PORT, '0.0.0.0', () => { console.error(`MCP monitor UI: http://0.0.0.0:${MONITOR_PORT}/monitor`); }); // Broadcast sessions + stats update every 2 seconds for real-time monitoring setInterval(() => { if (sseClients.size > 0) { broadcastSessionsUpdate(); broadcastSse("stats", { stats: requestMonitor.getSessionStats() }); } }, 2000); return monitorServer; }