diff --git a/apps/ollama-mcp/mcp-prometheus/index.js b/apps/ollama-mcp/mcp-prometheus/index.js new file mode 100644 index 0000000..557c606 --- /dev/null +++ b/apps/ollama-mcp/mcp-prometheus/index.js @@ -0,0 +1,254 @@ +const express = require('express'); +const axios = require('axios'); + +const app = express(); +const port = process.env.PORT || 3000; + +const PROMETHEUS_URL = process.env.PROMETHEUS_URL || 'http://localhost:9090'; +const GRAFANA_URL = process.env.GRAFANA_URL || 'http://localhost:3000'; +const GRAFANA_TOKEN = process.env.GRAFANA_TOKEN || ''; + +app.use(express.json()); +app.use((req, res, next) => { + res.header('Access-Control-Allow-Origin', '*'); + res.header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); + res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization'); + if (req.method === 'OPTIONS') return res.sendStatus(200); + next(); +}); + +const promQuery = async (query, time) => { + const params = { query }; + if (time) params.time = time; + const res = await axios.get(`${PROMETHEUS_URL}/api/v1/query`, { params }); + return res.data; +}; + +const promRangeQuery = async (query, start, end, step) => { + const params = { query, start, end, step: step || '60s' }; + const res = await axios.get(`${PROMETHEUS_URL}/api/v1/query_range`, { params }); + return res.data; +}; + +app.get('/health', (req, res) => res.json({ status: 'healthy', service: 'mcp-prometheus' })); + +app.get('/', (req, res) => res.json({ + service: 'MCP Prometheus/Grafana Server', + version: '1.0.0', + endpoints: [ + 'POST /api/query - Run instant PromQL query', + 'POST /api/query_range - Run range PromQL query', + 'POST /api/alerts - List firing alerts', + 'POST /api/targets - List scrape targets and health', + 'POST /api/pod_cpu - CPU usage by pod/namespace', + 'POST /api/pod_memory - Memory usage by pod/namespace', + 'POST /api/pod_restarts - Pod restart counts', + 'POST /api/node_resources - Node CPU/memory utilization', + 'POST /api/pvc_usage - PVC disk usage', + 'POST /api/http_errors - HTTP error rates (nginx ingress)', + 'POST /api/grafana_dashboards - List Grafana dashboards', + ] +})); + +// Raw PromQL query +app.post('/api/query', async (req, res) => { + try { + const { query, time } = req.body; + if (!query) return res.status(400).json({ error: 'query is required' }); + const data = await promQuery(query, time); + res.json(data); + } catch (e) { + res.status(500).json({ error: e.message }); + } +}); + +// Range query +app.post('/api/query_range', async (req, res) => { + try { + const { query, start, end, step } = req.body; + if (!query) return res.status(400).json({ error: 'query is required' }); + const now = Math.floor(Date.now() / 1000); + const data = await promRangeQuery( + query, + start || now - 3600, + end || now, + step || '60s' + ); + res.json(data); + } catch (e) { + res.status(500).json({ error: e.message }); + } +}); + +// Firing alerts +app.post('/api/alerts', async (req, res) => { + try { + const r = await axios.get(`${PROMETHEUS_URL}/api/v1/alerts`); + const alerts = r.data.data.alerts.filter(a => a.state === 'firing' || !req.body.firingOnly); + res.json({ count: alerts.length, alerts }); + } catch (e) { + res.status(500).json({ error: e.message }); + } +}); + +// Scrape targets +app.post('/api/targets', async (req, res) => { + try { + const r = await axios.get(`${PROMETHEUS_URL}/api/v1/targets`); + const targets = r.data.data.activeTargets.map(t => ({ + job: t.labels.job, + instance: t.labels.instance, + health: t.health, + lastScrape: t.lastScrape, + lastError: t.lastError || null + })); + const unhealthy = targets.filter(t => t.health !== 'up'); + res.json({ total: targets.length, unhealthy: unhealthy.length, targets }); + } catch (e) { + res.status(500).json({ error: e.message }); + } +}); + +// Pod CPU usage +app.post('/api/pod_cpu', async (req, res) => { + try { + const { namespace, pod, duration } = req.body; + const dur = duration || '5m'; + let query = `sum(rate(container_cpu_usage_seconds_total{container!=""}[${dur}])) by (pod, namespace)`; + if (namespace) query = `sum(rate(container_cpu_usage_seconds_total{container!="",namespace="${namespace}"}[${dur}])) by (pod, namespace)`; + if (pod) query = `sum(rate(container_cpu_usage_seconds_total{container!="",pod=~"${pod}.*"}[${dur}])) by (pod, namespace)`; + const data = await promQuery(query); + const results = (data.data?.result || []) + .map(r => ({ pod: r.metric.pod, namespace: r.metric.namespace, cpu_cores: parseFloat(r.value[1]).toFixed(4) })) + .sort((a, b) => b.cpu_cores - a.cpu_cores); + res.json({ count: results.length, results }); + } catch (e) { + res.status(500).json({ error: e.message }); + } +}); + +// Pod memory usage +app.post('/api/pod_memory', async (req, res) => { + try { + const { namespace, pod } = req.body; + let query = `sum(container_memory_working_set_bytes{container!=""}) by (pod, namespace)`; + if (namespace) query = `sum(container_memory_working_set_bytes{container!="",namespace="${namespace}"}) by (pod, namespace)`; + if (pod) query = `sum(container_memory_working_set_bytes{container!="",pod=~"${pod}.*"}) by (pod, namespace)`; + const data = await promQuery(query); + const results = (data.data?.result || []) + .map(r => ({ + pod: r.metric.pod, + namespace: r.metric.namespace, + memory_mb: (parseFloat(r.value[1]) / 1024 / 1024).toFixed(1) + })) + .sort((a, b) => b.memory_mb - a.memory_mb); + res.json({ count: results.length, results }); + } catch (e) { + res.status(500).json({ error: e.message }); + } +}); + +// Pod restarts +app.post('/api/pod_restarts', async (req, res) => { + try { + const { namespace, threshold } = req.body; + let query = `sum(kube_pod_container_status_restarts_total) by (pod, namespace, container)`; + if (namespace) query = `sum(kube_pod_container_status_restarts_total{namespace="${namespace}"}) by (pod, namespace, container)`; + const data = await promQuery(query); + const min = threshold || 0; + const results = (data.data?.result || []) + .map(r => ({ pod: r.metric.pod, namespace: r.metric.namespace, container: r.metric.container, restarts: parseInt(r.value[1]) })) + .filter(r => r.restarts > min) + .sort((a, b) => b.restarts - a.restarts); + res.json({ count: results.length, results }); + } catch (e) { + res.status(500).json({ error: e.message }); + } +}); + +// Node resource utilization +app.post('/api/node_resources', async (req, res) => { + try { + const cpuData = await promQuery(`100 - (avg by(node) (rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100)`); + const memData = await promQuery(`(1 - (node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes)) * 100`); + const diskData = await promQuery(`(1 - (node_filesystem_avail_bytes{mountpoint="/"} / node_filesystem_size_bytes{mountpoint="/"})) * 100`); + + const nodeMap = {}; + (cpuData.data?.result || []).forEach(r => { + const node = r.metric.node || r.metric.instance; + nodeMap[node] = { node, cpu_percent: parseFloat(r.value[1]).toFixed(1) }; + }); + (memData.data?.result || []).forEach(r => { + const node = r.metric.instance || r.metric.node; + if (nodeMap[node]) nodeMap[node].memory_percent = parseFloat(r.value[1]).toFixed(1); + }); + (diskData.data?.result || []).forEach(r => { + const node = r.metric.instance || r.metric.node; + if (nodeMap[node]) nodeMap[node].disk_percent = parseFloat(r.value[1]).toFixed(1); + }); + + res.json({ nodes: Object.values(nodeMap) }); + } catch (e) { + res.status(500).json({ error: e.message }); + } +}); + +// PVC usage +app.post('/api/pvc_usage', async (req, res) => { + try { + const { namespace } = req.body; + let query = `(kubelet_volume_stats_used_bytes / kubelet_volume_stats_capacity_bytes) * 100`; + if (namespace) query = `(kubelet_volume_stats_used_bytes{namespace="${namespace}"} / kubelet_volume_stats_capacity_bytes{namespace="${namespace}"}) * 100`; + const data = await promQuery(query); + const results = (data.data?.result || []) + .map(r => ({ + pvc: r.metric.persistentvolumeclaim, + namespace: r.metric.namespace, + used_percent: parseFloat(r.value[1]).toFixed(1) + })) + .sort((a, b) => b.used_percent - a.used_percent); + res.json({ count: results.length, results }); + } catch (e) { + res.status(500).json({ error: e.message }); + } +}); + +// HTTP error rates (nginx ingress) +app.post('/api/http_errors', async (req, res) => { + try { + const { duration } = req.body; + const dur = duration || '5m'; + const query = `sum(rate(nginx_ingress_controller_requests{status=~"5.."}[${dur}])) by (ingress, namespace, status)`; + const data = await promQuery(query); + const results = (data.data?.result || []) + .map(r => ({ + ingress: r.metric.ingress, + namespace: r.metric.namespace, + status: r.metric.status, + rps: parseFloat(r.value[1]).toFixed(4) + })) + .sort((a, b) => b.rps - a.rps); + res.json({ count: results.length, results }); + } catch (e) { + res.status(500).json({ error: e.message }); + } +}); + +// List Grafana dashboards +app.post('/api/grafana_dashboards', async (req, res) => { + try { + const headers = GRAFANA_TOKEN ? { Authorization: `Bearer ${GRAFANA_TOKEN}` } : {}; + const r = await axios.get(`${GRAFANA_URL}/api/search?type=dash-db`, { headers }); + res.json({ count: r.data.length, dashboards: r.data.map(d => ({ uid: d.uid, title: d.title, url: d.url, tags: d.tags })) }); + } catch (e) { + res.status(500).json({ error: e.message }); + } +}); + +app.use((req, res) => res.status(404).json({ error: 'Not found' })); + +app.listen(port, '0.0.0.0', () => { + console.log(`MCP Prometheus Server running on port ${port}`); + console.log(`Prometheus: ${PROMETHEUS_URL}`); + console.log(`Grafana: ${GRAFANA_URL}`); +});