UNPKG

@gais/advantech-gais-mcp

Version:

A MCP server implementation for monitoring system status via Grafana and Prometheus

306 lines (305 loc) 12.9 kB
#!/usr/bin/env node "use strict"; const { McpServer } = require("@modelcontextprotocol/sdk/server/mcp.js"); const { StdioServerTransport } = require("@modelcontextprotocol/sdk/server/stdio.js"); console.log("✅ MCP Server 啟動中:已註冊工具 getGPIO"); console.log("GRAFANA_URL =", process.env.GRAFANA_URL); console.log("GRAFANA_API_KEY =", process.env.GRAFANA_API_KEY ? "(已設定)" : "(未設定)"); // Create an MCP server const server = new McpServer({ name: "advantech-gais-mcp", version: "1.0.0" }); // 新增自動查詢 Prometheus uid 的 function async function getPrometheusUid(grafanaUrl, apiKey) { const res = await fetch(`${grafanaUrl.replace(/\/$/, "")}/api/datasources`, { headers: { "Authorization": `Bearer ${apiKey}` } }); if (!res.ok) throw new Error("Failed to fetch datasources"); const datasources = await res.json(); const prometheus = datasources.find((ds) => ds.type === "prometheus"); return prometheus ? prometheus.uid : null; } server.tool("GAISMCP.getSystemStatus", "Retrieve system status and hardware resource usage.", {}, async () => { const grafanaBaseUrl = process.env.GRAFANA_URL || "http://localhost:3000"; const grafanaUrl = `${grafanaBaseUrl.replace(/\/$/, "")}/api/ds/query?ds_type=prometheus`; const grafanaApiKey = process.env.GRAFANA_API_KEY || "glsa_0i8OTdzA0d9WhmLOQ9iLo9DmQ9twmeq5L_d6bce8d4"; // 自動查詢 Prometheus uid let prometheusUid; try { prometheusUid = await getPrometheusUid(grafanaBaseUrl, grafanaApiKey); if (!prometheusUid) throw new Error("找不到 Prometheus 資料來源 uid"); } catch (e) { return { content: [{ type: "text", text: `無法取得 Prometheus uid:${e.message}\n[DEBUG] GRAFANA_URL: ${grafanaUrl}` }] }; } const payload = { queries: [ { editorMode: "code", expr: "node_cpu_usage_percentage", legendFormat: "__auto", range: true, refId: "cpu", datasource: { type: "prometheus", uid: prometheusUid }, exemplar: false, requestId: "cpuA", utcOffsetSec: 28800, scopes: [], adhocFilters: [], interval: "", datasourceId: 1, intervalMs: 20000, maxDataPoints: 100 }, { editorMode: "code", expr: "((node_memory_MemTotal_bytes{instance=~\"node-exporter:9100\"} - node_memory_MemAvailable_bytes{instance=~\"node-exporter:9100\"}) / (node_memory_MemTotal_bytes{instance=~\"node-exporter:9100\"} )) * 100", legendFormat: "__auto", range: true, refId: "mem", datasource: { type: "prometheus", uid: prometheusUid }, exemplar: false, requestId: "memA", utcOffsetSec: 28800, scopes: [], adhocFilters: [], interval: "", datasourceId: 1, intervalMs: 20000, maxDataPoints: 100 }, { datasource: { type: "prometheus", uid: prometheusUid }, editorMode: "code", exemplar: false, expr: "node_filesystem_size_bytes{instance=~'node-exporter:9100',fstype=~\"ext.*|xfs\",mountpoint !~\".*pod.*\"}-0", format: "table", hide: false, instant: true, legendFormat: "Total", range: false, refId: "disk_total", requestId: "13C", utcOffsetSec: 28800, scopes: [], adhocFilters: [], interval: "", datasourceId: 1, intervalMs: 20000, maxDataPoints: 1186 }, { editorMode: "code", exemplar: false, expr: "node_filesystem_avail_bytes {instance=~'node-exporter:9100',fstype=~\"ext.*|xfs\",mountpoint !~\".*pod.*\"}-0", format: "table", instant: true, interval: "10s", legendFormat: "", range: false, refId: "disk_avail", datasource: { type: "prometheus", uid: prometheusUid }, requestId: "13A", utcOffsetSec: 28800, scopes: [], adhocFilters: [], datasourceId: 1, intervalMs: 20000, maxDataPoints: 1186 }, { datasource: { type: "prometheus", uid: prometheusUid }, editorMode: "code", exemplar: false, expr: "(node_filesystem_size_bytes{instance=~'node-exporter:9100',fstype=~\"ext.*|xfs\",mountpoint !~\".*pod.*\"}-node_filesystem_free_bytes{instance=~'node-exporter:9100',fstype=~\"ext.*|xfs\",mountpoint !~\".*pod.*\"}) *100/(node_filesystem_avail_bytes {instance=~'node-exporter:9100',fstype=~\"ext.*|xfs\",mountpoint !~\".*pod.*\"}+(node_filesystem_size_bytes{instance=~'node-exporter:9100',fstype=~\"ext.*|xfs\",mountpoint !~\".*pod.*\"}-node_filesystem_free_bytes{instance=~'node-exporter:9100',fstype=~\"ext.*|xfs\",mountpoint !~\".*pod.*\"}))", format: "table", hide: false, instant: true, legendFormat: "", range: false, refId: "disk_usage", requestId: "13B", utcOffsetSec: 28800, scopes: [], adhocFilters: [], interval: "", datasourceId: 1, intervalMs: 20000, maxDataPoints: 1186 }, { editorMode: "code", expr: "sum by (gpu) (DCGM_FI_DEV_POWER_USAGE)", interval: "", legendFormat: "GPU {{gpu}}", range: true, refId: "gpu_power", datasource: { type: "prometheus", uid: prometheusUid }, exemplar: false, requestId: "gpuPowerA", utcOffsetSec: 28800, scopes: [], adhocFilters: [], datasourceId: 1, intervalMs: 20000, maxDataPoints: 100 }, { editorMode: "code", expr: "sum by (gpu) (DCGM_FI_DEV_GPU_TEMP)", instant: false, interval: "", legendFormat: "GPU {{gpu}}", range: true, refId: "gpu_temp", datasource: { type: "prometheus", uid: prometheusUid }, exemplar: false, requestId: "gpuTempA", utcOffsetSec: 28800, scopes: [], adhocFilters: [], datasourceId: 1, intervalMs: 20000, maxDataPoints: 100 }, { editorMode: "code", expr: "sum by (gpu) (DCGM_FI_DEV_FB_USED / (DCGM_FI_DEV_FB_USED + DCGM_FI_DEV_FB_FREE))*100", instant: false, interval: "", legendFormat: "GPU {{gpu}}", range: true, refId: "gpu_vram", datasource: { type: "prometheus", uid: prometheusUid }, exemplar: false, requestId: "gpuVramA", utcOffsetSec: 28800, scopes: [], adhocFilters: [], datasourceId: 1, intervalMs: 20000, maxDataPoints: 100 }, { editorMode: "code", expr: "100 - ((node_filesystem_avail_bytes{instance=~\"node-exporter:9100\",mountpoint=\"/\",fstype=~\"ext4|xfs\"} * 100) / node_filesystem_size_bytes {instance=~\"node-exporter:9100\",mountpoint=\"/\",fstype=~\"ext4|xfs\"})", legendFormat: "__auto", range: true, refId: "disk", datasource: { type: "prometheus", uid: prometheusUid }, exemplar: false, requestId: "diskA", utcOffsetSec: 28800, scopes: [], adhocFilters: [], interval: "", datasourceId: 1, intervalMs: 20000, maxDataPoints: 100 } ], from: "now-5m", to: "now" }; try { const res = await fetch(grafanaUrl, { method: "POST", headers: { "Content-Type": "application/json", "Authorization": `Bearer ${grafanaApiKey}` }, body: JSON.stringify(payload) }); if (!res.ok) { throw new Error(`HTTP error! status: ${res.status}`); } const data = await res.json(); // 調試:檢查整個回應結構 console.log("[DEBUG] Full response structure:", Object.keys(data)); if (data.results) { console.log("[DEBUG] Available results:", Object.keys(data.results)); console.log("[DEBUG] GPU power frames:", data.results.gpu_power.frames); } const cpuValues = data.results.cpu.frames[0].data.values[1]; const memValues = data.results.mem.frames[0].data.values[1]; // GPU 資料處理 let gpuPowerText = "(無 GPU Power 資料)"; let gpuTempText = "(無 GPU Temp 資料)"; let gpuVramText = "(無 GPU VRAM 資料)"; try { // 每個 frame 代表一個 GPU 的時間序列 const gpuPowerFrames = data.results.gpu_power.frames; const gpuTempFrames = data.results.gpu_temp.frames; const gpuVramFrames = data.results.gpu_vram.frames; if (gpuPowerFrames && gpuPowerFrames.length > 0) { gpuPowerText = gpuPowerFrames.map((frame, idx) => { const values = frame.data.values[1]; // values[1] 是數值陣列 const lastValue = values[values.length - 1]; return `GPU${idx} Power: ${parseFloat(lastValue).toFixed(1)}W`; }).join(" "); } if (gpuTempFrames && gpuTempFrames.length > 0) { gpuTempText = gpuTempFrames.map((frame, idx) => { const values = frame.data.values[1]; const lastValue = values[values.length - 1]; return `GPU${idx} Temp: ${parseFloat(lastValue).toFixed(1)}°C`; }).join(" "); } if (gpuVramFrames && gpuVramFrames.length > 0) { gpuVramText = gpuVramFrames.map((frame, idx) => { const values = frame.data.values[1]; const lastValue = values[values.length - 1]; return `GPU${idx} VRAM: ${parseFloat(lastValue).toFixed(1)}%`; }).join(" "); } } catch (e) { console.log("[DEBUG] Error processing GPU data:", e); } const cpu = cpuValues[cpuValues.length - 1]; const mem = memValues[memValues.length - 1]; let diskText = ""; try { const diskValues = data.results.disk.frames[0].data.values[1]; const disk = diskValues[diskValues.length - 1]; diskText = `磁碟使用率:${disk.toFixed(1)}%`; } catch (e) { diskText = "(無法解析磁碟使用率)"; } return { content: [{ type: "text", text: `CPU 使用率:${cpu.toFixed(1)}%\n` + `記憶體使用率:${mem.toFixed(1)}%\n` + `${diskText}\n` + `${gpuPowerText}\n${gpuTempText}\n${gpuVramText}\n` + `\n[DEBUG] GRAFANA_URL: ${grafanaUrl}\n[DEBUG] GRAFANA_API_KEY: ${grafanaApiKey ? "(已設定)" : "(未設定)"}` }] }; } catch (err) { return { content: [{ type: "text", text: `無法取得系統狀態:${err.message}\n` + `\n[DEBUG] GRAFANA_URL: ${grafanaUrl}\n[DEBUG] GRAFANA_API_KEY: ${grafanaApiKey ? "(已設定)" : "(未設定)"}` }] }; } }); // Start receiving messages on stdin and sending messages on stdout const transport = new StdioServerTransport(); server.connect(transport).then(() => { console.log("✅ MCP Server ready and listening on stdio"); });