|
| 1 | +import { createServer, type Server } from "node:http"; |
| 2 | +import type { McpConnectorManager } from "./connector.js"; |
| 3 | +import type { ToolRegistry } from "./registry.js"; |
| 4 | +import type { AuditLogger } from "./logger.js"; |
| 5 | + |
| 6 | +export class Dashboard { |
| 7 | + private server: Server | null = null; |
| 8 | + |
| 9 | + constructor( |
| 10 | + private readonly connector: McpConnectorManager, |
| 11 | + private readonly registry: ToolRegistry, |
| 12 | + private readonly logger: AuditLogger, |
| 13 | + private port: number = 9100 |
| 14 | + ) {} |
| 15 | + |
| 16 | + start(): void { |
| 17 | + this.server = createServer((req, res) => { |
| 18 | + if (req.url === "/api/status") { |
| 19 | + res.writeHead(200, { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" }); |
| 20 | + res.end(JSON.stringify(this.getData())); |
| 21 | + return; |
| 22 | + } |
| 23 | + res.writeHead(200, { "Content-Type": "text/html" }); |
| 24 | + res.end(this.getHtml()); |
| 25 | + }); |
| 26 | + this.server.on("error", (err: Error & { code?: string }) => { |
| 27 | + if (err.code === "EADDRINUSE") { |
| 28 | + this.port++; |
| 29 | + console.error(`[dashboard] Port taken, trying ${this.port}...`); |
| 30 | + this.server!.listen(this.port, "127.0.0.1"); |
| 31 | + } |
| 32 | + }); |
| 33 | + this.server.listen(this.port, "127.0.0.1", () => { |
| 34 | + console.error(`[dashboard] http://localhost:${this.port}`); |
| 35 | + }); |
| 36 | + } |
| 37 | + |
| 38 | + stop(): void { |
| 39 | + this.server?.close(); |
| 40 | + } |
| 41 | + |
| 42 | + private getData() { |
| 43 | + const statuses = this.connector.getStatuses(); |
| 44 | + return { |
| 45 | + upstreams: statuses.map((s) => ({ |
| 46 | + ...s, |
| 47 | + tools: this.registry.getByProvider(s.name).map((t) => ({ |
| 48 | + ref: t.ref, |
| 49 | + name: t.originalName, |
| 50 | + title: t.title, |
| 51 | + description: t.description, |
| 52 | + })), |
| 53 | + })), |
| 54 | + recentLogs: this.logger.getEntries(30), |
| 55 | + }; |
| 56 | + } |
| 57 | + |
| 58 | + private getHtml(): string { |
| 59 | + return `<!DOCTYPE html> |
| 60 | +<html lang="en"> |
| 61 | +<head> |
| 62 | +<meta charset="utf-8"> |
| 63 | +<meta name="viewport" content="width=device-width,initial-scale=1"> |
| 64 | +<title>MCP Proxy Dashboard</title> |
| 65 | +<style> |
| 66 | +*{box-sizing:border-box;margin:0;padding:0} |
| 67 | +body{font-family:system-ui,-apple-system,sans-serif;background:#0f1117;color:#e1e4e8;padding:24px} |
| 68 | +h1{font-size:1.4rem;margin-bottom:20px;color:#58a6ff} |
| 69 | +.grid{display:grid;gap:16px;grid-template-columns:repeat(auto-fill,minmax(400px,1fr))} |
| 70 | +.card{background:#161b22;border:1px solid #30363d;border-radius:8px;padding:16px;overflow:hidden} |
| 71 | +.card h2{font-size:1rem;margin-bottom:8px;display:flex;align-items:center;gap:8px} |
| 72 | +.badge{display:inline-block;padding:2px 8px;border-radius:12px;font-size:.75rem;font-weight:600} |
| 73 | +.connected{background:#238636;color:#fff} |
| 74 | +.error{background:#da3633;color:#fff} |
| 75 | +.connecting{background:#d29922;color:#000} |
| 76 | +.tools{margin-top:12px} |
| 77 | +.tool{background:#0d1117;border:1px solid #21262d;border-radius:4px;padding:8px 10px;margin-top:6px;font-size:.85rem} |
| 78 | +.tool .name{color:#79c0ff;font-weight:600} |
| 79 | +.tool .desc{color:#8b949e;margin-top:2px;font-size:.8rem} |
| 80 | +.logs{margin-top:12px;max-height:200px;overflow-y:auto;background:#0d1117;border:1px solid #21262d;border-radius:4px;padding:8px;font-family:monospace;font-size:.75rem;line-height:1.5;white-space:pre-wrap;word-break:break-all;color:#8b949e} |
| 81 | +.error-msg{color:#f85149;margin-top:6px;font-size:.85rem;font-family:monospace;white-space:pre-wrap;word-break:break-all;background:#1c0c0c;border:1px solid #da3633;border-radius:4px;padding:8px;max-height:300px;overflow-y:auto} |
| 82 | +.meta{color:#8b949e;font-size:.8rem;margin-top:4px} |
| 83 | +h3{font-size:.85rem;color:#8b949e;margin-top:12px;margin-bottom:4px} |
| 84 | +.audit{margin-top:20px} |
| 85 | +.audit table{width:100%;border-collapse:collapse;font-size:.8rem} |
| 86 | +.audit th,.audit td{text-align:left;padding:6px 8px;border-bottom:1px solid #21262d} |
| 87 | +.audit th{color:#8b949e;font-weight:600} |
| 88 | +.audit .err{color:#f85149} |
| 89 | +.refresh{background:#21262d;color:#c9d1d9;border:1px solid #30363d;padding:6px 14px;border-radius:6px;cursor:pointer;font-size:.85rem;margin-bottom:16px} |
| 90 | +.refresh:hover{background:#30363d} |
| 91 | +</style> |
| 92 | +</head> |
| 93 | +<body> |
| 94 | +<h1>MCP Proxy Dashboard</h1> |
| 95 | +<button class="refresh" onclick="load()">Refresh</button> |
| 96 | +<div class="grid" id="grid"></div> |
| 97 | +<div class="audit" id="audit"></div> |
| 98 | +<script> |
| 99 | +async function load(){ |
| 100 | + const r=await fetch('/api/status'); |
| 101 | + const d=await r.json(); |
| 102 | + const grid=document.getElementById('grid'); |
| 103 | + grid.innerHTML=d.upstreams.map(u=>\` |
| 104 | + <div class="card"> |
| 105 | + <h2>\${esc(u.name)} <span class="badge \${u.status}">\${u.status}</span></h2> |
| 106 | + <div class="meta">Transport: \${u.transport} | Tools: \${u.toolCount}</div> |
| 107 | + \${u.tools.length?\`<h3>Tools</h3><div class="tools">\${u.tools.map(t=>\` |
| 108 | + <div class="tool"><span class="name">\${esc(t.name)}</span><div class="desc">\${esc(t.description)}</div></div> |
| 109 | + \`).join('')}</div>\`:''} |
| 110 | + \${u.logs.length?\`<h3>Logs</h3><div class="logs">\${esc(u.logs.join('\\n'))}</div>\`:''} |
| 111 | + </div> |
| 112 | + \`).join(''); |
| 113 | + const audit=document.getElementById('audit'); |
| 114 | + if(d.recentLogs.length){ |
| 115 | + audit.innerHTML=\`<h3>Recent Audit Log</h3><table> |
| 116 | + <tr><th>Time</th><th>Tool</th><th>Provider</th><th>Ms</th><th>Size</th><th>Error</th></tr> |
| 117 | + \${d.recentLogs.map(e=>\`<tr> |
| 118 | + <td>\${e.timestamp.slice(11,19)}</td><td>\${esc(e.tool)}</td><td>\${esc(e.provider)}</td> |
| 119 | + <td>\${e.executionTimeMs}</td><td>\${e.outputSize}</td> |
| 120 | + <td class="\${e.error?'err':''}">\${e.error?esc(e.error):'-'}</td> |
| 121 | + </tr>\`).join('')} |
| 122 | + </table>\`; |
| 123 | + } |
| 124 | +} |
| 125 | +function esc(s){const d=document.createElement('div');d.textContent=s;return d.innerHTML} |
| 126 | +load();setInterval(load,5000); |
| 127 | +</script> |
| 128 | +</body> |
| 129 | +</html>`; |
| 130 | + } |
| 131 | +} |
0 commit comments