> ## Documentation Index
> Fetch the complete documentation index at: https://radarhq.io/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Kubernetes MCP server

> Radar's built-in MCP server: token-optimized Kubernetes context for Claude Code, Codex, Cursor, GitHub Copilot, OpenCode, Google Antigravity, and any other MCP client, with RBAC-enforced tools to inspect, diagnose, and operate your cluster.

Radar includes a built-in [Model Context Protocol](https://modelcontextprotocol.io) (MCP) server that lets AI agents inspect, investigate, and operate your Kubernetes cluster.

For the design rationale and what the tools return, see the [MCP deep dive](https://radarhq.io/blog/mcp-for-kubernetes-live-cluster-context-for-ai-tools).

Looking for Radar's built-in [AI investigations](/docs/features/diagnose)? MCP clients use your own identity; see [investigation access](/docs/cloud/rbac#ai-investigation-access) for the permissions used by hosted investigations.

## Why MCP instead of raw kubectl?

Giving an AI agent raw `kubectl` access has problems:

* **Token waste**  -  `kubectl get pod -o yaml` returns verbose YAML full of managed fields, status conditions, and metadata noise that burns through LLM context windows
* **No enrichment**  -  raw output lacks topology relationships, health assessments, or cross-resource correlation
* **Write access risk**  -  kubectl can modify and delete resources

Radar's MCP server solves these:

* **Token-optimized**  -  resources are minified, stripping noise (managed fields, internal annotations, redundant status) while preserving what matters
* **Enriched data**  -  topology graphs, health assessments, deduplicated events, filtered logs (prioritizing errors/warnings)
* **Safe operations**  -  write tools such as restart, scale, apply, and rollback are identified for client confirmation and enforced through Kubernetes RBAC. Diagnosis is read-only by default; optional in-cluster route probing uses short-lived, self-deleting probe pods
* **Secret-safe**  -  Secret data is never exposed, environment values are redacted, log output is scrubbed for API keys and tokens
* **RBAC-aware**  -  respects your cluster's RBAC permissions
* **Vendor-neutral**  -  works with any MCP-compatible AI tool

## Enabling / Disabling

The MCP server is **enabled by default** when Radar starts. To disable it:

```bash theme={null}
radar --no-mcp
```

## MCP Endpoint

```
http://localhost:9280/mcp
```

The port matches your `--port` flag (default 9280). The MCP server uses HTTP transport with JSON-RPC.

## Catalog Introspection

MCP registries and inspectors can start Radar without a Kubernetes cluster when they only need the tool and resource catalog:

```bash theme={null}
radar --mcp-catalog-only --no-browser
```

This mode skips Kubernetes initialization and starts the `/mcp` endpoint for schema introspection. Cluster-backed tool calls still require a normal Radar process connected to Kubernetes.

For registries that launch MCP servers over stdio, use:

```bash theme={null}
radar --mcp-catalog-stdio
```

This exposes the same tool and resource catalog over stdio without starting the HTTP UI server.
The stdio mode is intended only for catalog introspection; normal Radar sessions use the HTTP endpoint above.

## Setup Instructions

Connect your AI tool to Radar's MCP server. Radar must be running first (`radar` or `kubectl radar`). The examples use the default endpoint `http://localhost:9280/mcp`; if Radar runs on another port, use the endpoint shown in Radar's MCP dialog.

### Claude Code

Run this command:

```bash theme={null}
claude mcp add --transport http radar http://localhost:9280/mcp
```

### Codex

Run this command:

```bash theme={null}
codex mcp add radar --url http://localhost:9280/mcp
```

Or add to `~/.codex/config.toml`:

```toml theme={null}
[mcp_servers.radar]
url = "http://localhost:9280/mcp"
```

### Cursor

Add to `~/.cursor/mcp.json` (all projects) or `.cursor/mcp.json` (one project):

```json theme={null}
{
  "mcpServers": {
    "radar": {
      "url": "http://localhost:9280/mcp"
    }
  }
}
```

### VS Code (GitHub Copilot)

Add to `.vscode/mcp.json` in your workspace:

```json theme={null}
{
  "servers": {
    "radar": {
      "type": "http",
      "url": "http://localhost:9280/mcp"
    }
  }
}
```

Or add it to your user profile from the command line:

```bash theme={null}
code --add-mcp '{"name":"radar","type":"http","url":"http://localhost:9280/mcp"}'
```

### GitHub Copilot CLI

Run this command:

```bash theme={null}
copilot mcp add --transport http radar http://localhost:9280/mcp
```

This writes the server to `~/.copilot/mcp-config.json`.

### OpenCode

Add to `~/.config/opencode/opencode.json` (all projects) or `opencode.json` in your project root:

```json theme={null}
{
  "mcp": {
    "radar": {
      "type": "remote",
      "url": "http://localhost:9280/mcp",
      "enabled": true
    }
  }
}
```

Radar can also run in-app investigations with an installed OpenCode CLI (tested
with 1.18.5). Install with `npm install -g opencode-ai`; authenticate with
`opencode auth login` or use your existing provider configuration, including AWS
Bedrock. Model overrides use `provider/model` names from `opencode models`.

OpenCode supports the **Your agent setup** execution profile only. Radar inherits
your provider environment and OpenCode configuration, and runs with `--auto`:
permission requests are approved automatically, including those from built-in
tools and configured MCP servers; explicit denials still apply. Radar does not
enforce a CLI sandbox. Diagnosis and follow-up use Radar's read-only investigation
MCP. A confirmed Apply starts a separate write-enabled session, then resumes the
original read-only session to verify the result. As with other agents using your
normal setup, Radar cannot authenticate write results and reports the Apply
outcome as uncertain until current-state verification. Terminal handoff and
reasoning-effort controls are not available for OpenCode.

### Google Antigravity

Run this command:

```bash theme={null}
agy mcp add radar http://localhost:9280/mcp
```

Or add to `~/.gemini/config/mcp_config.json` (all workspaces) or `.agents/mcp_config.json` (one workspace). Antigravity uses `serverUrl` for remote servers:

```json theme={null}
{
  "mcpServers": {
    "radar": {
      "serverUrl": "http://localhost:9280/mcp"
    }
  }
}
```

### Kiro

Add to `~/.kiro/settings/mcp.json` (all workspaces) or `.kiro/settings/mcp.json` (one workspace):

```json theme={null}
{
  "mcpServers": {
    "radar": {
      "url": "http://localhost:9280/mcp"
    }
  }
}
```

### JetBrains AI Assistant / Junie

AI Assistant: add via **Settings > Tools > AI Assistant > Model Context Protocol (MCP)**. Junie: add to `~/.junie/mcp/mcp.json` (all projects) or `.junie/mcp/mcp.json` (one project). Both use the same JSON:

```json theme={null}
{
  "mcpServers": {
    "radar": {
      "url": "http://localhost:9280/mcp"
    }
  }
}
```

### Cline

Add via the Cline MCP settings UI. Set `type` to `streamableHttp`; without it, Cline uses the legacy SSE transport:

```json theme={null}
{
  "mcpServers": {
    "radar": {
      "type": "streamableHttp",
      "url": "http://localhost:9280/mcp"
    }
  }
}
```

### Devin Desktop (formerly Windsurf)

Add to `~/.config/devin/mcp_config.json` (macOS/Linux) or `%APPDATA%\devin\mcp_config.json` (Windows). Both the default Devin Local agent and the legacy Cascade agent read `url` from this file:

```json theme={null}
{
  "mcpServers": {
    "radar": {
      "url": "http://localhost:9280/mcp"
    }
  }
}
```

### Claude Desktop

Claude Desktop's custom connectors connect from Anthropic's cloud and cannot reach `localhost`. For a local Radar, use the [`mcp-remote`](https://github.com/geelen/mcp-remote) bridge (requires Node.js). Add to `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or `%APPDATA%\Claude\claude_desktop_config.json` (Windows):

```json theme={null}
{
  "mcpServers": {
    "radar": {
      "command": "npx",
      "args": ["-y", "mcp-remote", "http://localhost:9280/mcp"]
    }
  }
}
```

`mcp-remote` accepts plain `http://` only for `localhost` and `127.0.0.1`. For any other `http://` host, add `"--allow-http"` after the URL.

### Zed

Add to your Zed `settings.json`:

```json theme={null}
{
  "context_servers": {
    "radar": {
      "url": "http://localhost:9280/mcp"
    }
  }
}
```

### Gemini CLI

For Gemini Code Assist Standard and Enterprise users. Run this command:

```bash theme={null}
gemini mcp add --transport http radar http://localhost:9280/mcp
```

Or add to `~/.gemini/settings.json`:

```json theme={null}
{
  "mcpServers": {
    "radar": {
      "httpUrl": "http://localhost:9280/mcp"
    }
  }
}
```

Any other MCP client that supports the Streamable HTTP transport can connect to the same endpoint.

## MCP Registry / Docker

Radar is listed on the [official MCP registry](https://registry.modelcontextprotocol.io) as `io.github.skyhook-io/radar`, packaged as the `ghcr.io/skyhook-io/radar` Docker image. The registry-suggested invocation is:

```bash theme={null}
docker run -p 127.0.0.1:9280:9280 \
  -v ~/.kube/config:/kubeconfig:ro \
  -e KUBECONFIG=/kubeconfig \
  ghcr.io/skyhook-io/radar:latest
```

**Prefer the native install** (`brew install skyhook-io/tap/radar`, krew, or the install script  -  see the [README](https://github.com/skyhook-io/radar/blob/main/README.md))  -  it uses your kubeconfig exactly as kubectl does. The Docker path has real limitations because the image is distroless (no shell, no cloud CLIs):

* **Exec-plugin auth does not work.** Kubeconfigs for GKE/EKS/AKS typically call `gke-gcloud-auth-plugin`, `aws eks get-token`, or `kubelogin`  -  none exist inside the container. Only kubeconfigs with embedded certificates or static tokens work.
* **Local clusters are unreachable.** kind/minikube/Docker Desktop API servers listen on the host's `127.0.0.1`, which inside the container is the container itself.
* **File ownership matters.** On Linux hosts a `0600` kubeconfig owned by your user is unreadable by the container's `nonroot` (uid 65532) user.

The Docker image's primary use is [in-cluster deployment](/docs/configuration/in-cluster) with a ServiceAccount, where none of these apply.

## `diagnose` evidence limits

For workload `diagnose` responses, `logCoverage.selectedPods` counts pods selected for log requests, not pods whose logs were successfully read. Per-container errors and `logsError` describe failed collection. `logCoverage.totalLines` counts diagnostic-filter output before the aggregate response cap (including fallback tail lines); `shownLines` counts the lines retained after that cap. Neither describes a container's complete log history. `totalPods` and `shownPods` count pods contributing at least one line before and after that cap - not all selected pods or successful empty reads. `eventsTotalGroups`, recent-change coverage/error fields, and log sampling/truncation must remain qualifications even when an agent reports no problem.

`podNames` lists the pods the diagnosed workload controls, resolved by controller ownership (pod → ReplicaSet → Deployment, pod → Job → CronJob, direct otherwise), sorted and cut to the first 50 with `podNamesTruncated`. It is the set to scope a follow-up `query_prometheus` to (`pod=~'^(a|b)$'`); a name prefix such as `pod=~'api-.*'` also selects a sibling workload's pods and is never the workload's membership. `pods` counts the same set before the cut.

For Pod, Deployment, StatefulSet, DaemonSet and Argo Rollout targets, `metrics` is a vitals snapshot captured inside the same call: `cpu` (cores, 5m rate), `memory` (working-set bytes) and `restarts` (counter increase over the trailing 1h, rounded per pod) summed over those pods across `since` (default 1h) at a step that yields at most 60 points per series and keeps the samples within an 8 KiB budget, with sample values rounded to four significant digits. Membership is the pods the workload controls at collection time, so a pod replaced earlier in the window is not covered even though the series spans it; a set larger than 50 is cut to the first 50 by name with `partial: true` and `omittedPods`. The field is omitted when no Prometheus is configured, the workload controls no pods, or the caller cannot `get` the diagnosed resource; when the availability probe does not answer within the 3 s budget the field is present with `error` saying so, rather than vanishing as if no Prometheus existed. When Prometheus is configured but unreachable, the branch exceeds its 3 s budget, or the whole field (queries included) exceeds a 24 KiB backstop, `series` is empty and `error` says why; when one category's query fails, that category is left out and named in `error` while the others stand. A workload whose pods the cluster cache cannot establish  -  a denied lister, an informer still filling, a namespace it does not watch  -  is reported as an error rather than charted as empty. Each entry carries the PromQL that produced it (`query`), so an empty `series` with no `error` means the window holds no samples, not that nothing was asked. Flat stretches are compressed: the interior of an evenly spaced run of equal values is dropped, keeping the point that opens it and the point that closes it, so an hour of a workload sitting still costs two points rather than sixty. `observedSamples` reports how many points Prometheus actually returned for that category before compression  -  a two-point series with `observedSamples: 56` is an hour of agreeing samples, one with `observedSamples: 2` is two lonely ones, and the window and step cannot tell them apart on their own because a range query returns nothing for an evaluation with no data. Values are compared after rounding, so movement too small to reach four significant digits is treated as flat; a run is only compressed when its samples are evenly spaced, so a gap in the series survives. The window ends at collection time and is not a shared observation time for the other sources.

Collecting vitals costs one Prometheus round trip on every `diagnose` of a pod-backed workload, and that round trip is the call's critical path: against a healthy backend a diagnose without vitals returns in roughly 130ms and one with them in roughly 650ms, because nothing else `diagnose` does takes long enough to hide it. The queries for the three categories run concurrently with each other and with the rest of the bundle, and the branch is bounded by its own 3s budget, so a slow or unreachable Prometheus delays `diagnose` but never fails it. They are collected by default because an agent that has to ask for resource data can silently never ask, which makes a healthy verdict reached without looking indistinguishable from one where the numbers were fine. If that trade stops being worth it, the whole branch is behind one switch (`DiagnoseMetricsEnabled` in `internal/mcp`): turning it off omits the field and leaves the rest of the bundle and `query_prometheus` untouched, so the data stays reachable on request.

`expectedPreviousLogAbsences` requires container status consistent with no previous instance and either an empty successful read or the specific previous-instance-not-found response. Permission failures, missing pods, unavailable servers, and interrupted reads remain collection errors.

One call combines cached Kubernetes resource state, historical events/changes, and live log requests. Its completion time records when the call finished, not a shared observation time or freshness guarantee for those sources. Event/change timestamps retain their source-specific meaning.

These public MCP fields are available to consumers running the updated Radar server. The investigation contract that turns them into Findings  -  the prompt suite, the verdict, its parser and the citation binder  -  lives in `pkg/investigation` and is shared by the OSS engine and Radar Hub; each product supplies its own evidence issuance, run orchestration and store, and declares on its `/agents` entry whether it performs Apply (`apply`) and the verification turn after it (`verification`).

The investigation's final JSON also carries the agent's case: `evidence` (up to 8 items of `ref`, `role`, optional one-sentence `claim`  -  required for `benign`, `demoted` and `rules_out`  -  and optional `subject`) and `ruled_out` (up to 5 hypotheses pointing at an evidence item by index). The server binds each item's ref against the run's retained ledger exactly like `root_cause_evidence_refs`: a ref from the current turn must match the exact private-transport payload; a ref from an earlier turn of the same run is accepted on the persisted `radarEvidence` flag its own validator wrote, so a revised assessment can keep resting on a result it can no longer re-read. A failing item is dropped on its own as `unlinked`, and binding runs for every assessment, healthy and inconclusive included. The server-authored `evidence` / `ruledOut` fields are `omitempty`, so a diagnosis with no bindable case carries neither: consumers must treat both as optional.

**The story contract.** The last fenced `json` block in the reply that carries a verdict field (`summary`, `root_cause`, `healthy`, `inconclusive`, `steps`, `evidence` or `remediation`) is the verdict and a divider, so a patch or spec quoted in a `json` fence inside the story is never mistaken for it: prose after it is the story (`report`), prose before it the agent's evidence ledger (`notes`, kept in Activity and never shown in Findings); a reply with nothing after the block keeps the older shape, where the prose before it is the story. The story may place a Radar result with `[[radar:evidence=N]]` alone on a line  -  N is the 0-based index into `evidence`  -  or mention it inline, which places the card under that paragraph when no own-line marker places it and otherwise references the placed card (with an arrow saying where it is); `[[radar:evidence=N|compact]]` places the card with its header and role label, without the agent's note; log cards also retain the last two selected log lines. Each evidence item may carry a `gap`: the agent's own statement of what that result does not cover, rendered under its note. Only a result that returned data can be cited: a call that errored, was denied, or was truncated leaves its item `unlinked`. An inline mention past the placement cap renders as a chip that reveals the card under Captured results. Findings renders the placed result's card at that position from Radar's own record; the agent never authors a card. A marker inside a code span (which may run across lines within a paragraph), a code block, an indented code line or a blockquote is literal text. The ledger's own form, `[[radar:evidence-ref=ev_…]]`, is not a placement: the parser rewrites one that names a cited item to its index, strips any marker from `summary` and `root_cause`, and the renderer shows an unresolvable one as a citation Radar cannot identify rather than as text. At most 6 results are placed; later placements become references; a marker Radar cannot resolve renders a visible "support unavailable" note at the sentence rather than vanishing. The caps are the shape of the page and nothing else: at most 8 evidence items, 5 ruled-out hypotheses, 6 steps, 3 unresolved items and 3 root-cause refs, with a 180-rune headline cut at a sentence boundary; no other text is ever cut; a steps, unresolved or ruled-out list that ran past its cap reports the valid entries it lost in `omittedEntries`, evidence losses stay in `unlinkedEvidence`, and more than three root-cause refs still invalidate that set. The verdict adds `summary` (the one-sentence takeaway, ≤ 180 runes, clamped at a sentence boundary; `root_cause` is the two-or-three-sentence paragraph under it), `certainty` (`established` | `likely` | `suspected`, rendered as the agent's word, never a Radar mark), `unresolved` (≤ 3 items, listed under *Still open*, followed by any read Radar could not complete and, on a healthy verdict, each adverse Radar card the agent explained with its note; an adverse card the verdict never addressed, or one the agent calls a cause or symptom, keeps a small amber header naming it; every other caveat stays on the claim it bounds  -  the gap line under the cause card, the precondition on a step  -  or inside Captured results), typed `steps` (`text`, `kind` of `mitigate` | `verify` | `investigate`, optional `precondition`; when present the server derives `remediation` from them so one list feeds Apply and `recommended_index`, which may only name a `mitigate` step), and on follow-up turns `revises_assessment`, which the server keeps only on a complete verdict (`summary` plus a root cause, healthy or inconclusive). Findings shows the newest assessment turn  -  initial, verification, or a question whose verdict revised it  -  and nothing else; Activity keeps every earlier assessment complete. With typed steps present, `inconclusive` is cleared only by a non-empty `root_cause` (a mitigation never establishes a cause). No role can hide, collapse, or recolor a card, and a claim that cannot be pinned to exactly one observation shows beside its source under Captured results ("Notes without a card") instead. A subject may omit `name` to cite a listing for what it does not contain (kind and namespace name the listing; uniqueness still decides placement); a subject that names an entry binds to the listing whether or not it holds the entry, and the story card then shows that row inline, or says the entry is not in the listing, or that the name matches several entries. `observation: "resource"` names anything a call returned about a resource: a single resource, a listing, a ranking or posture card, a Helm release, a permissions check or a neighborhood; the stream-shaped kinds (`logs`, `events`, `changes`, `metrics`, `alerts`, `issue`) keep their own word so one word still picks one observation of a diagnose bundle. A receipt is not a kind of its own but what a call of any kind returned when it found nothing (a metric search, a scan with nothing about the target), so a call whose only observation is a receipt is not held to the word; what the subject names still has to match. `observation` is one of `resource`, `logs`, `events`, `changes`, `metrics` (or `metrics:cpu` / `metrics:memory` / `metrics:restarts` for a diagnose bundle's charts), `alerts` (with `name` as the rule's name, since one rules result holds several rules) or `issue` (`issues` is accepted as an alias); any other word leaves the item unplaced. A metrics card placed as the cause or the symptom shows its chart inline. Only results Radar projects into cards can be placed (`diagnose`, `get_resource`, non-empty `list_resources`, `list_namespaces`, `list_helm_releases`, `list_packages` and `search`, `get_events`, `get_pod_logs`, `get_workload_logs`, `get_changes`, `get_neighborhood`, `get_topology`, `get_helm_release` with its hooks, revision history and user values, `get_subject_permissions` with its effective rules, `get_prometheus_rules`, `query_prometheus`, `discover_metrics` as a receipt, `issues`, `top_resources` as a ranking with the workload's own row marked, and `get_cluster_audit` or `get_cluster_upgrade_readiness` as a posture card holding only the findings about the investigated resource or its relations, a scan with none becoming a receipt); an empty listing or `get_dashboard` can be cited but renders a "no card" note at the sentence. A ranking or posture card placed as the cause or symptom shows the rows about the workload inline. Subject matching forgives two agent slips that name no other object: a built-in API group on a core kind (`apps` on a Pod) and a named entry on a listing (a specific ConfigMap cited against the ConfigMaps of a namespace, or a package named by the kind the agent knows it as when the listing holds that name); a vendor group on a core kind (a Knative Service) still names a different object. A question the agent answered with `revises_assessment: false` does not make the assessment "earlier": its reads join Captured results without demoting the headline or the steps. A backend that sends no `summary` and no placement markers renders in the previous shape.

## Cost and rightsizing evidence limits

### `recommendationReason`  -  why a row carries no recommendation

`get_rightsizing` rows may set `recommendedRequest: null`. The accompanying `recommendationReason` says why, and two of the values mean Radar **deliberately declined** to recommend rather than lacking the evidence to try. Reporting those as "no recommendation available" loses the reasoning that produced them:

| Reason | Meaning |
| - | - |
| `hpa_managed` | An HPA manages this resource. Review its target before changing the request  -  the two controls interact. |
| `oom_evidence` | A lower memory request is **withheld** because the container has OOM evidence in the window. Cutting it is the wrong move even though usage looks low. |
| `oom_evidence_unavailable` | Radar could not verify recent OOM history, so it withheld a memory reduction rather than recommend one blind. |
| `hpa_evidence_unavailable` | Radar could not verify HPA ownership, so it withheld a suggested request. |
| `recommended_request_exceeds_limit` | The evidence-based request would exceed the container's current limit; the limit is the thing to review first. |
| `request_within_fit_range` | The configured request is already within 30% of the evidence-based target. Nothing to change. |
| `insufficient_history` | Fewer than six hours of samples. Not a verdict of correctly sized. |

`reductionLimited: true` marks a different case: a recommendation exists, but it is a conservative step toward the demand-based target rather than the target itself. Those rows  -  and only those  -  also carry `demandTarget: {value, basis}`, the end state the step is heading toward, with `basis` naming how it was derived (`7d Max x 1.15` for memory, `7d P95 x 1.15` for CPU, or the minimum request). It is **not** a value to apply: apply `recommendedRequest`, observe a full window, then re-check. A memory target comes from the 7-day max, so a monthly or batch peak outside the window is invisible to it. The engine computes a target before its HPA, OOM and limit checks run, so a withheld row has one internally; it is never emitted there, because it would fill the `null` those checks left on purpose. `observedStatistic` names what `observed` is (`P95` for CPU, `Max` for memory). `bursty` and `p99Usage` qualify CPU rows whose P99 ran far above the P95 the recommendation is built on.

### `managedBy`  -  where a request change belongs

Workloads carry `managedBy: {tool, kind, group, namespace, name, signal}` when Radar can tell what owns their spec: `controller` (a controller ownerReference, such as an operator's custom resource), `flux`, `argocd`, `helm`, or `addon-manager` (`addonmanager.kubernetes.io/mode=Reconcile` only  -  `EnsureExists` does not revert edits). A patch applied live to such a workload is overwritten on that tool's next reconcile or upgrade, so the change belongs at the source. `signal` names the metadata that produced the answer, because labels get copied between templates.

A missing `managedBy` does **not** mean the workload is unmanaged. Argo CD's default tracking label is also every Helm chart's `app.kubernetes.io/instance`, and Terraform or `kubectl apply` leave nothing distinguishable.

### `efficiencyPercent` is not request headroom

`get_cost` emits `efficiencyPercent` on namespace and workload rows and `efficiencyPercent` on summary totals. The denominator is **cost allocation, which is the greater of requested and observed** usage  -  not the request. Two consequences that the numbers alone do not reveal:

* The value is capped at 100 and can never exceed it. A container consuming well above its request reads as \~100%, which is not the same as correctly sized.
* A low value is not by itself a defect and not recoverable money. Bursty workloads, P95-sized requests and HPA headroom all produce low ratios by design.

It answers "how much of what this is costing me is being used", not "how much can I cut". Use `get_rightsizing` for the second question  -  it applies the OOM and HPA gating above, which a ratio cannot.

`usageUnavailable: true` on a row means the usage evidence behind it is missing or incomplete  -  the usage query failed, the cost source reported no usage for the row. `efficiencyPercent` is then `null` rather than a number, and `unusedRequestHourlyCost` is not reported. That is distinct from measuring efficiency as zero, which a row using nothing genuinely does and which is emitted as `0`.

### What `hourlyCost` contains, and the two kinds of idle

`view=summary` totals report idle as two figures, because they are recovered by different actions:

* **`unallocatedHourlyCost`**  -  node compute capacity no workload requested or used. It comes back only when nodes are removed.
* **`unusedRequestHourlyCost`**  -  capacity requested but not used. It comes back only after rightsizing frees enough requests for a node to be removed.

The two do not overlap. Rows carry `unusedRequestHourlyCost` only; unrequested node capacity belongs to the cluster, so `unallocatedHourlyCost` is `null` on a namespace-scoped or RBAC-narrowed summary, and also when the source did not measure it or GPU spend on the nodes keeps it from being separated  -  never `0`, which would claim the nodes are fully packed. `unusedRequestHourlyCost` is likewise absent when no namespace has usage evidence, and it covers CPU and memory requests only  -  idle GPUs are not in it.

`allocatedHourlyCost` always sums the namespace allocations; `allocatedMonthlyProjection` projects that rate over 730 hours. `nodeHourlyCost` independently measures node capacity, with `nodeMonthlyProjection` on the same basis. Node figures are explicit `null` when unmeasured, including namespace-scoped summaries and Kubecost summaries (which do not query node assets). Never add allocation and node totals together: node capacity already contains unallocated capacity. Their component coverage differs, so allocation can exceed node cost. OpenCost allocations include CPU, memory and storage, but not GPU or network.

Workload-view totals carry allocation fields; node-view totals carry node fields. Monthly projections use the same 730-hour convention as the Costs UI. Component names such as `cpuHourlyCost` and `unusedRequestHourlyCost` explicitly identify hourly rates.

### Node rows carry no CPU/memory split

`view=nodes` accepts an optional exact `name` selector. Selection happens before the limit; totals and `nodesNotInCluster` cover only the selected rows. No match returns `reason: node_not_found`. It returns `hourlyCost`, `instanceType` and `region`, and deliberately **omits** per-node CPU and memory components. The OpenCost path derives them from `node_cpu_hourly_cost` / `node_ram_hourly_cost`, which are per-vCPU-hour and per-GiB-hour **unit prices**, while Kubecost reports whole-node **totals**  -  one field name for two different quantities. Use `hourlyCost` with `instanceType` to compare nodes.

Rows also carry `pool: {name, source}` and `capacityType` (`spot`, `preemptible`, `on-demand`, `reserved`) from node labels, resolved with the same pool rule as the Capacity views. Each is omitted when unknown: a missing spot label only means on-demand on GKE and AKS pool nodes, EKS managed node groups and Karpenter, which label every node's capacity type. `on-demand` is not list price  -  committed-use and savings-plan discounts are not visible. `nodesNotInCluster` counts nodes the cost source still reports that the cluster no longer has; their rows carry neither field. Each row is a node's rate while it ran, and totals sum every row, so after node churn inside the window they overstate the current run rate. Whether a pool can shrink (autoscaler limits) is not in this response.

### Fleet scan lifecycle

Namespace and cluster rightsizing runs continue in memory after a request ends. Each run has a three-minute ceiling, with 30-second budgets for individual metrics queries. `scanStatus=running` means the rows, counts, and rankings are provisional; follow `nextCall` after `pollAfterSeconds`. Requests wait at most 20 seconds for progress. `finished` means the scan stopped working, while `state` still distinguishes complete, partial, and unavailable evidence. `timed_out` and `cancelled` retain available results.

A normal call reuses the latest matching scan, retained for up to 15 minutes. Use `scan_id` with the same scope and namespaces to inspect an exact snapshot or change presentation filters without querying Prometheus again. After changing workload requests, omit `scan_id` and set `refresh=true` to start fresh. IDs are scoped to the caller and cluster; restart, expiry, or a cluster/backend change requires a new scan. There are at most two active scans, with no queue or automatic retry. Workload scope stays synchronous.

### How scan scopes rank workloads

`scope=namespace` and `scope=cluster` apply the Rightsizing screen's ordering rule, at a coarser grain  -  the screen ranks each **container** as its own entry while these scopes return **workloads**, so a workload's containers are summed here and the top-N can legitimately differ from the screen's. Both surfaces retain known actionable evidence when another resource lacks data; missing evidence yields `need_data` only when no actionable classification can be established from the available evidence. The rule is:

1. **Reliability priority**  -  evidenced OOM signals, limit conflicts, and throttled reductions first; then `increase`, ordinary `reduction`, routine `review` (burstiness-only, HPA-only, or scaled-to-zero), `need_data`, and `in_range`. Retained risk history does not promote scaled-to-zero workloads. Priority is computed before response limits; classification/filter values remain unchanged.
2. **`requestDelta`**  -  the replica-weighted absolute request change (`{cpu, memory}`, formatted quantities such as `-2250m`), normalized so a CPU change and a memory change are comparable.
3. Namespace, kind and name, so repeated scans return a stable order.

Rows needing manual review  -  an HPA, OOM history, a limit conflict, or a reduction against bursty or throttled usage  -  contribute **no** impact: that saving cannot be applied unattended, so counting it would rank a workload by waste it cannot give back. Unevidenced rows also contribute no delta. Missing evidence for one resource does not hide a known increase or review signal on another resource in the same container. Classification uses evidenced rows; `need_data` applies when none is evidenced, or when the evidenced rows are in range but other rows cannot be judged. Missing CPU/memory delta fields mean zero net change or no eligible evidence; inspect rows to distinguish these. Ranking by the largest *proportional* change instead would put an unrequested 64Mi sidecar above the workload wasting two cores.

`classificationCounts` reports all five classes (`reduction`, `increase`, `review`, `need_data`, `in_range`), including zero counts, across evaluated workloads before classification/row filtering and limits. It excludes workloads not evaluated, so partial scans are not whole-cluster counts; evaluated workloads can still lack evidence. Counts expose every class even when higher-priority entries fill the returned list. Use the `classification` filter to inspect those classes.

`totalWorkloads` is always present on available scans, including zero. It counts workloads matching the selection before the limit; `coverage.workloadsEvaluated` describes the whole scan. Use `classification=reduction|increase|review|need_data|in_range` to select a class before truncation. A classification filter returns every row of matching workloads, while scan effort and coverage stay unchanged.

### Rows that are never omitted

`include_all=true` returns balanced and unevidenced rows too. Workload scope always returns all rows. `evaluatedAt` is evaluation time, not source freshness; workload and scan responses use the same field. Row statistics are `sampleCoverage` (fraction), `p99Usage` (CPU P99), and `throttlePercent` (numeric percentage). `liveInventoryUnavailable` on a workload means current pod OOM status is unknown, even if its rows lack `currentPodOOM`. Workload failure reasons use stable codes: `queries_failed`, `no_usage_samples`, `no_owner_samples`, `pod_inventory_unreadable`, and `no_containers`.

By default a scan returns only `oversized`, `under_requested` and `missing_request` rows and reports the rest in `omitted`. Four exceptions:

* **`scope=workload` returns every row.** The caller named one workload; a handful of rows is the whole answer.
* **A `scaledToZero` workload returns every row.** Unless its evidence is missing (`need_data`), it classifies as `review`  -  its 7-day history may not describe the next scale-up  -  and the Rightsizing page lists it among the actions. A DaemonSet with zero desired pods, once its controller has observed its current spec, matches no node right now, so scans skip it, count it in `coverage.daemonSetsWithoutNodes` and name it in `skippedDaemonSets` (first 50, `skippedDaemonSetsTruncated` beyond that); a node pool scaled to zero still has history for it, which `scope=workload` reads as a `scaledToZero` workload. A zero the controller has not yet re-observed (a new DaemonSet, or a changed selector) is scanned instead, as a `scaledToZero` workload. A namespace holding only such DaemonSets is never reported in `namespacesWithoutWorkloads`, and a scope holding nothing else answers `reason: only_daemonsets_without_nodes` rather than `no_workloads`.
* **A returned container keeps its unevidenced rows.** The missing row explains the partial state beside any valid recommendation on the other resource. On scans such a row's `queryError` names the warning code carrying the real error, and `omitted.queryError` counts only the rows still hidden. Known increase/review signals still contribute to the workload classification.
* **A row carrying `currentPodOOM`, `windowOomEvidence`, `limitConflict`, significant throttling (≥10%), or an autoscaler is always returned**, whatever its `fit`. `fit` is settled from the request alone, so the classic OOM shape  -  request fine, **limit** too low  -  classifies as `balanced`. Filtering on `fit` would drop exactly the row an "is this under-requested?" question is looking for.

`oomEvidenceAvailable` is emitted on **memory rows only**. It never gates a CPU recommendation, and a literal `false` there would read as an evidence gap. `throttleAvailable` is its CPU counterpart: on CPU rows only, and `false` means throttling was not measured  -  not that the container is unthrottled.

### `state: partial` always names its cause

A partial response carries a `reason`, and `guidance` names the causes that are actually present rather than listing every field that could explain one. Causes:

| Reason | Cause |
| - | - |
| `row_evidence_incomplete` | Row-level gaps: rows short of history, failed usage queries, or a recommendation withheld for missing HPA/OOM evidence. Cluster coverage is complete. |
| `requested_namespaces_excluded` | A `namespaces` list included names that were not scanned. `excludedNamespaces` lists each with `access_denied`, `outside_namespace_scope`, or `not_cached` (Radar's informer cache does not hold that namespace); report only `effectiveNamespaces`. |
| `namespace_scope_limited` | The scan reached only the namespaces in `effectiveNamespaces`  -  scope was resolved from this identity's per-namespace access rather than a cluster-wide grant, or radar is pinned with `--namespace-scope`. `remediation` says which. |
| `limited_scope_no_workloads` | No workloads found, and the scan did not cover everything requested. An empty result here is **not** evidence the cluster has no workloads. |

`coverage.partiallyCachedKinds`  -  kinds whose informer covers only some namespaces  -  can be the **only** reason a scan is partial, and does **not** mean the kind is unreadable. It is counted separately from `restrictedKinds` and `unavailableKinds`: only those two decide whether every kind is genuinely unreadable (`state: unavailable`, `reason: workload_kinds_unavailable`).

`remediation` accompanies the reason on **every** state, not just `unavailable`.

### The `--namespace-scope` pin is not an access denial

A pin and an RBAC restriction produce the same namespace list. A request the pin excludes returns `reason: outside_namespace_scope` and a `remediation` naming the flag  -  never `access_denied`, which would tell a cluster-admin their permissions are the problem.

### Trend responses are pre-summarized

Raw `dataPoints` are omitted by default to keep responses small. Pass `include_points=true` with `view=trend` to include them. The default response includes this retry hint; summaries are identical either way.

`view=trend` returns each series with `startHourlyCost`, `endHourlyCost` and `changePercent`, plus `trendTotal` summed across series per timestamp. `trendTotal.from`/`to` are the first and last sample timestamps (RFC3339), possibly shorter than the requested range; `sampleCount` counts timestamps. Rates are hourly, not cumulative spend. `changePercent` is explicitly null for zero starting cost or a single sample. Trend has no monthly projection. Both the total and each series also carry `minHourlyCost`, `peakHourlyCost`, and `peakAt` (the earliest timestamp when the peak repeats). These describe observed samples, not interpolated values or peaks between samples; a series without samples omits these fields. The total peak comes from summed costs at each timestamp, not the sum of each namespace's separate peak. A small endpoint change can conceal an excursion: compare the minimum and peak, then request raw points to inspect the shape. Raw points retain the same namespace aggregation; they do not expand the remainder into individual namespaces.

Only the highest-spending namespaces get individual `type: namespace` series, each with `namespace`. The remainder is a `type: remainder` series with no namespace, distinct from a real namespace named `other`. `seriesCapped: true` reports aggregation; `truncated` is reserved for row-list limits. `namespaceCount` is the full scope's count, and `trendTotal` includes every namespace.

`trendTotal.basis` is `cpu_memory_allocation` for OpenCost via Prometheus (compare `endHourlyCost` with summary `allocatedHourlyCost` minus storage) or `namespace_allocation` for Kubecost (all allocated components, idle excluded). Differences can also come from window alignment.

`effectiveNamespaces` identifies the namespaces actually covered when scope is narrowed. Rightsizing `coverage.successfulBatches` counts batches whose queries all succeeded, not batches attempted.

Both tools return `guidance` as an array of interpretation notes; read notes about missing evidence and scope before reporting a conclusion.

## Available Tools

### Read Tools

For interpreting workload bundles, see [`diagnose` evidence limits](#diagnose-evidence-limits), including collection failures, sampling, source timestamps, and the public MCP versus local investigation boundary.

For `get_cost` and `get_rightsizing`, see [Cost and rightsizing evidence limits](#cost-and-rightsizing-evidence-limits)  -  what each `recommendationReason` means (two of them are deliberate withholdings) and why `efficiencyPercent` is not request headroom.

| Tool | Description | Parameters | |
| - | - | - | - |
| `issues` | "What's broken right now?"  -  a ranked, curated stream of live operational failures: failing workloads/pods, active native Helm release failures or stuck pending operations (`kind=HelmRelease`, `group=helm.sh`), dangling references, pod-startup blockers (unschedulable / admission-rejected / stuck post-bind), and False CRD conditions. No source filter; each row carries a `source` label sliceable via `filter`. Recovered Helm rollbacks are deploy history, not live issues; use `get_changes` for Helm deployment history and `get_helm_release` for native Helm full revision/history/hook diagnostics. Flux `HelmRelease` rows (`group=helm.toolkit.fluxcd.io`) are GitOps reconcilers and should use `diagnose`. For static posture use `get_cluster_audit`; for raw events use `get_events`. | `namespace` (optional), `severity` (optional: `critical,warning`), `kind` (optional), `filter` (optional CEL), `limit` (optional, default 200, max 1000) | |
| `diagnose` | Collect a bounded, point-in-time evidence bundle for one narrowed workload, GitOps reconciler, or network entry kind in a single call. It supports diagnosis but does not run an agent, provide an authoritative root-cause verdict, or exhaustively read every source. Pod/Deployment/StatefulSet/DaemonSet and Argo Rollout (`kind=Rollout`, `group=argoproj.io`) get minified resource + `resourceContext` + selected, capped current and previous container logs across pods + `crashCause` evidence that pairs active crashloop status with one highest-signal filtered log line (`logLineSelection` states how the line was chosen, in descending confidence: `fatal_pattern`, `traceback_header_only`, `last_matched_line`, `log_tail`) + filtered events + `startupBlockers`; Application/Kustomization/Flux HelmRelease get reconciler status + related parsed issues; Service/Ingress/HTTPRoute/GRPCRoute/Gateway get coverage-honest [reachability evidence](/docs/features/reachability): a `summary` (headline + tested/passed/failed/skipped counts over the intended routes), `routes` (per-route outcome + `confidence` - `indirect` = reached only via the API-server proxy, not the live-traffic path), `notTested` (with why + a copyable command), an optional named `brokenRoute` when established, and `path` (hops with static findings). Optional active reachability test (`probe: true`, bounded to a 3s total budget; a timeout returns an honest partial). The returned bundle is frozen from one call, but its sources are not read atomically; preserve its limits, errors, timestamps, and confidence. In `resourceContext`, `issueSummary` uses live-operational `critical`/`warning`, while `auditSummary` uses the Checks posture-remediation ladder `critical`/`high`/`medium`/`low` (current built-ins are `high`/`medium`) and is not evidence of an active outage. | `kind` (required: workload, GitOps reconciler, or network entry kind), `group` (optional API group; set it for CRDs or same-kind collisions, for example `argoproj.io` for `Rollout`; built-in groups are inferred when omitted), `namespace` (required), `name` (required), `probe` (optional: network kinds only - run DNS/TCP/TLS/HTTP probes as applicable; explicitly non-HTTP Service ports stop at TCP), `in_cluster` (optional on the full `/mcp` endpoint only, **MUTATING** - network kinds only: runs the applicable probe layers from inside the cluster by creating up to 5 short-lived, self-deleting probe pods under your RBAC; the only diagnose option with a side effect, requires `create jobs`, `list pods`, and `get pods/log`; forces `probe` on; omitted from and rejected by `/mcp-readonly`) | |
| `get_dashboard` | Cluster/namespace health overview  -  resource counts, failing pods, unhealthy workloads, warning-event groups (`warningGroups`, up to 20 recency-ordered; `totalWarningGroups`/`warningGroupsTruncated` signal when more exist), Helm status. Inventory-style triage before drilling in. | `namespace` (optional) | |
| `top_resources` | Live metrics ranked like \`kubectl top | sort\`, joined with K8s context (status, restarts, owner, requests/limits). Use for CPU/memory/OOM/load symptoms. | `kind` (optional: `pods` default, `workloads`, `nodes`), `namespace` (optional), `sort` (optional: `cpu` default, `memory`), `limit` (optional, default 20, max 100) |
| `list_resources` | List resources of a kind with minified summaries + per-row `summaryContext` (managedBy / health / issueCount). | `kind` (required), `group` (optional), `namespace` (optional), `context` (optional: default / `none`) | |
| `search` | Find resources by content/term match (config keys, env refs, images, label values, CRD fields, status messages). Tokens AND'd; secret values never indexed. Supports `kind:`/`ns:`/`label:`/`image:` modifiers and CEL `filter`. | `query` (required), `filter` (optional CEL), `limit` (optional) | |
| `get_resource` | Inspect one known resource: minified spec, status, and metadata, plus default-on `resourceContext` with relationships and available resource-specific summaries. Operational issues and static audit posture remain distinct; audit findings are not evidence of an active outage. See [integration documentation](/docs/features/integrations) for controller-specific context and limits. Optional supplemental data adds events, metrics, recent changes, rollback revisions, or detailed issue context. `include=issues` returns up to three `relatedIssues`, with `relatedIssuesTotal` and `relatedIssuesTruncated`; this is independent of `context=none`. For logs use `get_pod_logs`, `get_workload_logs`, or `diagnose` for supported kinds. | `kind` (required), `name` (required), `namespace` (optional  -  omit for cluster-scoped kinds), `group` (optional, for ambiguous kinds), `include` (optional, comma-separated: `events,metrics,changes,revisions,issues`; `revisions` lists rollback targets for Deployment/StatefulSet/DaemonSet/Rollout), `context` (optional: `basic` default, `none` to omit `resourceContext`) | |
| `get_topology` | Whole-namespace/cluster topology graph (nodes + edges). Use `summary` format for LLM-friendly text chains. Once you have a suspect root, prefer `get_neighborhood`. | `namespace` (optional), `view` (optional: `traffic` or `resources`), `format` (optional: `graph` or `summary`) | |
| `get_neighborhood` | BFS-expanded topology neighborhood around one known root  -  cheaper and clearer than `get_topology` for cross-resource failures (routing, selector/endpoint, refs, owner chains). RBAC-filtered. | `kind` (required), `namespace` (optional), `name` (required), `profile` (optional: `auto` default / `all`), `hops` (optional, default 1, max 2) | |
| `get_events` | Recent Kubernetes events, deduplicated and sorted **Warning-groups-first then by recency**  -  all types by default, so warnings lead and lifecycle events follow as timeline evidence. Filter by resource kind/name to scope; `type=Warning` for warnings only, `type=Normal` for lifecycle only. A namespace the caller cannot read returns an empty list with `accessDenied: true`, so an empty result without the marker means nothing is recorded for that scope. A request with no `namespace` means cluster-wide; when the caller may read only some namespaces the answer covers those alone and says so with `partialScope: true` and `scopeNamespaces`, so a narrowed empty result is never read as a clean cluster. | `namespace` (optional), `limit` (optional, default 20, max 100), `kind` (optional), `name` (optional), `type` (optional: `all` default, `Warning`, `Normal`) | |
| `get_changes` | Recent meaningful changes from the Kubernetes cluster timeline plus native Helm release deployment/operation history (`source: helm`). Use to investigate what changed before an incident, including failed upgrades, rollbacks, and current Helm revisions. If the response includes `sourcesErrored`, treat it as partial data for those sources. Use `get_helm_release include=history,operations` for the full Helm revision trail. | `namespace` (optional), `kind` (optional), `name` (optional), `since` (optional, e.g. `1h`, `30m`; default `1h`), `limit` (optional, default 20, max 50) | |
| `get_pod_logs` | Pod logs with secret redaction. Without `grep`, prioritizes errors/warnings and falls back to recent tail lines; with `grep`, returns only regex-matching lines instead of applying the diagnostic filter. Returns `container` when explicitly requested or resolved from a readable single-container Pod. If Pod metadata is unavailable, the log read still proceeds, but an unspecified container remains unknown. | `namespace` (required), `name` (required), `container` (optional; specify for multi-container Pods), `tail_lines` (optional, default 200), `grep` (optional), `previous` (optional), `since` (optional) | |
| `get_workload_logs` | Aggregated logs from all pods of a workload (Deployment, StatefulSet, DaemonSet, Job, Argo Workflow). Without `grep`, auto-filters for diagnostic relevance; with `grep`, returns only matching timestamp-prefixed lines. | `kind` (required), `namespace` (required), `name` (required), `container` (optional), `tail_lines` (optional, default 100 per pod), `grep` (optional) | |
| `get_cluster_audit` | `missingInputs`, when present, names unavailable inputs in the scan; zero findings do not establish that all checks passed. Static config posture  -  best-practice findings (Security / Reliability / Efficiency) with remediation. INDEPENDENT of operational health. Finding severity and the explicit `summary.critical`/`high`/`medium`/`low` counts are posture remediation priority; current built-in checks emit `high` or `medium`. For "what's broken right now?" use `issues`. | `namespace` (optional), `category` (optional), `severity` (optional: `critical`, `high`, `medium`, or `low`) | |
| `get_cluster_upgrade_readiness` | Upgrade impact analysis for a target Kubernetes minor  -  the evidenced check catalog (version skew, removed/deprecated API usage across live manifests / Helm sources / API-server metrics, node runtime + cgroup evidence, drain feasibility, webhook readiness, release-specific config checks). The verdict is not a readiness guarantee: `unknown` checks had incomplete evidence, `coverage` states what the caller's RBAC could actually inspect, and `no_access` returns no verdict at all. Tier 1 is one row per check; `check=<id>` expands into findings with evidence + remediation, paged 25 at a time (`offset` + `scan_id` bind pages to one scan snapshot). Results are memoized \~60s per caller; `refresh=true` re-scans after a fix. Fix findings at the source `evidence.source` names  -  live objects via `patch_resource`/`apply_resource`, Helm findings in the chart, GitOps-managed resources in Git. | `target` (optional, default next minor), `check` (optional), `level` (optional: `blocker`, `warning`, `review`), `offset` (optional), `scan_id` (required when `offset` > 0), `refresh` (optional) | |
| `list_packages` | Installed packages (Helm releases, label-managed workloads, CRDs, Argo Applications, Flux HelmReleases + Kustomizations) with source provenance, versions, and health, in one call. Response includes `sourceLegend` for the stable source codes. | `namespace` (optional), `source` (optional: `H`/`helm`, `L`/`labels`, `C`/`crds`, `A`/`argocd`, `F`/`fluxcd`), `chart` (optional substring) | |
| `list_helm_releases` | List Helm releases with status, resource health, storage namespace, Flux ownership, current `lastOperation`, and a capped `operations` trail when Helm history indicates failed upgrades, rollback-after-failure, rollbacks, or stuck pending operations. Use this first for Helm deployment debugging. | `namespace` (optional) | |
| `get_helm_release` | Returns the release with its owned resources, `hooks` with their outcome, `history` (the newest ten stored revisions; `include=history` returns them all) and the user-supplied `values` (redacted). Detailed Helm release info with owned resources, resource health, Flux ownership, current `lastOperation`, `operationInsight` (active/recovered state, likely resource to inspect, suggested compare), hooks, and failed/running hook diagnostics with live Job/Pod/Event/redacted-log evidence when still available. Use `include=history,operations` for the full Helm revision trail; `include=values` for key-aware redacted user values; `include=diff,values_diff,notes_diff,resource_diff` for revision comparison. Manifest `diff` redacts Secret values while preserving key changes; `notes_diff` uses pattern-based redaction. For releases with `storageNamespace`, pass that value as `namespace`. | `namespace` (required: Helm storage namespace), `name` (required), `include` (optional: `values,history,operations,diff,values_diff,notes_diff,resource_diff`), `diff_revision_1` (required when `include` contains a diff token) / `diff_revision_2` (optional, defaults to current) | |
| `list_namespaces` | List all namespaces with status | (none) | |
| `get_subject_permissions` | Effective RBAC permissions of a ServiceAccount / User / Group. Without `verb`/`resource`, returns the existing bindings, deduplicated flat rules, and (for SAs) Pods using it. With both fields, runs an authoritative SubjectAccessReview for a ServiceAccount and returns a focused allow/deny result; this requires the caller itself to have `create subjectaccessreviews.authorization.k8s.io`. | `kind` (required: `ServiceAccount`, `User`, or `Group`), `namespace` (required for ServiceAccount; omit for User/Group), `name` (required), `verb` + `resource` (optional pair; ServiceAccount only), `group`, `resource_namespace` (defaults to subject namespace; explicitly empty for a cluster-scoped or cluster-wide API request, not aggregation of namespace-local grants), `subresource`, `resource_name` (optional check refinements) | |
| `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. Empty results include a bounded list of related active metric names when a metric family can be inferred. Oversized results return a label-cardinality summary + suggested `topk` rewrite instead of raw data. Every response carries `selectors`, the vector selectors the query reads (metric + label matchers), with `selectorsUnknown: true` when the query could not be tokenized and its scope must be treated as unbounded. | `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) | |
| `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) | |
| `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) | |
| `get_cost` | Estimated costs from OpenCost or Kubecost: namespace summary, workload/node breakdown, or summarized trend. Hourly rates and 730-hour projections; availability, scope and missing evidence are explicit. See [evidence limits](#cost-and-rightsizing-evidence-limits). | `view` (summary default, workloads, nodes, trend), `namespace` (required for workloads; filters summary/trend), `kind` + `name` (one workload), `name` (one node), `range` (trend: 6h, 24h default, 7d), `include_points` (trend only, default false), `limit` (row views: 1–100, default 20) | |
| `get_rightsizing` | CPU/memory request recommendations from 7 days of usage. Workload scope is cheap; namespace/cluster scans run in the background for up to three minutes. Running responses include `scanId`, `scanStatus`, and `nextCall`; follow that call to retrieve progress without restarting completed batches. Scans are retained up to 15 minutes; use `refresh=true` after changes. Results prioritize evidenced safety risks, then increases, reductions, and routine reviews; ties use replica-weighted request delta. Check confidence and manual-review reasons; reductions do not directly imply bill savings. | `scope` (required: workload, namespace, cluster), `kind` + `name` (workload), `namespace` (workload or namespace scope), `namespaces` (namespace scope only, nonempty array; mutually exclusive with namespace), `include_all` (default false; workload always returns all), `classification` (scan only: reduction, increase, review, need\_data, in\_range), `limit` (scan only: 1–100, default 20), `scan_id` (retrieve one scan with the same scope/namespaces), `refresh` (scan only; start fresh, cannot combine with scan\_id) | |

API group is part of a resource's identity. Radar infers the canonical group for built-in kinds, but callers should pass `group` for a supported CRD or whenever a Kind can exist in more than one group. For example, diagnose an Argo Rollout with:

```json theme={null}
{"kind":"Rollout","group":"argoproj.io","namespace":"prod","name":"checkout"}
```

This selects the Argo Rollout workload path; it does not imply that `diagnose` supports arbitrary custom resource shapes.

For `issues`, read `timing_summary` when present; it explains timing combinations that are easy to misread without schema context. The raw provenance fields remain available for filtering. `first_seen` is an evidence-backed lower bound, `onset_unknown` means no contributing signal has a known onset, and `resource_created_at` is resource-age context rather than issue age. A missing `first_seen` is exposed to CEL as `0`; require `first_seen != 0` for any age filter, and also require `onset_coverage_unknown == 0` when the whole row must have exact timing.

### Write Tools

| Tool | Description | Parameters |
| - | - | - |
| `apply_resource` | Create or update a Kubernetes resource from YAML. Supports multi-document YAML, per-document partial-failure results, server-side dry-run preview, and SSA ownership-conflict reporting. | `yaml` (required), `mode` (optional: `apply` or `create`, default `apply`), `dry_run` (optional, default false), `namespace` (optional, override), `verify` (optional, default true: real mutations return post-mutation state, submitted-vs-live diff, workload rollout/pods, and related issues; dry runs return a preview diff), `force` (optional, default false: take SSA field ownership from other managers) |
| `patch_resource` | Patch one existing Kubernetes resource with JSON Patch, JSON Merge Patch, or strategic merge patch. Use for precise field/list edits when you know the exact path and do not want to rewrite the full manifest or take broad server-side-apply ownership. Strategic patch is for built-in Kubernetes kinds and name-keyed list edits, such as changing one container. | `kind` (required), `name` (required), `namespace` (required for namespaced resources), `group` (optional), `patch_type` (optional: `json` default, `merge`, or `strategic`), `patch` (required JSON string), `dry_run` (optional), `verify` (optional, default true: real mutations return compact post-patch state and JSON Patch field checks; dry runs return a preview diff) |
| `manage_workload` | Restart, scale, or rollback a Deployment, StatefulSet, DaemonSet, or Argo Rollout. Note: `scale` is not supported for DaemonSets. On a Rollout, `rollback` rewrites the pod template and therefore starts a **new** rollout that re-runs every canary step, pause, and analysis  -  use `manage_rollout` `abort` to revert traffic immediately, or follow the rollback with `promote-full`. | `action` (required: `restart`, `scale`, `rollback`), `kind` (required), `namespace` (required), `name` (required), `replicas` (for scale), `revision` (for rollback  -  enumerate with `get_resource include=revisions`) |
| `manage_rollout` | Drive an Argo Rollout's progressive delivery  -  the verbs with no Deployment equivalent. `abort` reverts traffic to the last stable version at once without touching spec and stays aborted until retried (the incident-response verb); `retry` clears an abort; `promote` clears the current pause to advance one step; `promote-full` skips all remaining steps, pauses, and analysis (emergency hotfix)  -  it waits for the controller to observe the current pod template first, so straight after a rollback it may report that the controller has not caught up, which is safe to retry; `skip-step` advances exactly one canary step (canary only). Rollback / restart / scale go through `manage_workload` with `kind=rollout`. | `action` (required: `abort`, `retry`, `promote`, `promote-full`, `skip-step`), `namespace` (required), `name` (required) |
| `manage_cronjob` | Trigger, suspend, or resume a CronJob | `action` (required: `trigger`, `suspend`, `resume`), `namespace` (required), `name` (required) |
| `manage_gitops` | Manage ArgoCD and FluxCD resources  -  sync, refresh, terminate, suspend, resume, rollback (Argo), reconcile (Flux), reconcile-with-source (Flux) | `action` (required), `tool` (required: `argocd` or `fluxcd`), `namespace` (required), `name` (required), `kind` (FluxCD only). For `sync`: `revision`, `prune`, `dry_run`, `force`, `apply_only`, `sync_options`. For `rollback` (Argo only): `history_id` (required), `prune`, `dry_run`. Per-action input validation rejects flags that don't apply to the action (e.g. `force` on `suspend`) so callers fail loudly instead of silently. |
| `manage_node` | Cordon, uncordon, or drain a Kubernetes node | `action` (required: `cordon`, `uncordon`, `drain`), `name` (required), `delete_empty_dir_data` (optional, default true), `force` (optional), `timeout` (optional, seconds, default 60) |

## Strimzi connector evidence

See [Strimzi Kafka connector evidence](/docs/features/integrations/strimzi) for the Issues/API/MCP coverage, required read access, and operator-snapshot limitations.

## Available Resources

| URI | Description |
| - | - |
| `cluster://health` | Cluster health summary (same data as `get_dashboard`) |
| `cluster://topology` | Full cluster topology graph |
| `cluster://events` | Recent warning events (up to 50) |

## Security

* **Safe by design**  -  most read tools do not change cluster state and are annotated with `readOnlyHint`. On the full `/mcp` endpoint, the exception is `diagnose(in_cluster=true)` on network entry resources, which creates up to five self-deleting probe pods; that full diagnose tool carries neither `readOnlyHint` nor `destructiveHint`, so clients that prompt only on `destructiveHint` will not prompt for it. On `/mcp-readonly`, diagnose omits and rejects `in_cluster` and carries `readOnlyHint`. Write tools (restart, scale, rollback, sync, apply, cordon/drain) are RBAC-enforced and annotated with `destructiveHint` so AI clients can prompt for confirmation. Some are genuinely destructive  -  `apply_resource force=true` can take field ownership from Helm/Flux, `manage_node drain` evicts pods, and `rollback`/`terminate` overwrite or abort desired state
* **RBAC-aware**  -  every call enforces RBAC at the same boundary as the REST API:
  * **Local binary**: the cache uses your kubeconfig identity, so MCP can only see what `kubectl` can see for that user
  * **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
  * **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
* **Audit subject access**  -  audit findings and counts, including resource summaries, require namespace visibility plus an explicit Secret list grant for Secret subjects or the exact resource list grant for cluster-scoped Crossplane subjects. Namespace-local Secret grants are honored even when cluster-wide Secret listing is denied. The HTTP `raw=true` option bypasses local audit settings, never authorization. Unused ConfigMap/Secret findings additionally require adequate consumer evidence; `configmap-references` / `secret-references` in `missingInputs` indicate excluded unknown evaluations. See [audit evidence under partial access](/docs/configuration/files#audit-evidence-under-partial-access).
* **Prometheus access has two tiers**  -  PromQL cannot be namespace-scoped server-side (arbitrary queries can aggregate across namespaces), so with auth enabled the unbounded surfaces  -  `query_prometheus`, `discover_metrics`, `get_prometheus_rules`, and the REST `/prometheus/query` and `/prometheus/cluster` routes  -  require a SubjectAccessReview for `list pods` across all namespaces, and the denial says so. The curated REST charts (per-resource, per-namespace, HPA) require only reading the resource they chart. Local no-auth mode is unchanged: every caller shares the kubeconfig identity's view
* **Secret redaction**  -  Secret `.data` and `.stringData` are never exposed; only key names are shown
* **Value redaction**  -  environment variable values and Helm values returned through MCP are scrubbed for known secret patterns; Helm values also use key-aware redaction for names like `password`, `token`, `privateKey`, and `secretKey`
* **Log redaction**  -  pod log output and Helm hook log evidence are scrubbed for secret patterns before being returned

### Pod/template differences in issue context

Existing OOM/restart, unschedulable, and image-pull issues can carry a neutral
`pod_template_divergence` fact. The UI labels it **Pod/template differences** in
the existing Context section, including resource drawers opened from topology.
It adds no issue, severity, health status, topology edge, or audit finding. MCP
`issues` and `diagnose` expose the same fact; `get_resource` requires
`include=issues` for detailed rows and keeps its default issue summary compact.

The baseline is the current template of the Pod's immediate ReplicaSet or Job,
matched by controller reference, API group, and UID. StatefulSet/DaemonSet
revision reconstruction, Pod-level resource budgets, and RuntimeClass overhead
are outside this comparison. OOM context compares lower
memory limits on status-identified OOM containers; scheduling context compares
higher requests, priority class, node selectors, required affinity, and lost or
changed tolerations; image-pull context compares the failing container's image.
Injected containers, equivalent quantities/image spellings, ordinary
limit-to-request defaulting, added tolerations, and preferred affinity stay quiet.
Environment values, commands, and arbitrary spec diffs are excluded.

One fact shows at most three fields and three candidate objects, with truncation
disclosed; candidate display order does not imply likelihood. A grouped issue
compares at most ten distinct affected Pods in representative order and labels
its first matching witness as an example. A specific Pod lookup compares only
that Pod, including members beyond the group's inline evidence cap. Existing
diagnostic facts take precedence when the four-fact budget is full.

Candidates are current configuration to inspect, **not attribution**: matching
LimitRange defaults for omitted resource keys, the observed global-default
PriorityClass when the template omitted it, and mutating webhooks whose current
core/v1 Pod CREATE rules and selectors match (UPDATE candidates apply only to
image differences). Pod resize subresource rules are not inspected. Namespace selectors use
namespace labels. CEL match conditions are not evaluated, so those webhooks are
omitted. CREATE-only configurations newer than the Pod are excluded. Validating
webhooks and Pod Security Admission are not mutation candidates. All named
evidence is permission-filtered before producing prose; unreadable or unsynced
candidate configuration does not suppress an observable Pod/template difference.
The existing OOM diagnosis also records its template source internally so an
unreadable ReplicaSet limit cannot reappear through the issue cause text.

Comparisons use cached current objects, not an admission-time snapshot. Resizing,
other writers, and later template/configuration edits can explain differences.
Candidate inspection is non-exhaustive and does not perform live admission calls
or start watches. Verify provenance with configuration history or
[Kubernetes admission audit records](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#monitoring-admission-webhooks).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.