Skip to content

Commit fa67579

Browse files
Add read-only Prometheus MCP tools (#930)
Expose three read-only MCP tools so an LLM can run PromQL against the cluster's Prometheus through radar: query_prometheus (instant + range), discover_metrics (label/metric discovery with metadata enrichment), and get_prometheus_rules (alerting/recording rules, filtered + flattened). The tools shape results for LLM consumption in the MCP layer — flatten, filter, limit, and summarize oversized results into a cardinality breakdown with a ready-to-run rewrite — while pkg/prom stays a generic, reusable Prometheus client. Non-finite samples serialize as JSON null (never a fabricated 0), and the charts split line/area paths across those gaps instead of bridging them. The MCP path needs a longer per-query budget than the rest of radar (model settable up to 180s), so it uses a dedicated http.Client backstop via Client.PromForMCP(); the shared Prom() client keeps the 10s socket cap that bounds REST/opencost. REST, opencost, and istio callers are unchanged. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent c4ebc0f commit fa67579

18 files changed

Lines changed: 2473 additions & 49 deletions

File tree

docs/mcp.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,9 @@ Add to `~/.gemini/settings.json`:
203203
| `get_helm_release` | Detailed Helm release info with optional values, history, and manifest diff | `namespace` (required), `name` (required), `include` (optional: `values,history,diff`), `diff_revision_1` (required when `include=diff`) / `diff_revision_2` (optional) |
204204
| `list_namespaces` | List all namespaces with status | (none) |
205205
| `get_subject_permissions` | Effective RBAC permissions of a ServiceAccount / User / Group: bindings (each with `inheritedFromGroup` set when applicable), deduplicated flat rule list, and (for SAs) the Pods running as it. Use to answer "is this SA over-privileged?" or "what's the blast radius if this Pod is compromised?" | `kind` (required: `ServiceAccount`, `User`, or `Group`), `namespace` (required for ServiceAccount; omit for User/Group), `name` (required) |
206+
| `query_prometheus` | Execute PromQL against the cluster's Prometheus (auto-discovered or `--prometheus-url`; works with PromQL-compatible backends: Thanos, VictoriaMetrics, Mimir). `type=instant` returns current values; `type=range` returns time-series history with automatic step adjustment. Oversized results return a label-cardinality summary + suggested `topk` rewrite instead of raw data. | `query` (required), `type` (optional: `instant` default, `range`), `since` (optional, e.g. `30m`, `1h`, `24h`, `7d`; default `1h`), `start` / `end` (optional RFC3339, override `since`), `step` (optional, auto-calculated when omitted), `max_points` (optional, default 300, max 600), `timeout` (optional seconds, default 30, max 180) |
207+
| `discover_metrics` | Discover exact metric names (enriched with type/help from Prometheus metadata) or values of one label before writing PromQL. Lists active series from the last hour; `truncated: true` means narrow the `match` selector. | `match` (PromQL series selector; required when `label` is empty), `label` (optional: list values of this label instead of metric names), `limit` (optional, default 100, max 500) |
208+
| `get_prometheus_rules` | List Prometheus alerting/recording rules with PromQL definitions, state, labels, annotations, and active alert instances. Alert-investigation entry point: fetch the rule definition, then run its query with `query_prometheus`. | `type` (optional: `alert`, `record`), `name` / `group` (optional substring filters), `state` (optional: `firing`, `pending`, `inactive`), `limit` (optional, default 50, max 200) |
206209

207210
### Write Tools
208211

@@ -230,6 +233,7 @@ Add to `~/.gemini/settings.json`:
230233
- **Local binary**: the cache uses your kubeconfig identity, so MCP can only see what `kubectl` can see for that user
231234
- **In-cluster (auth enabled)**: read tools intersect namespaced reads with the calling user's RBAC-allowed namespaces; cluster-scoped reads (Nodes, PVs, ClusterRoles, cluster-scoped CRDs) are gated per-kind via SubjectAccessReview, so cluster-wide pod visibility doesn't implicitly grant Node read; write tools, exec, and logs are fully impersonated so the apiserver enforces the user's RBAC end-to-end
232235
- **In-cluster (no auth)**: every MCP caller shares the pod ServiceAccount's view — only deploy this way when MCP isn't exposed beyond a trusted boundary
236+
- **Prometheus metric data is NOT namespace-filtered** — PromQL cannot be namespace-scoped server-side (arbitrary queries can aggregate across namespaces), so `query_prometheus` and `discover_metrics` follow the same stance as the REST `/prometheus/query` endpoint: any authenticated user may run PromQL. Deploy with auth enabled when Prometheus contains sensitive label values
233237
- **Secret redaction** — Secret `.data` and `.stringData` are never exposed; only key names are shown
234238
- **Value redaction** — environment variable values are scrubbed for known secret patterns (API keys, tokens, passwords, base64 blocks)
235239
- **Log redaction** — pod log output is scrubbed for secret patterns before being returned

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ require (
1616
github.com/hashicorp/yamux v0.1.2
1717
github.com/modelcontextprotocol/go-sdk v1.6.1
1818
github.com/prometheus/client_golang v1.23.2
19+
github.com/prometheus/common v0.67.5
1920
github.com/skyhook-io/radar/pkg v0.0.0
2021
github.com/wailsapp/wails/v2 v2.12.0
2122
golang.org/x/net v0.56.0

internal/mcp/tools.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -371,6 +371,44 @@ func registerTools(server *mcp.Server) {
371371
Annotations: readOnly,
372372
}, logToolCall("get_subject_permissions", handleGetSubjectPermissions))
373373

374+
// --- Prometheus tools (read-only) ---
375+
376+
mcp.AddTool(server, &mcp.Tool{
377+
Name: "query_prometheus",
378+
Description: "Use when the question needs metric VALUES or history: CPU/memory over time, " +
379+
"request rates, error ratios, saturation, restarts trend, 'was there a spike?'. Executes " +
380+
"PromQL against the cluster's Prometheus (auto-discovered or configured in Radar; also " +
381+
"works with PromQL-compatible backends: Thanos, VictoriaMetrics, Mimir). " +
382+
"type=instant returns current values; type=range returns time series for a window " +
383+
"(since=1h default). For live top-N snapshots prefer top_resources; for metric/label " +
384+
"NAME discovery use discover_metrics first — do not guess metric names. " +
385+
"High-cardinality queries must be wrapped in topk(5, ...): oversized results return a " +
386+
"summary with a suggested rewrite instead of data.",
387+
Annotations: readOnly,
388+
}, logToolCall("query_prometheus", handleQueryPrometheus))
389+
390+
mcp.AddTool(server, &mcp.Tool{
391+
Name: "discover_metrics",
392+
Description: "Use BEFORE query_prometheus when unsure of exact metric or label names. " +
393+
"Lists metric names matching a selector (match={__name__=~\"node_cpu.*\"}) with their " +
394+
"type (counter vs gauge — counters need rate()) and help text, or values of one label " +
395+
"(label=namespace). Returns up to limit values from the last hour of active series; " +
396+
"truncated=true means narrow the match. Never answer about metric values from this " +
397+
"tool — names prove existence, not magnitude.",
398+
Annotations: readOnly,
399+
}, logToolCall("discover_metrics", handleDiscoverMetrics))
400+
401+
mcp.AddTool(server, &mcp.Tool{
402+
Name: "get_prometheus_rules",
403+
Description: "List Prometheus alerting and recording rules with their PromQL definitions, " +
404+
"state (firing/pending/inactive), labels, annotations, and active alert instances. " +
405+
"When investigating a firing alert, ALWAYS fetch its rule definition here FIRST, then " +
406+
"run its query with query_prometheus to see the actual values. state=firing shows " +
407+
"everything currently alerting. Filters are substring matches; results cap at limit " +
408+
"(default 50) with truncated=true — narrow rather than paging.",
409+
Annotations: readOnly,
410+
}, logToolCall("get_prometheus_rules", handleGetPrometheusRules))
411+
374412
mcp.AddTool(server, &mcp.Tool{
375413
Name: "get_workload_logs",
376414
Description: "Get aggregated logs from all pods of a workload (Deployment, StatefulSet, " +

0 commit comments

Comments
 (0)