1. 项目概述当Claude遇见Kubernetes最近我完成了一个挺有意思的“缝合”项目让Claude也就是那个AI助手能够直接操作我的Kubernetes集群。听起来有点科幻对吧一个聊天窗口里的AI能帮你执行kubectl get pods、部署应用甚至排查线上问题。这可不是简单的“AI帮你写命令然后你复制粘贴”而是构建了一个名为MCPModel Context Protocol服务器的桥梁让Claude获得了对K8s集群的“直接控制权”。这个项目的核心价值在于它极大地简化了日常的K8s运维和开发工作流。想象一下你不再需要记忆复杂的kubectl命令语法或者在不同终端、配置文件之间来回切换。你只需要用自然语言告诉Claude“看看生产环境nginx服务的日志最近5分钟有没有错误”或者“把frontend应用的镜像版本升级到v2.1.0并滚动更新。”剩下的Claude会通过MCP服务器理解你的意图安全地执行相应的操作并把结果清晰地反馈给你。它特别适合几类人日常需要与K8s打交道的开发者可以告别命令手册团队负责人或架构师可以通过更直观的方式了解集群状态甚至是刚开始接触云原生的新手能通过对话式交互降低学习门槛。当然它的前提是你对Claude和Kubernetes都有基本的了解并且清楚在授予AI权限时需要谨慎行事。接下来我就把这个从构思到实现的完整过程包括背后的设计思路、踩过的坑和实际效果详细拆解一遍。2. 核心架构与设计思路拆解2.1 为什么是MCP首先得搞清楚我们为什么要用MCPModel Context Protocol来实现这个功能而不是其他方式比如直接给Claude开放一个SSH终端或者调用K8s API的封装脚本。MCP是Anthropic推出的一套协议专门设计用来让AI模型如Claude能够安全、结构化地访问和使用外部工具、数据源。你可以把它理解为AI的“插件”或“驱动”标准。它的核心优势在于声明式工具定义MCP要求服务器以结构化的方式比如JSON Schema声明自己提供哪些“工具”Tools和“资源”Resources。这就像给Claude一本清晰的说明书告诉它“我能帮你做A、B、C三件事做A需要你提供X和Y两个参数”。这种结构化的描述比让AI去理解一个自由文本的API文档要可靠得多。安全的执行沙箱AI模型Claude本身不直接执行代码。它只是根据对话生成一个符合MCP协议格式的“工具调用请求”。这个请求会被发送到MCP服务器由服务器这个受我们完全控制的实体来执行实际的操作比如调用kubectl。AI永远碰不到你的服务器权限这从根本上隔离了风险。双向数据流MCP不仅支持AI调用工具执行命令还支持服务器主动向AI提供“资源”Resources。比如我可以配置MCP服务器将集群的节点列表、命名空间清单作为“只读资源”暴露给Claude。这样Claude在回答关于集群的问题时就有了实时的数据上下文回答会更准确。相比之下给AI一个终端是极其危险且低效的而简单的API封装又缺乏结构化的交互能力和上下文提供能力。因此MCP是实现“AI可控地操作复杂系统”的理想中间层。2.2 整体架构设计基于MCP协议我设计的系统架构非常清晰主要包含三个部分ClaudeAI客户端用户与之交互的界面。用户用自然语言提出需求Claude理解后决定是否需要以及如何调用MCP服务器提供的工具。MCP服务器核心桥梁这是本项目的核心。它是一个独立的进程通常用Python或Node.js编写。它需要做几件事实现MCP协议按照MCP的规范实现服务器端能够处理来自Claude的连接、工具调用请求和资源查询请求。封装K8s操作内部集成Kubernetes客户端库如kubernetesPython包或kubernetes/client-node将kubectl命令对应的功能转化为安全的函数。声明工具和资源向Claude宣告“我提供了list_podsget_logsdeploy_application等工具以及/nodes/namespaces等资源。”Kubernetes集群最终被操作的对象。MCP服务器通过标准的Kubeconfig文件或ServiceAccount来认证和授权与集群的API Server进行通信。数据流是这样的用户问Claude - Claude生成工具调用请求 - 请求发送至MCP服务器 - 服务器解析请求调用对应的K8s客户端函数 - 函数与K8s API Server交互 - 获取结果 - 服务器将结果格式化为MCP响应 - 返回给Claude - Claude将结果组织成自然语言回复给用户。安全设计是这个架构的重中之重。我的原则是“最小权限原则”和“操作白名单”。MCP服务器进程本身以受限的权限运行其绑定的K8s ServiceAccount或Kubeconfig只有完成声明工具所必需的最低权限例如只有某个命名空间的读写权而非集群管理员权限。在MCP服务器内部我只暴露经过深思熟虑的、安全的工具。例如我绝不会暴露一个exec_into_pod进入Pod执行任意命令这样的高危工具。取而代之的是我会提供更安全的get_logs获取日志或describe_pod描述Pod详情。所有工具在执行前都可以加入参数校验和审计日志。3. 技术选型与实现细节3.1 MCP服务器框架选择实现MCP服务器首先得选一个趁手的SDK。目前社区有几个选择官方TypeScript SDK (modelcontextprotocol/sdk)由Anthropic维护功能最全更新最及时文档也相对完善。对于熟悉Node.js/TypeScript的开发者来说是首选。社区Python SDK有一些社区实现的Python版本例如mcp。如果你更习惯Python生态或者想利用Python丰富的K8s客户端库kubernetes这是一个不错的选择。我最终选择了官方的TypeScript SDK。主要原因有三点一是官方维护稳定性和未来兼容性有保障二是TypeScript的类型系统能在开发阶段就帮我规避很多协议格式错误三是Node.js的异步非阻塞模型很适合处理这种网络IO密集型的服务器应用。注意选择SDK时务必关注其与MCP协议版本的兼容性。协议本身还在演进中使用最新稳定版的SDK能避免一些潜在的兼容性问题。3.2 Kubernetes客户端集成在MCP服务器内部我们需要一个库来与真实的K8s集群通信。这里不能直接调用kubectl命令行因为那样会引入新的依赖和进程管理复杂度。应该使用编程式的客户端库。对于TypeScript/Node.js项目kubernetes/client-node是官方选择。它功能完整但API略显底层配置起来需要一些步骤。对于Python项目kubernetes这个PyPI包是标准选择它是对官方Go客户端的Python绑定非常强大。我使用的是kubernetes/client-node。它的初始化通常需要加载kubeconfig文件。为了安全我让MCP服务器从环境变量KUBECONFIG指定的路径读取配置或者支持使用内嵌的ServiceAccount token如果在Pod中运行。这样权限管理就完全依赖于K8s自身的RBAC机制。import * as k8s from kubernetes/client-node; export function createK8sApiClient(): k8s.CoreV1Api { const kc new k8s.KubeConfig(); // 优先从环境变量读取否则使用默认路径 const kubeConfigPath process.env.KUBECONFIG || k8s.KubeConfig.DEFAULT_KUBECONFIG_PATH; try { kc.loadFromFile(kubeConfigPath); } catch (error) { // 如果文件加载失败尝试使用集群内ServiceAccount如果在Pod中运行 kc.loadFromCluster(); } return kc.makeApiClient(k8s.CoreV1Api); }3.3 核心工具Tools设计与实现MCP的核心是工具。我设计的第一批工具主要围绕“查看”和“有限制的变更”两类操作遵循从简到繁、从只读到写的原则。1. 只读类工具用于获取信息list_pods: 列出指定命名空间下的所有Pod。这是最基础的工具。参数namespace(可选默认是default)。实现调用k8sApi.listNamespacedPod(namespace)。设计考量返回的信息需要精简和格式化。我不需要把Pod的整个status对象都扔给Claude那样会浪费上下文长度。我只提取name,status.phase,status.startTime等关键字段并格式化为清晰的Markdown表格或列表。这能显著提升Claude处理和分析信息的效率。get_pod_logs: 获取特定Pod的日志。参数podName,namespace(可选),tailLines(可选例如最近100行)。实现调用k8sApi.readNamespacedPodLog(podName, namespace, containerName, tailLines)。注意事项这里有个坑如果一个Pod有多个容器initContainers或多容器Pod需要指定containerName参数。我的工具实现里如果用户不指定默认获取第一个应用容器非initContainer的日志并在返回结果中提示用户该Pod包含的其他容器。2. 变更类工具需谨慎设计deploy_from_yaml: 通过YAML文件部署或更新资源。这是最灵活但也最需要管控的工具。参数yamlContent(字符串包含完整的K8s资源YAML定义)。实现使用k8s.KubernetesObjectApi的create或patch方法。安全加固权限校验MCP服务器使用的ServiceAccount必须被严格限制例如只能在一个特定的命名空间内创建Deployment,Service等资源。内容校验在应用YAML前可以也应该进行简单的校验比如检查namespace字段是否与允许的命名空间匹配或者使用K8s的DryRun特性先模拟运行一次看是否有语法或权限错误。审计日志任何创建/更新操作都必须记录详细的审计日志包括操作者可通过MCP会话标识、时间、操作的资源类型和名称。这为事后追溯提供了依据。scale_deployment: 伸缩Deployment的副本数。这是一个相对安全的变更操作因为它的影响范围明确。参数deploymentName,namespace,replicas(目标副本数)。实现调用k8s.AppsV1Api.patchNamespacedDeployment更新spec.replicas字段。3.4 资源Resources暴露策略除了工具MCP的“资源”特性也很有用。资源是服务器主动提供给AI的只读数据可以丰富AI的上下文。我设计了几个静态和动态资源resource://kubernetes/namespaces: 提供当前集群所有可访问的命名空间列表。这能帮助Claude在用户提问“列出所有Pod”时知道应该先询问用户要哪个命名空间或者提供一个列表供用户选择。resource://kubernetes/cluster-info: 提供集群的基本信息如Kubernetes版本、API Server地址等。这些信息很少变动可以作为静态资源提供。resource://kubernetes/nodes?readytrue: 这是一个动态资源的例子。它可能对应一个URI模板。当Claude请求这个资源时服务器会实时调用K8s API获取所有状态为Ready的节点列表并返回。这确保了Claude掌握的信息是最新的。资源的设计关键在于粒度。不宜一次性提供海量数据如所有Pod的所有信息而应该提供摘要或索引。例如/namespaces资源只返回名称列表而不是每个命名空间的详情。当Claude需要详情时它会去调用相应的工具如list_pods。4. 开发与部署实操全记录4.1 项目初始化与环境搭建首先创建一个新的Node.js项目。mkdir claude-k8s-mcp-server cd claude-k8s-mcp-server npm init -y安装核心依赖npm install modelcontextprotocol/sdk kubernetes/client-node npm install --save-dev typescript ts-node types/node初始化TypeScript配置npx tsc --init在tsconfig.json中确保设置target: ES2022,module: NodeNext,moduleResolution: NodeNext并启用严格模式。4.2 MCP服务器核心代码实现创建一个src/server.ts文件开始构建服务器。import { Server } from modelcontextprotocol/sdk/server/index.js; import { StdioServerTransport } from modelcontextprotocol/sdk/server/stdio.js; import { CallToolRequestSchema, ListToolsRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema, } from modelcontextprotocol/sdk/types.js; import * as k8s from kubernetes/client-node; import { z } from zod; // 用于参数校验强烈推荐安装 zod 或 ajv // 1. 创建K8s API客户端使用上一节的函数 const k8sApi createK8sApiClient(); const appsApi k8sApi.makeApiClient(k8s.AppsV1Api); // 用于Deployment操作 // 2. 创建MCP服务器实例 const server new Server( { name: claude-k8s-mcp-server, version: 0.1.0, }, { capabilities: { tools: {}, // 声明我们支持工具 resources: {}, // 声明我们支持资源 }, } ); // 3. 声明工具列表 server.setRequestHandler(ListToolsRequestSchema, async () { return { tools: [ { name: list_pods, description: List all pods in a specific namespace., inputSchema: { type: object, properties: { namespace: { type: string, description: The Kubernetes namespace. Defaults to default., }, }, }, }, { name: get_pod_logs, description: Get logs from a specific pod., inputSchema: { type: object, properties: { podName: { type: string, description: The name of the pod., required: true, }, namespace: { type: string, description: The namespace of the pod. Defaults to default., }, tailLines: { type: number, description: Number of lines from the end of the logs to retrieve., }, }, required: [podName], }, }, { name: scale_deployment, description: Scale a deployment to a specific number of replicas., inputSchema: { type: object, properties: { deploymentName: { type: string, description: The name of the deployment., required: true, }, namespace: { type: string, description: The namespace of the deployment. Defaults to default., }, replicas: { type: number, description: The desired number of replicas., required: true, }, }, required: [deploymentName, replicas], }, }, // ... 可以继续添加更多工具 ], }; }); // 4. 实现工具调用处理器 server.setRequestHandler(CallToolRequestSchema, async (request) { const { name, arguments: args } request.params; try { switch (name) { case list_pods: { const namespace (args as any).namespace || default; const response await k8sApi.listNamespacedPod(namespace); const pods response.body.items.map(pod ({ name: pod.metadata?.name, namespace: pod.metadata?.namespace, status: pod.status?.phase, node: pod.spec?.nodeName, age: pod.metadata?.creationTimestamp, })); // 格式化输出便于Claude阅读 const formattedOutput pods.map(p - ${p.name} (Status: ${p.status}, Node: ${p.node})).join(\n); return { content: [ { type: text, text: Pods in namespace **${namespace}**:\n${formattedOutput}, }, ], }; } case get_pod_logs: { const { podName, namespace default, tailLines } args as any; const response await k8sApi.readNamespacedPodLog( podName, namespace, undefined, // containerName这里简化处理取第一个容器 undefined, // follow undefined, // previous undefined, // sinceSeconds tailLines, // tailLines undefined, // timestamps undefined, // limitBytes undefined, // insecureSkipTLSVerifyBackend ); return { content: [ { type: text, text: Logs for pod **${podName}** in namespace **${namespace}**:\n\\\\n${response.body}\n\\\, }, ], }; } case scale_deployment: { const { deploymentName, namespace default, replicas } args as any; // 构造patch数据 const patch [ { op: replace, path: /spec/replicas, value: replicas, }, ]; // 在实际生产环境中这里应该加入权限校验和审计日志 console.log([AUDIT] Scaling deployment ${namespace}/${deploymentName} to ${replicas} replicas); const response await appsApi.patchNamespacedDeployment( deploymentName, namespace, patch, undefined, // pretty undefined, // dryRun undefined, // fieldManager undefined, // fieldValidation { headers: { Content-Type: application/json-patchjson } } ); return { content: [ { type: text, text: Successfully scaled deployment **${deploymentName}** in namespace **${namespace}** to **${replicas}** replicas., }, ], }; } default: throw new Error(Unknown tool: ${name}); } } catch (error: any) { // 错误处理非常重要要给Claude清晰的错误信息 const errorMessage error.body?.message || error.message || Unknown error; return { content: [ { type: text, text: Error executing tool **${name}**: ${errorMessage}, }, ], isError: true, }; } }); // 5. 声明和实现资源示例列出命名空间 server.setRequestHandler(ListResourcesRequestSchema, async () { return { resources: [ { uri: resource://kubernetes/namespaces, name: Kubernetes Namespaces, description: A list of all accessible namespaces in the cluster., mimeType: application/json, }, ], }; }); server.setRequestHandler(ReadResourceRequestSchema, async (request) { const { uri } request.params; if (uri resource://kubernetes/namespaces) { try { const response await k8sApi.listNamespace(); const namespaceList response.body.items.map(ns ns.metadata?.name).filter(Boolean); return { contents: [ { uri, mimeType: application/json, text: JSON.stringify({ namespaces: namespaceList }, null, 2), }, ], }; } catch (error: any) { return { contents: [ { uri, mimeType: text/plain, text: Error fetching namespaces: ${error.message}, }, ], }; } } // 处理其他资源URI... throw new Error(Resource not found: ${uri}); }); // 6. 启动服务器使用stdio传输这是与Claude Desktop等客户端通信的标准方式 async function main() { const transport new StdioServerTransport(); await server.connect(transport); console.error(Claude K8s MCP Server running on stdio...); } main().catch(console.error);4.3 配置Claude Desktop连接MCP服务器要让Claude Desktop能使用我们的服务器需要创建一个配置文件。Claude Desktop的MCP配置通常位于~/Library/Application Support/Claude/claude_desktop_config.json(macOS) 或类似位置。{ mcpServers: { kubernetes: { command: node, args: [ /absolute/path/to/your/project/dist/server.js // 指向编译后的JS文件 ], env: { KUBECONFIG: /path/to/your/kubeconfig // 可选如果服务器需要 } } } }关键点command必须是node因为我们的服务器是Node.js应用。args必须指向编译后的JavaScript入口文件。你需要先用tsc或npm run build将TypeScript编译成JS。env可以用来传递环境变量比如KUBECONFIG路径。这是一个非常重要的安全实践不要在代码中硬编码敏感路径而是通过环境变量注入。配置完成后重启Claude Desktop。如果一切正常在Claude的输入框里你应该能看到一个微小的“插件”图标被点亮或者当你输入“/”时能出现可用的工具列表。你可以尝试问它“你能用Kubernetes工具帮我列出default命名空间下的Pod吗”4.4 将MCP服务器部署为K8s内部服务可选但推荐为了让这个MCP服务器更稳定、更容易管理特别是如果你想在团队内共享可以将其容器化并部署到K8s集群内部。1. 编写DockerfileFROM node:18-alpine AS builder WORKDIR /app COPY package*.json ./ RUN npm ci --onlyproduction COPY . . RUN npm run build # 假设你的package.json里有build脚本运行tsc FROM node:18-alpine WORKDIR /app COPY --frombuilder /app/dist ./dist COPY --frombuilder /app/node_modules ./node_modules COPY package.json ./ # 以非root用户运行 USER node EXPOSE 3000 # 如果需要HTTP传输可以暴露端口。Stdio模式不需要。 CMD [node, dist/server.js]2. 创建K8s部署清单创建一个deployment.yaml关键点在于ServiceAccount的配置。apiVersion: v1 kind: ServiceAccount metadata: name: claude-mcp-server namespace: mcp-system # 建议创建一个独立的命名空间 --- apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: name: claude-mcp-server-role namespace: mcp-system rules: - apiGroups: [] resources: [pods, pods/log, namespaces] verbs: [get, list, watch] - apiGroups: [apps] resources: [deployments, deployments/scale] verbs: [get, list, patch] # 根据你暴露的工具精细配置权限 --- apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: name: claude-mcp-server-binding namespace: mcp-system roleRef: apiGroup: rbac.authorization.k8s.io kind: Role name: claude-mcp-server-role subjects: - kind: ServiceAccount name: claude-mcp-server namespace: mcp-system --- apiVersion: apps/v1 kind: Deployment metadata: name: claude-mcp-server namespace: mcp-system spec: replicas: 1 selector: matchLabels: app: claude-mcp-server template: metadata: labels: app: claude-mcp-server spec: serviceAccountName: claude-mcp-server # 使用上面创建的SA containers: - name: server image: your-registry/claude-k8s-mcp-server:latest imagePullPolicy: Always # Stdio模式不需要暴露端口但可以暴露一个健康检查端口 livenessProbe: exec: command: [node, -e, console.log(OK)] initialDelaySeconds: 30 periodSeconds: 10 resources: requests: memory: 128Mi cpu: 100m limits: memory: 256Mi cpu: 200m3. 调整Claude Desktop配置如果MCP服务器运行在集群内Claude Desktop运行在你的本地机器无法直接通过stdio连接到Pod里的进程。这时你需要使用MCP的另一种传输方式HTTP或SSH。更常见的做法是在本地开发和测试时使用stdio在生产或团队共享时将MCP服务器部署为一个HTTP服务并配置Claude Desktop通过HTTP连接。这需要对服务器代码进行改造实现HTTP传输并考虑认证如API Key和网络可达性。5. 安全、权限与最佳实践5.1 最小权限原则Principle of Least Privilege, POLP这是本项目的生命线。绝对不要给MCP服务器绑定cluster-admin这样的集群管理员权限。创建专属ServiceAccount如上文所示为MCP服务器创建一个独立的ServiceAccount。定义精细的RBAC角色仔细分析你暴露的每一个工具需要哪些API权限。使用kubectl auth can-i --list命令可以帮助你验证一个ServiceAccount的权限。只授予完成工具功能所必需的最小权限集合。list_pods需要pods资源的list,get权限。get_pod_logs需要pods/log资源的get权限。scale_deployment需要deployments/scale资源的patch权限。限制命名空间通过RoleBinding将权限限定在特定的命名空间内例如mcp-system和你的应用命名空间而不是整个集群的ClusterRoleBinding。5.2 输入验证与清理永远不要相信来自AI的输入。虽然Claude通常很可靠但MCP服务器作为一个开放接口必须对输入进行严格校验。使用Schema校验库在工具的实现中使用像zod或ajv这样的库对request.params.arguments进行严格的模式校验。确保参数类型正确、范围合理例如replicas不能是负数或极大值。防范注入攻击特别是对于deploy_from_yaml这样的工具要警惕YAML内容中可能包含的恶意指令。虽然K8s API Server本身会做校验但在服务器端也可以进行初步的语法检查和字段白名单过滤。资源名称规范校验podName,deploymentName等参数是否符合K8s的命名规范小写字母、数字、-防止潜在的路径遍历或其他攻击。5.3 审计与日志所有通过MCP服务器执行的操作都必须有迹可循。操作审计在服务器代码中每个工具函数执行前后都记录结构化日志。至少包括时间戳、工具名称、调用参数可脱敏、执行结果成功/失败、以及一个唯一的会话或请求ID。这些日志可以输出到标准错误console.error或发送到专门的日志聚合系统如Loki、Elasticsearch。K8s审计日志确保你的Kubernetes集群启用了审计日志Audit Logging。这样所有通过MCP服务器发出的API请求都会在K8s层面留下记录与服务器端的日志相互印证。5.4 性能与可靠性考量连接管理MCP服务器与K8s API Server的连接应该复用。避免在每次工具调用时都创建新的客户端。使用单例模式或依赖注入来管理K8s客户端实例。超时与重试对K8s API的调用设置合理的超时时间并实现简单的重试逻辑针对网络抖动或API Server临时不可用。但要注意对于写操作如scale重试需要是幂等的。资源限制限制MCP服务器容器本身的内存和CPU资源防止其异常时影响节点。同时对于返回大量数据的工具如get_pod_logswithouttailLines要设置默认的行数限制或大小限制避免拖垮服务器或占满Claude的上下文窗口。6. 典型使用场景与效果演示6.1 场景一日常运维巡检用户“帮我看看production命名空间里所有不是Running状态的Pod。”Claude交互过程Claude识别出这是一个关于K8s Pod状态查询的请求。它首先调用resource://kubernetes/namespaces资源确认production命名空间存在或者直接假设存在如果资源未提供此信息它可能会先询问用户。然后调用list_pods工具传入namespace: production。收到Pod列表后Claude在本地在其上下文中进行过滤找出status不是Running的Pod。它组织语言回复“在production命名空间中我发现以下Pod不处于Running状态web-app-7cbbf6c5f8-abcde(状态:Pending),redis-master-0(状态:CrashLoopBackOff)。需要我进一步查看某个Pod的日志或事件吗”价值将需要记忆命令和手动过滤的过程简化为一句自然语言。效率提升显著。6.2 场景二应用部署与更新用户“请使用附件的deployment.yaml文件在staging环境部署一个名为my-api的应用。”Claude交互过程用户通过附件或粘贴文本提供了YAML内容。Claude调用deploy_from_yaml工具参数yamlContent为提供的YAML字符串并可能自动补充或提示用户确认namespace: staging。MCP服务器收到请求校验权限、执行kubectl apply的等效操作。操作成功后Claude回复“已成功在staging命名空间部署应用my-api。部署的Deployment名为my-api目前副本数为2。需要我帮你检查一下Pod的启动状态吗”价值简化了部署流程尤其适合需要频繁部署不同配置到不同环境的场景。结合Claude的代码理解能力它甚至可以在部署前帮你检查YAML中的常见错误。6.3 场景三故障排查辅助用户“>