本教程带你用 Go 语言 + 官方 MCP SDK,从零构建一个名为 syswatch 的 MCP 服务器,把磁盘占用、内存状态、系统负载、日志尾部这四类运维能力封装成 AI 可调用的工具,最终接入 Claude Code 实战调用。学完你将掌握:Go SDK 的服务器与工具三件套写法、stdio 与 Streamable HTTP 双传输、结构化入参出参的写法、Claude Code 接入与调试闭环。适合有基本命令行和任意一门语言基础、想让 AI 看懂"机器此刻状态"的开发者与运维同学。
github.com/modelcontextprotocol/go-sdk v1.7.0+ 对应 MCP 规范 2026-07-28)Model Context Protocol(MCP)定义了"宿主—客户端—服务器"三层架构:你写的服务器把能力(工具 Tools、资源 Resources、提示 Prompts)暴露出去,Claude Code 这样的宿主通过内置客户端发现并调用它们。协议本身与语言无关,但选 Go 有三个硬理由:
go build 产出的是一个可执行文件,拷到任何机器直接跑,不像 Python 要管 venv、Node 要管 node_modules。对部署在运维机器上的监控工具尤其友好。modelcontextprotocol/go-sdk 是 Anthropic 维护的官方实现之一,与协议版本严格对齐,支持 OAuth、Streamable HTTP、中间件等全部能力。注意:2026-07-28 规范已将 roots/sampling/logging 三项特性标记为弃用(SEP-2577),但 SDK 仍保留至少 12 个月兼容窗口。本教程只用稳定核心的 工具(Tools) 能力,不碰弃用项。
mkdir syswatch && cd syswatch
go mod init github.com/yourname/syswatch
go get github.com/modelcontextprotocol/go-sdk
go get github.com/shirou/gopsutil/v3
说明:
.../go-sdk/mcp)。gopsutil,一个跨平台系统指标库,免去我们手写 /proc、wmic、sysctl 等平台特判。预期结果:目录下生成 go.mod、go.sum,且 go.sum 里能看到 modelcontextprotocol/go-sdk 的校验和。
常见问题:
| 现象 | 原因与解决 |
|---|---|
unknown import path 拉取失败 |
Go 代理被墙。设置 export GOPROXY=https://goproxy.cn,direct 后重试 |
gopsutil 在 Windows 编译报错 |
确认 v3 主版本,旧 v2 路径不同 |
所有 Go MCP 服务器都遵循同一个骨架,记住这三步即可:
// 1. 建服务器:给它一个名字和版本
server := mcp.NewServer(&mcp.Implementation{Name: "syswatch", Version: "0.1.0"}, nil)
// 2. 注册工具:把一个普通函数变成 AI 工具
mcp.AddTool(server, &mcp.Tool{Name: "disk_usage", Description: "查询磁盘占用"}, handler)
// 3. 跑传输:选择 stdio(本地)或 HTTP(远程)
server.Run(context.Background(), &mcp.StdioTransport{})
工具处理函数的签名有两种等价写法,本教程统一用第二种(结构化出参 + 指针入参):
func diskUsage(
ctx context.Context,
req *mcp.CallToolRequest,
p *DiskUsageParams, // 入参结构体指针
) (*mcp.CallToolResult, any, error) {
// ...业务逻辑...
return &mcp.CallToolResult{
Content: []mcp.Content{&mcp.TextContent{Text: msg}},
}, structuredOutput, nil
}
关键点:
json:"path" 控制字段名,jsonschema:"查询的路径,如 /" 自动生成给模型看的描述。SDK 会读这些 tag 生成 JSON Schema,模型据此决定何时、如何调用。CallToolResult(给人/模型读的文本或图片)、any(给程序读的结构化输出,可为 nil)、error(非 nil 时整次调用标记为失败)。*int),SDK 默认把无默认值的基本字段标为必填,指针字段视为可选。创建 main.go,先实现磁盘工具:
package main
import (
"context"
"fmt"
"log"
"github.com/modelcontextprotocol/go-sdk/mcp"
"github.com/shirou/gopsutil/v3/disk"
)
// DiskUsageParams 是 disk_usage 工具的入参。
type DiskUsageParams struct {
Path string `json:"path" jsonschema:"要查询的文件系统路径,如 / 或 C:\\"`
}
func gb(b uint64) float64 { return float64(b) / 1024 / 1024 / 1024 }
func diskUsage(ctx context.Context, req *mcp.CallToolRequest, p *DiskUsageParams) (*mcp.CallToolResult, any, error) {
path := p.Path
if path == "" {
path = "/"
}
u, err := disk.Usage(path)
if err != nil {
return nil, nil, fmt.Errorf("disk.Usage(%q): %w", path, err)
}
msg := fmt.Sprintf("磁盘 %s:总量 %.1f GB,可用 %.1f GB,已用 %.1f%%",
path, gb(u.Total), gb(u.Free), u.UsedPercent)
structured := map[string]any{
"path": path,
"total_gb": gb(u.Total),
"free_gb": gb(u.Free),
"used_pct": u.UsedPercent,
}
return &mcp.CallToolResult{
Content: []mcp.Content{&mcp.TextContent{Text: msg}},
}, structured, nil
}
func main() {
server := mcp.NewServer(&mcp.Implementation{Name: "syswatch", Version: "0.1.0"}, nil)
mcp.AddTool(server, &mcp.Tool{
Name: "disk_usage",
Description: "查询指定路径所在文件系统的磁盘占用(总量/可用/已用百分比)。",
}, diskUsage)
if err := server.Run(context.Background(), &mcp.StdioTransport{}); err != nil {
log.Fatal(err)
}
}
编译验证:
go build -o syswatch .
预期结果:当前目录生成可执行文件 syswatch。直接运行它会"卡住"——因为 stdio 传输在等 MCP 客户端通过 stdin 发握手包,这是正常的。
常见问题:
log(默认输出到 stderr)或显式写 stderr。go run github.com/modelcontextprotocol/go-sdk/cmd/mcp-devtools@latest inspector ./syswatch。把下面两个工具函数加到 main.go,并在 main() 里 AddTool 注册。先加内存与负载:
import (
"github.com/shirou/gopsutil/v3/load"
"github.com/shirou/gopsutil/v3/mem"
)
func memStatus(ctx context.Context, req *mcp.CallToolRequest, p struct{}) (*mcp.CallToolResult, any, error) {
v, err := mem.VirtualMemory()
if err != nil {
return nil, nil, fmt.Errorf("mem: %w", err)
}
msg := fmt.Sprintf("内存:总量 %.1f GB,可用 %.1f GB,已用 %.1f%%",
gb(v.Total), gb(v.Available), v.UsedPercent)
return &mcp.CallToolResult{
Content: []mcp.Content{&mcp.TextContent{Text: msg}},
}, map[string]any{"total_gb": gb(v.Total), "available_gb": gb(v.Available), "used_pct": v.UsedPercent}, nil
}
func loadAvg(ctx context.Context, req *mcp.CallToolRequest, p struct{}) (*mcp.CallToolResult, any, error) {
a, err := load.Avg()
if err != nil {
return nil, nil, fmt.Errorf("load: %w", err)
}
msg := fmt.Sprintf("系统负载:1分钟 %.2f,5分钟 %.2f,15分钟 %.2f", a.Load1, a.Load5, a.Load15)
return &mcp.CallToolResult{
Content: []mcp.Content{&mcp.TextContent{Text: msg}},
}, map[string]any{"load1": a.Load1, "load5": a.Load5, "load15": a.Load15}, nil
}
Windows 没有
loadavg概念,load.Avg()会返回错误。生产里建议根据runtime.GOOS选择性注册工具,让每个平台只暴露它能正确回答的能力。
再加一个纯标准库实现的 tail_log,演示"如何安全地把文件读取暴露成工具":
import (
"os"
"strings"
)
// TailLogParams 控制要读的日志与行数。
type TailLogParams struct {
Path string `json:"path" jsonschema:"日志文件的绝对路径"`
Lines *int `json:"lines" jsonschema:"返回末尾多少行,默认 50,上限 200"`
}
func tailLog(ctx context.Context, req *mcp.CallToolRequest, p *TailLogParams) (*mcp.CallToolResult, any, error) {
if p.Path == "" {
return nil, nil, fmt.Errorf("path 不能为空")
}
n := 50
if p.Lines != nil && *p.Lines > 0 {
n = *p.Lines
}
if n > 200 {
n = 200 // 硬上限,防止把超大输出灌进上下文
}
data, err := os.ReadFile(p.Path)
if err != nil {
return nil, nil, fmt.Errorf("读取 %s 失败: %w", p.Path, err)
}
lines := strings.Split(strings.TrimRight(string(data), "\n"), "\n")
if len(lines) > n {
lines = lines[len(lines)-n:]
}
return &mcp.CallToolResult{
Content: []mcp.Content{&mcp.TextContent{Text: strings.Join(lines, "\n")}},
}, map[string]any{"path": p.Path, "lines": len(lines)}, nil
}
最后更新 main(),把四个工具都注册上:
func main() {
server := mcp.NewServer(&mcp.Implementation{Name: "syswatch", Version: "0.1.0"}, nil)
mcp.AddTool(server, &mcp.Tool{Name: "disk_usage", Description: "查询指定路径的磁盘占用。"}, diskUsage)
mcp.AddTool(server, &mcp.Tool{Name: "mem_status", Description: "查询系统内存占用。"}, memStatus)
mcp.AddTool(server, &mcp.Tool{Name: "load_avg", Description: "查询系统负载均值(1/5/15分钟)。"}, loadAvg)
mcp.AddTool(server, &mcp.Tool{Name: "tail_log", Description: "读取日志文件末尾若干行(只读,上限200行)。"}, tailLog)
if err := server.Run(context.Background(), &mcp.StdioTransport{}); err != nil {
log.Fatal(err)
}
}
go build -o syswatch .
Claude Code 内置 MCP 客户端,最省事的方式是用 claude mcp add 命令注册本地 stdio 服务器。在项目目录执行:
# local 作用域:只在当前目录生效;想全局用把 local 换成 user
claude mcp add syswatch --scope local -- $(pwd)/syswatch
预期结果:命令无报错。用下面任一方式确认已挂载:
claude mcp list # 列出已注册服务器,应看到 syswatch
claude mcp get syswatch # 查看具体配置
也可以在项目根放一个 .mcp.json,让团队共享同一份配置(提交到仓库即可,Claude Code 启动时自动加载):
{
"mcpServers": {
"syswatch": {
"command": "/absolute/path/to/syswatch"
}
}
}
接着在 Claude Code 对话里直接提问,让它自己决定调哪个工具:
> 这台机器磁盘还剩多少空间?顺便看看最近 30 行系统日志有没有异常。
模型会依次调用 disk_usage、tail_log,把结果汇总成自然语言回答。你可以观察到一次典型的工具调用往返:Claude Code 发 tools/list → tools/call,服务器回 CallToolResult。
常见问题:
| 现象 | 原因与解决 |
|---|---|
claude mcp list 显示 server 但 Failed to connect |
二进制路径写错,或缺少可执行权限。用绝对路径并 chmod +x syswatch |
| 工具未被调用 | Description 写得太泛(如"查东西")。模型靠描述匹配意图,写清"做什么+输入+输出" |
| 调用返回乱码 | 业务代码往 stdout 打印了日志。改用 log 走 stderr |
当团队多人共用、或想把监控能力部署到跳板机时,stdio(每台机器各装一份)就不够了。换成 Streamable HTTP 传输:一个常驻进程,N 个客户端连。
把 main.go 末尾的 server.Run(...) 替换为 HTTP 模式(保留前面的工具注册不变):
import (
"net/http"
)
func main() {
server := mcp.NewServer(&mcp.Implementation{Name: "syswatch", Version: "0.1.0"}, nil)
mcp.AddTool(server, &mcp.Tool{Name: "disk_usage", Description: "查询指定路径的磁盘占用。"}, diskUsage)
mcp.AddTool(server, &mcp.Tool{Name: "mem_status", Description: "查询系统内存占用。"}, memStatus)
mcp.AddTool(server, &mcp.Tool{Name: "load_avg", Description: "查询系统负载均值。"}, loadAvg)
mcp.AddTool(server, &mcp.Tool{Name: "tail_log", Description: "读取日志末尾若干行。"}, tailLog)
// Streamable HTTP 处理器:按需返回同一个服务器实例
handler := mcp.NewStreamableHTTPHandler(func(req *http.Request) *mcp.Server {
return server
}, nil)
addr := ":8000"
log.Printf("syswatch MCP 监听 http://0.0.0.0%s/mcp", addr)
if err := http.ListenAndServe(addr, handler); err != nil {
log.Fatal(err)
}
}
启动服务:
go build -o syswatch . && ./syswatch
在另一台机器/另一个目录的 Claude Code 里以 HTTP 方式接入:
claude mcp add --transport http syswatch-remote http://<服务器IP>:8000/mcp
说明:NewStreamableHTTPHandler 的回调允许你"按请求返回不同服务器实例"——这是做多租户隔离、按 token 注入不同工具集的钩子;这里固定返回同一实例即可。
安全提示:HTTP 服务器默认无鉴权,公网直接暴露等于把机器状态开源。生产环境务必:(1) 套反向代理 + TLS;(2) 启用 SDK 的 auth 包做 OAuth 2.1;(3) 限制可访问的日志路径白名单。tail_log 当前能读任意路径,部署前请加上路径前缀校验,避免被诱导读出 /etc/shadow 之类的敏感文件。
想知道模型实际调了哪些工具、各耗时多少?加一段接收侧中间件,把每次方法调用打到 stderr:
func loggingMiddleware() mcp.Middleware {
return func(next mcp.MethodHandler) mcp.MethodHandler {
return func(ctx context.Context, method string, req mcp.Request) (mcp.Result, error) {
start := time.Now()
sid := req.GetSession().ID()
res, err := next(ctx, method, req)
status := "OK"
if err != nil {
status = "ERR"
}
log.Printf("[%s] sid=%s method=%s %v", status, sid, method, time.Since(start))
return res, err
}
}
}
// 在 main() 里、AddTool 之前加一行:
server.AddReceivingMiddleware(loggingMiddleware())
预期结果:每次 Claude Code 调用工具,服务器 stderr 会输出类似:
[OK] sid=0e3f... method=tools/call 4.2ms
这是排查"工具到底有没有被调到""为什么慢"的第一手证据。
把玩具变成可维护服务,至少做到:
ctx context.Context,调用慢系统命令时用 context.WithTimeout 包一层,避免把整个会话挂死。fmt.Errorf("...%w", err),模型在 IsError 分支能读到可操作的提示。tail_log 那种"行数硬上限"是范式,所有可能产生大输出的工具都该设。Implementation.Version 每次发版递增,配合 claude mcp get 能快速确认线上跑的是哪个版本。把上面九步串成一个闭环场景:把 syswatch 部署在一台生产跳板机上(HTTP 模式,加鉴权),然后用 Claude Code 的 -p(打印)模式写一个 5 分钟轮询脚本,让它连续观察机器并把异常摘要写到本地文件:
# watch.sh —— 依赖 syswatch 已用 claude mcp add 注册
while true; do
claude -p "调用 disk_usage 查 / 、mem_status、load_avg。\
若任一指标超阈值(磁盘>85%、内存>90%、load1>8),用一句中文告警写入 /tmp/syswatch.alert,否则清空该文件。"
sleep 300
done
这份脚本展示了一个完整的"AI 运维探针"链路:外部触发(cron/loop)→ Claude Code 宿主 → MCP 客户端 → syswatch 服务器 → 系统调用 → 结果回流 → 模型判断 → 落盘。把 sleep 300 换成 systemd timer,就是一套零成本的轻量值守。
flowchart TD
A[Claude Code 宿主] -->|tools/list tools/call JSON-RPC| B[MCP 客户端]
B -->|stdio 或 Streamable HTTP| C[syswatch 服务器 Go]
C --> D[disk_usage<br/>gopsutil/disk]
C --> E[mem_status<br/>gopsutil/mem]
C --> F[load_avg<br/>gopsutil/load]
C --> G[tail_log<br/>os.ReadFile]
D --> H[(系统/文件)]
E --> H
F --> H
G --> H
C -->|AddReceivingMiddleware| I[(stderr 日志)]
C -.->|可选: HTTP 远程| J[其他 Claude Code 客户端]
| 维度 | 评估 |
|---|---|
| 适合的训练营 | Go 后端 / DevOps / SRE / AI 工程化方向;2–3 天的"工具开发"专题模块 |
| 预计教学时长 | 授课 + 跟做约 3.5 小时:SDK 三件套 45 分钟,四工具实现 90 分钟,HTTP+鉴权+中间件 45 分钟,实战值守脚本 30 分钟 |
| 学员基础要求 | 会任一静态/强类型语言,懂 Linux 基本命令;零 MCP 背景可接受 |
| 可裁剪为 | 半天版只做 stdio + 两工具;一天版加 HTTP 与安全章节 |
| 延伸课题 | 接入 OAuth、把 tail_log 换成流式日志订阅、用资源(Resources)暴露配置文件、加 Prometheus 指标导出 |
modelcontextprotocol/go-sdk:源码、examples 目录、各特性文档,是本教程 API 的权威来源。claude mcp add/get/list 的全部作用域与配置项,以及 .mcp.json 团队共享约定。本文由微信公众号「51学通信」原创制作,站长 爱卫生(微信:gprshome201101)。关注公众号,获取更多通信与 AI 工程化的实战内容。
[!tip] 51学通信知识星球 星主:爱卫生(微信:gprshome201101)——公众号「51学通信」创办者,19 年通信行业运维与讲师经验,著有《GPRS网络信令实例详解》《EPC原理与实践》《5G核心网原理与实践》三本纸质书(各大电商平台有售)。 星球定位:深耕 5G 核心网、IMS、NFV/AI、云原生等 ICT 领域,主打运维岗位与信令分析。 当前规模:5200 篇主题 / 990 个回答,198 元/年。 加入福利:加入星球后请加爱卫生微信,拉您进知识星球年度陪伴与答疑群——不限量回复您的各种问题,另有群内分享。 AI 加持:51学通信知识星球已作为 skill 接入 WorkBuddy,使用 deepseek-4-flash 在海量星球帖子中精准找贴、找视频、找 FAQ 和案例,做总结、找文档和 PPT 等场景全覆盖。
