首页 › tutorials › 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 开发者会一眼看上的优势:

对比维度 官方 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
}

关键点解读:

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

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

// 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 都会污染协议流。


六、接入 Claude Code 验证

在项目根目录创建 .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"

七、补齐 Resources 与 Prompts

只有 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 升级到 Streamable HTTP

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 里这两条路径决定了部署形态:

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


九、远程模式接入 Claude Code

.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。


十三、延伸阅读


参考来源

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

训练营适用性评估

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