k8s-readonly-mcp
# k8s-readonly-mcp
A **read-only** [Model Context Protocol](https://modelcontextprotocol.io) (MCP) server that lets
an LLM *inspect* a Kubernetes cluster — list pods, read logs, describe resources — but **never
mutate it**.
## Why I built it
I spend a lot of time embedded in customer Kubernetes environments. The most common thing I want
from an LLM is "help me understand what's happening in this cluster" — *without* ever giving it the
ability to change anything. So I built the safety in structurally rather than trusting the model to
behave.
Every tool call routes through a single function that checks the `kubectl` verb against an
allow-list of read-only verbs (`get`, `describe`, `logs`, `top`, …). There is **no code path** that
can `apply`, `delete`, `scale`, or `exec`. If the model asks for a mutation, the server refuses.
This is the same least-privilege, human-in-the-loop instinct that any production agent needs.
## What it does
| Tool | Description |
|------|-------------|
| `list_namespaces` | List all namespaces |
| `list_pods` | List pods in a namespace (or all namespaces) |
| `describe_pod` | Full status, events, and config for one pod |
| `get_pod_logs` | Last N lines of a pod's logs |
| `list_deployments` | Deployments and their ready/up-to-date status |
## Quick start
> **Prerequisite:** `kubectl` must be installed and on your `PATH`.
```bash
# 1. Install (using uv — https://docs.astral.sh/uv/)
uv sync
# 2. Make sure kubectl points at a cluster.
# A local cluster is perfect for trying this safely:
# kind create cluster (or: minikube start)
kubectl get nodes
# 3. Run the server
uv run k8s-readonly-mcp
```
### Connect it to Claude Desktop
Add this to your Claude Desktop MCP config
(`~/Library/Application Support/Claude/claude_desktop_config.json` on macOS):
```json
{
"mcpServers": {
"k8s-readonly": {
"command": "uv",
"args": ["--directory", "/absolute/path/to/k8s-readonly-mcp", "run", "k8s-readonly-mcp"]
}
}
}
```
Then ask Claude things like *"What pods are failing in the default namespace, and why?"* — it will
call `list_pods` and `describe_pod`, reason over the output, and explain — but it physically cannot
change your cluster.
## Design decisions
- **Allow-list, not block-list.** I enumerate what's *permitted* rather than trying to block bad
verbs. A block-list is one forgotten verb away from a mutation; an allow-list fails closed.
- **One chokepoint.** All `kubectl` execution goes through `_run_kubectl`. The security boundary is
one function you can audit in 30 seconds.
- **Timeouts and clear errors.** Calls time out and surface `kubectl`'s stderr instead of hanging or
failing silently — the model gets actionable feedback.
## What I'd do next
- Add resource-level scoping (restrict to specific namespaces per connection).
- Stream large log outputs instead of buffering.
- Add a small eval that checks the server refuses every mutating verb.
## License
MIT — see [LICENSE](LICENSE).
TDQS
Scored across 5 tools
Each tool targets a distinct Kubernetes resource or operation: describe_pod for pod details, get_pod_logs for logs, list_deployments for deployments, list_namespaces for namespaces, list_pods for pods. No overlap in functionality.
All tool names follow a consistent verb_noun pattern (e.g., list_pods, describe_pod, get_pod_logs) using snake_case throughout, making them predictable and easy to understand.
With 5 tools, the server is well-scoped for a read-only Kubernetes interface. It provides essential operations without unnecessary bloat, fitting the typical 3-15 tool range.
While core pod and deployment listing is covered, the server lacks operations for other common Kubernetes resources like services, nodes, events, or configmaps, which are expected in a read-only toolset.