argus

One question in, one ranked diagnosis out.
Built for real 3AM incidents.
argus is a Kubernetes incident-diagnosis MCP server. You ask "why is checkout-api broken?" and
it answers with a ranked list of causes, each one citing the evidence it used.
Named for Argus Panoptes, the hundred-eyed watchman who never slept because only some of his eyes
closed at a time β it watches twelve resource kinds at once so you don't have to correlate them
by hand.
π See it in action β backendarchitect.github.io/argus
Status: pre-release. Six tools work end to end β ten detectors over 26 named causes,
ranked findings, mandatory evidence β verified against fixtures captured from real clusters and
against live clusters on two Kubernetes versions nightly. See Roadmap for what is
next, and for the one v0.2 item deliberately left unbuilt.
Why this exists
Every Kubernetes MCP server available today is kubectl with a JSON schema stapled on:
get_pods, describe_pod, get_logs. That is worse than nothing, because it pushes the
correlation work onto the model across ten round-trips, each one dumping 8KB of managedFields
into the context window. By pod four the context is full of YAML noise and the model starts
guessing.
argus takes the opposite position: the unit of work is a question, not a resource.
|
Resource-shaped servers |
argus |
| Tool surface |
40β100+ tools mirroring kubectl |
6 tools, one per SRE question |
| Correlation |
the model does it, over many calls |
done server-side, in one call |
| Output |
raw objects |
ranked findings with mandatory evidence |
| Context per pod |
2,000+ tokens |
under 400, enforced by test |
| Repeated events |
one line each |
deduplicated, with the blast radius kept |
| Writes |
usually available, sometimes gated |
none β no mutating call site exists |
What it does today
# Diagnose a workload from the terminal β the same pipeline the MCP tool uses.
argus diagnose checkout-api -n prod
# Run as an MCP server over stdio.
argus serve
# What is broken right now, across every namespace.
argus triage
# Why won't it schedule? Per-node arithmetic, not just "Insufficient memory".
argus pending checkout-api -n prod
# The Service is up and the pods are ready, so why does traffic not arrive?
argus trace checkout-api -n prod
# Logs for the failing container β previous instance if it is crashlooping.
argus logs checkout-api -n prod
# Collect a raw diagnosis snapshot as YAML (this is also the fixture generator).
argus capture deploy/checkout-api -n prod
A real diagnosis, captured verbatim:
DIAGNOSIS Deployment argus-broken/oom-victim
replicas 0/1 ready, 1 updated, 0 available
findings 1 (1 critical)
1. [critical Β· confidence 71%] oomkill.limit-too-low
Container "app" in oom-victim-7cb68bdb99 is being OOM-killed by its own memory limit
The kernel killed container "app" for exceeding its memory limit of 32Mi. It is OOMKilled
and not yet ready, so it has not recovered from the kill. Current usage is unavailable (the
metrics API did not answer), so there is no measured basis for a new limit; size it from the
workload's known working set.
evidence:
Β· pod/oom-victim-7cb68bdb99-4pmxn (pod.lastState):
container "app" last terminated with reason=OOMKilled, exitCode=137, 76s ago
Β· pod/oom-victim-7cb68bdb99-4pmxn (pod.spec):
container "app" memory limit is 32Mi
Β· pod/oom-victim-7cb68bdb99-4pmxn (pod.status):
container "app" has restarted 4 times; current state is OOMKilled
next: get_workload_logs(previous=true workload=oom-victim-7cb68bdb99) β the current container
restarted after the kill, so the output leading up to it is in the previous instance
incomplete β these lookups failed, and any finding relying on them has had its confidence reduced:
Β· metrics: the server could not find the requested resource (get pods.metrics.k8s.io)
(13 apiserver calls against kind-argus-test)
Note the confidence drop and the stated gap. That cluster had no metrics API, and saying so beats
implying memory looks fine.
Under the hood, one pass resolves a fuzzy name, fans out concurrently across the workload, its ReplicaSets, pods,
events, metrics, Services, endpoints, nodes, HPA and PDB, then projects all of it down to an
allowlisted, deduplicated snapshot. On a real production Deployment that is 13 apiserver calls
and ~3,300 tokens β the same data unprojected runs to roughly 10,400.
The snapshot is plain YAML by design. That single constraint is what makes the test suite
possible: capture writes fixtures, tests replay them with no cluster at all, and production runs
the identical code path.
argus capture β gather β Snapshot β yaml (writes testdata/snapshots/*.yaml)
go test β yaml β Snapshot β detect β assert (no cluster, sub-second)
argus serve β gather β Snapshot β detect β rank (production)
Read-only, and not on the honour system
argus never writes to your cluster. That is not a convention or a flag β there is no mutating call
site in the binary, and a test walks the entire source tree's AST and fails the build on any call
to Create, Update, Patch, Delete, Apply, Evict or friends.
This matters more than it sounds. The usual advice is "enforce read-only at the RBAC layer, not in
your code" β correct for an in-cluster deployment, but false for how you will actually run this,
which is from your own kubeconfig, where RBAC grants you everything. RBAC will not hold that line,
so the binary holds it instead.
Two more limits are built into the client rather than bolted on:
- A call budget. A hard cap on apiserver requests per invocation, enforced in an
http.RoundTripper so it counts every request and every retry. A diagnostic tool that DoSes the
control plane during an incident is a career-limiting artifact.
- A deadline with graceful degradation. A slow cluster produces a partial snapshot that records
what was missed, not a hung MCP session β and detectors reading partial data must lower their own
confidence rather than quietly reasoning from absence.
Full threat model, including prompt injection via log and event content: SECURITY.md.
Install
Requires Go 1.26+ and a working kubeconfig.
| Method |
Command |
| Go |
go install github.com/backendArchitect/argus@latest |
| Docker |
see below β the image runs as nonroot, so the kubeconfig needs mounting deliberately |
| Source |
git clone https://github.com/backendArchitect/argus && cd argus && go install . |
Prebuilt binaries (Linux/macOS/Windows Β· amd64 & arm64) are on the
Releases page.
Updating
argus update # replace this binary with the latest release
argus update -check # just report whether one is available
The download is verified against the SHA-256 published beside it and the swap is
atomic, so a failed update leaves the working binary untouched. It refuses to
overwrite a binary you built from a clone β use go install . for that, or
argus update -force if you really mean it.
The container image is distroless and runs as a nonroot uid, so mount the
kubeconfig file and name it explicitly β mounting ~/.kube as a directory
lands on a path the container user cannot read:
docker run --rm -u "$(id -u):$(id -g)" \
-v ~/.kube/config:/kube/config:ro \
ghcr.io/backendarchitect/argus \
diagnose checkout-api -n prod --kubeconfig /kube/config
Note the apiserver must be reachable from inside the container: this works for a
real cluster, but not for a local kind cluster listening on the host's
127.0.0.1 (add --network host for that).
Connect it to an AI editor:
claude mcp add argus -- argus serve
Roadmap
Working now
diagnose_workload β the flagship: one call in, a ranked diagnosis out
get_workload_logs β picks the failing pod and container over sidecars, reads the
previous instance on a crashloop, collapses repeated lines, redacts credentials, and budgets
by tokens. Took a real crashloop from 206 lines to 7
cluster_triage β what is broken right now, grouped by controller and with infrastructure
findings collapsed. Constant cost: 130 workloads in 10 apiserver calls, not the ~1,700 a loop
over diagnose_workload would have taken
explain_pending β why a pod will not schedule, with per-node arithmetic: what it asked
for, what each node has free, and how much is already reserved by other pods. Names the
constraints it does not evaluate rather than implying completeness
trace_service_path β the Service is up, the pods are ready, and traffic still does not
arrive. Walks Ingress rule β selector β targetPort β endpoint addresses β readiness and reports
the first hop that breaks, since everything past it is unreachable rather than unhealthy.
Catches the silent one: a targetPort naming a containerPort no container declares leaves the
EndpointSlice with no port at all, so every connection is refused while kubectl get svc,pods,ingress shows nothing wrong
argus update β verified, atomic self-update
- Ten detectors, 26 finding IDs β crash loop (which distinguishes a container the runtime cannot start from
one that starts and exits, and reads the exit code: wrong entrypoint, segfault, abort, exits-zero,
SIGTERM) Β· OOM limit too low Β· bad rollout Β· image pull (four distinct causes) Β·
readiness misconfigured Β· endpoint gap (selector typo vs readiness failure) Β·
unresolvable config reference (a missing ConfigMap, a missing Secret, or a key absent from one
that exists β read as env or mounted as a volume, which fail in different places) Β·
unparseable image reference Β· an autoscaler that cannot compute a replica count Β· a disruption
budget that blocks every eviction while healthy Β· node-caused, which widens scope and
suppresses the per-workload symptoms it explains
- The broken-fixture suite β each fixture asserts its detector fires and no others do,
with a healthy control that must produce nothing β plus a nightly end-to-end gate that runs the
detectors against live clusters on two Kubernetes versions, because fixtures cannot catch
upstream changing what it reports
- MCP server over stdio, with schemas derived from Go types
- Fuzzy workload resolution (Deployment / StatefulSet / DaemonSet / Argo Rollout), returning
candidates on ambiguity rather than guessing
- Concurrent gather with a call budget, deadline, and degradation tracking
- Projection layer: allowlisted fields, per-pod token budget, event deduplication
argus capture β snapshot to YAML, doubling as the fixture generator
- Read-only enforcement, verified by an AST test
v0.1 is complete.
v0.2 is complete except the informer cache, which is deliberately unbuilt β see below.
v0.3 β GKE integrations (Cloud Logging fallback for dead pods, Autopilot, Managed Prometheus),
compare_environments, check_reachability, in-cluster deployment with Workload Identity.
Folded in rather than built. diff_rollout was planned as its own tool; the semantic
template diff it would have provided lives inside the rollout.bad-template detector instead, so
you get it as part of a diagnosis rather than as a separate call. Noted here because it vanished
from the plan without explanation otherwise.
Argued against rather than deferred. The informer cache was on the v0.2 list to cut apiserver
calls on repeat queries. It buys nothing for the CLI, which is short-lived by construction, and for
serve it trades the call budget for a staleness window β a cache returning a 30-second-old pod
state mid-rollout is worse than 13 live calls, because the answer is confidently wrong rather than
slow. The measured cost is not a problem: 13 calls for a diagnosis, 10 for a 130-workload triage,
against a budget of 60. If it is ever built, the defensible shape is watches on Events and Pods
only, with cache age recorded in notes so a detector can dock its confidence.
Later, maybe never β mutations against a cluster. That is where the liability is; read-only
diagnosis is where nearly all the value is. Note the one exception already shipped: argus update
replaces argus's own binary, verified against a published checksum. It gives argus no ability to
write to a cluster.
Learn more
In one line: argus is a read-only Kubernetes diagnosis MCP server that answers SRE questions
with ranked, evidence-backed findings instead of handing a model a pile of YAML.
License
MIT