k8s-mcp-server
by gaelsg
README.md
# k8s-mcp-server
Servidor MCP de solo lectura sobre un cluster Kubernetes (k3s) propio. Idea 6 (última) del roadmap big-tech: cierra el círculo conectando todo lo construido en las ideas anteriores — el cluster corre en un LXC provisionado por [`proxmox-iac`](https://github.com/gaelsg/proxmox-iac), su credencial vive en [`vault-secrets`](https://github.com/gaelsg/vault-secrets), y sus tools quedan disponibles para el Diagnostician de [`devops-multiagent`](https://github.com/gaelsg/devops-multiagent).
## Arquitectura
- **Cluster:** k3s v1.36.4+k3s1, un solo nodo, LXC dedicado (`192.168.8.92`, provisionado con OpenTofu). Ver `proxmox-iac/environments/k3s/`.
- **Credencial:** no es el kubeconfig admin que genera k3s por defecto. Es un kubeconfig separado, de solo lectura, para una `ServiceAccount` propia (`mcp-agent`, namespace `kube-system`) atada al `ClusterRole` built-in `view` + un `ClusterRole` custom acotado a `nodes` (`view` no cubre recursos a nivel de cluster). El kubeconfig admin nunca salió del nodo.
- **Secretos:** el kubeconfig de solo lectura vive en Vault (`secret/k8s-mcp-server`), igual que el resto del roadmap desde la Idea 2 — `secrets_loader.py` hace login AppRole y lo escribe a un archivo temporal (0600) antes de que el cliente de Kubernetes lo use.
- **Cliente:** librería oficial `kubernetes` (Python), no `kubectl` shelleado — mismo criterio que `proxmoxer` en `proxmox-mcp-server`.
## Tools expuestas (todas de solo lectura, sin guardrail en código — no hace falta, no existen tools de escritura)
- `list_k8s_nodes()`
- `list_k8s_pods(namespace=None)`
- `list_k8s_deployments(namespace=None)`
- `list_k8s_services(namespace=None)`
- `get_k8s_pod_logs(namespace, pod_name, tail_lines=50)`
Prefijo `k8s_` deliberado — mismo criterio que `docker_tools.py` en `proxmox-mcp-server` (`list_docker_containers` en vez de `list_containers`): evita que un agente con varios servidores MCP conectados (Proxmox tiene su propio `list_nodes`) no sepa cuál tool corresponde a qué sistema.
## Setup
```bash
uv sync
cp .env.example .env
# completar VAULT_ROLE_ID/VAULT_SECRET_ID (ver vault-secrets/scripts/onboard-k8s-mcp-server.sh)
```
Registro en Claude Code:
```bash
claude mcp add k8s --scope user -- uv run --directory ~/projects/k8s-mcp-server k8s-mcp-server
```
## Ejecutar
```bash
uv run k8s-mcp-server
```
## Tracing
Con `OTEL_EXPORTER_OTLP_ENDPOINT` seteado (ver `.env.example`), cada `tools/call` queda como un span en Jaeger, correlacionado con el trace del agente que lo llamó. Ver [`devops-multiagent`](https://github.com/gaelsg/devops-multiagent#tracing-distribuido-opentelemetry--jaeger) para el detalle completo.
## Supply chain (build, scan, SBOM, firma)
`.github/workflows/supply-chain.yml` — en cada push a `master`: Trivy sobre `uv.lock` (dependencias), build con `docker/build-push-action`, push a `ghcr.io/gaelsg/k8s-mcp-server`, Trivy sobre la imagen construida, SBOM (CycloneDX, via `syft`), firma keyless con `cosign` (OIDC de GitHub, sin llave privada), y attestation del SBOM. Corre en un runner **GitHub-hosted**, no el self-hosted de `devops-multiagent` — build/scan/firma no necesitan alcanzar la red del homelab, y separarlo de un runner persistente reduce superficie. Todas las actions de terceros fijadas por SHA de commit, no por tag mutable.
```bash
cosign verify \
--certificate-identity-regexp "https://github.com/gaelsg/k8s-mcp-server/.github/workflows/supply-chain.yml.*" \
--certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
ghcr.io/gaelsg/k8s-mcp-server:latest
```
**Encontrado y corregido durante la implementación (no solo teoría):** Trivy detectó `msgpack`/`setuptools` con CVEs HIGH reales en la imagen — venían empaquetados dentro de `pip` mismo (nunca usados por el proyecto), resuelto sacando `pip` de la imagen final. Una corrida posterior en el runner de CI (base image recién pulleada, distinta al cache local) encontró un CVE HIGH real en `openssl`/`libssl3t64` con fix disponible, no visible en el build local — resuelto con `apt-get upgrade` en el stage final.
## Notas / gotchas reales encontrados
- `load_kube_config()` del cliente oficial de Kubernetes **no** lee la variable `KUBECONFIG` sola (a diferencia de `kubectl`) — hay que pasarle `config_file=` explícito.
- `read_namespaced_pod_log()` tiene un bug conocido: deserializa la respuesta como el `repr()` de bytes (`"b'...'"`) en vez de decodificarla. Se evita pidiendo la respuesta cruda (`_preload_content=False`) y decodificando a mano.
- El `ClusterRole` built-in `view` no incluye recursos a nivel de cluster (`nodes`, `namespaces`, etc.) — solo recursos namespaced. Hizo falta un `ClusterRole` adicional acotado (`get/list/watch` sobre `nodes`) para que `list_k8s_nodes()` funcione.
Detalle completo, incluyendo el incidente de `keyctl` durante el provisionamiento del LXC, en `docs/29110/idea6-k8s/`.
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues