<!-- title: Go造MCP：把系统监控变工具 -->

## 概述

本教程带你用 **Go 语言 + 官方 MCP SDK**，从零构建一个名为 `syswatch` 的 MCP 服务器，把磁盘占用、内存状态、系统负载、日志尾部这四类运维能力封装成 AI 可调用的工具，最终接入 Claude Code 实战调用。学完你将掌握：Go SDK 的服务器与工具三件套写法、stdio 与 Streamable HTTP 双传输、结构化入参出参的写法、Claude Code 接入与调试闭环。适合有基本命令行和任意一门语言基础、想让 AI 看懂"机器此刻状态"的开发者与运维同学。

## 前置条件

- **Go 1.23 或更高版本**（官方 SDK `github.com/modelcontextprotocol/go-sdk` v1.7.0+ 对应 MCP 规范 2026-07-28）
- **Claude Code CLI** 已安装并能正常对话
- 一台 Linux/macOS 或 Windows 机器（系统监控类工具有跨平台差异，下方会注明）
- 了解 JSON 与函数指针的基本概念即可，无需先学 MCP 协议细节

---

## 第一步：理解为什么用 Go 来造 MCP 服务器

Model Context Protocol（MCP）定义了"宿主—客户端—服务器"三层架构：你写的**服务器**把能力（工具 Tools、资源 Resources、提示 Prompts）暴露出去，Claude Code 这样的宿主通过内置客户端发现并调用它们。协议本身与语言无关，但选 Go 有三个硬理由：

1. **单二进制、零运行时依赖**：`go build` 产出的是一个可执行文件，拷到任何机器直接跑，不像 Python 要管 venv、Node 要管 node_modules。对部署在运维机器上的监控工具尤其友好。
2. **天生并发**：采集 CPU、磁盘、网络这类 I/O 与系统调用，goroutine + context 能优雅处理超时与取消。
3. **官方 SDK 成熟**：`modelcontextprotocol/go-sdk` 是 Anthropic 维护的官方实现之一，与协议版本严格对齐，支持 OAuth、Streamable HTTP、中间件等全部能力。

> 注意：2026-07-28 规范已将 roots/sampling/logging 三项特性标记为弃用（SEP-2577），但 SDK 仍保留至少 12 个月兼容窗口。本教程只用稳定核心的 **工具（Tools）** 能力，不碰弃用项。

## 第二步：初始化项目与依赖

```bash
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
```

**说明**：

- 第 2 行创建 Go module。
- 第 3 行拉取官方 MCP SDK（核心包路径是 `.../go-sdk/mcp`）。
- 第 4 行拉取 `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 SDK 的"三件套"

所有 Go MCP 服务器都遵循同一个骨架，记住这三步即可：

```go
// 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{})
```

工具处理函数的签名有两种等价写法，本教程统一用第二种（结构化出参 + 指针入参）：

```go
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
}
```

**关键点**：

- **入参靠结构体 tag 描述**：`json:"path"` 控制字段名，`jsonschema:"查询的路径，如 /"` 自动生成给模型看的描述。SDK 会读这些 tag 生成 JSON Schema，模型据此决定何时、如何调用。
- **返回值三段**：`CallToolResult`（给人/模型读的文本或图片）、`any`（给程序读的结构化输出，可为 `nil`）、`error`（非 nil 时整次调用标记为失败）。
- **指针 vs 值**：可选参数用指针类型（如 `*int`），SDK 默认把无默认值的基本字段标为必填，指针字段视为可选。

## 第四步：动手实现第一个工具 disk_usage

创建 `main.go`，先实现磁盘工具：

```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)
	}
}
```

**编译验证**：

```bash
go build -o syswatch .
```

**预期结果**：当前目录生成可执行文件 `syswatch`。直接运行它会"卡住"——因为 stdio 传输在等 MCP 客户端通过 stdin 发握手包，这是正常的。

**常见问题**：

- **stdio 服务器绝不能往 stdout 打印业务日志**。MCP 协议把 stdout 当作协议信道，任何非 JSON-RPC 的字节都会让客户端解析崩溃。需要打日志时用 `log`（默认输出到 stderr）或显式写 stderr。
- 想在终端手动验证，可用官方调试器：`go run github.com/modelcontextprotocol/go-sdk/cmd/mcp-devtools@latest inspector ./syswatch`。

## 第五步：补齐其余三个工具

把下面两个工具函数加到 `main.go`，并在 `main()` 里 `AddTool` 注册。先加内存与负载：

```go
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`，演示"如何安全地把文件读取暴露成工具"：

```go
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()`，把四个工具都注册上：

```go
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)
	}
}
```

```bash
go build -o syswatch .
```

## 第六步：接入 Claude Code（stdio）

Claude Code 内置 MCP 客户端，最省事的方式是用 `claude mcp add` 命令注册本地 stdio 服务器。在项目目录执行：

```bash
# local 作用域：只在当前目录生效；想全局用把 local 换成 user
claude mcp add syswatch --scope local -- $(pwd)/syswatch
```

**预期结果**：命令无报错。用下面任一方式确认已挂载：

```bash
claude mcp list                      # 列出已注册服务器，应看到 syswatch
claude mcp get syswatch              # 查看具体配置
```

也可以在项目根放一个 `.mcp.json`，让团队共享同一份配置（提交到仓库即可，Claude Code 启动时自动加载）：

```json
{
  "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 |

## 第七步：升级为远程 HTTP 服务器

当团队多人共用、或想把监控能力部署到跳板机时，stdio（每台机器各装一份）就不够了。换成 **Streamable HTTP 传输**：一个常驻进程，N 个客户端连。

把 `main.go` 末尾的 `server.Run(...)` 替换为 HTTP 模式（保留前面的工具注册不变）：

```go
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)
	}
}
```

启动服务：

```bash
go build -o syswatch . && ./syswatch
```

在另一台机器/另一个目录的 Claude Code 里以 HTTP 方式接入：

```bash
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：

```go
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
```

这是排查"工具到底有没有被调到""为什么慢"的第一手证据。

## 第九步：可观测的工程化检查清单

把玩具变成可维护服务，至少做到：

1. **超时控制**：所有工具都接受 `ctx context.Context`，调用慢系统命令时用 `context.WithTimeout` 包一层，避免把整个会话挂死。
2. **结构化错误**：返回带上下文的 `fmt.Errorf("...%w", err)`，模型在 `IsError` 分支能读到可操作的提示。
3. **输出有界**：文本工具的返回值会进模型上下文窗口。`tail_log` 那种"行数硬上限"是范式，所有可能产生大输出的工具都该设。
4. **最小权限**：只暴露只读操作；写操作（重启服务、删文件）默认不开放，确需时单独审批并加二次确认。
5. **版本化**：`Implementation.Version` 每次发版递增，配合 `claude mcp get` 能快速确认线上跑的是哪个版本。

---

## 实战案例：让 Claude Code 当"无人值守值班员"

把上面九步串成一个闭环场景：把 `syswatch` 部署在一台生产跳板机上（HTTP 模式，加鉴权），然后用 Claude Code 的 `-p`（打印）模式写一个 5 分钟轮询脚本，让它连续观察机器并把异常摘要写到本地文件：

```bash
# 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，就是一套零成本的轻量值守。

## 工作流程图

```mermaid
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 指标导出 |

## 延伸阅读

- **官方 Go SDK 仓库** `modelcontextprotocol/go-sdk`：源码、examples 目录、各特性文档，是本教程 API 的权威来源。
- **MCP 规范与 SDK 总览** modelcontextprotocol.io/docs/sdk：对照查看工具/资源/提示三大原语与传输规范。
- **SEP-2577**：roots/sampling/logging 弃用提案，了解协议演进方向，避免新代码踩坑。
- **gopsutil 文档**：跨平台系统指标采集的完整 API 参考。
- **Claude Code MCP 文档**：`claude mcp add/get/list` 的全部作用域与配置项，以及 `.mcp.json` 团队共享约定。

## 参考来源

1. [modelcontextprotocol/go-sdk（官方 Go SDK）](https://github.com/modelcontextprotocol/go-sdk) — README、quick_start 与 examples（http、auth、middleware）构成教程中所有 API 的依据
2. [go-sdk on pkg.go.dev](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk) — 包级 API 文档与版本兼容表（v1.7.0+ 对应 MCP 规范 2026-07-28）
3. [How to Build an MCP Server in Go (2026 Guide) — Fastio](https://fast.io/resources/mcp-server-golang/) — stdio/SSE/HTTP 传输与工具注册的实践参考
4. [Build MCP Servers in Go — MCPcat](https://mcpcat.io/guides/building-mcp-server-go/) — Go MCP 服务器并发与生产化模式
5. [Building a Model Context Protocol (MCP) Server in Go — Navendu Pottekkat](https://navendu.me/posts/mcp-server-go/) — Go 语言实现 MCP 服务器的入门讲解

---

> 本文由微信公众号「**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 等场景全覆盖。

![51学通信知识星球海报-扫码加入](https://wiki.500nian.com/images/zhixingqiu_qrcode.png)
