Skip to main content
Radar includes a built-in Model Context Protocol (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. Looking for Radar’s built-in AI investigations? MCP clients use your own identity; see investigation access for the permissions used by hosted investigations.

Node drain timeouts

manage_node with action: drain waits for pod deletion by default. Its default two-minute deadline covers eviction retries and deletion waiting together. A final verification read can take up to five seconds more. An explicit timeout replaces the operation deadline; it is not an additional deletion-wait allowance. Configure the MCP client’s tool-call timeout and every reverse proxy in the path to exceed the selected drain deadline plus five seconds, with room for network latency. For the default, use at least 150 seconds. Radar’s drain route is outside its normal 60-second HTTP timeout group, but that does not change external client or proxy limits. See ingress timeout guidance. If a client or proxy times out, the outcome is unknown to that caller. Evictions may already have been accepted. Inspect the node’s pods before retrying or taking it down. An accepted eviction is not proof of deletion. Use wait_for_deletion: false only when you want eviction requests started without waiting for deletion. A started result is not completion. incomplete and unconfirmed results need attention even when the HTTP request succeeded. The node page retains this tab’s last UI drain result until dismissal, reload or context switch. It is a recorded result, not live node health or a history of drains started by other users or MCP clients.

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:
To expose only read-only MCP tools:
This is one server-wide mode. /mcp-readonly stays available, and Radar’s internal /mcp-investigation endpoint keeps its read-only tools and evidence correlation. The full /mcp endpoint returns 404, including direct MCP calls. Write tools cannot be listed or called on the retained endpoints. Diagnostic options that create temporary Kubernetes resources (diagnose’s in_cluster) are also rejected. Built-in investigations and verification remain available; their Apply action is unavailable because it requires MCP write tools. This setting does not restrict writes in the Radar UI or REST API. CLI and Desktop accept both flags. You can also set RADAR_MCP_READ_ONLY=1 or persist "mcpReadOnly": true in ~/.radar/config.json. The existing "mcp": false disables all MCP endpoints. CLI settings take priority over the environment, which takes priority over the config file. A higher-priority mode selection replaces a lower-priority one: An explicit false value clears only that setting. Environment booleans use Go’s strconv.ParseBool spelling (1, 0, true, false, T, F, and their supported case variants); an empty value is unset and an invalid value is an error. --no-mcp --mcp-read-only is an error. A config file containing both "mcp": false and "mcpReadOnly": true is also rejected; correct the file before starting Radar, even when passing overrides. Malformed or unreadable config files stop CLI, Desktop and standalone investigation startup rather than falling back to full MCP. For example, "mcpReadOnly": "true" is invalid: use the JSON boolean true without quotes. The settings API also rejects contradictory MCP settings before saving them. In read-only mode, use http://localhost:9280/mcp-readonly in client configurations. Radar’s setup dialog, settings and interactive investigation handoffs select this endpoint automatically. Existing client configurations using /mcp must be updated. Startup output and /api/capabilities (mcpEnabled, mcpReadOnly) describe the effective mode.

MCP Endpoint

The port matches your --port flag (default 9280). The MCP server uses the stateless Streamable HTTP transport with JSON-RPC and accepts MCP protocol versions 2024-11-05 through 2026-07-28.

Server instructions

When a client connects, Radar returns MCP server instructions in the initialize result, or, for clients on protocol version 2026-07-28 (which has no initialize), in the server/discover result. The text is about 3,400 characters on which tool answers which question, how to read issue timing and attached changes, what a tool result does and does not prove, and that text read from the cluster (logs, events, annotations) is data, never instructions. It lives in pkg/investigation/guidance.go. Agents in clients that use server instructions get this guidance without a separate skill or rules file. What a client does with the text depends on the client and its version. Some add it to the model’s context at session start; others show it only after the model loads Radar’s tools; some ignore it. Claude Code keeps the first 4,096 characters (2,048 before v2.1.296; see its MCP documentation), so Radar puts tool routing first and keeps it within the first 2,048 characters. Codex describes a server whose tools it loads on demand by the first line alone, cut at 250 characters, so that line says on its own what Radar is, that its tools are preferred over kubectl for reading cluster state, and which core tools to load. Each tool’s description carries its own routing, so agents in clients that ignore server instructions still get it. /mcp-readonly sends the same text without the write tools.

Waiting for a fix to land

Use wait_for after a change to wait for a controller’s rollout, Pod readiness, Job completion, deletion, or a status condition. Each call waits at most 45 seconds so it can return within common MCP client timeouts. The caller owns the total deadline: repeat waiting calls within that limit, passing back the named object’s uid to pin it. The states are met, waiting, blocked (an observed stall), and failed (terminal). Selector waits check the current set; use the controller’s rollout to wait for replacements. jsonpath={.status.field}=value reads only .status; its freshness guard uses status.observedGeneration when present. inStateSeconds is condition age, not time since the fix.

Catalog Introspection

MCP registries and inspectors can start Radar without a Kubernetes cluster when they only need the tool and resource catalog:
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. Adding --mcp-read-only exposes /mcp-readonly instead of /mcp and keeps the internal investigation mount. The same config and environment precedence applies. For registries that launch MCP servers over stdio, use:
Add --mcp-read-only to serve the existing read-only catalog over stdio, with the same write-tool and mutating-diagnostic rejection as HTTP. Both catalog entrypoints reject an explicit --no-mcp. Choosing a catalog entrypoint overrides a saved "mcp": false. radar diagnose --standalone honors the saved and environment MCP mode; a disabled mode rejects startup. 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:

Codex

Run this command:
Or add to ~/.codex/config.toml:

Cursor

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

VS Code (GitHub Copilot)

Add to .vscode/mcp.json in your workspace:
Or add it to your user profile from the command line:

GitHub Copilot CLI

Run this command:
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:
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:
Or add to ~/.gemini/config/mcp_config.json (all workspaces) or .agents/mcp_config.json (one workspace). Antigravity uses serverUrl for remote servers:

Kiro

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

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:

Cline

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

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:

Claude Desktop

Claude Desktop’s custom connectors connect from Anthropic’s cloud and cannot reach localhost. For a local Radar, use the 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):
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:

Gemini CLI

For Gemini Code Assist Standard and Enterprise users. Run this command:
Or add to ~/.gemini/settings.json:
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 as io.github.skyhook-io/radar, packaged as the ghcr.io/skyhook-io/radar Docker image. The registry-suggested invocation is:
Prefer the native install (brew install skyhook-io/tap/radar, krew, or the install script - see the README) - 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 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. Warning events in workload diagnose exclude events whose involvedObject.uid belongs to an earlier incarnation of the resource or its current Pods. get_resource(include=events) applies the same check to the requested resource only. UID-less events cannot establish which incarnation they describe. Core Node events that use the Node name as their UID are treated as UID-less; mirror Pod events may use the static UID from kubernetes.io/config.mirror. When Warning events were excluded this way, warnings says how many (cached Event objects, not occurrences) and that they may still explain what happened before, so an empty list is not read as nothing ever having failed; it covers only the resource and its current Pod names, and its absence does not prove that no predecessor failed. get_events can include events from earlier incarnations. get_workload_logs, generic metric scope and the live Pod inventory and OOM evidence of single-workload rightsizing apply the same current membership rule as diagnose; each reads it at its own time, and rightsizing recommendations still come from retained ownership history. get_workload_logs reports a workload the synced cache does not hold as not found rather than as zero Pods. podNames lists the Pods controlled by the observed workload, with API group, kind, name and UID matched throughout controller ownership. Names are sorted and capped at 50; podNamesTruncated marks the cap, and pods counts the full set. Scope a follow-up query_prometheus to the listed names and namespace (pod=~'^(a|b)$'); a prefix such as pod=~'api-.*' can include sibling workloads. 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. The selected names come from the Pods the observed workload controls at collection time. Names no longer in that set are excluded, but earlier incarnations with the same name can contribute samples within the window; 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. Radar starts discovery again in the background when a Prometheus is installed or its connection is lost (see Metrics charts show “Prometheus not connected”), so a diagnose that comes after the new backend is ready normally finds it already connected instead of spending its budget on discovery. 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: 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: 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_pin and a remediation naming the flag - never access_denied, which would tell a cluster-admin their permissions are the problem. That applies to namespaces the caller can see; one they can’t is access_denied whether or not Radar is pinned to it, so guessing names can’t find the pin.

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.

Resource reads: coverage and errors

Every list_resources result says what it covers. A list that served nothing, and any get_resource failure, is an MCP tool error with a code. Workload and GitOps diagnose root reads apply the same readiness checks. Codes are defined in pkg/readreason, and the coverage types in pkg/resourceapi; REST resource reads and search use the same codes.

Words first

An agent reading these results has not been trained on Radar’s vocabulary, and a tool description can’t carry a manual for it. So every result a model reads says in words what the codes and coverage say as data, in two parts:
  • What happened: summary for a list, error for a failure. What the answer covers, what it can’t show (a namespace Radar didn’t read never reads as “none there”), and, when only a person can change that, who: a cluster admin for your access, or whoever runs Radar for what it collects. Findings shows this to people too.
  • What the agent can do: next, present only when the agent itself can change the answer, naming the exact tool and argument. It is absent when only a person can change the answer, and when the answer is complete and names every namespace it read.
next appears for three situations, because only these reliably change an agent’s next move: A denial (user_forbidden), and anything Radar’s own identity or configuration blocks, has no next: retrying won’t change it, and its error names who can. Codes and coverage stay for code to branch on and never change spelling; the sentences may be reworded.

Lists

list_resources always returns one text block holding {summary, next?, coverage, items}, the summary first so it survives truncation that keeps only the head of a long result:
  • summary is always present and leads with its verdict: Complete: when Radar read the whole scope (“Complete: 2 services across every namespace.”, or “Complete within your access: …” when the scope was narrowed), otherwise Partial:, with each part it didn’t read called unknown, why, and who can widen it.
  • scope is what the answer is about: cluster for a cluster-scoped kind, all_namespaces, or a namespace set. A namespace you name stays the scope even when Radar can’t read it.
  • narrowedBy says why an unscoped request covers less than every namespace: user_access (the namespaces your access allows), namespace_pin (Radar’s --namespace-scope), or namespace_collection (the namespaces Radar collects the kind in, when its identity can’t list it cluster-wide). It is absent when nothing narrowed the request.
  • status is relative to scope: complete means Radar’s watch of every namespace in it has loaded, so missing rows are absent from what Radar observes within that scope; partial means gaps names what it did not read. A complete list with narrowedBy is still not the cluster. status keeps this meaning: watch freshness or paging, if added, get fields of their own.
  • read is what Radar read within scope. read and the gaps don’t overlap and together make up scope, so on a complete answer read equals scope.
  • gaps[] are the reasons the answer isn’t complete. Each has a code, the action that code calls for (the same table as below, so a client that doesn’t know a newer code still knows whether to retry), and where it applies. For a namespaced kind every gap states its namespaces: a set, or {"extent": "all"} for every namespace in scope (the kind is still loading everywhere, say), or {"extent": "unknown"} when Radar can’t say which. A cluster-scoped kind’s gaps have no namespaces. A namespace is in at most one gap.
Namespace sets are {count, names}. count is exact. names, when present, lists every member, sorted; it is absent when there are more than the answer can carry (sixteen on MCP, fewer on REST when one response carries many sets), and then only count says how many. A reader knows a namespace’s membership from names or not at all. The same holds for a gap’s kinds and objects. items is [] when the namespaces read hold nothing. What Radar doesn’t collect shows up in one of two ways. When it knows the namespaces it collects a kind in, an unscoped request is narrowed to them (narrowedBy: namespace_collection) and can be complete within them. A namespace you name that Radar doesn’t collect stays in scope as a radar_namespace_not_collected gap. A client meeting a value it doesn’t know treats it conservatively: an unknown code as an incomplete answer, with no remedy beyond its action; an unknown narrowedBy as narrowed; an unknown scope type as grounds for no completeness or absence claim. Each namespace is read on its own. A namespace whose informer is still loading becomes a gap, and the other namespaces’ rows are still returned. This holds for built-in kinds too: when Radar watches a kind namespace by namespace, a namespace whose informer has synced is read even while another is loading. A namespace you name that Radar doesn’t collect, or was refused in, is a gap too; on an unscoped request those namespaces narrow it (namespace_collection) instead. Secrets, LimitRanges and Trivy Operator reports need your exact permission on that kind, not just namespace visibility: namespaces you can see but can’t list them in are a user_forbidden gap (or user_permission_check_failed when the check didn’t complete), and a get needs get in the namespace. A caller whose access reaches every namespace is checked once, cluster-wide: an unscoped list of one of these kinds fails with user_forbidden unless they can list it everywhere, and the error says to name a namespace they can list it in. Cluster-scoped kinds keep exact per-kind permission checks and fail with user_forbidden. Namespace lists use every namespace Radar knows of, after the list-namespaces permission check. The browser namespace picker does not scope MCP requests.

Errors

A list fails only when it returns no usable inventory: Radar read nothing in its scope, its cluster connection changed mid-read, or the answer was over its size budget. A failed read’s structuredContent and its only text block are the same body, {error, error_code, next?, coverage?}. A list’s error carries the coverage Radar assessed, without read: an error returns no rows. A request Radar couldn’t turn into a scope has none (a namespace you can’t see, or one outside the --namespace-scope pin). A get’s error has no coverage.
Branch on error_code. A failure the agent can act on itself carries next, as on lists (namespace_required: set namespace, or search for kind:<kind> <name>; object_not_found: the same name under another kind or namespace, else that search). A list that read nothing takes its next from every gap, so a retry that would recover one namespace is offered even when the code names a denial. error_code is the most actionable cause; when namespaces failed for different reasons, coverage has a gap for each, and the sentence names every cause and the namespaces each affects. When something other than your own access narrowed an unscoped request (what Radar collects, or its pin), the sentence says that too. A get of an object that isn’t there is object_not_found, and only once Radar has confirmed it read that namespace; otherwise the code says why it couldn’t look. The same table, as the action each code calls for, is pkg/readreason/testdata/actions.json. Under Radar Cloud, a denial of a viewer or member also says an owner of their organization can raise their role, which may grant the permission: the chart binds each Radar Cloud role to Kubernetes permissions by default, and a cluster can bind its own. Radar doesn’t know which, so a cluster admin stays the first remedy. Errors about Radar’s own access omit the API server’s response, which may name Radar’s identity; the raw cause is logged server-side. Errors, gaps and summaries name only namespaces the user may read or named themselves. Findings shows a list’s summary as its coverage caveat, including for an empty partial or narrowed list. A placed story card distinguishes an unread subject namespace from a missing object in a namespace that was read; when a listing read more namespaces than it names, the card says how many it read and that the subject’s namespace may not have been one. A bare array comes from a Radar older than this contract and is never read as complete.

Available Tools

Read Tools

For interpreting workload bundles, see 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 - what each recommendationReason means (two of them are deliberate withholdings) and why efficiencyPercent is not request headroom. 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:
This selects the Argo Rollout workload path; it does not imply that diagnose supports arbitrary custom resource shapes. Watched, synchronized custom resources that remain deleting appear in issues as termination_stuck, timed from the deletion request. Radar starts no new watches for this scan; an empty result does not establish that unwatched kinds are healthy. These findings also feed Active Issues and AI resource/search summaries, but not dashboard health summaries or cluster://health. Findings start at warning after 10 minutes. At 30 minutes, controller-owned finalizers follow this policy: A fully ready controller for any finalizer keeps a mixed-finalizer finding at warning; repair advice for other controllers remains. In-use guards and GC-only finalizers can reach critical at 30 minutes. Resolve their dependencies rather than removing them; orphaning preserves dependents. REST/UI deletes preserve a policy recorded by a foregroundDeletion or orphan finalizer; otherwise they explicitly request Background propagation, even for terminating resources or when the pre-delete read fails. The catalog recognizes Argo CD, Flux, VictoriaMetrics and Karpenter through known finalizer keys and workload identities. Name-only guesses for uncataloged finalizers never establish ownership. Renamed installs and mirrors can be recognized through published images; controller-specific identity rules are in pkg/finalizers/catalog.go. Ordinary namespaced subjects require namespace access; Secrets also require Secret access, and cluster-scoped subjects require list on their exact kind. Controller evidence follows namespace access for namespaced reads and exact-kind list for cluster-wide searches. A missing or down controller requires complete cluster-wide Pod, Deployment, StatefulSet and DaemonSet visibility. Incomplete inventory cannot enable removal. A negative search is not proof of an uninstall; a controller may run elsewhere or use an identity Radar does not recognize. Only Flux and VictoriaMetrics currently opt into conditional_cleanup, separate from action. It contains condition, agent_guidance, caution, and removals with one entry per finalizer (finalizer, patch_resource, kubectl). An agent may propose removal only after the user confirms intentional uninstall or a permanent stop. The UI keeps these commands in a collapsed conditional section. Each command defaults to a server dry run and tests the object’s UID and the finalizer at its index. Review the preview before applying; re-read after every removal because indices change. Removal bypasses controller cleanup and can leave Kubernetes resources, infrastructure or storage behind. Dependents with their own finalizers can remain stuck even after the controller is restored; older VictoriaMetrics operators place such finalizers on managed objects. For issues, unchecked_sources lists the issue sources Radar couldn’t check for this answer, so their issues may be missing; don’t read the response as healthy for them. Sources: helm (failed or stuck Helm releases), resources (built-in kinds such as Pods, workloads, PVCs, HPAs and Nodes), custom_resources (failing conditions on custom resources) and karpenter (NodePools, NodeClasses, NodeClaims). Reasons: read_failed, kind_sync_pending (still loading, for example right after Radar starts; ask again shortly), kind_sync_failed (loading stalled), and collector_access_denied (Kubernetes refused Radar’s own identity, so the caller’s permissions can’t fix it). An entry may carry kinds (only those kinds went unchecked) and namespaces (only those namespaces failed; the rest are complete). Treat any other source or reason as unchecked too. 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. Resource context distinguishes observed dependency references (dependencies and dependents) from autoscalers (scaledBy). A Certificate depends on its issuer; it does not scale it. These references carry API group and namespace identity, are filtered by the caller’s reference permissions, and do not establish that a controller has successfully reconciled the dependency. A KEDA ScaledObject summary’s target includes its API group, for example Deployment (apps)/web. Missing or empty target kind and API version default to Deployment and apps/v1; a Rollout target without an API version is reported as Rollout (apps)/worker. HPA hpaSummary.metrics matches current values to targets by source and selector. Targets without a current value have status: missing; unmatched observations have status: status_only. Core Namespace metric names use the HPA’s namespace. Resource context exposes routing direction through exposedBy (upstream resources routing to this resource, including entrypoints reached through a selecting Service) and backends (resources this routes to). The compatibility field exposes is their union and does not imply a direction. Pod containment shortcuts on Deployments and Nodes are excluded from routing backends. All three fields carry exact group/kind/namespace/name references and apply the caller’s reference permissions independently. Monitoring uses monitors and monitorTargets; policy selections use protects, while non-enforcing staged policies use stagedPolicies rather than selectedBy. Cluster API Clusters expose their healthChecks; a MachineHealthCheck’s Cluster is a dependency. Resource context’s uses includes declared PodSpec references from regular, init and ephemeral containers, projected volumes, image-pull credentials and volume-driver credentials. referencedBy on core ConfigMaps, Secrets, PVCs and ServiceAccounts includes both workload templates and actual Pods, including controlled Pods whose admitted spec may differ from the current template. ReplicaSet templates remain separate evidence. References are scoped and permission-filtered, with field paths and a bounded visible-only count; they describe declarations rather than proving a successful mount or active read. Capped results round-robin readable resource kinds, preserving examples of workload templates, revision templates and admitted Pods even with many replicas. If any consumer inventory fails, omitted marks referencedBy as unavailable. References from inventories that loaded remain available; an empty result with this omission does not establish that no consumers exist. Only named PodSpec declarations are included: implicit workload defaults and template-generated PVC names are not inferred. An absent omission does not by itself establish cache freshness or complete namespace coverage. Resource context’s ingressSummary.tlsSecrets lists named TLS Secrets in the Ingress’s namespace. A Secret’s referencedBy includes readable Ingresses with the path spec.tls[].secretName. Resource-view topology links only observed, visible TLS Secrets. Empty Secret names do not resolve controller defaults. When a Service’s selected Pods come from more than one workload, resource context’s serviceSummary.workloads lists each workload with its Pod and ready counts, and diagnose adds the same breakdown to the Pods hop as an info finding, for example “Selects 2 pods from 2 workloads: frontend (1), search (1)”. This is context only. Workloads the caller can’t read are withheld, and omitted says so. Radar raises a service_mixed_backends warning, never critical, and resource context adds the mixed_backends warning with the same sentence in serviceSummary.mixedBackends, only when the workloads that receive traffic don’t all serve the Service:
  • Different applications: they share no image, compared by name without registry, tag or digest. “Service X routes to N different applications (…)”.
  • A port some don’t declare: some declare the numeric targetPort while others declare other ports but not it. “Service X routes to N workloads; Y’s pods don’t declare port P”.
Deliberate setups stay quiet: stable and canary Deployments, blue/green cutovers, Argo Rollouts, versions of one app (a shared app label and a version label on every Pod), mesh sidecars and headless Services. A new tag or registry is the same image, and a workload that declares no ports is not evidence. In the MCP issues tool the warning’s correlated_changes show recent edits to the Service, such as a selector change.

Write Tools

Strimzi connector evidence

See Strimzi Kafka connector evidence for the Issues/API/MCP coverage, required read access, and operator-snapshot limitations.

Available Resources

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 is annotated readOnlyHint: false and destructiveHint: false, 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 subjects, including PersistentVolumes. 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 and no-consumer PVC findings additionally require adequate reference evidence. missingInputs lists only inputs whose absence left a subject unevaluated, such as configmap-references, secret-references or pvc-consumers; a gap that still lets a finding fire is stated in the finding. See audit evidence under partial access and storage review checks.
  • 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

Node placement context

For a core Kubernetes Node, get_resource includes resourceContext.referencedBy with cached, nonterminal Pods assigned through spec.nodeName. Controlled Pods and assigned Pending Pods are included; unassigned Pods, Pods on other nodes and completed Pods are excluded. References are permission-filtered per namespace, sorted deterministically and capped at 20. total counts only readable Pods; truncated marks the item cap. This is observed placement in Radar’s cache scope, not a cluster-wide workload count or a list of potential scheduling candidates. When Relationships already collected placement, context reuses that result. Unavailable Pod data, including a missing placement provider, records omitted: [{field: "referencedBy", reason: "unavailable"}], distinguishing a failed lookup from an observed empty result. ConfigMap and Secret reverse-reference context uses the same omission when a scanned workload or Pod source cannot be read. References from available sources remain visible. This projection scans selected built-in workload templates and controllerless Pods; absence of an omission does not establish complete consumer coverage or prove that a resource is unused. Storage audit uses its own consumer inventory and coverage rules.

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 its candidates are capped; both disclose truncation, and candidates are found from every difference, not only the shown ones. 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; such webhooks are listed as not evaluated. Fully described webhook candidates show their configuration’s creation time; those past the line cap are named only. 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, and the fact says which candidate sources it could not inspect. 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.

Pod/template differences without an issue

get_resource (default context) adds resourceContext.templateDivergence for a Pod, ReplicaSet, Deployment or Job whose live Pods differ from the current template of their own ReplicaSet or Job, so a rolling update is not a difference; diagnose adds it for a Pod or Deployment. It is context, not proof: a summary, the differences (for a workload, podsCompared, podsDiffering and an examplePod), and sources, the same candidate list as the issue fact. StatefulSet and DaemonSet Pods are not compared.
  • Reported: resource requests and limits, images, node selector, required affinity, removed or changed tolerations, a template-set priority class, and the runtime class.
  • Quiet: what admission adds routinely (injected containers, volumes and env, added tolerations, extended resources, limit-to-request copies, a defaulted priority class, a digest pinned onto the template’s tag, Istio’s auto image), and resource values while a resize is in progress.
With context=none, get_resource returns a one-line divergence note in top-level warnings, outside the returned object.

Cluster-scoped admission changes in a namespace view

Secret data updates enter the default get_changes and issues recent_changes feed when referenced by a workload template or current Pod spec at query time; kind=Secret lists all updates. consumed_by means referenced by, and running_previous_value names Pods likely retaining earlier env values after an observed write; its absence means not established, rather than no stale Pods. Both lists name only workloads and Pods the caller may read: consumed_by_withheld and running_previous_value.withheld count the rest as {count, reason: "user_forbidden"}, so a filtered list is not read as complete. running_previous_value appears only for callers who may read Pods in the namespace, and its pods count covers only those Pods. Repeated Secret updates collapse into the newest row with merged changed keys, updates_in_window and first_update_at; rows carrying stale evidence for different keys stay separate. kind=Secret and name= queries retain every update. A namespace get_changes (no kind or name) adds clusterScopedChanges; a single-namespace issues answer adds cluster_scoped_changes when it has rows or a coverage gap (notes that only say Radar started inside the window are left out there). It lists MutatingWebhookConfiguration and ValidatingWebhookConfiguration changes in the window, capped with truncation disclosed, each with applicability and applies_because:
  • applies: a changed webhook’s current selectors and rules reach the namespace.
  • may_apply: Radar cannot establish that, for example because of unevaluated object selectors or CEL, rules of unknown scope, unreadable labels, or a deleted or re-scoped webhook.
An empty list is a complete negative only without error, incomplete or truncated. Webhooks unchanged in the window are not listed; the Pod/template candidates above cover them. List order does not indicate likelihood.