<!-- title: Go官方SDK构建MCP服务器实战 -->

## 概述

本教程带你用 Anthropic 与 Google 联合维护的**官方 Go SDK**（`github.com/modelcontextprotocol/go-sdk`）从零构建一个完整的 MCP 服务器。你将掌握 Tools / Resources / Prompts 三大原语的 Go 写法，理解 stdio 与 Streamable HTTP 两种传输的取舍，并最终把一个可运行的"代码仓库体检"服务器接入 Claude Code。适用人群：有 Go 基础（懂 `context`、struct tag、`net/http`）的后端工程师，想用 Go 的强类型与并发优势为 LLM 打造高性能工具。

---

## 一、为什么用 Go 写 MCP 服务器

在官方 Go SDK 出现之前，生态里主要由社区库 `mark3labs/mcp-go`（8k+ star）撑起 Go 阵地。2025 年，MCP 官方组织与 Google 合作推出了 `modelcontextprotocol/go-sdk`，带来了几个 Go 开发者会一眼看上的优势：

- **强类型处理器签名**：工具的输入输出直接用 struct + jsonschema tag 描述，无需手写 JSON Schema 字符串，编译期就能抓住参数错误。
- **遵循 Go 惯例**：全程 `context.Context` 传递、`io.Closer` 资源管理、最小化可扩展 API。
- **协议版本紧跟规范**：v1.4.0 已支持到 `2025-11-25` 规范，回溯兼容 `2025-03-26`、`2024-11-05`。
- **单进程高并发**：Streamable HTTP 模式下，一个 Go 二进制可同时服务成百上千个会话，这是脚本语言 SDK 在长连接场景下难以企及的。

| 对比维度 | 官方 `go-sdk` | 社区 `mcp-go` |
|---|---|---|
| 维护方 | MCP 官方 + Google | mark3labs（Ed Zynda 原创） |
| API 风格 | 强类型 struct 处理器 | `mcp.CallToolRequest` + helper |
| 规范覆盖 | 最全（含 auth/oauthex） | 核心完整，高级特性推进中 |
| 上手成本 | 需懂 Go tag 体系 | 更"高层"，样板少 |
| 适用场景 | 生产级、长期维护 | 快速原型、Demo |

> 官方 SDK 的 README 明确致谢了 mcp-go 等社区库，二者可以并存。本教程以官方 SDK 为主线，因为它代表了规范的最准确实现。

---

## 二、前置条件

```bash
# 1. Go 1.23+（官方 SDK 只支持受维护的 Go 版本）
go version

# 2. Claude Code CLI（用于联调客户端）
claude --version

# 3. 可选：MCP Inspector（图形化协议调试）
npx @modelcontextprotocol/inspector --version
```

确认 Go 版本符合 `go.dev/doc/devel/release#policy` 的支持窗口。低于 1.23 的环境需要先升级，否则 `go get` 会报 `unsupported go version`。

---

## 三、环境准备与项目初始化

```bash
mkdir coderepo-doctor && cd coderepo-doctor
go mod init github.com/you/coderepo-doctor

# 拉取官方 SDK（写本文时稳定版为 v1.4.x 系列）
go get github.com/modelcontextprotocol/go-sdk/mcp@latest
```

预期输出：

```
go: downloading github.com/modelcontextprotocol/go-sdk v1.4.0
go: added github.com/modelcontextprotocol/go-sdk v1.4.0
go: added github.com/google/jsonschema v0.x.x
```

官方 SDK 内部依赖 `google/jsonschema` 自动从 Go struct 生成工具入参的 JSON Schema——这正是"强类型"的底座。模块结构建议如下：

```
coderepo-doctor/
├── go.mod
├── main.go            # 入口：组装 server + 选择 transport
├── tools.go           # 工具定义与处理器
├── resources.go       # 资源定义
├── prompts.go         # 提示模板
└── Makefile           # build/run/inspect 快捷命令
```

---

## 四、第一个工具：强类型处理器

官方 SDK 的核心心智模型：**定义 Input/Output struct → 注册带类型签名的函数 → 跑在 Transport 上**。下面是一个统计目录文件行数的工具：

```go
// tools.go
package main

import (
	"context"
	"fmt"
	"os"
	"path/filepath"
	"strings"

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

// CountLinesInput 由 SDK 自动转成 JSON Schema 发给客户端。
type CountLinesInput struct {
	Path    string   `json:"path" jsonschema:"the directory or file to scan"`
	Exts    []string `json:"exts,omitempty" jsonschema:"file extensions to include, e.g. [\".go\"]"`
}

// CountLinesOutput 结构化的工具产出，客户端可直接解析。
type CountLinesOutput struct {
	Files  int   `json:"files"`
	Lines  int   `json:"lines"`
	Bytes  int64 `json:"bytes"`
}

// 处理器签名固定为：
// (ctx, *mcp.CallToolRequest, Input) -> (*mcp.CallToolResult, Output, error)
func CountLines(ctx context.Context, req *mcp.CallToolRequest, in CountLinesInput) (
	*mcp.CallToolResult, CountLinesOutput, error,
) {
	var out CountLinesOutput
	err := filepath.WalkDir(in.Path, func(p string, d os.DirEntry, err error) error {
		if err != nil || d.IsDir() {
			return err
		}
		if len(in.Exts) > 0 && !hasExt(p, in.Exts) {
			return nil
		}
		data, e := os.ReadFile(p)
		if e != nil {
			return nil // 跳过无权限文件，不中断
		}
		out.Files++
		out.Lines += strings.Count(string(data), "\n") + 1
		out.Bytes += int64(len(data))
		return nil
	})
	if err != nil {
		// 返回 error：客户端会收到一个 JSON-RPC 错误
		return nil, CountLinesOutput{}, err
	}
	// 第一个返回值通常给 nil；SDK 会把 Output 序列化为结构化内容
	return nil, out, nil
}

func hasExt(p string, exts []string) bool {
	for _, e := range exts {
		if strings.HasSuffix(p, e) {
			return true
		}
	}
	return false
}
```

**关键点解读**：

1. `jsonschema:"..."` tag 是文档，SDK 会同时把它放进生成 schema 的 `description` 字段，LLM 据此判断何时调用。
2. 处理器返回三元组 `(result, output, error)`。返回 `error` 表示协议级失败；返回 `result` 非 nil 时可塞入自定义内容（如多段文本、图片）。
3. `context.Context` 必须接收——客户端中断时 ctx 会被取消，长时间 `WalkDir` 应配合 `select` 响应取消。

---

## 五、组装服务器并跑在 stdio 上

```go
// main.go
package main

import (
	"context"
	"flag"
	"log"

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

func main() {
	addr := flag.String("http", "", "empty=stdio; otherwise listen addr like :8080")
	flag.Parse()

	server := mcp.NewServer(
		&mcp.Implementation{Name: "coderepo-doctor", Version: "0.1.0"},
		nil, // 选项：日志、能力声明等
	)

	// 注册工具：Name/Description 单独给 struct，便于复用处理器
	mcp.AddTool(server,
		&mcp.Tool{Name: "count_lines", Description: "Count files/lines/bytes under a path"},
		CountLines,
	)

	ctx := context.Background()
	if *addr == "" {
		// stdio：被 Claude Code 作为子进程拉起
		if err := server.Run(ctx, &mcp.StdioTransport{}); err != nil {
			log.Fatal(err)
		}
		return
	}
	// HTTP 分支见第八节
	serveHTTP(ctx, server, *addr)
}
```

编译：

```bash
go build -o coderepo-doctor .
```

`server.Run` 是阻塞调用，它负责完成 MCP 的 `initialize` 握手、能力协商、消息循环，直到客户端断开。`StdioTransport{}` 把 JSON-RPC 消息打到 stdin/stdout——**这是 stdio 模式下唯一允许的输出通道，任何 `fmt.Println` 都会污染协议流**。

---

## 六、接入 Claude Code 验证

在项目根目录创建 `.mcp.json`（团队共享配置）：

```json
{
  "mcpServers": {
    "coderepo-doctor": {
      "command": "/绝对路径/coderepo-doctor",
      "args": [],
      "env": {}
    }
  }
}
```

启动 Claude Code：

```bash
claude
# 进入会话后输入
/mcp
```

预期看到 `coderepo-doctor` 状态为 `connected`，并列出 `count_lines` 工具。然后试着提问：

> 统计一下当前目录下所有 .go 文件有多少行

Claude 会自动决定调用 `count_lines`，传入 `{"path": ".", "exts": [".go"]}`，并读回结构化的 Files/Lines/Bytes。

**常见问题**：

| 现象 | 原因 | 解决 |
|---|---|---|
| `/mcp` 显示 `failed to connect` | 二进制路径不对或无执行权限 | 用绝对路径，`chmod +x` |
| 连接成功但工具调用超时 | 处理器里有阻塞 IO 未响应 ctx | 用 `filepath.WalkDir` 而非递归 `ReadDir`，必要时检查 `ctx.Done()` |
| 工具参数被 LLM 误填 | jsonschema description 太简略 | 在 tag 里写清约束，如 `jsonschema:"required; absolute path"` |

---

## 七、补齐 Resources 与 Prompts

只有 Tools 还不够，让服务器也能"被订阅知识"和"提供提示模板"。

```go
// resources.go
func addResources(s *mcp.Server) {
	mcp.AddResource(s,
		&mcp.ResourceTemplate{
			URIDescription: "file content by path",
			URITemplate:    "file:///{path}",
			Name:           "source_file",
			MimeType:       "text/plain",
		},
		func(ctx context.Context, req *mcp.ReadResourceRequest) (*mcp.ReadResourceResult, error) {
			path := req.Params.URI[len("file:///"):]
			data, err := os.ReadFile(path)
			if err != nil {
				return nil, err
			}
			return &mcp.ReadResourceResult{
				Contents: []*mcp.ResourceContents{
					{URI: req.Params.URI, MIMEType: "text/plain", Text: string(data)},
				},
			}, nil
		},
	)
}
```

```go
// prompts.go
type ReviewPromptInput struct {
	FilePath string `json:"filePath" jsonschema:"the file to review"`
	Focus    string `json:"focus,omitempty" jsonschema:"security|performance|style"`
}

func addPrompts(s *mcp.Server) {
	mcp.AddPrompt(s,
		&mcp.Prompt{Name: "code_review", Description: "Generate a focused code review prompt"},
		func(ctx context.Context, req *mcp.PromptRequest, in ReviewPromptInput) (*mcp.PromptResult, error) {
			focus := in.Focus
			if focus == "" {
				focus = "general"
			}
			msg := fmt.Sprintf("请对 %s 做 %s 维度的代码评审，先指出最关键的 3 个问题。",
				in.FilePath, focus)
			return &mcp.PromptResult{
				Messages: []*mcp.PromptMessage{
					{Role: "user", Content: &mcp.TextContent{Text: msg}},
				},
			}, nil
		},
	)
}
```

在 `main.go` 里调用 `addResources(server)` 与 `addPrompts(server)`。注意 `ResourceTemplate` 用的是 RFC 6570 URI 模板，`{path}` 会成为 LLM 可填的变量。

---

## 八、从 stdio 升级到 Streamable HTTP

stdio 适合本地单机；当你的 MCP 工具要被团队共享、部署到云上，就需要 **Streamable HTTP** 传输。这是 `2025-03-26` 规范引入、取代旧版 HTTP+SSE 的新传输：单一端点，POST 携带 JSON-RPC，响应可降级为普通 JSON 或升级为 SSE 流，由 `Mcp-Session-Id` 头维系会话。

官方 SDK 通过 `NewStreamableHTTPHandler` 实现，它本身是个标准 `http.Handler`：

```go
// main.go 的 HTTP 分支
func serveHTTP(ctx context.Context, s *mcp.Server, addr string) {
	// 每个新会话都返回同一个 server 实例（共享工具集）
	handler := mcp.NewStreamableHTTPHandler(
		func(r *http.Request) *mcp.Server { return s },
		&mcp.StreamableHTTPOptions{
			// StatelessHi: true, // 置 true 则走无状态模式（见下文）
		},
	)
	mux := http.NewServeMux()
	mux.Handle("/mcp", handler)

	srv := &http.Server{Addr: addr, Handler: mux}
	log.Printf("streamable HTTP listening on %s/mcp", addr)
	go func() { <-ctx.Done(); srv.Shutdown(context.Background()) }()
	if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
		log.Fatal(err)
	}
}
```

启动远程模式：

```bash
./coderepo-doctor -http :8080
# 日志：streamable HTTP listening on :8080/mcp
```

**stateful vs stateless**——源码 `streamable.go` 里这两条路径决定了部署形态：

- **Stateful（默认）**：支持 GET/POST/DELETE，用 `Mcp-Session-Id` 头维护持久会话与订阅，适合长连接、需要服务端推送通知的场景。要求部署环境支持会话粘滞（sticky session）。
- **Stateless**：只支持 POST，每次请求创建临时会话，用完即弃，天然适合 Serverless / 多副本无状态扩缩容，代价是失去服务端主动推送能力。

选型口诀：**需要通知/进度推送 → stateful；要多副本水平扩展 → stateless。**

---

## 九、远程模式接入 Claude Code

`.mcp.json` 的 stdio 配置换成 HTTP 类型：

```json
{
  "mcpServers": {
    "coderepo-doctor": {
      "type": "http",
      "url": "http://localhost:8080/mcp"
    }
  }
}
```

远程（需鉴权）则用：

```json
{
  "mcpServers": {
    "coderepo-doctor": {
      "type": "http",
      "url": "https://doctor.example.com/mcp",
      "headers": { "Authorization": "Bearer <token>" }
    }
  }
}
```

官方 SDK 配套提供 `auth` 与 `oauthex` 包，支持 OAuth Protected Resource Metadata，可与 `2025-11-25` 规范的远程认证流程对接。

---

## 十、完整工作流程图

```text
flowchart TD
    A[go mod init + go get go-sdk] --> B[定义 Input/Output struct]
    B --> C[mcp.NewServer + AddTool/AddResource/AddPrompt]
    C --> D{传输选择}
    D -->|本地子进程| E[StdioTransport<br/>server.Run]
    D -->|远程共享| F[NewStreamableHTTPHandler<br/>stateful/stateless]
    E --> G[Claude Code .mcp.json: command]
    F --> H[Claude Code .mcp.json: http url]
    G --> I[/mcp 验证 + 工具调用]
    H --> I
    I --> J[MCP Inspector 抓包调试]

    style D fill:#e1f5ff,stroke:#0288d1
    style F fill:#fff3e0,stroke:#f57c00
```

---

## 十一、实战案例：可运行的"仓库体检"服务器

把前面零散片段拼成完整闭环——这个服务器能数行数、能按模板读文件、能生成评审提示。完整 `main.go`：

```go
package main

import (
	"context"
	"flag"
	"log"
	"net/http"

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

func main() {
	addr := flag.String("http", "", "empty=stdio, else listen addr")
	flag.Parse()

	srv := mcp.NewServer(
		&mcp.Implementation{Name: "coderepo-doctor", Version: "0.1.0"}, nil)

	mcp.AddTool(srv, &mcp.Tool{
		Name: "count_lines", Description: "Count files/lines/bytes under a path",
	}, CountLines)

	addResources(srv)
	addPrompts(srv)

	ctx := context.Background()
	if *addr == "" {
		log.Fatal(srv.Run(ctx, &mcp.StdioTransport{}))
	}
	handler := mcp.NewStreamableHTTPHandler(
		func(*http.Request) *mcp.Server { return srv }, nil)
	mux := http.NewServeMux()
	mux.Handle("/mcp", handler)
	log.Println("listening", *addr+"/mcp")
	log.Fatal(http.ListenAndServe(*addr, mux))
}
```

跑通三步验证：

```bash
# 1) 用 Inspector 图形化调试 stdio
npx @modelcontextprotocol/inspector ./coderepo-doctor

# 2) 本地 stdio 接 Claude Code
./coderepo-doctor          # 默认 stdio

# 3) 远程 HTTP 模式 + 压测并发
./coderepo-doctor -http :8080
# 另开终端，用 ab/wrk 打 /mcp，观察 Go 单二进制扛并发的能力
```

用 Inspector 时，左侧应能看到 `tools/list` 返回 `count_lines`，`resources/templates/list` 返回 `file:///{path}`，`prompts/list` 返回 `code_review`。手动 `tools/call` 一次 `count_lines`，右侧响应体应包含结构化的 `files/lines/bytes`。

---

## 十二、常见问题与排错

**Q1：stdio 模式下工具能跑，切到 HTTP 后 `/mcp` 报 400。**
`NewStreamableHTTPHandler` 的 `getServer` 回调若返回 `nil`，SDK 会直接回 400。确认回调始终返回非 nil 的 `*mcp.Server`。另外 v1.4.0 起内置 DNS rebinding 防护，反代时要保证 `Host` 头透传。

**Q2：JSON Schema 没出现枚举/范围约束。**
Go struct 的 jsonschema tag 支持有限，复杂约束（如 `enum`、`minimum`）需用 `//go:generate` 配合 `google/jsonschema` 或手写 `mcp.Tool.Hint` 补充。这是官方 SDK 相对 mcp-go 的一个小短板。

**Q3：stateful 模式下多副本部署会话丢失。**
要么前置负载均衡做 session 粘滞（按 `Mcp-Session-Id` hash），要么切 stateless 模式放弃服务端推送。

**Q4：Handler 里的 `fmt.Println` 把客户端搞挂了。**
stdio 模式下 stdout 是协议通道，所有调试日志必须走 `log` 包（默认输出到 stderr），切勿 `fmt.Println` 到 stdout。

---

## 十三、延伸阅读

- 官方 Go SDK 仓库与 `docs/` 目录：`github.com/modelcontextprotocol/go-sdk`（含 spec 到包的映射文档）
- 社区 `mcp-go` 文档站：`mcp-go.dev/getting-started`（高层 API 对照参考）
- MCP 规范与 Build a server 指南：`modelcontextprotocol.io/docs/develop/build-server`
- Streamable HTTP 传输规范：MCP `2025-03-26` revision（`NewStreamableHTTPHandler` 的协议依据）
- DeepWiki 自动文档：`deepwiki.com/modelcontextprotocol/go-sdk`（按源码文件梳理调用关系）

---

## 参考来源

1. [modelcontextprotocol/go-sdk（官方 Go SDK）](https://github.com/modelcontextprotocol/go-sdk) — Anthropic 与 Google 联合维护，本文 Tools/HTTP API、版本兼容表的权威来源。
2. [mcp-go Getting Started](https://mcp-go.dev/getting-started/) — 社区主流 SDK，用于高层 API 与官方 SDK 的对比。
3. [Build MCP Servers in Go — MCPcat Guide](https://mcpcat.io/guides/building-mcp-server-go/) — 生产级 Go MCP 服务器构建的并发与传输实践。
4. [go-sdk streamable.go 源码](https://github.com/modelcontextprotocol/go-sdk/blob/main/mcp/streamable.go) — `NewStreamableHTTPHandler`、stateful/stateless 路径与 `Mcp-Session-Id` 处理的实现细节。
5. [Model Context Protocol — Build a server](https://modelcontextprotocol.io/docs/develop/build-server) — 协议层 initialize 握手、能力协商与传输选择的标准说明。

---

## 训练营适用性评估

- **适用类型**：Go 后端工程训练营、MCP 生态开发专题营、LLM 工具链进阶营。不适合零基础——要求学员具备 Go struct tag、`net/http`、`context` 的实战经验。
- **教学时长**：建议 1 个全天（6 小时）。其中环境与首工具 1.5h，Resources/Prompts 1.5h，Streamable HTTP 与远程部署 2h，完整实战与排错 1h。
- **分组建议**：2 人一组，一人写工具处理器、一人写客户端/Inspector 调试，最后互换。远程 HTTP 部分可设为加分项（部署到内网 + 鉴权头）。
- **考核点**：能否独立完成 stdio → HTTP 切换、能否正确处理 `ctx` 取消、能否区分 stateful/stateless 并说出选型依据。

> [!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)
