Skip to main content

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 logs and kubectl 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). Needs uv, kubectl and an MCP client. Remove it with claude 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 uvx downloads it automatically if you don't have it. (Installing with pip on an older Python fails with No matching distribution found for kubetriage, which means "wrong Python", not "no such package".) The installer needs curl or wget; after installing uv, open a new shell so uvx is on your PATH.
  • kubectl on your PATH. kubetriage reads clusters through it, so whatever already works with kubectl --context <name> works here, including cloud credential plugins.
  • A kubeconfig context for the cluster you want to look at. get_runbook and check_command need no cluster and no kubectl.

What it can and cannot do to your cluster

  • It never changes cluster state. Every cluster read is kubectl get or kubectl logs; list_contexts uses kubectl config view, which reads the local file and contacts no cluster. There is no write code path to switch on.
  • check_command is advisory: a linter, not a guardrail. It classifies a kubectl command as safe, needs-approval or blocked and 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-crash offers kubectl rollout undo, which helps only if a recent rollout caused the crash, and fails on a workload with no earlier revision (kubectl rollout history shows 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_workload returns 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 kubectl works with them, through their usual credential plugins; there is no direct cloud authentication.
  • scan_cluster is shallow by design: it reads Warning events to classify, but returns no logs, events or runbook per workload. Follow up on a row with diagnose_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)

Source distribution for kubetriage 0.1.0
File Size Uploaded
kubetriage-0.1.0.tar.gz 71.2 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for kubetriage 0.1.0
File Interpreter ABI Platform
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}

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page