Startseite / Artikel

MCP-Server für DevOps: Was sie sind und zwei, die Sie diese Woche bauen können

MCP-Server für DevOps: Sie ermöglichen Claude einen direkten Zugriff auf Kubernetes, AWS sowie Runbooks. Die drei Servertypen, ein nur zum Lesen geeigneter Kubernetes-MCP-Server und häufige Fehler.

Verlag: REACTAPP.TOP10 Min. Lesezeit2253 Wörter

Quellen: What Is an MCP Server and Why Every DevOps Team Should Have One in 2026

MCP, das Model Context Protocol, ist ein offener Standard von Anthropic, der KI-Modellen ermöglicht, sich auf strukturierte Weise mit externen Systemen zu verbinden: Tools, APIs, Datenbanken und Dienste. Ohne ihn befindet sich Claude in einer isolierten Umgebung, wobei jedes Faktum manuell eingefügt werden muss. Mit MCP kann Claude direkt auf diese Systeme zugreifen und sie um Informationen bitten.

Der „Server“ in MCP ist lediglich ein kleines Programm. Es stellt Tools und Daten Claude zur Verfügung, kümmert sich um den Austausch von Informationen und läuft dort, wo sich die Daten befinden – sei es auf einem Laptop, in einem Cluster oder auf einem Server. Es handelt sich dabei nicht um eine aufwändige Infrastruktur; ein funktionsfähiger MCP-Server kann bereits aus einem 50 Zeilen langen Python-Skript bestehen.

Eine hilfreiche Analogie

Die Integrationen in Slack bieten einen guten Vergleich. Davor postete jemand manuell „Deployment in die Produktion gestartet“. Danach postet der CI/CD-Pipeline dies automatisch, und Slack wird zum Zentrum, das über den Fortschritt informiert ist. MCP erledigt das für Claude. Zuvor musste eine Person ihm Fakten über die Infrastruktur liefern. Danach besteht eine direkte Verbindung, sodass Claude selbst nachprüfen kann.

Warum DevOps-Teams am meisten profitieren

Die meisten MCP-Entwicklungen richten sich an Anwendungsentwickler. DevOps-, SRE- und Plattformingenieure profitieren noch stärker, denn ihre gesamte Arbeit besteht darin, Systeme zu verwalten, die bereits APIs haben: Kubernetes, AWS, Datadog oder Grafana, PagerDuty, Terraform Cloud, GitHub, Confluence und Notion. Jedes dieser Systeme kann über MCP mit Claude verbunden werden. Sobald das erledigt ist, muss man nicht mehr zwischen verschiedenen Tools hin- und herwechseln, um Informationen zu sammeln. Die Arbeit besteht dann darin, Fragen zu stellen.

Was Teams mit MCP-Servern machen

  • Fragen zu Clustern. Wenn man fragt, welche Produktivumgebungen länger als 30 Tage ohne Aktualisierung laufen, schickt Claude den Anfrage an den Kubernetes-MCP-Server, der den Cluster abfragt und die Antwort zurückgibt. Es wird weder kubectl verwendet noch Text kopiert und eingefügt.
  • Fragen zu Kosten. Wenn man fragt, welche AWS-Dienste diese Woche im Vergleich zur letzten Woche einen Anstieg gezeigt haben, schickt Claude die Anfrage an einen AWS-Server sowie Cost Explorer, und die Antwort erfolgt in einfachem Englisch.
  • Kontext zu Vorfällen. Eine Frage zu den PagerDuty-Vorfällen der letzten 24 Stunden sowie dazu, ob noch einige offen sind, holt den aktuellen Zustand von einem PagerDuty-Server ab und fasst ihn zusammen.
  • Auskunft aus Runbooks. Wenn man nach dem Runbook zum Thema Erschöpfung des Datenbankverbindungs-Pools fragt, durchsucht Claude die Wissensdatenbank über einen Confluence-Server und gibt das genaue Dokument zurück.
  • Sicherheitsaudits. Wenn man prüft, ob eine Sicherheitsgruppe 0.0.0.0/0 auf Ports außerhalb von 443 erlaubt, scannt ein AWS-Server diese Gruppen und markiert Verstöße.
  • Drei Arten von Servern

    Teams, die damit beginnen, erstellen in der Regel drei Arten von Servern.

    Nur zum Lesen geeignete Überwachungsserver

    Diese verbinden Claude mit Überwachungs- und Infrastruktur-APIs, damit es den aktuellen Zustand beschreiben kann. Typische Beispiele sind ein Zustandsserver für Kubernetes-Cluster, ein Inventarserver für AWS, ein Metriksserver für Datadog und ein Logsserver für CloudWatch. Das Risiko ist sehr gering, da Claude nur lesen kann. Hier sollte man anfangen, um Vertrauen aufzubauen, bevor jeglicher Schreibzugriff gewährt wird.

    Wissensbasisserver

    Diese verknüpfen Claude mit internen Dokumentationen zur Suche und Auswertung: einem Confluence- oder Notion-Runbook-Server, einem GitHub-Wiki-Server, einer Ordnerstruktur mit lokalen Markdown-Dateien oder einer Sammlung von Architekturentscheidungsprotokollen. Das Risiko ist im Grunde genommen null, da es sich um Suchvorgänge in Dokumenten handelt. Sie sind wichtig, weil sie Claude das Gefühl geben, das Unternehmen wirklich zu kennen.

    Aktionsserver

    Diese ermöglichen es Claude, Aufgaben wie die Erstellung von Tickets, das Auslösen von Pipelines oder das Neustarten von Diensten durchzuführen. Beispiele hierfür sind die Erstellung von Jira-Tickets, das Incident-Management über PagerDuty, die Erstellung von GitHub-Pull-Requests sowie Slack-Benachrichtigungen. Das Risiko liegt bei mittlerem bis hohem Niveau, da Claude nun Änderungen vornimmt. Genehmigungsprozesse sollten stets vor Schreibvorgängen stehen, sodass ein Pod-Neustart niemals ohne Bestätigungsschritt erfolgt.

    Erstellung eines Kubernetes-MCP-Servers

    Was folgt, ist ein funktionsfähiger, nur zum Lesen geeigneter Kubernetes-Server in Python. Installieren Sie zunächst das SDK:

    pip install mcp kubernetes --break-system-packages
    

    Speichern Sie anschließend den Server als 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())
    

    Um es mit Claude Desktop zu verbinden, bearbeiten Sie die Konfigurationsdatei, die sich an einem dieser Pfade befindet:

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

    und fügen Sie die Server-Einträge hinzu:

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

    Starten Sie Claude Desktop erneut – dann ist es bereit. Wenn man fragt, wie viele Pods derzeit in Produktion laufen, prüft Claude tatsächlich nach.

    Erstellung eines Runbook-Servers

    Dieser kann sogar noch wertvoller sein. Er weist Claude auf die Runbooks des Teams hin, sodass Claude mitten in einem Incident das richtige Runbook findet, anstatt dass jemand manuell in Confluence suchen muss.

    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())
    

    Fügen Sie es zur Claude Desktop-Konfiguration neben dem anderen Server hinzu:

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

    Danach liefert eine Frage wie „Wie sieht das Handbuch für hohen Speicherverbrauch in den API-Pods aus?“ das eigene Handbuch des Teams und nicht ein allgemeines Suchergebnis.

    Bereits vorhandene Server

    Nicht alles muss von Grund auf entwickelt werden. Die offizielle Sammlung von Referenzservern auf GitHub unter der Organisation modelcontextprotocol umfasst GitHub (Repositorien, Pull Requests, Issues und Commits), Postgres, Slack, eine Suchfunktion in der AWS-Wissensdatenbank, Zugriff auf das lokale Dateisystem sowie die Brave-Websuche. Von der Community erstellte Server gibt es für Datadog (Metriken, Dashboards und Monitore), PagerDuty (Vorfälle und Benachrichtigungen) sowie Jira (Tickets und Sprints). Es lohnt sich, zunächst in dieser Sammlung zu suchen, denn jemand könnte bereits das Gewünschte entwickelt haben.

    Fehler, die vermieden werden sollten

    • Zu früh Schreibzugriff gewähren. Beginnen Sie mit nur Lesezugriff. Erst nachdem Vertrauen aufgebaut wurde, fügen Sie die Schreibwerkzeuge nacheinander hinzu.
    • Ein riesiger Server erstellen. Kleine, fokussierte Server sind einfacher zu warten, zu debuggen und an bestimmte Personen weiterzugeben. Ein k8s-server sollte nur Kubernetes-Werkzeuge enthalten, ein aws-server nur AWS-Werkzeuge und ein runbook-server nur Dokumente.
    • Protokollierung überspringen. Protokollieren Sie jeden Aufruf der Werkzeuge – einschließlich dessen, was Claude abgefragt hat, wann und was zurückgegeben wurde – insbesondere bevor Schreibwerkzeuge hinzugefügt werden.
    • Genehmigung für Schreibvorgänge überspringen. Alles, was ein Ticket erstellt, einen Dienst neu startet oder einen Pull Request zusammenführt, erfordert einen BestätigungsSchritt, wobei Claude zunächst zeigt, was es vorhat zu tun.

    Ein Morgen mit einer ordnungsgemäßen Einrichtung

    Eine typische Sitzung beginnt mit der Abfrage des Produktionszustands. Claude überprüft den Kubernetes-Server und berichtet über Deployments sowie Pods, markiert Pods mit wiederholten Neustarts und kümmert sich anschließend um das Problem.

    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.
    

    Der gesamte Austausch dauert etwa 90 Sekunden – ohne Tabwechsel, ohne Kopieren-Einfügen und ohne Suchen.

    Ab dieser Woche

    Es ist ein Fehler, gleichzeitig fünf Server zu erstellen. Wählen Sie einen aus. Für die meisten DevOps-Teams ist der beste erste Ansatz der Runbook-Server, da er nur zum Lesen dient, sofort Nutzen bringt und mit den bereits vorhandenen Dokumenten funktioniert. Der Start erfolgt in drei Schritten:

    1. Sammeln Sie die Runbooks des Teams in einem einzigen Ordner, z. B. ~/runbooks/.
    2. Nehmen Sie den oben genannten Code für den Runbook-Server und weisen Sie ihn auf diesen Ordner hin.
    3. Fügen Sie ihn in die Konfiguration von Claude Desktop ein und starten Sie neu.

    So erhalten Sie einen Assistenten, der die Verfahren des Teams kennt. Wenn er zuverlässig funktioniert, fügen Sie anschließend den Kubernetes-Server hinzu, dann AWS und so weiter.

    Der größere Überblick

    MCP ist noch jung und das Ökosystem wächst rasant, doch bereits jetzt sind einige Dinge klar. Teams mit MCP-Servern haben eine deutlich andere KI-Erfahrung als Teams ohne solche Server. Um von einem Chatfenster zu etwas überzugehen, das wie ein Teamkollege für die Betriebsabläufe fungiert, ist eines unerlässlich: ein Server, der die richtigen Systeme bereitstellt. Die Teams, die derzeit solche Lösungen entwickeln, legen damit den Standard dafür, wie KI in Infrastrukturarbeiten eingesetzt wird. Es handelt sich dabei nicht um ein Forschungsprojekt – Teams nutzen diese Lösungen bereits in der Produktion, und der oben genannte Kubernetes-Server kann in etwa 20 Minuten in Betrieb genommen werden.

    Verwandte Artikel

  • Sichere MCP-Server „by Default“ für Kubernetes, Kafka und Cloud Ops — Acht MCP-Server ermöglichen es Agenten, mit Kubernetes, Kafka, ClickHouse und Azure unter Verwendung von nur-Lese-Modus, Allowlists, Löschflaggen, Audit-Logs und Anmerkungen zu arbeiten.