@gais/advantech-gais-mcp
Version:
A MCP server implementation for monitoring system status via Grafana and Prometheus
306 lines (305 loc) • 12.9 kB
JavaScript
;
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");
});