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.4z 8 sierpnia 2026, wcześniejv4.1.3siódmego,v4.1.228 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/sdk1.26.0,@kubernetes/client-node1.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-kubernetesCodex CLI:
codex mcp add kubernetes -- npx mcp-server-kubernetesClaude 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 komendakubectlz 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:
ALLOWED_TOOLS— lista nazw po przecinku, tylko te narzędzia istnieją dla klienta,ALLOW_ONLY_READONLY_TOOLS=true— osiem narzędzi tylko do czytania,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_applyna istniejącym Deploymencie, wykonaćupgrade_helm_chart, przeskalować co chce do zera i wejść przezexec_in_poddo 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_verifyi--insecure-skip-tls-verifyto dla kubectl ta sama flaga, - sprawdzana jest zarówno forma rozdzielona (
--serverx), jak i połączona (--server=x), - alias
-sjest wyłapywany również w środku klastra skrótów:-Ashttps://napastnikto dlapflag-Aplus-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
flagsi tablicaargszkubectl_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. Argumentname: "--server=https://napastnik"w zwykłymkubectl_getbyłby dlapflagflagą, 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:
KUBECONFIG_YAML— cały kubeconfig jako łańcuch YAML,KUBECONFIG_JSON— to samo w JSON-ie,K8S_SERVER+K8S_TOKEN— minimalna konfiguracja ze zmiennych,- konfiguracja in-cluster, jeśli serwer działa w podzie,
KUBECONFIG_PATH— własna ścieżka,KUBECONFIG— standardowa zmienna,~/.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-kubernetesPowstaje 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-kubernetesZmienna 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/mcpAutorzy 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_getpluskubectl_logszprevious, - „Czemu ten pod nie startuje” —
kubectl_describei 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_rolloutz historią, - „Sprawdź, czy migracje przeszły na wszystkich replikach” —
exec_in_podz["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ć przezkubectl_generic, czyli narzędzie wyłączane w trybie non-destructive. To najbardziej dokuczliwa luka, bokubectl topjest operacją czysto odczytową, - brak narzędzi powłokowych (#272) — żadnego
jq,grepaniheadpo stronie serwera; filtrowanie robi model na pełnym wyniku, co przy dużych klastrach kosztuje tokeny, - brak ograniczania podkomend (#194) —
ALLOWED_TOOLSdziała na poziomie narzędzia, więc nie da się dopuścićkubectl_deletetylko dla podów, spawnSync kubectl ENOENT(#254) — klasyk konfiguracji:kubectlnie jest wPATHprocesu uruchomionego przez klienta GUI, choć w terminalu działa,- dodawanie klastrów do
kubectxjest 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 wytrychkubectl_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-diagnosez 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.