摘要:使用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)8ms0.3ms26倍
单请求延迟(P99)25ms1.2ms20倍
50并发QPS18002800015倍
内存占用45MB8MB5.6倍
启动时间1.2s0.05s24倍
二进制大小需要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_timeecho 两个工具。调用 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设计已经非常成熟,从工具定义到传输层抽象都很干净,上手成本不高。


相关推荐

Logo

欢迎加入 MCP 技术社区!与志同道合者携手前行,一同解锁 MCP 技术的无限可能!

更多推荐