Kubernetes Read Only MCP Server
# mcp-k8s-ro
A read-only MCP server that gives Claude access to Kubernetes clusters. Built in Go, communicates over stdio using the MCP protocol.
## Design
- **Read-only** — only `get`, `describe`, `logs`, and `top` style operations. No create, update, or delete. If a mutating operation is needed, the server prints the equivalent `kubectl` command for you to run manually. Safe to use while on-call at night: Claude can never accidentally mutate your cluster, even under prompt fatigue.
- **Secret-safe** — secret values are masked before being sent to the model, so your secrets cannot leak due to misconfiguration or prompt injection.
- **Token-efficient** — responses include only relevant fields (name, status, restarts, etc.) rather than raw Kubernetes API objects, keeping context usage low.
- **Cluster-aware** — every response includes the active context and cluster name, so Claude always knows which cluster it is talking to.
- **Context-pinned** — the server locks to the active kubeconfig context at startup. Switching contexts in another terminal has no effect on the running server.
- **No extra infra** — runs as a local binary or Docker container, connects to whatever kubeconfig context is active at startup.
## Redacted fields
| Object/Field | Reason |
|--------------------------------------------------------|----------------------------------------------------------|
| Secret.data | Secret leak prevention |
| Secret.stringData | Secret leak prevention |
| CertificateSigningRequest.spec.request | Large base64 PEM blob, no diagnostic value, saves tokens |
| Certificate (cert-manager) .spec.keystores | Cert chain PEM blobs, no diagnostic value, saves tokens |
| Certificate (cert-manager) status.conditions[].message | Cert chain PEM blobs, no diagnostic value, saves tokens |
| *.managedFields | No diagnostic value, saves tokens |
## Tools
| Tool | Description |
|---------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `k8s_list_resources` | List any resource type by name — pods, deployments, CRDs, etc. Accepts optional namespace filter. Returns name, status, readiness, restarts, node, IP, and more depending on resource kind. |
| `k8s_describe_resource` | Return the full YAML of a single resource. Secret data is masked. |
| `k8s_list_resource_types` | List all available resource types via the discovery API. Accepts optional API group filter. |
| `k8s_get_logs` | Fetch pod logs. Supports container selector, tail lines, and `--previous` for crashed containers. |
| `k8s_get_events` | List Kubernetes events for a namespace or the whole cluster, sorted by most recent. |
| `k8s_top_pods` | CPU and memory usage per pod, with per-container breakdown. Requires metrics-server. |
| `k8s_top_nodes` | CPU and memory usage per node, with percentage of allocatable capacity. Requires metrics-server. |
## Configuration
| Environment variable | Default | Description |
|----------------------|------------------|-------------------------|
| `KUBECONFIG` | `~/.kube/config` | Path to kubeconfig file |
## Usage with Claude
### Binary
Build the binary and add it to your Claude Desktop or `claude` CLI configuration:
```bash
make build
# binary is written to bin/mcp-k8s-ro
```
```json
{
"mcpServers": {
"k8s": {
"type" : "stdio",
"command": "/path/to/bin/mcp-k8s-ro",
"env": {
"KUBECONFIG": "/path/to/.kube/config"
}
}
}
}
```
Or via the CLI:
```bash
claude mcp add --transport stdio --scope user mcp-k8s-ro [path to binary]
```
### Docker
Pull the image from GitHub Container Registry (pinning a specific version is recommended):
```bash
docker pull ghcr.io/your-ko/mcp-k8s-ro:latest
```
Add it to your Claude Desktop or `claude` CLI configuration. The kubeconfig directory is mounted read-only into the container:
```json
{
"mcpServers": {
"k8s": {
"command": "docker",
"args": [
"run", "--rm", "-i",
"-v", "/path/to/.kube:/home/nonroot/.kube:ro",
"ghcr.io/your-ko/mcp-k8s-ro:latest"
]
}
}
}
```
If your kubeconfig is in a non-standard location, pass it via `KUBECONFIG`:
```json
{
"mcpServers": {
"k8s": {
"command": "docker",
"args": [
"run", "--rm", "-i",
"-e", "KUBECONFIG=/config/my-kubeconfig",
"-v", "/path/to/my-kubeconfig:/config/my-kubeconfig:ro",
"ghcr.io/your-ko/mcp-k8s-ro:latest"
]
}
}
}
```
## Single-cluster design
The server intentionally operates on one kubeconfig context and provides no tool to switch clusters at runtime. The reasons are:
- **Prompt injection isolation** — a malicious value in one cluster's resources (e.g. a pod annotation) cannot instruct Claude to pivot to a different cluster, including production.
- **Explicit audit boundary** — every tool response includes the context and cluster name, so there is never ambiguity about which cluster was queried.
**To point the server at a different cluster**, stop the server, switch context, and restart:
```bash
kubectl config use-context my-other-cluster
# then restart the MCP server / reload Claude Desktop
```
**To work with multiple clusters simultaneously**, register a separate server instance per cluster in your MCP config:
```json
{
"mcpServers": {
"k8s-staging": {
"type": "stdio",
"command": "/path/to/bin/mcp-k8s-ro",
"env": { "KUBECONFIG": "/path/to/.kube/config" }
},
"k8s-prod": {
"type": "stdio",
"command": "/path/to/bin/mcp-k8s-ro",
"env": { "KUBECONFIG": "/path/to/.kube/config-prod" }
}
}
}
```
Claude will address each [.claude.json](../../.claude.json)server by name and each instance only ever sees its own cluster.