Головна / Статті

Сервери MCP для DevOps: що це таке та два, які ви можете створити цього тижня

Сервери MCP для DevOps: надають Claude можливість безпосереднього доступу до Kubernetes, AWS та посібників з роботи. Три типи серверів, сервер MCP Kubernetes лише для читання та поширені помилки.

Видавець: REACTAPP.TOP10 хв читання2253 слів

Джерела: What Is an MCP Server and Why Every DevOps Team Should Have One in 2026

MCP, протокол Model Context Protocol, — це відкритий стандарт від Anthropic, який дозволяє моделям ШІ підключатися до зовнішніх систем у структурованому форматі: інструментів, API, баз даних та сервісів. Без нього Claude функціонує ізольовано, і кожен факт доводиться вводити вручну. З ним Claude може безпосередньо звертатися до цих систем.

„Сервер“ у MCP — це просто невелика програма. Вона надає Claude доступ до інструментів та даних, обробляє взаємодію та працює там, де знаходяться дані, чи то у ноутбуці, кластері чи сервері. Це не важка інфраструктура, і працездатний сервер MCP може бути скриптом на Python із 50 рядків.

Аналогія, яка допомагає

Інтеграції з Slack є гарним прикладом для порівняння. Раніше хтось вручну публікував повідомлення „Розгортання у продакшн розпочалося“. Після впровадження цих інтеграцій процес CI/CD автоматично публікує такі повідомлення, а Slack стає центром, який відстежує всі події. MCP робить те саме для Claude. Раніше людина сама надавала йому інформацію про інфраструктуру. Тепер він має пряме з’єднання та може перевіряти інформацію самостійно.

Чому команди DevOps отримують найбільшу користь

Більшість прикладів використання MCP орієнтовані на розробників додатків. Однак DevOps-інженери, SRE та інженери платформ отримують ще більшу користь, адже їхня робота полягає у керуванні системами, які вже мають API: Kubernetes, AWS, Datadog чи Grafana, PagerDuty, Terraform Cloud, GitHub, Confluence та Notion. Кожну з цих систем можна під’єднати до Claude за допомогою MCP. Після цього не потрібно більше перемикатися між інструментами для збору інформації — достатньо лише ставити запитання.

Як команди використовують сервери MCP

  • Питання щодо кластерів. Якщо запитати, які продакшн-розгортання працюють вже понад 30 днів без оновлень, Claude звертається до сервера MCP Kubernetes, який запитує кластер та повертає відповідь. Для цього не потрібні інструменти kubectl чи копіювання-вставка.
  • Питання щодо витрат. Якщо запитати, які сервіси AWS мали пік використання цього тижня порівняно з минулим, Claude звертається до сервера AWS та інструменту Cost Explorer, і відповідь надається у простій англійській мові.
  • Контекст інцидентів. Запит про інциденти у PagerDuty за останні 24 години та про те, чи є серед них ще відкриті, отримує поточний стан з сервера PagerDuty та узагальнює його.
  • Пошук інструкцій. Якщо запитати про інструкції щодо вичерпання пулу з’єднань до бази даних, система шукає в базі знань через сервер Confluence та повертає саме цей документ.
  • Аудити безпеки. Питання про те, чи дозволяє якась група безпеки доступ з 0.0.0.0/0 на портах, відмінних від 443, змушує сервер AWS перевірити ці групи та виявити порушення.
  • Три типи серверів

    Команди, які починають роботу, зазвичай створюють три типи серверів.

    Сервери для спостереження лише для читання

    Вони з’єднують Claude з API моніторингу та інфраструктури, щоб він міг описувати поточний стан речей. Типовими прикладами є сервер стану кластера Kubernetes, сервер інвентаризації AWS, сервер метрик Datadog та сервер журналів CloudWatch. Ризик дуже низький, оскільки Claude може лише читати. Це ідеальна точка початку, яка допомагає збудувати довіру перед наданням будь-якого права на запис.

    Сервери бази знань

    Ці посилання пов’язують Claude з внутрішньою документацією для пошуку та отримання інформації: сервером інструкцій у Confluence чи Notion, сервером wiki у GitHub, папкою з локальними файлами у форматі markdown або базою даних записів про архітектурні рішення. Ризик практично дорівнює нулю, оскільки йдеться про пошук у документах. Вони мають значення, тому що дають Claude відчуття, ніби він справді знає компанію.

    Сервери дій

    Вони дозволяють Claude виконувати такі завдання, як створення заявок, запуск пайплайнів чи перезапуск сервісів. Прикладами є створення заявок у Jira, керування інцидентами у PagerDuty, створення pull-запитів у GitHub та сповіщення у Slack. Ризик коливається від середнього до високого, оскільки Claude тепер змінює щось у системі. Механізми схвалення завжди мають бути перед операціями запису, тож перезапуск поду ніколи не відбувається без кроку підтвердження.

    Створення сервера MCP для Kubernetes

    Далі наведено працюючий сервер Kubernetes тільки для читання на Python. Спочатку встановіть SDK:

    pip install mcp kubernetes --break-system-packages
    

    Потім збережіть сервер як 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())
    

    Щоб під’єднати його до Claude Desktop, відредагуйте файл конфігурації, який знаходиться за однією з цих адрес:

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

    і додайте запис сервера:

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

    Перезапустіть Claude Desktop — і він буде готовий. Запит про те, скільки подів зараз працює в продакшені, змушує Claude фактично перевірити це.

    Створення сервера з інструкціями

    Цей інструмент може бути ще кориснішим. Він спрямовує Claude до командних інструкцій, тож під час інциденту Claude знаходить потрібні інструкції замість того, щоб хтось вручну шукав їх у 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())
    

    Додайте його до конфігурації Claude Desktop разом з іншим сервером:

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

    Після цього запит на кшталт "який посібник щодо високого споживання пам’яті в API-подах?" повертає власний посібник команди, а не загальний результат пошуку.

    Вже існуючі сервери

    Не все потрібно створювати з нуля. Офіційна колекція серверів-зразків на GitHub у організації modelcontextprotocol охоплює GitHub (репозиторії, запити на з’єднання, проблеми та комітти), Postgres, Slack, пошук у базі знань AWS, доступ до локальної файлової системи та пошук у Brave. Існують сервери, створені спільнотою, для Datadog (метрики, панелі керування та моніторинг), PagerDuty (інциденти та сповіщення) та Jira (замовлення та сесії роботи). Варто спочатку ознайомитися з цією колекцією, адже хтось може вже створити потрібне.

    Помилки, яких слід уникати

    • Надання прав на запис занадто рано. Почніть із режиму лише для читання. Після того, як буде досягнута довіра, додавайте інструменти для запису по одному.
    • Створення одного величезного сервера. Маленькі, спеціалізовані сервери легше підтримувати, їх легше відлагоджувати та передавати конкретним особам. k8s-server має містити лише інструменти Kubernetes, aws-server — лише інструменти AWS, а runbook-server — лише документацію.
    • Ігнорування журналізації. Фіксуйте кожен виклик інструменту, включаючи те, що запитував Claude, коли це відбулося та що було отримано у відповідь, особливо до того, як з’являться інструменти для запису.
    • Ігнорування схвалення запису. Будь-яка дія, яка створює заявку, перезапускає сервіс чи об’єднує pull request, потребує кроку підтвердження, під час якого Claude спочатку показує, що саме збирається зробити.

    Ранок із належною налаштованістю

    Типова сесія починається з запиту про стан виробництва. Claude перевіряє сервер Kubernetes та надає інформацію про розгортання та поди, позначає поди з повторними перезапусками, а потім продовжує працювати над проблемою.

    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.
    

    Увесь цей процес займає близько 90 секунд без перемикання вкладок, без копіювання-вставки та без пошуку.

    Починаючи з цього тижня

    Створення п’яти серверів одночасно — це помилка. Виберіть один. Для більшості команд DevOps найкращим першим варіантом є сервер з посібниками, оскільки він є лише для читання, негайно приносить користь та працює з будь-якими вже наявними документами. Для початку потрібно виконати три кроки:

    1. Зіберіть посібники команди в одну папку, наприклад ~/runbooks/.
    2. Візьміть наведений вище код сервера з посібниками та налаштуйте його на цю папку.
    3. Додайте його до конфігурації Claude Desktop та перезапустіть її.

    Це дасть асистента, який знає процедури команди. Коли він підтвердить свою надійність, додайте сервер Kubernetes, потім AWS, а потім все інше.

    Більш широка картина

    MCP ще молодий, і екосистема швидко розвивається, але деякі речі вже стали зрозумілими. Команди, які мають сервери MCP, мають значно інший досвід роботи з ШІ порівняно з тими, у кого його немає. Щоб перейти від чат-вікна до системи, яка функціонує як колега з операційного забезпечення, потрібен один ключовий елемент — сервер, який надає доступ до потрібних систем. Команди, які зараз створюють такі рішення, визначають стандарти використання ШІ у роботі з інфраструктурою. Це не дослідницький проєкт — команди вже використовують їх у продакшені, а сервер Kubernetes може почати працювати приблизно за 20 хвилин.

    Пов’язана література

  • MCP-сервери з безпекою за замовчуванням для Kubernetes, Kafka та Cloud Ops — Вісім MCP-серверів дозволяють агентам працювати з Kubernetes, Kafka, ClickHouse та Azure за замовчуванням у режимі лише для читання, за допомогою списків дозволів, прапорців видалення, журналів аудиту та анотацій.