首页 › tutorials › 用Go官方SDK造首个MCP工具

用 Go 官方 SDK 造首个 MCP 工具:从零到接入 Claude Code

概述

本教程带你用 Go 语言 和 Anthropic 官方维护的 modelcontextprotocol/go-sdk,从零写出一个可被 Claude Code 直接调用的 MCP 服务器。学完你将掌握:Go SDK 的"结构体即接口"开发范式、多工具注册、stdio 传输、JSON Schema 自动生成,以及把服务器接入 Claude Code 的完整闭环。适用人群:有 Go 基础(懂 goroutine、结构体标签即可)、想用静态类型语言给大模型造工具的后端/运维工程师。

为什么用 Go 造 MCP 服务器

之前几轮我们用 Python(FastMCP)、TypeScript(MCP SDK)、Rust 都造过 MCP 服务器。那 Go 值不值得再单独学一遍?答案取决于你的场景:

维度 Go + 官方 go-sdk Python(FastMCP) TypeScript SDK
部署产物 单文件二进制,零依赖 需 Python 运行时+依赖 需 Node 运行时
类型安全 编译期强类型 运行时 编译期
启动速度 毫秒级 较慢(导入开销) 中等
Schema 生成 结构体标签自动派生 装饰器自动派生 Zod/手动

最大的杀手锏是单二进制部署:Claude Code 通过 command 字段直接拉起一个进程,Go 编译出的可执行文件不需要装解释器、不需要 npm install、不需要 venv。这对运维工具、CLI 工具尤其友好——你写完 go build,把二进制丢进 PATH,Claude Code 就能调。

官方 SDK 截至 2026 年已迭代到 v1.7.0+,支持最新的 MCP 规范版本 2026-07-28,并且完整实现了 Tools / Resources / Prompts / Sampling / Elicitation 等全部原语。本教程聚焦最常用的 Tools 原语 + stdio 传输。

前置条件

  1. Go 1.23+(建议用最新稳定版,SDK 跟随 Go 的支持窗口)
  2. Claude Code 已安装并可用(命令行能跑 claude --version)
  3. 一点点 Go 基础:会写 package main、懂 context.Context、看过结构体 json 标签

不需要你之前学过其他语言的 MCP SDK——Go 版的范式和它们都不太一样。

核心:Go SDK 的"结构体即接口"范式

这是整个教程最重要的一节,请仔细读。

其他语言的 SDK 多用装饰器或 builder 模式声明工具入参。Go 官方 SDK 走了一条更符合 Go 哲学的路:你定义一个普通结构体作为 Input,SDK 自动从结构体反射出 JSON Schema 暴露给模型;模型调用时传入的 JSON 参数,SDK 又自动反序列化成这个结构体交给你的 handler。

// Input 结构体 = 工具的入参契约
type ProbeInput struct {
    Host    string `json:"host"    jsonschema:"the hostname or IP to probe"`
    Port    int    `json:"port"    jsonschema:"the TCP port number (1-65535)"`
    Timeout int    `json:"timeout" jsonschema:"timeout in milliseconds, default 3000"`
}

三个标签各司其职:

你的 handler 函数签名是固定的泛型三返回值:

func TCPProbe(ctx context.Context, req *mcp.CallToolRequest, in ProbeInput) (*mcp.CallToolResult, ProbeOutput, error) {
    // in 已经是反序列化好的强类型结构体,直接用
    ...
    return nil, ProbeOutput{...}, nil   // 第一个返回值通常是 nil
}

返回值三件套的含义:

  1. *mcp.CallToolResult —— 当你想返回非结构化的自由文本/多模态内容时填它(比如一段日志、一张图);纯结构化返回时给 nil
  2. ProbeOutput —— 结构化输出,SDK 会按它的标签生成 output schema,并在结果里塞一份 structuredContent
  3. error —— 协议级错误(基础设施挂了)。注意:业务上的"探测失败"不该用 error 返回,而应该正常返回带 IsError: true 的 CallToolResult,否则模型会以为工具本身坏了

这套设计的好处是:你的工具逻辑是纯 Go 函数,没有任何 MCP/JSON 知识渗透进来,单元测试就是普通的 Go 测试。

第一步:初始化项目

mkdir netcheck-mcp && cd netcheck-mcp
go mod init github.com/yourname/netcheck-mcp
go get github.com/modelcontextprotocol/go-sdk/mcp@latest

go get 会拉取最新稳定版。本教程代码基于 v1.7.x API。

第二步:写第一个工具(Hello 起步)

先写一个最小可运行版本,确认链路通了,再加复杂工具。新建 main.go:

package main

import (
    "context"
    "log"

    "github.com/modelcontextprotocol/go-sdk/mcp"
)

// ---- 第一个工具:问候 ----
type HelloInput struct {
    Name string `json:"name" jsonschema:"the name of the person to greet"`
}

type HelloOutput struct {
    Greeting string `json:"greeting" jsonschema:"the greeting text returned to the user"`
}

func SayHi(ctx context.Context, req *mcp.CallToolRequest, in HelloInput) (*mcp.CallToolResult, HelloOutput, error) {
    return nil, HelloOutput{Greeting: "你好," + in.Name + "!我是用 Go 写的 MCP 工具。"}, nil
}

func main() {
    server := mcp.NewServer(&mcp.Implementation{Name: "netcheck", Version: "v0.1.0"}, nil)

    // 注册工具:传入 server、工具元数据、handler
    mcp.AddTool(server, &mcp.Tool{
        Name:        "greet",
        Description: "向指定的人打招呼,用于验证 MCP 链路是否通畅",
    }, SayHi)

    // 通过 stdin/stdout 运行,直到客户端断开
    if err := server.Run(context.Background(), &mcp.StdioTransport{}); err != nil {
        log.Fatal(err)
    }
}

关键点解读:

编译验证:

go build -o netcheck-mcp .
./netcheck-mcp        # 会卡住等待输入,按 Ctrl+C 退出 —— 这是正常的

卡住等输入是对的,说明它在 stdio 上待命。先别关,下一步直接让 Claude Code 来连它。

第三步:接入 Claude Code 验证

在另一个终端,进入任意项目目录,注册这个本地服务器:

claude mcp add netcheck -- /absolute/path/to/netcheck-mcp

claude mcp add 把服务器详情写进配置文件(默认注册到 local 作用域,仅当前项目可见)。-- 后面是启动命令;用绝对路径最稳妥,避免 PATH 找不到。

验证注册成功:

claude mcp list        # 应该看到 netcheck
claude mcp get netcheck   # 查看完整配置

然后启动 Claude Code 交互会话:

claude

在对话里输入:请用 greet 工具向 "张工" 打个招呼。

预期结果: 模型识别到你有 greet 工具,自动填入 {"name":"张工"} 调用它,最终输出 你好,张工!我是用 Go 写的 MCP 工具。。

常见问题排查:

现象 原因 解决
claude mcp list 显示 failed to connect 二进制路径不对或无执行权限 用绝对路径;chmod +x netcheck-mcp
模型说"没有这个工具" 未重启会话,或注册到错的 scope 在目标项目目录下重新 claude mcp add
工具报错"invalid input" 结构体标签没写对 检查 json / jsonschema 标签拼写
连上但模型乱传参 Description 太模糊 把工具描述写具体,包含取值范围

链路一通,我们就可以放心加真正的实战工具了。

第四步:实战工具——网络体检三件套

现在把项目升级成一个真正有用的运维 MCP 服务器,包含三个工具:DNS 解析、TCP 端口探测、HTTP 探测。全部用 Go 标准库,零额外依赖。

把 main.go 替换为完整版本:

package main

import (
    "context"
    "encoding/json"
    "fmt"
    "log"
    "net"
    "net/http"
    "time"

    "github.com/modelcontextprotocol/go-sdk/mcp"
)

const defaultTimeout = 3000 // 毫秒

// ========== 工具 1:DNS 解析 ==========
type DNSInput struct {
    Host string `json:"host" jsonschema:"the hostname to resolve, e.g. example.com"`
}

type DNSOutput struct {
    Host string   `json:"host"`
    IPs  []string `json:"ips" jsonschema:"resolved IP addresses"`
}

func DNSLookup(ctx context.Context, req *mcp.CallToolRequest, in DNSInput) (*mcp.CallToolResult, DNSOutput, error) {
    ips, err := net.LookupHost(in.Host)
    if err != nil {
        // 业务失败:返回带 IsError 的结果,而不是 protocol error
        return &mcp.CallToolResult{
            Content: []mcp.Content{&mcp.TextContent{Text: fmt.Sprintf("DNS 解析失败: %v", err)}},
            IsError: true,
        }, DNSOutput{}, nil
    }
    return nil, DNSOutput{Host: in.Host, IPs: ips}, nil
}

// ========== 工具 2:TCP 端口探测 ==========
type ProbeInput struct {
    Host    string `json:"host"    jsonschema:"hostname or IP to probe"`
    Port    int    `json:"port"    jsonschema:"TCP port, 1-65535"`
    Timeout int    `json:"timeout" jsonschema:"timeout in ms, optional, default 3000"`
}

type ProbeOutput struct {
    Host     string `json:"host"`
    Port     int    `json:"port"`
    Reachable bool  `json:"reachable" jsonschema:"whether the port accepted a connection"`
    RTTMs    int64  `json:"rtt_ms" jsonschema:"round-trip time in milliseconds, -1 if unreachable"`
}

func TCPProbe(ctx context.Context, req *mcp.CallToolRequest, in ProbeInput) (*mcp.CallToolResult, ProbeOutput, error) {
    timeout := in.Timeout
    if timeout <= 0 {
        timeout = defaultTimeout
    }
    addr := fmt.Sprintf("%s:%d", in.Host, in.Port)
    start := time.Now()
    conn, err := net.DialTimeout("tcp", addr, time.Duration(timeout)*time.Millisecond)
    rtt := time.Since(start).Milliseconds()
    if err != nil {
        return nil, ProbeOutput{Host: in.Host, Port: in.Port, Reachable: false, RTTMs: -1}, nil
    }
    conn.Close()
    return nil, ProbeOutput{Host: in.Host, Port: in.Port, Reachable: true, RTTMs: rtt}, nil
}

// ========== 工具 3:HTTP 探测 ==========
type HTTPInput struct {
    URL     string `json:"url"     jsonschema:"full URL including scheme, e.g. https://example.com"`
    Timeout int    `json:"timeout" jsonschema:"timeout in ms, optional, default 3000"`
}

type HTTPOutput struct {
    URL        string `json:"url"`
    StatusCode int    `json:"status_code"`
    RTTMs      int64  `json:"rtt_ms"`
    Server     string `json:"server,omitempty"`
    FinalURL   string `json:"final_url,omitempty"`
}

func HTTPProbe(ctx context.Context, req *mcp.CallToolRequest, in HTTPInput) (*mcp.CallToolResult, HTTPOutput, error) {
    timeout := in.Timeout
    if timeout <= 0 {
        timeout = defaultTimeout
    }
    client := &http.Client{Timeout: time.Duration(timeout) * time.Millisecond}
    start := time.Now()
    resp, err := client.Get(in.URL)
    rtt := time.Since(start).Milliseconds()
    if err != nil {
        return &mcp.CallToolResult{
            Content: []mcp.Content{&mcp.TextContent{Text: fmt.Sprintf("HTTP 请求失败: %v", err)}},
            IsError: true,
        }, HTTPOutput{}, nil
    }
    defer resp.Body.Close()
    return nil, HTTPOutput{
        URL:        in.URL,
        StatusCode: resp.StatusCode,
        RTTMs:      rtt,
        Server:     resp.Header.Get("Server"),
        FinalURL:   resp.Request.URL.String(),
    }, nil
}

// ========== 主函数:注册并运行 ==========
func main() {
    server := mcp.NewServer(&mcp.Implementation{Name: "netcheck", Version: "v0.2.0"}, nil)

    mcp.AddTool(server, &mcp.Tool{
        Name:        "dns_lookup",
        Description: "解析主机名到 IP 地址列表。当用户想知道某个域名解析到哪些 IP 时使用。",
    }, DNSLookup)

    mcp.AddTool(server, &mcp.Tool{
        Name:        "tcp_probe",
        Description: "探测目标主机的 TCP 端口是否可达并测量连接耗时(毫秒)。用于检查服务端口是否开放。",
    }, TCPProbe)

    mcp.AddTool(server, &mcp.Tool{
        Name:        "http_probe",
        Description: "对目标 URL 发起 HTTP GET,返回状态码、耗时、Server 头。用于快速检查网站是否在线。",
    }, HTTPProbe)

    log.Printf("netcheck MCP server starting, %d tools registered", 3) // 走 stderr,不污染协议
    if err := server.Run(context.Background(), &mcp.StdioTransport{}); err != nil {
        log.Fatal(err)
    }
}

重新编译并让 Claude Code 重新加载:

go build -o netcheck-mcp .

注意:之前注册过 netcheck,配置里指向的还是同一个二进制路径,你只要重新编译覆盖那个文件即可,无需重新 claude mcp add。但在已开的 Claude Code 会话里,需要重启会话才会加载新版二进制(MCP 服务器是在会话启动时 fork 的)。

进入新会话后试这些指令:

模型会根据每个工具的 Description 自动选对工具、填对参数,并用人话解读结构化结果。

第五步:用 .mcp.json 让团队共享配置

claude mcp add 是个人配置。如果想让整个团队的项目都用上这个服务器,把配置写进项目根目录的 .mcp.json,提交到版本库:

{
  "mcpServers": {
    "netcheck": {
      "command": "/opt/tools/netcheck-mcp",
      "args": [],
      "env": {}
    }
  }
}

Claude Code 在该项目目录启动时,会读取 .mcp.json 并提示用户授权连接(出于安全,项目级 MCP 服务器首次连接需用户确认)。这样同事 clone 项目后,开箱即得同一套工具。

type 字段:stdio 服务器可省略(默认就是 stdio);若是远程 HTTP/SSE 服务器则写 "type": "http" + "url"。

实战案例:让 Claude Code 当你的网络排障助手

把上面三件套串起来,就是一个真实可用的"网络排障"工作流。重新编译后,在 Claude Code 里给一个复合任务:

我怀疑 api.example.com 有问题。帮我排查:先 DNS 解析看 IP 是否正常,再探测它的 443 端口,最后对 https://api.example.com 做一次 HTTP 探测,综合告诉我结论。

预期行为:模型会串行调用三个工具(DNS → TCP → HTTP),把每一步的结构化输出汇总,最终给你一个判断:"DNS 正常解析到 x.x.x.x,443 端口可达(耗时 45ms),但 HTTP 返回 503,可能是后端服务异常。"——而这三步的"排障判断"逻辑是模型基于工具返回的强类型数据自己推理的。

这就是 MCP 的价值:你只负责把能力暴露成可靠的、强类型的、自描述的工具,编排与推理交给模型。

整体工作流架构

flowchart LR
    U[用户在 Claude Code<br/>输入排障指令] --> CC[Claude Code<br/>模型推理+编排]
    CC -->|读取 .mcp.json<br/>发现工具列表| REG[工具注册表]
    REG --> T1[dns_lookup]
    REG --> T2[tcp_probe]
    REG --> T3[http_probe]
    CC -->|JSON-RPC over stdio<br/>fork 进程| BIN[/netcheck-mcp<br/>Go 二进制/]
    BIN --> T1
    BIN --> T2
    BIN --> T3
    T1 -->|net.LookupHost| NET[(真实网络)]
    T2 -->|net.DialTimeout| NET
    T3 -->|http.Client| NET
    T1 & T2 & T3 -->|结构化 Output| CC
    CC -->|自然语言结论| U
    style BIN fill:#00ADD8,color:#fff
    style CC fill:#d97706,color:#fff

进阶方向

通了的链路只是起点,Go SDK 还有几个值得探索的能力:

  1. Resources 原语:把只读数据(配置文件、当前路由表)作为 resource 暴露,模型可主动读取而非调用
  2. 低层 ToolHandler:当你需要手写 Schema 校验逻辑(比如约束 port 在 1-65535、自定义错误消息)时,用底层 handler 自己控制 unmarshalAndValidate,官方 toolschemas 示例有完整范式
  3. HTTP/SSE 传输:把 StdioTransport{} 换成 streamable HTTP transport,就能把 Go 服务器部署成远程服务,给团队共享——配合上一轮讲过的远程部署模式
  4. 中间件:SDK 支持 per-call 中间件,可统一加日志、限流(rate-limiting 示例)、鉴权(auth-middleware 示例)
  5. 单元测试:因为 handler 是纯函数,直接构造 mcp.CallToolRequest{} + Input 结构体调用即可断言 Output,无需起真实传输层

训练营适用性评估

评估项 说明
适合训练营类型 Go 后端训练营、运维/DevOps 自动化训练营、AI 工程实战训练营
前置技能 Go 基础(结构体、context、标准库 net)
难度 中等(API 直观,但需理解 JSON Schema 反射)
预计教学时长 3-4 小时(含动手:1h 讲范式 + 2h 写三件套 + 1h 接入排障)
学员产出 一个可带回家复用的 Go 单二进制 MCP 运维工具
与已有课程衔接 可作为"Go 进阶"的实战收尾,或"AI Agent 工程"的本地工具化模块

参考来源

  1. modelcontextprotocol/go-sdk 官方仓库 README — 官方 Go SDK,含本教程全部核心 API(NewServer/AddTool/StdioTransport/结构体 I/O 范式)与版本兼容表
  2. go-sdk 包文档(pkg.go.dev) — API 参考,含 mcp、jsonrpc、auth 等子包
  3. toolschemas 示例源码 — 高层 ToolHandlerFor 与底层 ToolHandler 手写 Schema 校验两种范式
  4. Connect Claude Code to tools via MCP(官方文档) — claude mcp add、.mcp.json、stdio/HTTP 配置与作用域说明
  5. Build a Go MCP Server: Step-by-Step Tutorial — 第三方完整图文教程,覆盖工具注册与多传输模式

本文由「51学通信」原创制作,站长爱卫生(微信:gprshome201101)持续分享通信与 AI 工程实战。想交流 MCP 落地、Go 工具开发,欢迎留言。

[!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学通信知识星球海报-扫码加入