1. 为什么要在 Golang 里手搓一个 MCP 服务器
MCP 服务器说白了就是给大模型装的一双手:模型本身只会输出文字,但通过 MCP 协议,它可以按结构化格式调用你注册的工具,去查 K8s 资源、读日志、改配置。Golang 写 MCP 服务器的好处很直接——编译出来是单个二进制,扔到跳板机或容器里就能跑,没有 Python 那套虚拟环境依赖,特别适合 MCP-K8S 这种要贴着集群跑的运维场景。
我这次的目标很明确:用 Golang 从零搭一个能连 Kubernetes 的 MCP 服务器,工具集覆盖get_api_resources、get_resource、list_resources这几个查询动作,写操作先留开关默认关闭。然后把它接到 AI 工具链里,让模型能通过统一入口调用。这里的关键卡点其实不在 Go 代码,而在“模型侧怎么稳定拿到 Key、怎么把 MCP 服务器注册进客户端”。所以我会用 TaoToken 做统一 Key 和 API 通道,把模型调用和 MCP 工具调用串成一条链路,最后交付可复制的config.toml和settings.json骨架。
适合谁看:会一点 Go、手上有个测试集群、想让 AI 帮你查 Pod 和 Deployment 的同学。全程不需要你懂协议底层,照着配置改路径就能跑。
2. TaoToken 前置准备:统一 Key 与通道
在写代码之前,先把模型侧的入口准备好。TaoToken 在这里扮演的是统一 Key 和 API 通道的角色——你不用在 Cline、CC Switch、Coding Plan 里各配一套 Key,而是拿一个 Key 走同一个 API 地址,MCP 服务器和模型对话都从这里过。
第一步,打开官网注册并进入控制台:
https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=第二步,在控制台里创建 API Key。路径是 console 页面下的 api-keys:
https://taotoken.net/console/api-keys?utm_source=taotoken_aicg_blog_end&utm_campaign=rewrite&utm_content=创建完把 Key 复制出来,形如sk-xxxx,先存到环境变量里,别硬编码进代码:
export TAOTOKEN_API_KEY="sk-你的key"第三步,确认 API 基地址。所有请求走这个地址,注意它不带任何查询参数:
https://taotoken.net/api如果你后面要用 Claude Code 这类工具,Anthropic 兼容入口在:
https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_campaign=rewrite&utm_content=注意:Key 只创建一次就够,多个客户端共用同一个 Key。如果怀疑泄露,在 api-keys 页面直接吊销重建,不用改代码。
3. Golang MCP 服务器配置骨架
3.1 项目结构与依赖
先建目录,用mcp-go这个 SDK,它把协议消息处理、工具注册、stdio/SSE 传输都封装好了:
mkdir mcp-k8s && cd mcp-k8s go mod init github.com/yourname/mcp-k8s go get github.com/mark3labs/mcp-go go get k8s.io/client-go@v0.29.0 go get k8s.io/apimachinery@v0.29.0目录按职责拆开,后面加工具不会乱:
mcp-k8s/ ├── cmd/server/main.go # 入口,解析参数、启动 ├── internal/config/ # 读取 config.toml ├── internal/k8s/ # K8s 客户端初始化 └── internal/tools/ # 每个工具一个文件3.2 config.toml 配置骨架
把模型通道和 K8s 连接信息都收进一个配置文件,代码里只读不写死。下面这份可以直接复制改:
# config.toml [server] transport = "stdio" # stdio 或 sse host = "localhost" port = 8080 [ai] # TaoToken 统一通道 base_url = "https://taotoken.net/api" api_key_env = "TAOTOKEN_API_KEY" # 从环境变量读,不落盘 model = "claude-3-5-sonnet" [k8s] kubeconfig = "/home/you/.kube/config" context = "" [tools] enable_create = false enable_update = false enable_delete = false读取用BurntSushi/toml,几行就够:
package config import "github.com/BurntSushi/toml" type Config struct { Server struct { Transport string `toml:"transport"` Host string `toml:"host"` Port int `toml:"port"` } `toml:"server"` AI struct { BaseURL string `toml:"base_url"` APIKeyEnv string `toml:"api_key_env"` Model string `toml:"model"` } `toml:"ai"` K8s struct { Kubeconfig string `toml:"kubeconfig"` Context string `toml:"context"` } `toml:"k8s"` Tools struct { EnableCreate bool `toml:"enable_create"` EnableUpdate bool `toml:"enable_update"` EnableDelete bool `toml:"enable_delete"` } `toml:"tools"` } func Load(path string) (*Config, error) { var c Config if _, err := toml.DecodeFile(path, &c); err != nil { return nil, err } return &c, nil }3.3 注册 K8s 查询工具
工具定义三要素:名字、描述、参数。描述要写清楚,模型靠它判断什么时候调。以get_resource为例:
package tools import ( "context" "encoding/json" "fmt" "github.com/mark3labs/mcp-go/server" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) var GetResourceTool = server.Tool{ Name: "get_resource", Description: "获取指定 Kubernetes 资源的详细信息,需要 apiVersion、kind、name", Parameters: []server.Parameter{ {Name: "apiVersion", Description: "资源 API 版本,如 v1", Type: "string", Required: true}, {Name: "kind", Description: "资源类型,如 Pod", Type: "string", Required: true}, {Name: "name", Description: "资源名称", Type: "string", Required: true}, {Name: "namespace", Description: "命名空间,集群级资源可省略", Type: "string", Required: false}, }, Handler: handleGetResource, } type getResourceParams struct { APIVersion string `json:"apiVersion"` Kind string `json:"kind"` Name string `json:"name"` Namespace string `json:"namespace"` } func handleGetResource(ctx context.Context, raw json.RawMessage) (interface{}, error) { var p getResourceParams if err := json.Unmarshal(raw, &p); err != nil { return nil, fmt.Errorf("参数解析失败: %w", err) } dyn, err := k8s.DynamicClient() if err != nil { return nil, err } gvr, namespaced, err := k8s.ResolveGVR(p.APIVersion, p.Kind) if err != nil { return nil, err } var obj *unstructured.Unstructured if namespaced { ns := p.Namespace if ns == "" { ns = "default" } obj, err = dyn.Resource(gvr).Namespace(ns).Get(ctx, p.Name, metav1.GetOptions{}) } else { obj, err = dyn.Resource(gvr).Get(ctx, p.Name, metav1.GetOptions{}) } if err != nil { return nil, fmt.Errorf("获取资源失败: %w", err) } return obj.Object, nil }ResolveGVR用 discovery 客户端把apiVersion+kind映射成 GVR,这是动态客户端调用的必要一步,别跳过。
3.4 启动入口与传输选择
入口负责把配置、K8s 客户端、工具注册串起来:
func main() { cfgPath := flag.String("config", "config.toml", "配置文件路径") flag.Parse() cfg, err := config.Load(*cfgPath) if err != nil { log.Fatalf("加载配置失败: %v", err) } if err := k8s.Init(cfg.K8s.Kubeconfig, cfg.K8s.Context); err != nil { log.Fatalf("初始化 K8s 客户端失败: %v", err) } opts := []server.ServerOption{server.WithLogger(log.Default())} if cfg.Server.Transport == "sse" { opts = append(opts, server.WithTransport(server.TransportSSE), server.WithHost(cfg.Server.Host), server.WithPort(cfg.Server.Port), ) } else { opts = append(opts, server.WithTransport(server.TransportStdio)) } s, err := server.NewServer(opts...) if err != nil { log.Fatalf("创建服务器失败: %v", err) } tools.Register(s, &tools.Options{ EnableCreate: cfg.Tools.EnableCreate, EnableUpdate: cfg.Tools.EnableUpdate, EnableDelete: cfg.Tools.EnableDelete, }) if err := s.Start(); err != nil { log.Fatalf("启动失败: %v", err) } }编译:
go build -o bin/mcp-k8s ./cmd/server4. 客户端接入与连通性验证
4.1 settings.json 配置骨架
Cline 或 CC Switch 里注册 MCP 服务器,stdio 模式直接指向二进制:
{ "mcpServers": { "mcp-k8s": { "command": "/home/you/mcp-k8s/bin/mcp-k8s", "args": ["-config", "/home/you/mcp-k8s/config.toml"], "env": { "TAOTOKEN_API_KEY": "sk-你的key" } } } }如果走 SSE 模式,先启动服务:
./bin/mcp-k8s -config config.toml # config.toml 里 transport = "sse"客户端改成 URL 形式:
{ "mcpServers": { "mcp-k8s": { "url": "http://localhost:8080/sse" } } }4.2 验证请求与成功结果
先单独验证 MCP 服务器能不能连上集群。用 stdio 模式手动喂一条初始化请求:
echo '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}' | ./bin/mcp-k8s -config config.toml正常会返回工具列表,包含get_api_resources、get_resource、list_resources。如果返回空数组,说明工具注册那步没生效,回去检查Register函数。
再验证模型通道。用 curl 打 TaoToken 的 API,确认 Key 有效:
curl -s https://taotoken.net/api/v1/messages \ -H "x-api-key: $TAOTOKEN_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "content-type: application/json" \ -d '{ "model": "claude-3-5-sonnet", "max_tokens": 64, "messages": [{"role":"user","content":"ping"}] }'返回里带content字段就说明通道通了。最后在客户端里发一句“列出 default 命名空间的 Pod”,模型会调用list_resources,你能看到工具调用记录和返回的 Pod 列表,这条本地到集群的链路就算跑通了。
5. 本篇常见错排查
报错一:exec: "mcp-k8s": executable file not foundsettings.json 里的command必须写绝对路径。相对路径在客户端的工作目录下解析,基本找不到。用which或realpath确认。
报错二:unable to load kubeconfigconfig.toml里的kubeconfig路径写的是~/.kube/config,但 Go 不展开~。改成/home/you/.kube/config这种绝对路径。
报错三:工具调用返回the server does not support the resourceResolveGVR没查到对应资源。检查apiVersion和kind大小写,Pod 是v1+Pod,Deployment 是apps/v1+Deployment。可以先调get_api_resources看集群支持哪些。
报错四:SSE 模式客户端连不上host配成localhost时,如果客户端在容器里,要改成0.0.0.0或容器可达的地址。端口被占用就换port。
报错五:模型通道返回 401TAOTOKEN_API_KEY没传进 MCP 服务器的进程环境。stdio 模式下客户端启动子进程时,env字段要显式带上,别指望继承 shell 变量。
6. 把链路固定下来
跑通之后建议做两件事。一是把config.toml里的写操作开关保持false,等查询类工具用稳了再逐个打开,避免模型误删资源。二是把 MCP 服务器和模型通道分开验证:先用tools/list确认工具注册没问题,再用 curl 确认 Key 有效,最后才在客户端里联调。这样出问题时能快速定位是 Go 侧还是通道侧。
长期在编码和 Agent 场景里用的话,可以走 Coding Plan 把模型调用额度固定下来:
https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_campaign=rewrite&utm_content=需要看完整接入文档和参数说明的,从这里进:
https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_campaign=rewrite&utm_content=模型对话调试入口:
https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=我自己的习惯是先把get_api_resources和list_resources这两个只读工具调顺,确认模型能正确拼参数,再考虑加写操作。MCP-K8S 这类贴着集群跑的服务,权限收得越紧越省心。