首页 › tutorials › Go造MCP:把系统监控变工具

概述

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

前置条件


第一步:理解为什么用 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) 能力,不碰弃用项。

第二步:初始化项目与依赖

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.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 服务器都遵循同一个骨架,记住这三步即可:

// 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
}

关键点:

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

创建 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 发握手包,这是正常的。

常见问题:

第五步:补齐其余三个工具

把下面两个工具函数加到 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(stdio)

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

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

当团队多人共用、或想把监控能力部署到跳板机时,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

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

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

把玩具变成可维护服务,至少做到:

  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 分钟轮询脚本,让它连续观察机器并把异常摘要写到本地文件:

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

延伸阅读

参考来源

  1. modelcontextprotocol/go-sdk(官方 Go SDK) — README、quick_start 与 examples(http、auth、middleware)构成教程中所有 API 的依据
  2. go-sdk on pkg.go.dev — 包级 API 文档与版本兼容表(v1.7.0+ 对应 MCP 规范 2026-07-28)
  3. How to Build an MCP Server in Go (2026 Guide) — Fastio — stdio/SSE/HTTP 传输与工具注册的实践参考
  4. Build MCP Servers in Go — MCPcat — Go MCP 服务器并发与生产化模式
  5. Building a Model Context Protocol (MCP) Server in Go — Navendu Pottekkat — 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学通信知识星球海报-扫码加入