Initial commit
This commit is contained in:
224
mcp-server/monitor.js
Normal file
224
mcp-server/monitor.js
Normal file
@@ -0,0 +1,224 @@
|
||||
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 `<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>MCP Monitor</title>
|
||||
<style>
|
||||
body { font-family: sans-serif; background: #111827; color: #e5e7eb; margin: 0; padding: 2rem; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>MCP Monitor</h1>
|
||||
<p>No se encontró el archivo de interfaz en <code>${monitorHtmlPath}</code>.</p>
|
||||
</body>
|
||||
</html>`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user