kubetriage
Something in your Kubernetes cluster is broken and your coding agent is about to run a dozen
kubectl commands to find out what. kubetriage is an MCP server that answers in one call:
which workloads are unhealthy, the failure mode (OOM, bad image, crash loop, failing probe,
unschedulable, …), the evidence behind it, and a runbook for the fault with the commands
rendered for that workload.
- Read-only. It runs
kubectl get,kubectl logsandkubectl config view, nothing else, and never changes cluster state. - Install: make a kubeconfig holding only the clusters it may read (below),
then
claude mcp add kubetriage -- uvx kubetriage --kubeconfig /ABS/PATH/kubeconfig(other clients below). Needsuv,kubectland an MCP client. Remove it withclaude mcp remove kubetriage.
Works with any MCP client that runs a local stdio server: Claude Code, Codex CLI, Cursor, Cline, Windsurf, Zed.
Why not let the agent run kubectl itself? It can, and it keeps that access: kubetriage
replaces the exploring, not the fixing. Classifying a failure from exit codes, termination
reasons, conditions and events is deterministic work that a model otherwise redoes, at the
cost of a dozen calls and their output, every time. Here it is one call, and the same cluster
state always gets the same answer.
Requirements
- uv 0.9.0 or newer. kubetriage
needs Python 3.14+, and
uvxdownloads it automatically if you don't have it. (Installing withpipon an older Python fails withNo matching distribution found for kubetriage, which means "wrong Python", not "no such package".) The installer needscurlorwget; after installing uv, open a new shell souvxis on yourPATH. kubectlon yourPATH. kubetriage reads clusters through it, so whatever already works withkubectl --context <name>works here, including cloud credential plugins.- A kubeconfig context for the cluster you want to look at.
get_runbookandcheck_commandneed no cluster and no kubectl.
What it can and cannot do to your cluster
- It never changes cluster state. Every cluster read is
kubectl getorkubectl logs;list_contextsuseskubectl config view, which reads the local file and contacts no cluster. There is no write code path to switch on. check_commandis advisory: a linter, not a guardrail. It classifies a kubectl command assafe,needs-approvalorblockedand says why. kubetriage never runs the command and cannot stop your agent from running it. It helps only if the agent asks and listens. Keep your client's own approval settings for shell commands.- The commands in a runbook are advice. Some of them change cluster state (a rollback, a
new memory limit). kubetriage never runs them; review them before your agent does.
Runbooks are written per fault, not per cause:
app-crashofferskubectl rollout undo, which helps only if a recent rollout caused the crash, and fails on a workload with no earlier revision (kubectl rollout historyshows how many there are). Read the evidence (logs, events) before following one. - Container logs and Warning events go to your agent, and from there to whichever model
it uses.
diagnose_workloadreturns the failing containers' last log lines as the cluster reports them. Your kubeconfig's credentials never appear in a response.
Pin it to the clusters you mean: --kubeconfig
kubetriage has no setting that restricts it to one cluster. The agent chooses a context on
every call, and a call that names none reads your kubeconfig's current context. Without
--kubeconfig, the server can read every context in your default kubeconfig, production
included. Pointing it at a kubeconfig that contains only the clusters you intend is the only
way to limit what it can reach. Find the context name (kubectl config get-contexts), then:
mkdir -p ~/.kube
(umask 077; kubectl config view --raw --minify --flatten --context <your-context> > ~/.kube/kubetriage.yaml)
The umask keeps the file, which holds credentials, readable only by you. --raw stops kubectl
replacing the certificate data with DATA+OMITTED, and --flatten copies in any certificate
files the context points to, so the new file stands alone. A plain kubectl config view --minify writes DATA+OMITTED, and every cluster read through that file fails (kubetriage
reports kubeconfig-invalid and says why). The config blocks below
take an absolute path; none of these clients documents expanding ~ inside args.
Nothing else goes in the MCP config: no tokens, no environment variables. kubetriage
authenticates exactly the way kubectl does with that file.
Add it to your agent
Replace /ABS/PATH/kubeconfig with your pinned kubeconfig, e.g. /home/you/.kube/kubetriage.yaml.
Claude Code
claude mcp add kubetriage -- uvx kubetriage --kubeconfig /ABS/PATH/kubeconfig
The -- keeps --kubeconfig from being read as a claude option. This registers it for the
current project only (--scope local, the default); add --scope user to have it everywhere.
Check with claude mcp list. Remove: claude mcp remove kubetriage (add --scope user if
you registered it there). To share it with a team, put the same server in the project's
.mcp.json:
{
"mcpServers": {
"kubetriage": {
"type": "stdio",
"command": "uvx",
"args": ["kubetriage", "--kubeconfig", "/ABS/PATH/kubeconfig"]
}
}
}
Codex CLI
codex mcp add kubetriage -- uvx kubetriage --kubeconfig /ABS/PATH/kubeconfig
or in ~/.codex/config.toml:
[mcp_servers.kubetriage]
command = "uvx"
args = ["kubetriage", "--kubeconfig", "/ABS/PATH/kubeconfig"]
# The first start may download Python 3.14; Codex's default startup timeout is 10 s.
startup_timeout_sec = 60
Remove: codex mcp remove kubetriage.
Cursor: ~/.cursor/mcp.json (all projects) or .cursor/mcp.json (one project):
{
"mcpServers": {
"kubetriage": {
"type": "stdio",
"command": "uvx",
"args": ["kubetriage", "--kubeconfig", "/ABS/PATH/kubeconfig"]
}
}
}
Remove: delete the entry.
Cline: in the Cline panel, open MCP Servers → Configure → Configure MCP
Servers, which opens cline_mcp_settings.json, and add:
{
"mcpServers": {
"kubetriage": {
"command": "uvx",
"args": ["kubetriage", "--kubeconfig", "/ABS/PATH/kubeconfig"],
"disabled": false,
"autoApprove": []
}
}
}
Leave autoApprove empty. Remove: delete the entry (or set "disabled": true).
Windsurf (renamed Devin Desktop in June 2026): open the MCP config file from the Cascade panel's … menu → MCPs → open the config file, rather than guessing its path (it has moved between releases), and add:
{
"mcpServers": {
"kubetriage": {
"command": "uvx",
"args": ["kubetriage", "--kubeconfig", "/ABS/PATH/kubeconfig"]
}
}
}
Remove: delete the entry.
Zed: in settings.json:
{
"context_servers": {
"kubetriage": {
"command": "uvx",
"args": ["kubetriage", "--kubeconfig", "/ABS/PATH/kubeconfig"],
"env": {}
}
}
}
Remove: delete the entry.
If a desktop app cannot find uvx: apps started from a dock or launcher may not see your
shell's PATH. Use the absolute path from which uvx (often ~/.local/bin/uvx, written out
in full) as "command". The same applies to kubectl: the server finds it on the PATH the
client gives it.
Remove it completely: remove the client entry as shown above, delete the pinned kubeconfig
you made (it holds credentials), and run uv cache clean kubetriage. Its dependencies (about
40 MB) stay in uv's shared cache, which other tools reuse; uv cache clean with no argument
empties that cache entirely. If uvx downloaded Python 3.14 for it and nothing else uses
that, uv python uninstall 3.14 removes it. kubetriage writes no files of its own; the
kubectl it runs keeps its usual API discovery cache under ~/.kube/cache.
The tools
| Tool | What it does | Needs a cluster |
|---|---|---|
scan_cluster |
Every unhealthy Deployment, StatefulSet and DaemonSet across the cluster (or one namespace), each with its failure mode, plus nodes that are NotReady, under memory/disk/PID pressure, or cordoned. Most severe first, capped to stay small. Says exactly what RBAC kept it from seeing, and never reports a scan it could not run as "0 unhealthy". |
yes |
diagnose_workload |
Deep verdict on one Deployment, StatefulSet or DaemonSet, or the workload behind a pod name: healthy or not, the failure mode (oom, image-pull, app-crash, config-error, run-error, probe-fail, unschedulable, volume-mount, init-crash, create-failed, unknown, or pending while pods start), why and how confident, container states, Warning events, the failing containers' logs, and the matching runbook rendered for that workload. |
yes |
get_runbook |
The remediation runbook for a fault (the failure modes above, plus cpu, memory-pressure, disk, dependency, node): steps, commands, how to verify. An unrecognised key returns the unknown runbook, and fault in the response says which you got. Pass ns, kind, workload, selector (the workload's label selector), container, and for oom the container's current memory_limit (the runbook proposes double it), to render the commands for your target; anything omitted stays a visible <placeholder>. diagnose_workload fills all of these in itself. |
no |
check_command |
Classifies a kubectl command as safe, needs-approval or blocked, with reasons. Advisory only (see above). |
no |
list_contexts |
The kubeconfig's contexts and which one is current. Reads the local file only (through kubectl config view) and runs no credential plugin. |
no (needs kubectl) |
Every tool that reads a cluster takes an optional context and names the context and
cluster it actually read in its response. Tools only: no MCP resources or prompts.
What a call looks like
A Deployment crash-looping on a kind cluster. The agent calls
diagnose_workload({"name": "checkout", "namespace": "shop"}) and gets back (trimmed: …
marks removed fields, the second pod and pod UIDs):
{
"schema_version": 2,
"context": "kind-dev",
"namespace": "shop",
"target": {"requested": "checkout", "kind": "Deployment", "name": "checkout", "via_pod": null},
"healthy": false,
"fault": "app-crash",
"confidence": "high",
"reason": "the process exits with code 1 (Error) (pod checkout-7f468b7569-5x76p, 0/2 available)",
"replicas": {"desired": 2, "ready": 0, "available": 0},
"pods": [{"name": "checkout-7f468b7569-gpr26", "mode": "app-crash", "container": "app",
"containers": [{"name": "app", "ready": false, "restarts": 2, "state": "waiting",
"reason": "CrashLoopBackOff", "last_reason": "Error",
"last_exit_code": 1, …}], …}, …],
"events": [{"reason": "BackOff", "object": "Pod/checkout-7f468b7569-gpr26",
"message": "Back-off restarting failed container app in pod checkout-7f468b7569-gpr26_shop(…)",
"count": 3, …}],
"logs": [{"pod": "checkout-7f468b7569-gpr26", "container": "app", "previous": true,
"lines": ["checkout 2.4.1 starting", "FATAL: DATABASE_URL is not set"]}],
"unseen": [],
"runbook": {
"fault": "app-crash",
"title": "CrashLoopBackOff — container exits non-zero on start",
"commands": ["kubectl logs -l app.kubernetes.io/name=checkout -n shop --previous --tail=100",
"kubectl rollout history deployment/checkout -n shop",
"kubectl rollout undo deployment/checkout -n shop"],
"verify": ["kubectl rollout status deployment/checkout -n shop --timeout=60s"],
…
},
…
}
scan_cluster({}) on the same small cluster (7 workloads, one node), trimmed only of
cluster:
{
"schema_version": 2,
"context": "kind-dev",
"namespace": null,
"complete": true,
"counts": {"workloads": 7, "healthy": 5, "faulty": 2, "pending": 0},
"unhealthy": [
{"namespace": "shop", "kind": "Deployment", "name": "web", "fault": "image-pull",
"confidence": "high", "replicas": "0/1",
"reason": "ErrImagePull: the image cannot be pulled (pod web-6c7f8b46c8-569l6, 0/1 available)"},
{"namespace": "shop", "kind": "Deployment", "name": "checkout", "fault": "app-crash",
"confidence": "high", "replicas": "0/2",
"reason": "the process exits with code 1 (Error) (pod checkout-7f468b7569-5x76p, 0/2 available)"}
],
"omitted": 0,
"more": null,
"nodes": {"read": true, "total": 1, "unhealthy": [], "omitted": 0, "runbook": null},
"unseen": [],
"notes": []
}
Scope
- One cluster per call, chosen by kubeconfig context. Deployments, StatefulSets and DaemonSets are diagnosed. Jobs, CronJobs and pods with no controller are not (asking about one says so); their pods are only attributed to an owner.
- Clusters you reach through a local kubeconfig. EKS, AKS and GKE work when
kubectlworks with them, through their usual credential plugins; there is no direct cloud authentication. scan_clusteris shallow by design: it reads Warning events to classify, but returns no logs, events or runbook per workload. Follow up on a row withdiagnose_workload.
Versioning
Every response carries schema_version (currently 2). It changes only when a response
changes incompatibly, independently of the package version. If you parse responses, check
it, and pin the package (uvx kubetriage@0.1.0) if you need a fixed shape. kubetriage is
0.x: a minor release may change behaviour; the changelog
says what changed. kubetriage --version prints the version (to stderr, like --help,
because stdout is the MCP channel).
When the first call fails
Errors come back as JSON with a type and a message written for you, not raw kubectl output:
error.type |
Usually means |
|---|---|
kubectl-not-found |
kubectl is not on the PATH the client started the server with. Install it, or see "If a desktop app cannot find uvx" above, then restart the client. |
kubeconfig-invalid |
The --kubeconfig path is wrong or unreadable, kubectl rejects the file, or its certificates read DATA+OMITTED because it was saved without --raw. |
context-not-found |
The context the agent asked for (or the current one) is not in that kubeconfig. Ask the agent to call list_contexts. |
auth-failed |
The cluster rejected the credentials, or a credential plugin (aws, gke-gcloud-auth-plugin, kubelogin, …) is not installed, is not on the client's PATH, or needs you to log in again. Run kubectl --kubeconfig <file> get ns yourself to see the prompt. |
unreachable |
The API server did not answer, its hostname does not resolve, or its TLS certificate is not trusted. |
workload-not-found |
No Deployment, StatefulSet, DaemonSet or pod by that name in that namespace (or no such namespace). Check namespace; a call without one uses the context's namespace, else default. |
unsupported-kind |
The name belongs to a Job, CronJob or a pod with no controller, which this version does not diagnose. |
kubectl-timeout |
kubectl did not finish in time: a slow API server, or a credential plugin waiting for an interactive login it cannot get. Run kubectl --kubeconfig <file> get ns yourself once. |
forbidden |
RBAC refused every read the tool needed. Partial refusals are not errors: the result lists them in unseen. |
Contributing and security
See CONTRIBUTING.md for
the layering rules, the invariants and the changes to discuss in an issue first. A
check_command that calls a destructive command safe is a security bug: report it privately
as described in SECURITY.md.
Config verification
The Claude Code and Codex CLI blocks were tested by registering the server and calling it. The Cursor, Cline, Windsurf and Zed blocks were checked against each client's documentation on 2026-10-04 but not run.
Licence
Apache-2.0. See LICENSE.
Metadata
Release files for kubetriage 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| kubetriage-0.1.0.tar.gz | 71.2 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| kubetriage-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 162.2 kB
Release files / kubetriage-0.1.0.tar.gz
| Download URL | kubetriage-0.1.0.tar.gz |
|---|---|
| Size | 71.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
0dc0be5d48cc08044b535a212a754236af20e3ffb3a6f130dd0e8cd0221d0924
|
|
BLAKE2b-256 checksum How to use checksums |
57b419831467381fd9e82ce050abd674ae501388c4aade79175ed5387167b2b4
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.12.23 {"installer":{"name":"uv","version":"0.12.23","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / kubetriage-0.1.0-py3-none-any.whl
| Download URL | kubetriage-0.1.0-py3-none-any.whl |
|---|---|
| Size | 91.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
ffd5b790916989ed64947482cf2ccf2faa969a099a5606513d9b7cc2139706e7
|
|
BLAKE2b-256 checksum How to use checksums |
83007c4876785dac2ddb5ce49ef0daedd394c8c0d4011ae8a69dc94a3b31d03a
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.12.23 {"installer":{"name":"uv","version":"0.12.23","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|