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 rawkubectl access has problems:
- Token waste -
kubectl get pod -o yamlreturns 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
- 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:/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
--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 theinitialize 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
Usewait_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:/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:
--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:~/.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:
GitHub Copilot CLI
Run this command:~/.copilot/mcp-config.json.
OpenCode
Add to~/.config/opencode/opencode.json (all projects) or opencode.json in your project root:
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:~/.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. Settype 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 reachlocalhost. 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 Zedsettings.json:
Gemini CLI
For Gemini Code Assist Standard and Enterprise users. Run this command:~/.gemini/settings.json:
MCP Registry / Docker
Radar is listed on the official MCP registry asio.github.skyhook-io/radar, packaged as the ghcr.io/skyhook-io/radar Docker image. The registry-suggested invocation is:
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, orkubelogin- 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
0600kubeconfig owned by your user is unreadable by the container’snonroot(uid 65532) user.
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.
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.
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:
- Reliability priority - evidenced OOM signals, limit conflicts, and throttled reductions first; then
increase, ordinaryreduction, routinereview(burstiness-only, HPA-only, or scaled-to-zero),need_data, andin_range. Retained risk history does not promote scaled-to-zero workloads. Priority is computed before response limits; classification/filter values remain unchanged. 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.- Namespace, kind and name, so repeated scans return a stable order.
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=workloadreturns every row. The caller named one workload; a handful of rows is the whole answer.- A
scaledToZeroworkload returns every row. Unless its evidence is missing (need_data), it classifies asreview- 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 incoverage.daemonSetsWithoutNodesand name it inskippedDaemonSets(first 50,skippedDaemonSetsTruncatedbeyond that); a node pool scaled to zero still has history for it, whichscope=workloadreads as ascaledToZeroworkload. A zero the controller has not yet re-observed (a new DaemonSet, or a changed selector) is scanned instead, as ascaledToZeroworkload. A namespace holding only such DaemonSets is never reported innamespacesWithoutWorkloads, and a scope holding nothing else answersreason: only_daemonsets_without_nodesrather thanno_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
queryErrornames the warning code carrying the real error, andomitted.queryErrorcounts 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 itsfit.fitis settled from the request alone, so the classic OOM shape - request fine, limit too low - classifies asbalanced. Filtering onfitwould 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
RawdataPoints 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
Everylist_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:
summaryfor a list,errorfor 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:
-
summaryis 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), otherwisePartial:, with each part it didn’t read called unknown, why, and who can widen it. -
scopeis what the answer is about:clusterfor a cluster-scoped kind,all_namespaces, or a namespace set. A namespace you name stays the scope even when Radar can’t read it. -
narrowedBysays why an unscoped request covers less than every namespace:user_access(the namespaces your access allows),namespace_pin(Radar’s--namespace-scope), ornamespace_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. -
statusis relative toscope:completemeans Radar’s watch of every namespace in it has loaded, so missing rows are absent from what Radar observes within that scope;partialmeansgapsnames what it did not read. A complete list withnarrowedByis still not the cluster.statuskeeps this meaning: watch freshness or paging, if added, get fields of their own. -
readis what Radar read withinscope.readand the gaps don’t overlap and together make upscope, so on a complete answerreadequalsscope. -
gaps[]are the reasons the answer isn’t complete. Each has acode, theactionthat 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 itsnamespaces: 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 nonamespaces. A namespace is in at most one gap.
{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’sstructuredContent 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.
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, seediagnose 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:
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”.
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/mcpendpoint, the exception isdiagnose(in_cluster=true)on network entry resources, which creates up to five self-deleting probe pods; that full diagnose tool is annotatedreadOnlyHint: falseanddestructiveHint: false, so clients that prompt only ondestructiveHintwill not prompt for it. On/mcp-readonly, diagnose omits and rejectsin_clusterand carriesreadOnlyHint. Write tools (restart, scale, rollback, sync, apply, cordon/drain) are RBAC-enforced and annotated withdestructiveHintso AI clients can prompt for confirmation. Some are genuinely destructive -apply_resource force=truecan take field ownership from Helm/Flux,manage_node drainevicts pods, androllback/terminateoverwrite 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
kubectlcan 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
- Local binary: the cache uses your kubeconfig identity, so MCP can only see what
- 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=trueoption bypasses local audit settings, never authorization. Unused ConfigMap/Secret and no-consumer PVC findings additionally require adequate reference evidence.missingInputslists only inputs whose absence left a subject unevaluated, such asconfigmap-references,secret-referencesorpvc-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/queryand/prometheus/clusterroutes - require a SubjectAccessReview forlist podsacross 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
.dataand.stringDataare 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, andsecretKey - 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 neutralpod_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
autoimage), and resource values while a resize is in progress.
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 defaultget_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.
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.