Go SDK实战:用Go语言开发高性能MCP Server
摘要:使用Go语言和mark3labs/mcp-go SDK开发高性能MCP Server,涵盖工具定义、上下文管理、并发处理和部署,适合对性能有要求的MCP服务场景。
Go SDK实战 用Go语言开发高性能MCP Server
上个月公司有个内部工具平台要接入MCP协议,我本来想用Python快速搞定的,结果压测的时候发现Python Server在并发场景下扛不住。QA同学一上来就开了50个并发请求,CPU直接飙到90%,响应延迟从50ms飙升到800ms。我咬咬牙花了一个周末用Go重写了整个Server,同样的压测CPU只用了15%,延迟稳定在20ms以内。这次经历让我彻底理解了为什么官方会推出Go SDK。
Go MCP SDK是什么
Anthropic官方维护的Go SDK仓库在 github.com/modelcontextprotocol/go-sdk,目前最新版本是v1.7.0,支持MCP协议规范2026-07-28。这个SDK由几个核心包组成。
mcp 包是主力,提供了构建Server和Client的全部API。jsonrpc 包给自定义传输层用。auth 包处理OAuth认证。oauthex 包是OAuth的扩展功能。
说到Go SDK的选择,市面上其实有好几个。官方SDK出来之前,社区已经有 mcp-go(mark3labs维护)、mcp-golang(metoro-io维护)、go-mcp(ThinkInAI维护)三个比较成熟的方案。官方SDK的README里专门感谢了这些项目。我的建议是如果是新项目直接用官方SDK,老项目可以暂时不迁移,等官方SDK稳定后再说。
环境搭建和项目初始化
先确认Go版本,官方SDK要求Go 1.23以上。我本地用的是Go 1.24。
# 初始化项目
mkdir mcp-go-server && cd mcp-go-server
go mod init mcp-go-server
# 安装官方SDK
go get github.com/modelcontextprotocol/go-sdk@latest
安装完成后看一下go.mod,确认依赖拉下来了。
// go.mod 内容示例
module mcp-go-server
go 1.24
require github.com/modelcontextprotocol/go-sdk v1.7.0
项目结构我建议这样组织。
mcp-go-server/
├── go.mod
├── go.sum
├── main.go // 程序入口
├── tools/
│ ├── calc.go // 计算器工具
│ ├── files.go // 文件操作工具
│ └── http.go // HTTP请求工具
├── resources/
│ └── config.go // 配置资源
└── prompts/
└── code_review.go // 代码审查提示
用Go写MCP三大件
MCP Server的核心就三样东西,Tools、Resources、Prompts。我一个个说。
工具(Tool)
Go SDK定义工具的方式很优雅,用结构体加jsonschema标签来描述输入输出,编译器帮你检查类型。
package main
import (
"context"
"fmt"
"log"
"github.com/modelcontextprotocol/go-sdk/mcp"
)
// CalcInput 计算器工具的输入参数
// jsonschema标签会被SDK自动提取生成JSON Schema
type CalcInput struct {
Operation string `json:"operation" jsonschema:"the operation to perform, enum=add,enum=subtract,enum=multiply,enum=divide"`
A float64 `json:"a" jsonschema:"the first operand"`
B float64 `json:"b" jsonschema:"the second operand"`
}
// CalcOutput 计算器工具的输出
type CalcOutput struct {
Result float64 `json:"result" jsonschema:"the calculation result"`
}
// CalcHandler 工具处理函数
// 签名固定,ctx + 请求 + 输入 -> 结果 + 输出 + error
func CalcHandler(ctx context.Context, req *mcp.CallToolRequest, input CalcInput) (*mcp.CallToolResult, CalcOutput, error) {
var result float64
switch input.Operation {
case "add":
result = input.A + input.B
case "subtract":
result = input.A - input.B
case "multiply":
result = input.A * input.B
case "divide":
if input.B == 0 {
// 返回错误结果而不是Go的error
// 这样客户端能看到具体的错误信息
return &mcp.CallToolResult{
IsError: true,
Content: []mcp.Content{
&mcp.TextContent{Text: "division by zero"},
},
}, CalcOutput{}, nil
}
result = input.A / input.B
default:
return nil, CalcOutput{}, fmt.Errorf("unknown operation: %s", input.Operation)
}
// 正常返回,第一个参数通常为nil
// SDK会自动把output序列化成structured content
return nil, CalcOutput{Result: result}, nil
}
这里有个我踩过的坑。一开始我以为返回Go的error客户端就能看到,结果发现error信息在客户端那边被吞掉了,用户只看到一个"Tool execution failed"。后来翻SDK源码才搞明白,MCP协议区分两种错误,工具级错误和协议级错误。工具执行过程中出的问题(比如除零、查询无结果)应该用 IsError: true 的CallToolResult返回,这样错误信息会透传给用户。协议级错误(参数格式不对)才返回Go的error。
资源(Resource)
资源用来暴露数据给客户端读取,比如配置文件、数据库记录。
// ConfigResource 配置资源处理器
// 通过URI来标识和读取资源
func registerResources(server *mcp.Server) {
// 注册静态资源
mcp.AddResource(server, &mcp.Resource{
URI: "config://app/settings",
Name: "app-config",
Description: "Application configuration",
MimeType: "application/json",
}, func(ctx context.Context, req *mcp.ReadResourceRequest) (*mcp.ReadResourceResult, error) {
// 这里可以从数据库或文件读取实际配置
configJSON := `{"debug": false, "maxConnections": 100, "timeout": 30}`
return &mcp.ReadResourceResult{
Contents: []mcp.ResourceContents{
&mcp.TextResourceContents{
URI: "config://app/settings",
MimeType: "application/json",
Text: configJSON,
},
},
}, nil
})
// 注册资源模板,支持动态URI
mcp.AddResourceTemplate(server, &mcp.ResourceTemplate{
URITemplate: "user://{userId}/profile",
Name: "user-profile",
Description: "Get user profile by ID",
}, func(ctx context.Context, req *mcp.ReadResourceRequest) (*mcp.ReadResourceResult, error) {
// 从URI中提取参数
// SDK会自动匹配模板并传入具体URI
uri := req.Params.URI
profile := fmt.Sprintf(`{"uri": "%s", "name": "Alice", "role": "engineer"}`, uri)
return &mcp.ReadResourceResult{
Contents: []mcp.ResourceContents{
&mcp.TextResourceContents{
URI: uri,
Text: profile,
},
},
}, nil
})
}
提示(Prompt)
提示模板给客户端提供预定义的交互模式。
// CodeReviewInput 代码审查提示的参数
type CodeReviewInput struct {
Language string `json:"language" jsonschema:"programming language, enum=go,enum=python,enum=javascript"`
Code string `json:"code" jsonschema:"the code to review"`
FocusArea string `json:"focusArea" jsonschema:"focus area, enum=security,enum=performance,enum=readability"`
}
// registerPrompts 注册提示模板
func registerPrompts(server *mcp.Server) {
mcp.AddPrompt(server, &mcp.Prompt{
Name: "code-review",
Description: "Review code for potential issues",
}, func(ctx context.Context, req *mcp.GetPromptRequest, input CodeReviewInput) (*mcp.GetPromptResult, error) {
// 构造提示消息
systemMsg := fmt.Sprintf(
"You are an expert code reviewer focusing on %s. "+
"Review the following %s code.",
input.FocusArea, input.Language,
)
return &mcp.GetPromptResult{
Description: "Code review prompt",
Messages: []mcp.PromptMessage{
{
Role: mcp.RoleAssistant,
Content: []mcp.Content{
&mcp.TextContent{Text: systemMsg},
},
},
{
Role: mcp.RoleUser,
Content: []mcp.Content{
&mcp.TextContent{Text: input.Code},
},
},
},
}, nil
})
}
Go vs Python性能对比
我在实际项目中做了对比测试。用同样的工具逻辑,分别用Python SDK(FastMCP)和Go SDK实现,然后用wrk压测。
| 指标 | Python (FastMCP) | Go (官方SDK) | 差距 |
|---|---|---|---|
| 单请求延迟(P50) | 8ms | 0.3ms | 26倍 |
| 单请求延迟(P99) | 25ms | 1.2ms | 20倍 |
| 50并发QPS | 1800 | 28000 | 15倍 |
| 内存占用 | 45MB | 8MB | 5.6倍 |
| 启动时间 | 1.2s | 0.05s | 24倍 |
| 二进制大小 | 需要Python运行时 | 12MB单文件 | 无依赖 |
Go的goroutine天然适合MCP的并发场景。每个客户端连接可以分配一个goroutine处理,开销极小。Python虽然有asyncio,但GIL的限制在CPU密集型工具上还是很明显。
有个细节我特别注意了。Go SDK在处理工具调用时,如果handler是同步函数,SDK内部会用goroutine包装。如果你自己的handler里有IO操作,可以直接用 context.Context 来做超时控制。
func HttpFetchHandler(ctx context.Context, req *mcp.CallToolRequest, input FetchInput) (*mcp.CallToolResult, FetchOutput, error) {
// 利用ctx做超时控制
// 客户端取消请求时ctx会被自动取消
select {
case <-ctx.Done():
return nil, FetchOutput{}, ctx.Err()
default:
}
// 创建带超时的子context
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
// 执行HTTP请求
httpReq, err := http.NewRequestWithContext(ctx, "GET", input.URL, nil)
if err != nil {
return nil, FetchOutput{}, err
}
resp, err := http.DefaultClient.Do(httpReq)
if err != nil {
return nil, FetchOutput{}, err
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
return nil, FetchOutput{Content: string(body)}, nil
}
并发处理的优势
Go在MCP并发场景下有几个天然优势。
goroutine的创建成本极低,一个goroutine只占几KB栈空间。相比之下Python的协程虽然也轻量,但GIL限制了真正的并行。Go的channel天然适合做工具间的数据传递和同步。
我在实际项目中用了一个worker pool模式来处理限流。
// 启动时创建固定数量的worker
// 避免无限创建goroutine导致OOM
var sem = make(chan struct{}, 100) // 限制100个并发
func RateLimitedHandler(ctx context.Context, req *mcp.CallToolRequest, input QueryInput) (*mcp.CallToolResult, QueryOutput, error) {
// 获取令牌
select {
case sem <- struct{}{}:
defer func() { <-sem }()
case <-ctx.Done():
return nil, QueryOutput{}, ctx.Err()
}
// 执行实际工作
result := doExpensiveWork(input)
return nil, QueryOutput{Data: result}, nil
}
完整代码
下面是一个可以直接运行的完整Go MCP Server,包含工具、资源和提示三大件。
package main
import (
"context"
"fmt"
"log"
"os"
"time"
"github.com/modelcontextprotocol/go-sdk/mcp"
)
// ========== 工具定义 ==========
// TimeInput 时间查询工具输入
type TimeInput struct {
Timezone string `json:"timezone" jsonschema:"timezone name, optional"`
Format string `json:"format" jsonschema:"output format, enum=iso,enum=unix,enum=human"`
}
// TimeOutput 时间查询工具输出
type TimeOutput struct {
Time string `json:"time" jsonschema:"the current time"`
Timezone string `json:"timezone" jsonschema:"the timezone used"`
}
// GetCurrentTime 获取当前时间的工具
func GetCurrentTime(ctx context.Context, req *mcp.CallToolRequest, input TimeInput) (*mcp.CallToolResult, TimeOutput, error) {
// 默认使用UTC时区
loc, err := time.LoadLocation(input.Timezone)
if err != nil {
loc = time.UTC
}
now := time.Now().In(loc)
var timeStr string
switch input.Format {
case "iso":
timeStr = now.Format(time.RFC3339)
case "unix":
timeStr = fmt.Sprintf("%d", now.Unix())
case "human":
timeStr = now.Format("2006-01-02 15:04:05 MST")
default:
timeStr = now.Format(time.RFC3339)
}
return nil, TimeOutput{
Time: timeStr,
Timezone: loc.String(),
}, nil
}
// EchoInput 回声工具输入
type EchoInput struct {
Message string `json:"message" jsonschema:"the message to echo"`
Count int `json:"count" jsonschema:"number of times to repeat, default=1"`
}
// EchoOutput 回声工具输出
type EchoOutput struct {
Echoes []string `json:"echoes" jsonschema:"the echoed messages"`
}
// Echo 回声工具,用于测试连接是否正常
func Echo(ctx context.Context, req *mcp.CallToolRequest, input EchoInput) (*mcp.CallToolResult, EchoOutput, error) {
if input.Count <= 0 {
input.Count = 1
}
if input.Count > 100 {
// 工具级错误,告诉用户参数不合理
return &mcp.CallToolResult{
IsError: true,
Content: []mcp.Content{
&mcp.TextContent{Text: "count must be between 1 and 100"},
},
}, EchoOutput{}, nil
}
echoes := make([]string, input.Count)
for i := 0; i < input.Count; i++ {
echoes[i] = fmt.Sprintf("[%d] %s", i+1, input.Message)
}
return nil, EchoOutput{Echoes: echoes}, nil
}
// ========== 资源定义 ==========
// registerResources 注册服务器资源
func registerResources(server *mcp.Server) {
// 静态资源 系统信息
mcp.AddResource(server, &mcp.Resource{
URI: "system://info",
Name: "system-info",
Description: "Server system information",
MimeType: "application/json",
}, func(ctx context.Context, req *mcp.ReadResourceRequest) (*mcp.ReadResourceResult, error) {
hostname, _ := os.Hostname()
info := fmt.Sprintf(`{
"hostname": "%s",
"startTime": "%s",
"goVersion": "%s",
"pid": %d
}`, hostname, time.Now().Format(time.RFC3339), "go1.24", os.Getpid())
return &mcp.ReadResourceResult{
Contents: []mcp.ResourceContents{
&mcp.TextResourceContents{
URI: "system://info",
MimeType: "application/json",
Text: info,
},
},
}, nil
})
}
// ========== 提示定义 ==========
// GreetingInput 问候提示参数
type GreetingInput struct {
Name string `json:"name" jsonschema:"the name to greet"`
Style string `json:"style" jsonschema:"greeting style, enum=formal,enum=casual,enum=funny"`
}
// registerPrompts 注册提示模板
func registerPrompts(server *mcp.Server) {
mcp.AddPrompt(server, &mcp.Prompt{
Name: "greeting",
Description: "Generate a greeting message",
}, func(ctx context.Context, req *mcp.GetPromptRequest, input GreetingInput) (*mcp.GetPromptResult, error) {
var template string
switch input.Style {
case "formal":
template = "Good day, %s. I hope this message finds you well."
case "casual":
template = "Hey %s! What's up?"
case "funny":
template = "Well well well, if it isn't %s! Ready to save the world?"
default:
template = "Hello, %s!"
}
return &mcp.GetPromptResult{
Description: "A greeting message",
Messages: []mcp.PromptMessage{
{
Role: mcp.RoleUser,
Content: []mcp.Content{
&mcp.TextContent{Text: fmt.Sprintf(template, input.Name)},
},
},
},
}, nil
})
}
// ========== 主函数 ==========
func main() {
// 创建Server实例
// Implementation包含名称和版本,客户端会用来识别Server
server := mcp.NewServer(&mcp.Implementation{
Name: "go-mcp-demo",
Version: "1.0.0",
}, nil)
// 注册工具
// mcp.AddTool会自动从函数签名和结构体标签生成JSON Schema
mcp.AddTool(server, &mcp.Tool{
Name: "get_current_time",
Description: "Get the current time in a specified timezone",
}, GetCurrentTime)
mcp.AddTool(server, &mcp.Tool{
Name: "echo",
Description: "Echo a message multiple times, useful for testing",
}, Echo)
// 注册资源
registerResources(server)
// 注册提示
registerPrompts(server)
// 使用stdio传输模式启动
// 对于本地运行的工具型Server,stdio是最简单的选择
log.Println("Starting Go MCP Server on stdio...")
if err := server.Run(context.Background(), &mcp.StdioTransport{}); err != nil {
log.Fatalf("Server failed: %v", err)
}
}
运行和测试方式。
# 编译
go build -o mcp-server main.go
# 直接用Claude Desktop或Cursor配置
# 在配置文件中指定命令路径即可
# 也可以用MCP Inspector调试
npx @modelcontextprotocol/inspector ./mcp-server
效果验证
我在本地用MCP Inspector测试了这个Server。工具列表正确返回了 get_current_time 和 echo 两个工具。调用 get_current_time 传入 {"timezone": "Asia/Shanghai", "format": "human"} 返回了正确的时间。调用 echo 传入 {"message": "hello", "count": 3} 返回了三条编号消息。
资源列表显示了 system://info,读取后返回了包含hostname和PID的JSON。提示 greeting 在传入 {"name": "World", "style": "funny"} 后生成了对应的消息。
性能方面我用Go写了个简单的并发测试客户端,100个goroutine同时调用echo工具,10000次请求总耗时只有1.8秒,平均每次0.18ms。同样的逻辑用Python实现,10000次串行请求就要4.7秒。
常见问题与避坑
坑1,jsonschema标签写法不对导致Schema生成失败。 SDK用的是 jsonschema 标签来生成JSON Schema,写法和标准库的 json 标签不一样。比如枚举值要写成 enum=add,enum=subtract 而不是 enum=add|subtract。我第一次写的时候枚举值全挤在一起,客户端解析出来的Schema全是乱码,模型根本看不懂参数含义。
坑2,stdout被污染导致协议解析失败。 stdio传输模式下Server的stdout只能输出MCP协议消息。我调试的时候习惯性地用 fmt.Println 打日志,结果客户端收到非JSON数据直接报错崩了。日志必须输出到stderr,用 log 包默认就是输出到stderr的,但如果你用 fmt.Println 就会出问题。解决方案是统一用 log.Println 或者自己封装一个logger写stderr。
坑3,context取消没有正确传播。 Go SDK的handler函数签名里有 context.Context,但如果你在handler内部启动了新的goroutine做异步操作,需要手动把ctx传进去。我有一次在handler里用 go 启动了一个后台任务但没传ctx,客户端断开连接后那个goroutine还在跑,最后内存泄漏了。正确做法是所有子操作都要继承父ctx,或者用 context.WithCancel 手动管理生命周期。
坑4,tool handler返回nil result导致panic。 SDK要求正常情况返回 nil 作为第一个返回值,SDK会自动帮你构造result。但如果你在某些分支路径上忘了处理,返回了未初始化的指针,运行时就会panic。建议把所有 return nil, Output{}, err 这种路径检查一遍,确保error为nil时output有值。
小结
Go SDK的优势集中在三个地方。性能,goroutine天然适合MCP的并发模型,单机处理几万QPS毫无压力。部署,编译出单个二进制文件,不需要装运行时环境,扔到Docker里几MB搞定。类型安全,编译期就能抓住大部分参数错误,比Python的运行时报错舒服太多。
选择建议很简单。如果你的Server是给本地IDE用的轻量工具,Python够用了,开发速度快。如果你的Server要跑在生产环境承受高并发,或者需要做成单文件分发,Go是更好的选择。Go SDK的API设计已经非常成熟,从工具定义到传输层抽象都很干净,上手成本不高。
相关推荐
更多推荐

所有评论(0)