本教程带你用 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 SDK 出现之前,生态里主要由社区库 mark3labs/mcp-go(8k+ star)撑起 Go 阵地。2025 年,MCP 官方组织与 Google 合作推出了 modelcontextprotocol/go-sdk,带来了几个 Go 开发者会一眼看上的优势:
context.Context 传递、io.Closer 资源管理、最小化可扩展 API。2025-11-25 规范,回溯兼容 2025-03-26、2024-11-05。| 对比维度 | 官方 go-sdk |
社区 mcp-go |
|---|---|---|
| 维护方 | MCP 官方 + Google | mark3labs(Ed Zynda 原创) |
| API 风格 | 强类型 struct 处理器 | mcp.CallToolRequest + helper |
| 规范覆盖 | 最全(含 auth/oauthex) | 核心完整,高级特性推进中 |
| 上手成本 | 需懂 Go tag 体系 | 更"高层",样板少 |
| 适用场景 | 生产级、长期维护 | 快速原型、Demo |
官方 SDK 的 README 明确致谢了 mcp-go 等社区库,二者可以并存。本教程以官方 SDK 为主线,因为它代表了规范的最准确实现。
# 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。
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 上。下面是一个统计目录文件行数的工具:
// 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
}
关键点解读:
jsonschema:"..." tag 是文档,SDK 会同时把它放进生成 schema 的 description 字段,LLM 据此判断何时调用。(result, output, error)。返回 error 表示协议级失败;返回 result 非 nil 时可塞入自定义内容(如多段文本、图片)。context.Context 必须接收——客户端中断时 ctx 会被取消,长时间 WalkDir 应配合 select 响应取消。// 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)
}
编译:
go build -o coderepo-doctor .
server.Run 是阻塞调用,它负责完成 MCP 的 initialize 握手、能力协商、消息循环,直到客户端断开。StdioTransport{} 把 JSON-RPC 消息打到 stdin/stdout——这是 stdio 模式下唯一允许的输出通道,任何 fmt.Println 都会污染协议流。
在项目根目录创建 .mcp.json(团队共享配置):
{
"mcpServers": {
"coderepo-doctor": {
"command": "/绝对路径/coderepo-doctor",
"args": [],
"env": {}
}
}
}
启动 Claude Code:
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" |
只有 Tools 还不够,让服务器也能"被订阅知识"和"提供提示模板"。
// 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
},
)
}
// 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 适合本地单机;当你的 MCP 工具要被团队共享、部署到云上,就需要 Streamable HTTP 传输。这是 2025-03-26 规范引入、取代旧版 HTTP+SSE 的新传输:单一端点,POST 携带 JSON-RPC,响应可降级为普通 JSON 或升级为 SSE 流,由 Mcp-Session-Id 头维系会话。
官方 SDK 通过 NewStreamableHTTPHandler 实现,它本身是个标准 http.Handler:
// 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)
}
}
启动远程模式:
./coderepo-doctor -http :8080
# 日志:streamable HTTP listening on :8080/mcp
stateful vs stateless——源码 streamable.go 里这两条路径决定了部署形态:
Mcp-Session-Id 头维护持久会话与订阅,适合长连接、需要服务端推送通知的场景。要求部署环境支持会话粘滞(sticky session)。选型口诀:需要通知/进度推送 → stateful;要多副本水平扩展 → stateless。
.mcp.json 的 stdio 配置换成 HTTP 类型:
{
"mcpServers": {
"coderepo-doctor": {
"type": "http",
"url": "http://localhost:8080/mcp"
}
}
}
远程(需鉴权)则用:
{
"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 规范的远程认证流程对接。
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:
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))
}
跑通三步验证:
# 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。
docs/ 目录:github.com/modelcontextprotocol/go-sdk(含 spec 到包的映射文档)mcp-go 文档站:mcp-go.dev/getting-started(高层 API 对照参考)modelcontextprotocol.io/docs/develop/build-server2025-03-26 revision(NewStreamableHTTPHandler 的协议依据)deepwiki.com/modelcontextprotocol/go-sdk(按源码文件梳理调用关系)NewStreamableHTTPHandler、stateful/stateless 路径与 Mcp-Session-Id 处理的实现细节。net/http、context 的实战经验。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 等场景全覆盖。
