Metadata-Version: 2.5
Name: kubetriage
Version: 0.1.0
Summary: Read-only Kubernetes diagnosis MCP server for CLI coding agents.
Project-URL: Homepage, https://github.com/Jenish2911/kubetriage
Project-URL: Repository, https://github.com/Jenish2911/kubetriage
Project-URL: Issues, https://github.com/Jenish2911/kubetriage/issues
Project-URL: Changelog, https://github.com/Jenish2911/kubetriage/blob/main/CHANGELOG.md
Author: Jenish Mistry
License-Expression: Apache-2.0
License-File: LICENSE
Keywords: diagnosis,kubectl,kubernetes,mcp,sre
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: System Administrators
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: System :: Systems Administration
Requires-Python: >=3.14
Requires-Dist: mcp<3,>=2.2
Description-Content-Type: text/markdown

# 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](#pin-it-to-the-clusters-you-mean---kubeconfig)),
  then `claude mcp add kubetriage -- uvx kubetriage --kubeconfig /ABS/PATH/kubeconfig`
  (other clients [below](#add-it-to-your-agent)). 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](https://docs.astral.sh/uv/getting-started/installation/) 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`](https://kubernetes.io/docs/tasks/tools/) 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:

```sh
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`.

<!-- Verification status of each block is listed under "Config verification" at the end. -->

**Claude Code**

```sh
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`:

```json
{
  "mcpServers": {
    "kubetriage": {
      "type": "stdio",
      "command": "uvx",
      "args": ["kubetriage", "--kubeconfig", "/ABS/PATH/kubeconfig"]
    }
  }
}
```

**Codex CLI**

```sh
codex mcp add kubetriage -- uvx kubetriage --kubeconfig /ABS/PATH/kubeconfig
```

or in `~/.codex/config.toml`:

```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):

```json
{
  "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:

```json
{
  "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:

```json
{
  "mcpServers": {
    "kubetriage": {
      "command": "uvx",
      "args": ["kubetriage", "--kubeconfig", "/ABS/PATH/kubeconfig"]
    }
  }
}
```

Remove: delete the entry.

**Zed**: in `settings.json`:

```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):

```json
{
  "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`:

```json
{
  "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](https://github.com/Jenish2911/kubetriage/blob/main/CHANGELOG.md)
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](https://github.com/Jenish2911/kubetriage/blob/main/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](https://github.com/Jenish2911/kubetriage/blob/main/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](https://github.com/Jenish2911/kubetriage/blob/main/LICENSE).
