首页 / 文章

DevOps领域的MCP服务器:什么是以及本周即可搭建的两种类型

DevOps用MCP服务器:让Claude能够实时访问Kubernetes、AWS及运行手册。这三种服务器类型,以及只读型Kubernetes MCP服务器和常见错误。

发布者:REACTAPP.TOP阅读约 10 分钟2253 词

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

MCP,即模型上下文协议,是Anthropic推出的一项开放标准,它让人工智能模型能够以结构化的方式连接到外部系统:各类工具、API、数据库和服务。没有它,Claude就如同被限制在封闭空间中,所有信息都必须手动输入;而有了它,Claude可以直接向外部的系统发起请求。

MCP中的“服务器”其实只是一个小型程序。它负责向Claude提供工具和数据,处理数据之间的交互,并在数据所在的位置运行,无论是笔记本电脑、计算集群还是服务器均可。这并非复杂的基础设施,一个可用的MCP服务器甚至可以是只有50行的Python脚本。

一个有助于理解的类比

Slack集成是一个很好的对比例子。在引入这些集成之前,需要手动发布“已开始部署到生产环境”的消息;引入之后,CI/CD流水线会自动发布该消息,而Slack则成为掌握实时情况的中心平台。MCP也为Claude实现了类似功能:之前需要人工向它输入基础设施的相关信息,之后则可以通过直接连接自行查询。

为何DevOps团队受益最多

大多数MCP相关开发都是针对应用程序开发者的。而DevOps工程师、SRE工程师以及平台工程师能获得更大收益,因为他们的工作就是管理那些已经具备API的系统,比如Kubernetes、AWS、Datadog或Grafana、PagerDuty、Terraform Cloud、GitHub、Confluence和Notion。这些系统都可以通过MCP与Claude相连。一旦实现连接,就无需在各种工具之间来回切换来收集信息,工作方式转变为直接提出问题即可。

团队如何使用MCP服务器

  • 集群相关问题。询问哪些生产环境部署已运行30天以上且未进行过更新时,Claude会连接到Kubernetes MCP服务器,该服务器查询集群后返回答案。无需使用kubectl,也无需复制粘贴。
  • 成本相关问题。询问本周与上周相比哪些AWS服务的使用量激增时,Claude会连接到AWS服务器及Cost Explorer,随后以通俗易懂的英文形式给出分析结果。
  • 事件背景信息。询问过去24小时内PagerDuty记录的事件以及是否有未处理的事件时,系统会从PagerDuty服务器获取当前状态并进行汇总。
  • 运行手册查询。询问关于数据库连接池耗尽的解决方案时,系统会通过Confluence服务器在知识库中搜索并返回对应的文档。
  • 安全审计。通过检查是否有安全组允许在443端口之外的端口上使用0.0.0.0/0,可让AWS服务器扫描这些组并标记违规项。
  • 三种类型的服务器

    通常,团队最终会构建三种类型的服务器。

    只读监控服务器

    这类服务器将Claude与监控及基础设施API相连,使其能够描述当前的状态。典型例子包括Kubernetes集群状态服务器、AWS资产清单服务器、Datadog指标服务器以及CloudWatch日志服务器。由于Claude仅能读取数据,因此风险极低。这是最佳的起点,在授予任何写入权限之前可先建立信任。

    知识库服务器

    这些链接将 Claude 与用于搜索和检索的内部文档相连:Confluence 或 Notion 的操作手册服务器、GitHub Wiki 服务器、本地 Markdown 文件夹,或是架构决策记录的存储库。由于是对文档进行搜索,其风险实际上为零。它们之所以重要,是因为能让 Claude 产生仿佛真正了解公司的感觉。

    操作服务器

    这类服务器能让 Claude 执行创建工单、触发流水线或重启服务等操作。示例包括创建 Jira 工单、管理 PagerDuty 事件、创建 GitHub pull request 以及发送 Slack 通知。由于 Claude 现在正在修改某些内容,其风险属于中等到高水平。所有写操作都应设置审批流程,因此没有确认步骤的话,绝不能重启容器。

    构建 Kubernetes MCP 服务器

    以下是一个用 Python 编写的可运行的只读 Kubernetes 服务器。请先安装 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 后即可使用。当询问当前生产环境中运行了多少个 Pod 时,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查询了什么、何时查询以及得到了什么结果,尤其是在添加写入工具之前。
    • 跳过写入操作的审批流程。任何会创建工单、重启服务或合并拉取请求的操作都应经过确认步骤,且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服务器的团队所获得的AI体验与没有该服务器的团队存在显著差异。要从单纯的聊天窗口转变为类似操作团队的工具,关键在于拥有能够提供所需系统的服务器。而目前正在构建这类系统的团队,正在为AI在基础设施工作中的应用树立典范。这并非研究项目——各团队如今已在实际环境中使用它们,上述Kubernetes服务器大约20分钟即可启动运行。

    相关阅读

  • Kubernetes、Kafka及云运维领域的默认安全MCP服务器 — 八种MCP服务器通过只读模式、允许列表、删除标记、审计日志和注释等功能,让代理能够安全地操作Kubernetes、Kafka、ClickHouse及Azure系统。