Strona główna / Artykuły

Serwery MCP dla DevOps: czym są i dwa, które możesz stworzyć w tym tygodniu

Serwery MCP dla DevOps: umożliwiają Claude dostęp w czasie rzeczywistym do Kubernetes, AWS oraz dokumentacji instrukcyjnej. Trzy typy serwerów, serwer MCP do Kubernetes tylko do odczytu oraz częste błędy.

Wydawca: REACTAPP.TOP10 min czytania2253 słów

Źródła: What Is an MCP Server and Why Every DevOps Team Should Have One in 2026

MCP, czyli Model Context Protocol, to otwarty standard stworzony przez Anthropic, który umożliwia modelom sztucznej inteligencji połączenie się w uporządkowany sposób z zewnętrznymi systemami: narzędziami, API, bazami danych i usługami. Bez niego Claude funkcjonuje w izolacji, a każdy fakt musi być ręcznie wprowadzany. Dzięki niemu Claude może bezpośrednio kontaktować się z tymi systemami i pytać je o informacje.

„Serwer” w MCP to zwykły mały program. Udostępnia on narzędzia i dane Claude’owi, obsługuje komunikację w obie strony i działa tam, gdzie znajdują się dane – czy to na laptopie, klastrze, czy serwerze. Nie jest to ciężka infrastruktura, a działający serwer MCP może być zaledwie skryptem w Pythonie składającym się z 50 linii kodu.

Analogia, która pomaga

Integracje z Slackiem stanowią dobry przykład porównania. Przed ich wprowadzeniem ktoś ręcznie publikował komunikat „Rozpoczęto wdrożenie do środowiska produkcyjnego”. Po ich wprowadzeniu pipeline CI/CD publikuje ten komunikat automatycznie, a Slack staje się centrum, które wie, co się dzieje. MCP robi to dla Claude’a – wcześniej osoba musiała sama dostarczać informacje o infrastrukturze, natomiast teraz istnieje bezpośrednie połączenie, które pozwala na samodzielną weryfikację.

Dlaczego zespoły DevOps czerpią największe korzyści

Większość przykładów implementacji MCP jest skierowana do programistów aplikacji. Zespoły DevOps, SRE oraz inżynierów platform czerpią większe korzyści, ponieważ ich głównym zadaniem jest zarządzanie systemami, które już posiadają API: Kubernetes, AWS, Datadog lub Grafana, PagerDuty, Terraform Cloud, GitHub, Confluence i Notion. Każdy z tych systemów może być połączony z Claude’em za pomocą MCP. Gdy to zostanie zrobione, nie ma już potrzeby przechodzenia między narzędziami w celu zbierania informacji – praca sprowadza się do zadawania pytań.

Jak zespoły wykorzystują serwery MCP

  • Pytania dotyczące klastrów. Pytanie o to, które wdrożenia produkcyjne działają już ponad 30 dni bez aktualizacji, wysyła Claude do serwera Kubernetes MCP, który bada klastrowanie i zwraca odpowiedź. Nie ma potrzeby używania kubectl ani kopiowania i wklejania.
  • Pytania o koszty. Pytanie o to, które usługi AWS wykazały gwałtowny wzrost w tym tygodniu w porównaniu z poprzednim, wysyła Claude do serwera AWS oraz Cost Explorer, a odpowiedzią jest szczegółowy opis w prostym języku.
  • Kontekst incydentów. Pytanie o incydenty w PagerDuty z ostatnich 24 godzin oraz o to, czy jakiekolwiek z nich są nadal otwarte, pobiera aktualny stan z serwera PagerDuty i go podsumowuje.
  • Wyszukiwanie instrukcji. Prośba o instrukcje dotyczące wyczerpania puli połączeń do bazy danych powoduje przeszukanie bazy wiedzy za pośrednictwem serwera Confluence i zwraca dokładny dokument.
  • Audyt bezpieczeństwa. Sprawdzenie, czy jakakolwiek grupa bezpieczeństwa zezwala na dostęp 0.0.0.0/0 na portach innych niż 443, powoduje, że serwer AWS skanuje te grupy i zaznacza naruszenia.

Trzy rodzaje serwerów

Zespoły, które zaczynają, zazwyczaj ostatecznie tworzą trzy typy serwerów.

Serwery do obserwacji tylko do odczytu

One łączą Claude z interfejsami API do monitoringu i infrastruktury, dzięki czemu może on opisywać aktualny stan rzeczy. Typowymi przykładami są serwer stanu klastra Kubernetes, serwer inwentaryzacji AWS, serwer metryk Datadog oraz serwer logów CloudWatch. Ryzyko jest bardzo niskie, ponieważ Claude może jedynie odczytywać dane. To jest odpowiednie miejsce na początek, ponieważ buduje zaufanie przed udzieleniem jakiegokolwiek dostępu do zapisu.

Serwery bazy wiedzy

Te linki łączą Claude z dokumentacją wewnętrzną służącą do wyszukiwania i odzyskiwania informacji: serwerem z instrukcjami w Confluence lub Notion, serwerem wiki na GitHubie, folderem z plikami markdown lokalnymi lub bazą z zapisami decyzji architektonicznych. Ryzyko jest praktycznie zerowe, ponieważ chodzi o wyszukiwanie w dokumentach. Są one ważne, ponieważ sprawiają, że Claude ma wrażenie, iż naprawdę zna firmę.

Serwery akcji

Umożliwiają one Claude wykonywanie takich czynności jak tworzenie zleceń, uruchamianie procesów lub restartowanie usług. Przykładami są tworzenie zleceń w Jira, zarządzanie incydentami w PagerDuty, tworzenie pull requestów na GitHubie oraz powiadamiania w Slacku. Ryzyko jest od średniego do wysokiego, ponieważ Claude teraz zmienia różne elementy. Brama zatwierdzeń powinna zawsze znajdować się przed operacjami zapisu, więc restart poda nigdy nie następuje bez kroku potwierdzenia.

Budowanie serwera MCP w Kubernetes

Poniżej znajduje się działający serwer Kubernetes tylko do odczytu napisany w Pythonie. Najpierw zainstaluj SDK:

pip install mcp kubernetes --break-system-packages

Następnie zapisz serwer jako k8s_mcp_server.py:

import asyncio
from mcp.server import Server
from mcp.server.stdio import stdio_server
from mcp import types
from kubernetes import client, config as k8s_config

# Load kubeconfig
try:
    k8s_config.load_incluster_config()
except:
    k8s_config.load_kube_config()
v1 = client.CoreV1Api()
apps_v1 = client.AppsV1Api()

# Create MCP server
app = Server("k8s-devops-server")
@app.list_tools()
async def list_tools() -> list[types.Tool]:
    """Tell Claude what tools are available."""
    return [
        types.Tool(
            name="get_pods",
            description="Get all pods in a namespace with their status, restarts, and age.",
            inputSchema={
                "type": "object",
                "properties": {
                    "namespace": {
                        "type": "string",
                        "description": "Kubernetes namespace (e.g. production, staging)"
                    }
                },
                "required": ["namespace"]
            }
        ),

        types.Tool(
            name="get_pod_logs",
            description="Get recent logs from a pod or deployment.",
            inputSchema={
                "type": "object",
                "properties": {
                    "name": {
                        "type": "string",
                        "description": "Pod or deployment name"
                    },
                    "namespace": {
                        "type": "string",
                        "description": "Kubernetes namespace"
                    },
                    "lines": {
                        "type": "integer",
                        "description": "Number of log lines to return (default 50)"
                    }
                },
                "required": ["name", "namespace"]
            }
        ),

        types.Tool(
            name="get_deployments",
            description="List all deployments with replica counts and status.",
            inputSchema={
                "type": "object",
                "properties": {
                    "namespace": {
                        "type": "string",
                        "description": "Kubernetes namespace"
                    }
                },
                "required": ["namespace"]
            }
        ),

        types.Tool(
            name="get_events",
            description="Get recent Kubernetes events. Useful for diagnosing issues.",
            inputSchema={
                "type": "object",
                "properties": {
                    "namespace": {
                        "type": "string",
                        "description": "Kubernetes namespace"
                    }
                },
                "required": ["namespace"]
            }
        ),
    ]

@app.call_tool()
async def call_tool(name: str, arguments: dict) -> list[types.TextContent]:
    """Handle tool calls from Claude."""

    if name == "get_pods":
        namespace = arguments["namespace"]
        pods = v1.list_namespaced_pod(namespace=namespace)

        lines = [f"Pods in {namespace}:\n"]
        for pod in pods.items:
            phase = pod.status.phase
            restarts = sum(
                cs.restart_count
                for cs in (pod.status.container_statuses or [])
            )
            name_str = pod.metadata.name
            lines.append(f"  {name_str}: {phase} | restarts: {restarts}")
        return [types.TextContent(type="text", text="\n".join(lines))]

    elif name == "get_pod_logs":
        pod_name = arguments["name"]
        namespace = arguments["namespace"]

        lines = arguments.get("lines", 50)
        import subprocess
        result = subprocess.run(
            f"kubectl logs {pod_name} -n {namespace} --tail={lines}",
            shell=True, capture_output=True, text=True
        )
        output = result.stdout or result.stderr or "No logs found"
        return [types.TextContent(type="text", text=output)]

    elif name == "get_deployments":
        namespace = arguments["namespace"]
        deployments = apps_v1.list_namespaced_deployment(namespace=namespace)

        lines = [f"Deployments in {namespace}:\n"]
        for d in deployments.items:
            desired = d.spec.replicas or 0
            available = d.status.available_replicas or 0
            status = "✅" if desired == available else "⚠️"
            lines.append(
                f"  {status} {d.metadata.name}: {available}/{desired} replicas"
            )
        return [types.TextContent(type="text", text="\n".join(lines))]

    elif name == "get_events":
        namespace = arguments["namespace"]
        events = v1.list_namespaced_event(namespace=namespace)

        # Sort by last timestamp, show recent ones
        sorted_events = sorted(
            events.items,
            key=lambda e: e.last_timestamp or e.event_time,
            reverse=True
        )[:20]

        lines = [f"Recent events in {namespace}:\n"]
        for e in sorted_events:
            lines.append(
                f"  [{e.type}] {e.reason}: {e.message[:100]}"
            )

        return [types.TextContent(type="text", text="\n".join(lines))]

    return [types.TextContent(type="text", text=f"Unknown tool: {name}")]

async def main():
    async with stdio_server() as (read_stream, write_stream):
        await app.run(read_stream, write_stream, app.create_initialization_options())

if __name__ == "__main__":
    asyncio.run(main())

Aby połączyć je z Claude Desktop, edytuj plik konfiguracyjny, który znajduje się w jednej z następujących ścieżek:

Mac: ~/Library/Application Support/Claude/claude_desktop_config.json
Windows: %APPDATA%\Claude\claude_desktop_config.json

i dodaj wpis serwera:

{
  "mcpServers": {
    "kubernetes": {
      "command": "python",
      "args": ["/path/to/k8s_mcp_server.py"]
    }
  }
}

Przeprowadź ponowne uruchomienie Claude Desktop – będzie gotowe. Pytanie o to, ile podów jest obecnie w użyciu w produkcji, sprawia, że Claude faktycznie to sprawdza.

Budowanie serwera z instrukcjami postępowania

To może być jeszcze bardziej przydatne. Wskazuje ono Claude’owi na instrukcje zespołu, dzięki czemu w trakcie incydentu Claude może szybko znaleźć odpowiednią instrukcję zamiast tego, by ktoś ręcznie szukał w Confluence.

import asyncio
import os
import glob
from mcp.server import Server
from mcp.server.stdio import stdio_server
from mcp import types

app = Server("runbook-server")

RUNBOOKS_PATH = os.path.expanduser("~/runbooks")  # point to your runbooks folder


def load_all_runbooks() -> dict[str, str]:
    """Load all markdown runbooks from the folder."""
    runbooks = {}
    for filepath in glob.glob(f"{RUNBOOKS_PATH}/**/*.md", recursive=True):
        with open(filepath, 'r', errors='replace') as f:
            content = f.read()
        name = os.path.relpath(filepath, RUNBOOKS_PATH)
        runbooks[name] = content
    return runbooks

@app.list_tools()
async def list_tools() -> list[types.Tool]:
    return [
        types.Tool(
            name="search_runbooks",
            description=(
                "Search your team's runbooks for a specific topic, service, or error. "
                "Use this when someone asks about a procedure, a known issue, "
                "or how to fix something."

           ),
            inputSchema={
                "type": "object",
                "properties": {
                    "query": {
                        "type": "string",
                        "description": "What to search for (e.g. 'database connection pool', 'nginx 502', 'SSL cert renewal')"
                    }
                },
                "required": ["query"]
            }
        ),

        types.Tool(
            name="list_runbooks",
            description="List all available runbooks by category.",
            inputSchema={
                "type": "object",
                "properties": {}
            }
        ),

        types.Tool(
            name="get_runbook",
            description="Get the full content of a specific runbook by name.",
            inputSchema={
                "type": "object",
                "properties": {
                    "name": {
                        "type": "string",
                        "description": "Runbook filename (from list_runbooks)"
                    }
                },
                "required": ["name"]
            }
        )
    ]

@app.call_tool()
async def call_tool(name: str, arguments: dict) -> list[types.TextContent]:
    runbooks = load_all_runbooks()

    if name == "list_runbooks":
        names = sorted(runbooks.keys())
        output = "Available runbooks:\n" + "\n".join(f"  - {n}" for n in names)
        return [types.TextContent(type="text", text=output)]
    elif name == "search_runbooks":
        query = arguments["query"].lower()
        matches = []
        for rb_name, content in runbooks.items():
            if query in content.lower() or query in rb_name.lower():
                # Return first 500 chars as preview
                preview = content[:500].strip()
                matches.append(f"📄 {rb_name}\n{preview}\n...")
        if not matches:
            return [types.TextContent(
                type="text",
                text=f"No runbooks found for '{query}'"
            )]
        return [types.TextContent(
            type="text",
            text=f"Found {len(matches)} runbook(s):\n\n" + "\n\n---\n\n".join(matches)
        )]

    elif name == "get_runbook":
        rb_name = arguments["name"]
        content = runbooks.get(rb_name)
        if not content:
            return [types.TextContent(type="text", text=f"Runbook not found: {rb_name}")]
        return [types.TextContent(type="text", text=content)]
    return [types.TextContent(type="text", text=f"Unknown tool: {name}")]

async def main():
    async with stdio_server() as (read_stream, write_stream):
        await app.run(read_stream, write_stream, app.create_initialization_options())

if __name__ == "__main__":
    asyncio.run(main())

Dodaj to do konfiguracji Claude Desktop obok pozostałego serwera:

{
  "mcpServers": {
    "kubernetes": {
      "command": "python",
      "args": ["/path/to/k8s_mcp_server.py"]
    },
    "runbooks": {
      "command": "python",
      "args": ["/path/to/runbook_mcp_server.py"]
    }
  }
}

Po tym pytanie typu „jakie jest podręcznik postępowania w przypadku wysokiego zużycia pamięci w kontenerach API?” zwraca własny podręcznik zespołu, a nie ogólny wynik wyszukiwania.

Serwery, które już istnieją

Nie wszystko musi być budowane od zera. Oficjalna kolekcja serwerów referencyjnych na GitHubie, znajdująca się w organizacji modelcontextprotocol, obejmuje GitHub (repositoria, prośby o pobranie kodu, problemy i komitety), Postgres, Slack, wyszukiwanie w bazie wiedzy AWS, dostęp do lokalnego systemu plików oraz wyszukiwanie w przeglądarce Brave. Istnieją również serwery stworzone przez społeczność dla Datadog (wskaźniki, tablice kontrolne i monitorowanie), PagerDuty (incydenty i alerty) oraz Jira (zlecenia i sprints). Warto najpierw przejrzeć tę kolekcję, ponieważ ktoś inny mógł już stworzyć to, czego potrzebujemy.

Błędy, których należy unikać

  • Zbyt wczesne przyznanie dostępu do zapisu. Zacznij od trybu tylko do odczytu. Gdy nawiąże się zaufanie, dodawaj narzędzia do zapisu po jednym.
  • Budowa jednego ogromnego serwera. Małe, skoncentrowane serwery są łatwiejsze w utrzymaniu, łatwiejsze do debugowania i łatwiejsze do przekazania konkretnym osobom. k8s-server powinien zawierać tylko narzędzia Kubernetes, aws-server tylko narzędzia AWS, a runbook-server tylko dokumentację.
  • Pomijanie logowania. Rejestruj każde wezwanie narzędzia, w tym to, co zapytał Claude, kiedy i co wróciło, szczególnie przed dodaniem narzędzi do zapisu.
  • Pomijanie zatwierdzenia zapisów. Wszystko, co tworzy ticket, restartuje usługę lub łączy pull request, wymaga kroku potwierdzenia, przy czym Claude musi najpierw pokazać, co zamierza zrobić.

Poranek z prawidłowym ustawieniem

Typowa sesja zaczyna się od zapytania o stan produkcji. Claude sprawdza serwer Kubernetes i podaje informacje na temat implementacji oraz kontenerów, zaznacza kontenery, które są restartowane wielokrotnie, a następnie bada przyczynę problemu.

You: What's the state of production right now?

Claude: [checks kubernetes MCP server]
  3 deployments all healthy, 47/47 pods running.
  One pod in payments namespace has 8 restarts - api-worker-6d4f9b.
  Everything else looks stable.
You: Why is that pod restarting?

Claude: [checks pod logs via MCP]
  Logs show OOMKilled. Memory limit is 256Mi but it's hitting
  248Mi regularly. Looks like it needs a bump to at least 512Mi.
  Want me to check if we have a runbook for this?
You: Yes

Claude: [searches runbook MCP server]
  Found it - "memory-limit-tuning.md"
  The runbook says: increase limit in Helm values,
  run helm upgrade, monitor for 10 minutes.
  Last updated by Rahul on March 12th.
You: What did our AWS bill look like this week?

Claude: [checks AWS MCP server]
  $4,823 this week vs $4,210 last week - up 14.5%.
  Spike is from EC2 in us-east-1. Looks like
  3 r6g.2xlarge instances were added on Tuesday.

Cała ta operacja trwa około 90 sekund, bez zmieniania kart, bez kopiowania i wklejania oraz bez wyszukiwania.

Początek od tego tygodnia

Budowa pięciu serwerów jednocześnie to błąd. Wybierz jeden. Dla większości zespołów DevOps najlepszym pierwszym wyborem jest serwer z instrukcjami, ponieważ jest tylko do odczytu, natychmiast przynosi korzyści i działa z dowolnymi już istniejącymi dokumentami. Proces rozpoczęcia składa się z trzech kroków:

  1. Zbierz instrukcje zespołu do jednej folderu, na przykład ~/runbooks/.
  2. Weź powyższy kod serwera z instrukcjami i skieruj go na ten folder.
  3. Dodaj go do konfiguracji Claude Desktop i uruchom ponownie.

Dzięki temu otrzymujemy asystenta, który zna procedury zespołu. Gdy okaże się niezawodny, dodaj serwer Kubernetes, potem AWS, a następnie wszystko, co pojawi się później.

Szerszy kontekst

MCP jest jeszcze młody, a ekosystem rozwija się szybko, ale kilka rzeczy jest już jasnych. Zespoły posiadające serwery MCP mają znacznie lepsze doświadczenia z AI w porównaniu z zespołami bez nich. Przejście od okna czatu do rozwiązania działającego jak współpracownik operacyjny wymaga jednej rzeczy – serwera, który udostępnia odpowiednie systemy. Zespoły budujące takie rozwiązania obecnie określają standardy wykorzystania AI w pracach infrastrukturalnych. To nie jest projekt badawczy – zespoły używają ich już w produkcji, a powyższy serwer Kubernetes może zostać uruchomiony w ciągu około 20 minut.

Literatura pokrewna

  • Bezpieczne-domyślnie serwery MCP dla Kubernetesa, Kafki i cloud ops — Osiem serwerów MCP umożliwia agentom pracę z Kubernetesem, Kafką, ClickHouse i Azure dzięki domyślnym ustawieniom tylko do odczytu, listom dozwolonych elementów, flagom usuwania, logom audytowym oraz adnotacjom.