Blog
Kubernetes20 min czytania

MCP Server Kubernetes — klaster pod kontrolą asystenta AI przez kubectl i Helm

mcp-server-kubernetes daje asystentowi AI 23 narzędzia opakowujące kubectl i Helm — od odczytu podów po drenowanie węzłów i exec w kontenerze. Rozbieramy trzy tryby ograniczania uprawnień, listę flag blokowanych na wypadek prompt injection z logów poda oraz to, co README obiecuje, a czego w kodzie 4.1.4 już nie ma.

mcp-server-kubernetes to serwer Model Context Protocol, który udostępnia asystentowi AI klaster Kubernetes. Nie przez własnego klienta REST, nie przez operator w klastrze — przez kubectl i helm, które masz już na maszynie. Serwer składa argumenty, uruchamia binarkę, oddaje wynik modelowi.

Brzmi to jak cienka warstwa i w dużej mierze nią jest. Cała ciekawa robota siedzi nie w samym wywołaniu kubectl, a w tym, co projekt robi wokół niego: w filtrowaniu listy narzędzi, w denyliście flag, która ma zatrzymać wyprowadzenie tokena, i w maskowaniu sekretów. To o tym jest ten wpis, bo to te rzeczy decydują, czy taki serwer wolno w ogóle podpiąć pod produkcję.

Stan projektu na dziś

Repozytorium: github.com/Flux159/mcp-server-kubernetes. Autorzy: Paras Patel i Suyog Sonwalkar. Pierwszy commit w grudniu 2024, licencja MIT, kod w TypeScripcie, budowany Bunem, testowany Vitestem.

  • 1573 gwiazdki, 281 forków, 6 otwartych issues,
  • ostatnie wydanie v4.1.4 z 8 sierpnia 2026, wcześniej v4.1.3 siódmego, v4.1.2 28 lipca — tempo wydań jest wyraźnie żywe,
  • 95 wersji na npm, około 11 tysięcy pobrań tygodniowo,
  • zależności trzymane na sztywnych wersjach (@modelcontextprotocol/sdk 1.26.0, @kubernetes/client-node 1.3.0) i podbijane osobnymi commitami.

Dla porządku: ostatnie dwa wydania to podbicia zależności po alertach bezpieczeństwa (express 4.21.2 → 4.22.2, js-yaml 4.3.0 → 4.3.1). Publikacja na npm idzie wyłącznie przez trusted publishing z GitHub Actions — to zapisane w SECURITY.md i jest istotne przy narzędziu, które uruchamiasz przez npx.

Instalacja: jedna komenda i cztery warunki wstępne

Warunki wstępne są proste, ale żadnego nie da się pominąć: kubectl w PATH, poprawny kubeconfig z kontekstami, dostęp do klastra i — opcjonalnie, jeśli chcesz operacji helmowych — Helm v3 w PATH. Zanim cokolwiek podłączysz, sprawdź kubectl get pods w terminalu. Serwer nie robi nic, czego nie zrobiłby kubectl z tego samego konta.

Claude Code:

claude mcp add kubernetes -- npx mcp-server-kubernetes

Codex CLI:

codex mcp add kubernetes -- npx mcp-server-kubernetes

Claude Desktop, VS Code i Cursor — ten sam blok konfiguracyjny:

{
  "mcpServers": {
    "kubernetes": {
      "command": "npx",
      "args": ["mcp-server-kubernetes@4.1.4"]
    }
  }
}

W dokumentacji projektu w tym miejscu jest ["mcp-server-kubernetes"], bez wersji. Dopisanie @4.1.4 to nasza rekomendacja i wynika z arytmetyki: 95 wydań w dwadzieścia miesięcy oznacza, że npx bez przypięcia wciąga w losowym momencie nową wersję narzędzia, które ma pełne uprawnienia do Twojego klastra. Aktualizacja jest wtedy Twoją decyzją, nie efektem ubocznym restartu klienta.

Jest też wariant bez konfiguracji ręcznej: projekt publikuje rozszerzenie mcpb, dostępne w Claude Desktopie w Settings → Extensions → Browse Extensions. Instaluje się jednym kliknięciem i korzysta z lokalnego kubectl oraz kubeconfiga.

Co realnie umie: 23 narzędzia

Wersja 4.x porzuciła narzędzia typu „jedna operacja na zasób” (delete_pod, scale_deployment) na rzecz zestawu odwzorowującego podkomendy kubectl. Pełna lista zarejestrowana w src/index.ts w 4.1.4:

  • odczyt: kubectl_get, kubectl_describe, kubectl_logs, explain_resource, list_api_resources,
  • zapis: kubectl_apply, kubectl_create, kubectl_patch, kubectl_scale, kubectl_rollout, kubectl_delete,
  • Helm: install_helm_chart, upgrade_helm_chart, uninstall_helm_chart,
  • dostęp do środka: port_forward, stop_port_forward, exec_in_pod,
  • węzły i porządki: node_management, cleanup,
  • kontekst i diagnostyka połączenia: kubectl_context, kubectl_reconnect, ping,
  • wytrych: kubectl_generic — dowolna komenda kubectl z dowolnymi flagami.

Pierwsza pułapka: dokumentacja obiecuje więcej, niż jest w kodzie

README wymienia trzy narzędzia, których w 4.1.4 nie ma na liście rejestrowanej przez serwer: cleanup_pods (miało czyścić pody w stanach Evicted, CrashLoopBackOff, ImagePullBackOff), helm_template_apply i helm_template_uninstall. Osobny plik docs/pod-cleanup.md jest już zaktualizowany i opisuje sprzątanie podów jako kombinację kubectl_get i kubectl_delete z selektorami pól — czyli tak, jak to teraz faktycznie działa:

{
  "name": "kubectl_delete",
  "arguments": {
    "resourceType": "pods",
    "namespace": "default",
    "fieldSelector": "status.phase=Failed",
    "force": true,
    "gracePeriodSeconds": 0
  }
}

Dostępne selektory: status.phase=Failed, status.phase=Succeeded, status.phase=Pending i status.conditions[?(@.type=='Ready')].status=False. Sensowny przepływ to najpierw kubectl_get z tym samym selektorem, przegląd listy, potem usuwanie.

Wniosek na przyszłość jest ogólniejszy niż ten jeden projekt: przy serwerach MCP źródłem prawdy o dostępnych narzędziach jest odpowiedź na tools/list, nie README. Najszybszy sposób, żeby ją zobaczyć, wykorzystuje walidację, którą projekt wprowadził z innego powodu — podanie w ALLOWED_TOOLS nazwy, której nie ma, wypisuje pełną listę istniejących narzędzi i kończy proces:

ALLOWED_TOOLS="zmyslone_narzedzie" npx mcp-server-kubernetes
# ALLOWED_TOOLS contains unknown tool name: zmyslone_narzedzie
# Available tools: cleanup, exec_in_pod, explain_resource, ...

Alternatywnie: npx @modelcontextprotocol/inspector node dist/index.js.

Trzy tryby ograniczania uprawnień i ich kolejność

To najważniejsza część konfiguracji, bo decyduje o tym, ile szkód asystent może wyrządzić przez pomyłkę albo przez tekst wstrzyknięty do kontekstu. Zmienne środowiskowe rozstrzygają się w ustalonej kolejności — pierwsza ustawiona wygrywa i pozostałe przestają mieć znaczenie:

  1. ALLOWED_TOOLS — lista nazw po przecinku, tylko te narzędzia istnieją dla klienta,
  2. ALLOW_ONLY_READONLY_TOOLS=true — osiem narzędzi tylko do czytania,
  3. ALLOW_ONLY_NON_DESTRUCTIVE_TOOLS=true — wszystko poza pięcioma narzędziami uznanymi za destrukcyjne.

Tryb read-only daje: kubectl_get, kubectl_describe, kubectl_logs, kubectl_context, kubectl_reconnect, explain_resource, list_api_resources, ping. Osiem pozycji, żadna nie zmienia stanu klastra. Jedno zastrzeżenie: kubectl_context ma operację set, więc asystent może przełączyć kontekst — nie zmienia to niczego w klastrze, ale zmienia to, którego klastra dotyczą kolejne wywołania.

Tryb non-destructive wyłącza pięć narzędzi: kubectl_delete, uninstall_helm_chart, cleanup, kubectl_generic i node_management. Zostaje osiemnaście.

„Non-destructive” nie znaczy „bezpieczny”. W tym trybie asystent nadal może zrobić kubectl_apply na istniejącym Deploymencie, wykonać upgrade_helm_chart, przeskalować co chce do zera i wejść przez exec_in_pod do kontenera. Nie usunie zasobu — i to jest cała gwarancja, jaką dostajesz.

exec_in_pod jest tu wart osobnego zdania, bo w kodzie ma adnotację destructiveHint: true, a mimo to nie znalazło się na liście narzędzi wyłączanych w trybie non-destructive. Klient MCP, który respektuje adnotacje, poprosi użytkownika o potwierdzenie. Filtr serwera go nie zatrzyma. Jeśli chcesz mieć profil, który realnie tylko patrzy, użyj ALLOW_ONLY_READONLY_TOOLS albo jawnej listy ALLOWED_TOOLS — nie trybu non-destructive.

Przy okazji dwie rozbieżności w dokumentacji chartu Helmowego, które kosztują czas przy pierwszym wdrożeniu. Komentarz w values.yaml wymienia narzędzia wyłączane w trybie non-destructive bez node_management (kod je wyłącza) i podaje w przykładowej liście dostępnych narzędzi nazwę start_port_forward, której nie ma — narzędzie nazywa się port_forward. Wklejenie tej nazwy do security.allowedTools skończy się tym, że serwer nie wystartuje, bo walidacja ALLOWED_TOOLS odrzuca nieznane nazwy. To zachowanie jest zamierzone i słuszne: literówka po cichu zawężająca zestaw narzędzi byłaby gorsza od jawnego błędu.

Praktyczny profil dla klastra produkcyjnego, w którym asystent ma pomagać w diagnostyce i niczym więcej:

{
  "mcpServers": {
    "k8s-prod": {
      "command": "npx",
      "args": ["mcp-server-kubernetes@4.1.4"],
      "env": {
        "ALLOWED_TOOLS": "kubectl_get,kubectl_describe,kubectl_logs,list_api_resources,explain_resource,ping",
        "K8S_CONTEXT": "prod",
        "MASK_SECRETS": "true"
      }
    },
    "k8s-staging": {
      "command": "npx",
      "args": ["mcp-server-kubernetes@4.1.4"],
      "env": {
        "ALLOW_ONLY_NON_DESTRUCTIVE_TOOLS": "true",
        "K8S_CONTEXT": "staging"
      }
    }
  }
}

Dwa wpisy zamiast jednego, każdy z własnym kontekstem i własnym zestawem narzędzi. Asystent widzi wtedy dwa różne serwery i sam nie ma możliwości pomylić klastrów — nie polegasz na tym, że model pamięta, w którym kontekście jest.

Denylista flag, czyli obrona przed prompt injection z logów

To najciekawszy fragment kodu w całym projekcie i powód, dla którego warto go traktować poważniej niż typowy wrapper na CLI. Plik src/security/kubectl-flags.ts opisuje w komentarzu scenariusz ataku wprost: napastnik, który jest w stanie wpłynąć na argumenty narzędzia — na przykład przez tekst w logach poda, które asystent właśnie czyta — mógłby przekazać --server albo --token i wyprowadzić token operatora na własny host.

Serwer blokuje więc całą grupę flag, niezależnie od tego, w którym polu wejścia się pojawią:

  • przekierowanie celu: --server, --kubeconfig, --cluster, --user, --tls-server-name,
  • obejście TLS: --insecure-skip-tls-verify, --certificate-authority, --client-certificate, --client-key,
  • podstawienie poświadczeń: --token, --username, --password, --auth-provider, --exec-command, --exec-arg, --exec-env,
  • podszywanie się: --as, --as-group, --as-uid,
  • zapis w dowolne miejsce systemu plików: --profile-output, --log-file, --cache-dir,
  • helmowe odpowiedniki: --kube-apiserver, --kube-token, --kube-ca-file, --kube-as-user, --kube-insecure-skip-tls-verify.

Implementacja jest zrobiona z wiedzą o tym, jak pflag naprawdę parsuje argumenty, i to jest różnica między denylistą, która działa, a taką, która wygląda, że działa:

  • flagi normalizowane są do postaci długiej z pominięciem wielkości liter i z zamianą _ na -, bo --insecure_skip_tls_verify i --insecure-skip-tls-verify to dla kubectl ta sama flaga,
  • sprawdzana jest zarówno forma rozdzielona (--server x), jak i połączona (--server=x),
  • alias -s jest wyłapywany również w środku klastra skrótów: -Ashttps://napastnik to dla pflag -A plus -s https://napastnik, więc skaner idzie litera po literze i zatrzymuje się dopiero na skrócie przyjmującym wartość,
  • kontrolowany jest nie tylko obiekt flags i tablica args z kubectl_generic, ale cała gotowa lista argumentów — również pozycje, w które poszczególne narzędzia wstawiają nazwy zasobów i węzłów. Argument name: "--server=https://napastnik" w zwykłym kubectl_get byłby dla pflag flagą, nie nazwą.

Wyjątkiem jest świadomie zostawiony --context: może wskazać tylko kontekst już obecny w załadowanym kubeconfigu, a bez --server i --kubeconfig nie przekieruje nigdzie na zewnątrz. Awaryjne wyłączenie całego mechanizmu to ALLOW_KUBECTL_UNSAFE_FLAGS=true; nazwa jest uczciwa i nie ma powodu jej używać na produkcji.

exec_in_pod zamyka drugi oczywisty wektor: komenda przyjmowana jest wyłącznie jako tablica łańcuchów i wykonywana przez execFileSync, bez powłoki. Potoki, przekierowania i łączenie komend nie działają — z założenia. Wywołanie wygląda tak:

{
  "name": "exec_in_pod",
  "arguments": {
    "name": "app-7d9f8b6c4-xk2mn",
    "namespace": "production",
    "container": "php",
    "command": ["php", "artisan", "queue:monitor", "redis:default"],
    "timeout": 15000
  }
}

Domyślny timeout to 60 000 ms.

Maskowanie sekretów i jego zasięg

Domyślnie włączone (MASK_SECRETS inne niż false): wartości w sekcji data obiektów typu Secret zostają zamienione na ***, z zachowaniem struktury i metadanych. Maskowanie jest zakresowane po kind, a nie po obecności klucza data, więc ConfigMapy pozostają czytelne, a sekrety w wyniku listowym są maskowane bezwarunkowo.

Zasięg jest jednak węższy, niż sugeruje nazwa zmiennej, i warto to wiedzieć: maskowanie jest wpięte w kubectl_get. Nie ma go w kubectl_generic, więc command: "get", resourceType: "secrets", outputFormat: "yaml" zwróci base64 bez zmian. Dokumentacja mówi też wprost, że maskowanie nie dotyczy logów — jeśli aplikacja wypisuje hasło do stdout, żaden przełącznik tego nie zatrzyma. W praktyce te dwa fakty składają się w jedną zasadę: profil, w którym kubectl_generic jest dostępny, to profil bez ochrony sekretów. Tryb non-destructive wyłącza to narzędzie i zamyka dziurę niejako przy okazji.

Uwierzytelnianie: siedem źródeł kubeconfiga

Serwer szuka konfiguracji w ustalonej kolejności, pierwsze znalezione wygrywa:

  1. KUBECONFIG_YAML — cały kubeconfig jako łańcuch YAML,
  2. KUBECONFIG_JSON — to samo w JSON-ie,
  3. K8S_SERVER + K8S_TOKEN — minimalna konfiguracja ze zmiennych,
  4. konfiguracja in-cluster, jeśli serwer działa w podzie,
  5. KUBECONFIG_PATH — własna ścieżka,
  6. KUBECONFIG — standardowa zmienna,
  7. ~/.kube/config.

Wariant minimalny wygląda tak:

export K8S_SERVER='https://prod-cluster.example.com'
export K8S_TOKEN='eyJhbGciOiJSUzI1NiIsImtpZCI6...'
export K8S_CA_DATA='LS0tLS1CRUdJTi...'   # CA w base64, opcjonalne
export K8S_CONTEXT='production'
export K8S_NAMESPACE='my-app'
export K8S_SKIP_TLS_VERIFY='false'

K8S_CA_DATA pozwala mieć weryfikację TLS bez pełnego kubeconfiga i jest wykluczające się z K8S_SKIP_TLS_VERIFY=true — gdy podasz CA, pomijanie weryfikacji jest wymuszane na false, bo Kubernetes na obu ustawionych naraz rzuca błędem. K8S_CONTEXT i K8S_NAMESPACE działają jako nadpisania przy każdej z siedmiu metod.

Duże klastry: ENOBUFS

Wynik kubectl wraca przez bufor procesu potomnego, którego domyślny rozmiar w Node.js to 1 MB (dokładnie 1048577 bajtów w kodzie). Na dużym klastrze kubectl_get po wszystkich namespace'ach potrafi ten bufor przekroczyć, a objawia się to błędem spawnSync ENOBUFS. Lekarstwo to jedna zmienna:

{
  "env": {
    "SPAWN_MAX_BUFFER": "5242880"
  }
}

To nie jest błąd projektu, tylko domyślna wartość Node.js, na którą trzeba mieć gotową odpowiedź, zamiast szukać jej w trakcie awarii.

Transport HTTP i dlaczego nazywa się „unsafe”

Domyślny transport to stdio: klient uruchamia proces, rozmawia z nim przez potoki, nic nie nasłuchuje na porcie. Tryb sieciowy trzeba włączyć jawnie, a nazwa zmiennej mówi wszystko:

ENABLE_UNSAFE_STREAMABLE_HTTP_TRANSPORT=1 PORT=3001 HOST=0.0.0.0 npx mcp-server-kubernetes

Powstaje endpoint /mcp (POST, GET, DELETE), domyślnie na porcie 3000. „Unsafe” bierze się z tego, że każdy, kto dosięgnie tego adresu, korzysta z Twojego kubeconfiga i Twoich uprawnień do klastra. Starszy transport SSE (ENABLE_UNSAFE_SSE_TRANSPORT, endpoint /sse) jest oznaczony jako przestarzały na rzecz streamable HTTP.

Projekt dokłada do tego dwie warstwy ochrony, obie warte znajomości.

Ochrona przed DNS rebinding — włączona domyślnie

Serwer sprawdza nagłówek Host, żeby złośliwa strona otwarta w przeglądarce nie mogła wysyłać żądań do lokalnego serwera MCP. Domyślna lista dozwolonych obejmuje 127.0.0.1, localhost, ::1 (z portem i bez) oraz skonfigurowaną wartość HOST, więc lokalnie nie wymaga niczego. Przy hostowaniu pod własną domeną trzeba ją dopisać:

DNS_REBINDING_ALLOWED_HOST=mcp.example.com \
  ENABLE_UNSAFE_STREAMABLE_HTTP_TRANSPORT=1 PORT=3001 HOST=0.0.0.0 \
  npx mcp-server-kubernetes

Zmienna przyjmuje dziś jedną nazwę hosta. Przy kilku domenach trzeba je zbić na jednym kanonicznym hoście — zwykle reverse proxy — i podać właśnie jego, a nie nazwę kontenera po stronie upstreamu. Port dopisujemy tylko wtedy, gdy klienci faktycznie wysyłają go w nagłówku (przy :80 i :443 zwykle nie). Wyłączenie mechanizmu (DNS_REBINDING_PROTECTION=false) jest możliwe i przy nasłuchu na 0.0.0.0 serwer wypisuje wtedy ostrzeżenie przy starcie.

Nagłówek X-MCP-AUTH

Najprostsza forma uwierzytelnienia bez stawiania pełnego proxy z OAuth: ustawiasz MCP_AUTH_TOKEN, a serwer zaczyna wymagać zgodnego nagłówka X-MCP-AUTH przy każdym żądaniu MCP. Endpointy /health i /ready zostają nieuwierzytelnione, żeby sondy Kubernetesa działały.

curl -X POST http://localhost:3001/mcp
# 401

curl -X POST -H "X-MCP-AUTH: my-secret-token" -H "Content-Type: application/json" \
  -d '{"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {"capabilities": {}}}' \
  http://localhost:3001/mcp

Autorzy są uczciwi co do zakresu tego rozwiązania: to warstwa dla wdrożeń wewnątrz klastra, nie dla endpointu wystawionego do internetu. Tam nadal potrzebne jest proxy z OIDC, mTLS w service mesh albo NetworkPolicy ograniczające dostęp.

Wdrożenie w klastrze: chart Helmowy

Repozytorium zawiera własny chart (helm-chart/) z Deploymentem, Service, Ingressem, HPA, PodDisruptionBudgetem, NetworkPolicy, RBAC i testami Helma. Domyślny transport w charcie to http na porcie 3001, więc token uwierzytelniający przestaje być opcjonalny:

helm install mcp-server-k8s ./helm-chart \
  -n mcp-system --create-namespace \
  --set security.mcpAuthToken=$(openssl rand -hex 32)

Bez niego chart wypisuje przy instalacji ostrzeżenie, że endpoint jest nieuwierzytelniony — a to znaczy, że każdy pod, który dosięgnie Service, wykonuje komendy z uprawnieniami tego wdrożenia. Komentarz w values.yaml stawia to jasno i słusznie.

Domyślny ClusterRole jest zbudowany wokół najmniejszych potrzebnych uprawnień, ale „najmniejsze potrzebne” dla tego narzędzia jest wciąż szerokie. Warto przeczytać go przed instalacją, a nie po:

rules:
  - apiGroups: [""]
    resources: ["pods", "services", "endpoints", "persistentvolumeclaims",
                "configmaps", "secrets", "namespaces", "nodes"]
    verbs: ["get", "list", "watch"]
  - apiGroups: [""]
    resources: ["pods", "services", "endpoints", "persistentvolumeclaims", "configmaps"]
    verbs: ["create", "update", "patch", "delete"]
  - apiGroups: [""]
    resources: ["pods/exec", "pods/log", "pods/portforward"]
    verbs: ["create", "get"]
  - apiGroups: ["apps"]
    resources: ["deployments", "replicasets", "daemonsets", "statefulsets"]
    verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
  - apiGroups: ["rbac.authorization.k8s.io"]
    resources: ["roles", "rolebindings", "clusterroles", "clusterrolebindings"]
    verbs: ["get", "list", "watch"]

Dwie rzeczy do zauważenia. Pierwsza: secrets są w regule odczytu, więc pod ma prawo je czytać w całym klastrze — maskowanie w kubectl_get chroni model, nie klaster. Druga: pods/exec to prawo wejścia do każdego kontenera. Jeżeli wdrażasz to na produkcji, ClusterRole jest pierwszym miejscem do przycięcia, a nie ostatnim. Chart daje też rbac.useLegacyRules z szerszym zestawem — domyślnie false i tak powinno zostać.

Po stronie kubeconfiga chart obsługuje kilka providerów: aws (wiele klastrów EKS z roleArn per klaster), gcp (GKE ze wskazaniem projektu i strefy), url (kubeconfig pobierany z adresu HTTP) i volume (własny Secret albo ConfigMap podmontowany pod wskazaną ścieżkę). Kontekst pod securityContext jest sensowny: runAsNonRoot, runAsUser: 1000, readOnlyRootFilesystem: true.

Jedna przestroga z values.yaml, która oszczędza pół dnia debugowania: MCP korzysta z połączeń strumieniowych, a nie każdy load balancer je obsługuje. AWS Classic Load Balancer nie obsługuje strumieniowania (potrzebny NLB), NGINX Ingress przed ELB potrafi zerwać połączenie na timeoucie, a CloudFlare buforuje odpowiedzi i trzeba mu to wyłączyć. NetworkPolicy jest w charcie domyślnie wyłączona — przy transportcie HTTP to kolejna rzecz, którą warto włączyć samodzielnie.

Prompt /k8s-diagnose

Poza narzędziami serwer wystawia jeden prompt MCP — k8s-diagnose — z argumentami keyword (wymagany, dopasowuje nazwy podów i węzłów) i namespace (opcjonalny, domyślnie wszystkie). To gotowy scenariusz diagnostyczny w dziewięciu krokach, od golden signals i typu zasobu, przez stan bieżący, historię zdarzeń i rolloutów, logi, DNS i montowania, zależności (ConfigMapy, Sekrety, RBAC, initContainery), limity i quoty, aż po taints, tolerations i wersję klastra.

Ciekawsze od samej listy są reguły wyjścia, które prompt narzuca modelowi: każde ustalenie z obserwacją, wagą (CRITICAL, WARNING, INFO) i dowodem — nazwą obiektu, fragmentem wyjścia kubectl, znacznikiem czasu. Przy więcej niż czterech pasujących zasobach model ma wybrać maksymalnie trzy o najpoważniejszych objawach. Na końcu zwięzła przyczyna źródłowa i konkretne komendy naprawcze. To akurat dobry wzorzec do podpatrzenia przy pisaniu własnych promptów diagnostycznych — ogranicza rozlewanie się odpowiedzi lepiej niż prośba o zwięzłość.

Serwer wystawia też pięć zasobów MCP do odczytu: k8s://default/pods, k8s://default/deployments, k8s://default/services, k8s://namespaces i k8s://nodes. Trzy pierwsze są zaszyte na namespace default, więc przy pracy w innym trzeba używać narzędzi, nie zasobów.

Obserwowalność: ślady OpenTelemetry

Opcja domyślnie wyłączona, którą warto włączyć od razu, jeśli asystent ma dostęp do klastra dzielonego z innymi ludźmi. Po włączeniu każde wywołanie narzędzia zostawia span z nazwą, czasem trwania, statusem, kontekstem i namespace'em — czyli dostajesz audyt tego, co model faktycznie robił.

# Jaeger lokalnie
docker run -d --name jaeger \
  -e COLLECTOR_OTLP_ENABLED=true \
  -p 16686:16686 -p 4317:4317 \
  jaegertracing/all-in-one:latest

# Produkcja: 5% próbkowania
export ENABLE_TELEMETRY=true
export OTEL_EXPORTER_OTLP_ENDPOINT=http://tempo.observability:4317
export OTEL_TRACES_SAMPLER=traceidratio
export OTEL_TRACES_SAMPLER_ARG=0.05
export OTEL_SERVICE_NAME=kubernetes-mcp-server
export OTEL_RESOURCE_ATTRIBUTES="deployment.environment=production,k8s.cluster=prod-us-west"

Samplery: always_on (dev), always_off, traceidratio z argumentem 0.0–1.0. Domyślna nazwa usługi to kubernetes. Jest też przełącznik prywatnościowy: OTEL_CAPTURE_RESPONSE_METADATA=false wyłącza zapisywanie metadanych odpowiedzi, czyli liczby elementów i rozmiarów. Backend dowolny zgodny z OTLP — Jaeger, Tempo, Grafana Cloud, Datadog, Honeycomb.

W naszym stacku: Laravel na Kubernetesie

Nasze aplikacje jadą na Laravelu z Octane i Horizonem, więc typowe pytania do klastra nie dotyczą Kubernetesa jako takiego, a tego, co robi PHP w środku. Tu ten serwer siada dobrze, bo cała pętla „przeczytaj logi, opisz poda, zrestartuj rollout, sprawdź jeszcze raz” dzieje się bez przeklejania wyjścia kubectl do okna czatu.

Rzeczy, które faktycznie zamawiamy asystentowi z profilem tylko do czytania:

  • „Pokaż pody Horizona, które restartowały się w ostatniej godzinie, i wyciągnij logi poprzedniej instancji”kubectl_get plus kubectl_logs z previous,
  • „Czemu ten pod nie startuje”kubectl_describe i zdarzenia; przy braku obrazu, złym sekrecie albo nieudanej sondzie odpowiedź jest w zdarzeniach, nie w logach,
  • „Który Deployment dostał ostatnią zmianę i co się w niej zmieniło”kubectl_rollout z historią,
  • „Sprawdź, czy migracje przeszły na wszystkich replikach”exec_in_pod z ["php", "artisan", "migrate:status"].

Na środowisku stagingowym, gdzie profil dopuszcza zapis, dochodzą operacje typu kubectl_rollout restart po zmianie ConfigMapy z konfiguracją, kubectl_scale workerów kolejki przed testem obciążeniowym i port_forward do panelu Horizona bez ręcznego terminala.

Granica jest za to prosta i nie ma sensu jej przesuwać: na produkcji asystent czyta, człowiek zmienia. Nie dlatego, że model jest głupi, ale dlatego że kontekst modelu zawiera dane z klastra — logi, zdarzenia, opisy zasobów — czyli tekst, który wpisał ktoś inny. Denylista flag broni przed jednym konkretnym atakiem w tej kategorii; nie broni przed sytuacją, w której model po prostu wykona sensownie brzmiące polecenie znalezione w logu aplikacji. Zestaw narzędzi bez operacji zapisu ucina cały ten problem taniej niż jakikolwiek prompt.

Czego brakuje

Otwarte issues są krótką i uczciwą listą ograniczeń:

  • brak kubectl_top (#262) — zużycie CPU i pamięci trzeba dziś brać przez kubectl_generic, czyli narzędzie wyłączane w trybie non-destructive. To najbardziej dokuczliwa luka, bo kubectl top jest operacją czysto odczytową,
  • brak narzędzi powłokowych (#272) — żadnego jq, grep ani head po stronie serwera; filtrowanie robi model na pełnym wyniku, co przy dużych klastrach kosztuje tokeny,
  • brak ograniczania podkomend (#194) — ALLOWED_TOOLS działa na poziomie narzędzia, więc nie da się dopuścić kubectl_delete tylko dla podów,
  • spawnSync kubectl ENOENT (#254) — klasyk konfiguracji: kubectl nie jest w PATH procesu uruchomionego przez klienta GUI, choć w terminalu działa,
  • dodawanie klastrów do kubectx jest wprost oznaczone jako niezaplanowane.

Do tego rozjazd dokumentacji z kodem, opisany wyżej. Nie jest groźny, ale każe traktować README jako materiał orientacyjny, a nie specyfikację.

Podsumowanie

Wrapper na kubectl to nie jest rzecz, którą trudno napisać — trudne jest sprawienie, żeby dawało się go bezpiecznie oddać modelowi. mcp-server-kubernetes ma to przemyślane lepiej niż większość serwerów MCP, jakie widzieliśmy: filtrowanie narzędzi na trzech poziomach z jasną kolejnością, argumentowa denylista napisana ze znajomością parsera pflag, wykonywanie komend bez powłoki, maskowanie sekretów, opcjonalne ślady OTel i chart z RBAC pisanym pod najmniejsze potrzebne uprawnienia.

Co dostajesz:

  • 23 narzędzia pokrywające odczyt, zapis, Helma, port forward, exec, zarządzanie węzłami i wytrych kubectl_generic,
  • instalację jedną komendą w Claude Code, Codeksie, Claude Desktopie (także jako rozszerzenie mcpb), VS Code i Cursorze,
  • trzy tryby ograniczania uprawnień — jawna lista, read-only (8 narzędzi), non-destructive (18 narzędzi) — z odmową startu przy literówce w nazwie narzędzia,
  • siedem źródeł kubeconfiga, w tym in-cluster i konfigurację ze zmiennych, z nadpisaniem kontekstu i namespace'u,
  • transport stdio domyślnie, streamable HTTP na życzenie, z ochroną przed DNS rebinding włączoną z pudełka i uwierzytelnianiem przez X-MCP-AUTH,
  • prompt k8s-diagnose z dziewięciokrokowym scenariuszem i wymuszonym formatem wniosków,
  • chart Helmowy z RBAC, NetworkPolicy, HPA i testami — do wdrożenia w klastrze.

O czym pamiętać: „non-destructive” nie znaczy „tylko do czytania” — zostają w nim kubectl_apply, operacje Helma i exec_in_pod. Maskowanie sekretów działa w kubectl_get, nie w kubectl_generic ani w logach. Domyślny ClusterRole chartu czyta sekrety w całym klastrze i ma pods/exec. Transport HTTP bez MCP_AUTH_TOKEN to otwarty dostęp do klastra dla wszystkiego, co dosięgnie Service. I przypnij wersję w npx — 95 wydań w dwadzieścia miesięcy to nie tempo, przy którym chcesz automatycznych aktualizacji narzędzia z takimi uprawnieniami.

Licencja: MIT — bez zastrzeżeń przy użyciu komercyjnym, w tym u klienta i w zamkniętym środowisku. Nie ma tu klauzuli sieciowej znanej z AGPL, więc wystawienie serwera w klastrze klienta niczego nie pociąga; wystarczy zachować notę licencyjną. Autorzy proszą w README o cytowanie projektu, jeśli okaże się przydatny w publikacji — to prośba, nie warunek licencji.