The thesis
Correlation belongs in the server, not the context window
Every other Kubernetes MCP server is kubectl with a JSON schema stapled on:
get_pods, describe_pod, get_logs. That pushes the
correlation onto the model across ten round-trips, each dumping kilobytes of
managedFields into the window. By pod four the context is noise, and a model
reasoning from noise guesses.
Real output
What a diagnosis actually looks like
Generated from real output rather than retyped — this panel is produced from the bytes argus diagnose actually printed, confidence drop included, because that cluster had no metrics API.
The difference
Resource-shaped vs question-shaped
| Typical k8s MCP server | argus | |
|---|---|---|
| Tool surface | 40–100+, mirroring kubectl | 6, one per SRE question |
| Correlation | the model does it, over many calls | server-side, in one call |
| Output | raw objects to interpret | ranked findings, evidence mandatory |
| Context per pod | 2,000+ tokens | under 400, enforced by test |
| Repeated events | one line each | deduplicated, blast radius kept |
| Confidence | absent, or implied certainty | stated, and lowered on partial data |
| Writes | usually available, sometimes gated | none — no mutating call site exists |
Measured, not estimated
Context discipline
From one real production Deployment with 93 environment variables and 11 ReplicaSets.
The correlation engine
Eleven detectors, twenty-seven named causes
Each is a pure function over a snapshot, so each is replayable against a committed fixture. Every fixture asserts its detector fires and that no others do — the negative half is the half that matters, because a tool that reports three wrong causes during an incident costs more time than it saves.
crashloop.*
Reads the exit code instead of restating CrashLoopBackOff. A broken entrypoint, a container that exits zero and should be a Job, SIGSEGV, an abort, a liveness-probe SIGTERM — each gets its own remedy.
lastState reason + exit code + the runtime's own erroroomkill.limit-too-low
The kernel killed it for exceeding its own limit — with a suggested new limit when usage data supports one.
lastState.reason=OOMKilled + limit + peak usagerollout.bad-template
The new revision is failing and the previous one was healthy. Reports the fields that changed between them.
RS health pair + semantic template diffimage.*
Distinguishes a typo'd tag from a missing pull secret, refused credentials, and a registry rate limit — four causes, one symptom. A fifth is not a pull at all: an unparseable reference is rejected before any registry is contacted, so it sends you to the manifest rather than the registry.
waiting.reason + the runtime's own error textendpoints.*
Tells a selector typo (matches nothing) apart from a readiness failure (matches pods, none ready). Invisible to kubectl get.
probe.startup-too-short
The slow boot mistaken for a crash loop. Liveness and readiness probes do not run until a startup probe passes — so while started is false, nothing else can be doing the killing. Suppresses the crash-loop finding, whose advice to read the logs is a dead end when the logs show a healthy boot.
probe.readiness-misconfigured
Running, alive, not crashing — and still never ready. Compares the probe's real deadline against observed uptime.
initialDelay + period×failureThresholdconfig.missing-*
A ConfigMap or Secret the pod references does not exist — or exists without the key it asks for, which kubectl get shows as perfectly healthy. Carries no log hint, deliberately: the container was never created, so the stream is empty and always will be.
hpa.cannot-scale
The autoscaler that is not autoscaling. Every object reads healthy — the HPA exists, the Deployment is Ready, the replica count looks deliberate — but no replica count is being computed, so the next traffic spike is an outage with no cause in any log.
HPA ScalingActive=False + its own reasonpdb.blocks-disruption
The cause of the incident that does not look like one: a drain or upgrade that hangs for hours. Gated on the workload being healthy — every broken one also allows zero disruptions, and that is the budget working correctly.
disruptionsAllowed vs currentHealthynode.unhealthy-host
Widens scope. If the node is the cause, it says so and suppresses the per-workload symptoms rather than reporting one cause three times.
node conditions + affected pod setSafety
Read-only, and not on the honour system
The usual advice is “enforce read-only through RBAC, not in code.” That is correct for an in-cluster deployment and false for how you will actually run this — from your own kubeconfig, where RBAC grants you everything. So the binary holds the line instead.
No mutating call site
A test walks the entire source AST and fails the build on any call to Create, Update, Patch, Delete, Apply or Evict — with a companion test proving the walk can actually fail.
Prompt injection can mislead, never act
Log and event text is user-controlled and reaches model context. argus frames it as untrusted data — but the real mitigation is structural: there is no mutation path to reach.
Secrets never leave
Secret objects are never read. Environment variable values are never projected — only key names, which is all a detector needs. Verified by test.
Self-update fails closed
The one thing argus writes is its own binary. The published SHA-256 must match or the update is abandoned, HTTPS is required across redirects, and the swap is a rename — a failure leaves the working binary intact.
It cannot flood your control plane
A hard apiserver call budget lives in an http.RoundTripper, counting every request and retry. A diagnostic tool is run during an incident. Cluster-wide triage costs 10 calls whatever the cluster size — looping the per-workload diagnosis would have been ~1,700.
Install
Install, connect, run
Or drive it straight from a terminal, no MCP involved:
And to update it later — checksum-verified, atomic, and it will not clobber a build you made from a clone:
Roadmap
Honest about what exists
- shippingdiagnose_workload — the flagship: a fuzzy workload name in, ranked causes out, every one citing the evidence it used
- shippingcluster_triage — what is broken right now, grouped by controller. 130 workloads in 10 apiserver calls, constant in cluster size, because it inverts the data flow instead of looping the diagnosis
- shippingget_workload_logs — picks the failing container over sidecars, reads the
previousinstance on a crashloop, collapses repeated lines, redacts credentials, budgets by tokens. Took a real crashloop from 206 lines to 7 - shippingEleven detectors over 27 named causes · the broken-fixture suite, plus a nightly gate that runs them against live clusters on two Kubernetes versions · projection & event dedup · fuzzy resolution across Deployment/StatefulSet/DaemonSet/Rollout ·
captureas the fixture generator · read-only enforced as a build gate · checksum-verified self-update - shippingexplain_pending — why a pod will not schedule, with the arithmetic per node: what it asked for, what is free, and how much other pods already reserve. Names the constraints it does not evaluate
- shippingtrace_service_path — the Service is up, the pods are Ready, and traffic still 503s. Walks Ingress → selector → targetPort → endpoints → readiness and names the first hop that breaks. Catches the silent one: a
targetPortnaming a port no container declares leaves the EndpointSlice with no port at all, so every connection is refused whilekubectl getshows a healthy system - not buildinginformer cache — argued against rather than deferred. It buys nothing for the CLI, and for the server it trades the call budget for a staleness window: a 30-second-old pod state mid-rollout is worse than 13 live calls, because the answer is confidently wrong rather than slow
- v0.3GKE Cloud Logging fallback for evicted pods · compare_environments · check_reachability · in-cluster deployment with Workload Identity
- maybe neverMutations against a cluster. That is where the liability is; read-only diagnosis is where nearly all the value is. The one exception already shipped is
argus update, which replaces argus's own binary against a published checksum and grants no cluster write access.