<!-- title: 用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**。

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

三个标签各司其职：

- `json:"host"` —— 序列化/反序列化的字段名（模型传 JSON 时用的 key）
- `jsonschema:"..."` —— 这个字段会出现在给模型看的工具描述里，**写清楚它能极大提升模型调用准确率**
- 字段的 Go 类型（`string` / `int`）会自动映射成 Schema 里的 `type: string` / `type: integer`

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

```go
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 测试。

## 第一步：初始化项目

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

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

**关键点解读：**

- `mcp.NewServer(实现信息, 配置)` 第一个参数是服务器的名字和版本（会出现在协议握手里），第二个传 `nil` 用默认配置
- `mcp.AddTool` 的第二个参数是 `*mcp.Tool`，注意 `Name` 是模型调用时用的标识符，**必须唯一**；`Description` 是给模型看的，写清"干什么、什么时候用"
- `mcp.StdioTransport{}` 是本地集成的标准传输：Claude Code 会 fork 你的进程，通过它的 stdin/stdout 收发 JSON-RPC 消息。**你的工具里绝对不能往 stdout 打印调试信息**——那会污染协议通道，必须用 `log`（走 stderr）或返回值

**编译验证：**

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

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

## 第三步：接入 Claude Code 验证

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

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

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

验证注册成功：

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

然后启动 Claude Code 交互会话：

```bash
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` 替换为完整版本：

```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 重新加载：**

```bash
go build -o netcheck-mcp .
```

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

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

- `查一下 github.com 解析到哪些 IP`
- `探测一下 8.8.8.8 的 53 端口通不通`
- `检查 https://httpbin.org/status/200 是否在线，状态码是多少`

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

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

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

```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 的价值：你只负责把**能力**暴露成可靠的、强类型的、自描述的工具，**编排与推理**交给模型。

## 整体工作流架构

```mermaid
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](https://github.com/modelcontextprotocol/go-sdk) — 官方 Go SDK，含本教程全部核心 API（`NewServer`/`AddTool`/`StdioTransport`/结构体 I/O 范式）与版本兼容表
2. [go-sdk 包文档（pkg.go.dev）](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk) — API 参考，含 `mcp`、`jsonrpc`、`auth` 等子包
3. [toolschemas 示例源码](https://github.com/modelcontextprotocol/go-sdk/tree/main/examples/server/toolschemas) — 高层 `ToolHandlerFor` 与底层 `ToolHandler` 手写 Schema 校验两种范式
4. [Connect Claude Code to tools via MCP（官方文档）](https://code.claude.com/docs/en/mcp) — `claude mcp add`、`.mcp.json`、stdio/HTTP 配置与作用域说明
5. [Build a Go MCP Server: Step-by-Step Tutorial](https://thedailyworkflow.com/mcp/tutorial/build-your-first-mcp-server-in-go) — 第三方完整图文教程，覆盖工具注册与多传输模式

---

本文由「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学通信知识星球海报-扫码加入](https://wiki.500nian.com/images/zhixingqiu_qrcode.png)
