Go 实现 MCP Server——Model Context Protocol 服务端从零实现
Go 实现 MCP Server——Model Context Protocol 服务端从零实现
适读人群:想为 Claude Desktop / Cursor 等 MCP 客户端开发自定义工具的 Go 开发者 | 阅读时长:约 18 分钟 | 核心价值:从协议理解到完整 MCP Server 实现,让 AI 工具能访问你的私有系统
上个月有个朋友在一个做金融数据服务的公司,他们有一套内部的 K 线数据 API,每次分析行情都要手动查数据、复制到 Claude 里。他问我能不能让 Claude 直接调用他们的内部 API,像用 Python 一样自然。
这就是 MCP(Model Context Protocol)要解决的问题。MCP 是 Anthropic 2024 年发布的开放协议,允许 AI 客户端(Claude Desktop、Cursor、Cline 等)直接调用外部工具和数据源。
这篇文章讲怎么用 Go 从零写一个 MCP Server,把你自己的业务系统接入 Claude。
MCP 是什么
MCP 定义了一套标准的通信协议,基于 JSON-RPC 2.0,通过 stdio 或 HTTP(SSE)传输。
一个 MCP Server 可以暴露三类内容:
- Tools:可执行的函数(Claude 主动调用)
- Resources:可读取的数据(文件、数据库查询结果等)
- Prompts:预定义的提示词模板
对于大多数业务场景,重点实现 Tools 就够了。
协议流程:
Claude (MCP Client) MCP Server (你写的)
| |
|-- initialize ----------->|
|<-- capabilities ---------|
| |
|-- tools/list ----------->|
|<-- [工具列表] ------------|
| |
|-- tools/call ----------->| (用户或 AI 触发)
|<-- 执行结果 --------------|用 Go 实现 MCP Server
目前有个 Go 的 MCP SDK:github.com/mark3labs/mcp-go,这个库封装了协议细节,推荐直接用。
go get github.com/mark3labs/mcp-go一个完整的金融数据 MCP Server
package main
import (
"context"
"encoding/json"
"fmt"
"log"
"math/rand"
"time"
"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
)
// 模拟金融数据服务
type MarketDataService struct{}
func (s *MarketDataService) GetKLine(symbol string, period string, limit int) []map[string]interface{} {
// 实际项目中这里调用真实 API
var klines []map[string]interface{}
basePrice := 100.0
t := time.Now()
for i := limit - 1; i >= 0; i-- {
open := basePrice + rand.Float64()*5 - 2.5
close := open + rand.Float64()*3 - 1.5
high := max(open, close) + rand.Float64()*2
low := min(open, close) - rand.Float64()*2
basePrice = close
klines = append(klines, map[string]interface{}{
"time": t.Add(-time.Duration(i) * time.Hour).Format("2006-01-02 15:04"),
"open": fmt.Sprintf("%.2f", open),
"high": fmt.Sprintf("%.2f", high),
"low": fmt.Sprintf("%.2f", low),
"close": fmt.Sprintf("%.2f", close),
"volume": rand.Intn(100000) + 10000,
})
}
return klines
}
func (s *MarketDataService) GetFinancials(symbol string) map[string]interface{} {
return map[string]interface{}{
"symbol": symbol,
"pe_ratio": fmt.Sprintf("%.1f", rand.Float64()*30+5),
"pb_ratio": fmt.Sprintf("%.2f", rand.Float64()*5+0.5),
"eps": fmt.Sprintf("%.2f", rand.Float64()*10),
"roe": fmt.Sprintf("%.1f%%", rand.Float64()*30),
"revenue_yoy": fmt.Sprintf("%.1f%%", rand.Float64()*40-10),
}
}
func max(a, b float64) float64 {
if a > b {
return a
}
return b
}
func min(a, b float64) float64 {
if a < b {
return a
}
return b
}
func main() {
svc := &MarketDataService{}
// 创建 MCP Server
s := server.NewMCPServer(
"financial-data-server",
"1.0.0",
server.WithToolCapabilities(true),
)
// 注册工具:获取 K 线数据
klineTool := mcp.NewTool("get_kline",
mcp.WithDescription("获取指定股票的 K 线数据,支持不同周期"),
mcp.WithString("symbol",
mcp.Required(),
mcp.Description("股票代码,例如 600519、AAPL"),
),
mcp.WithString("period",
mcp.Description("K 线周期:1m/5m/15m/1h/1d,默认 1d"),
),
mcp.WithNumber("limit",
mcp.Description("获取的数量,默认 20,最大 100"),
),
)
s.AddTool(klineTool, func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
symbol, _ := req.Params.Arguments["symbol"].(string)
period, _ := req.Params.Arguments["period"].(string)
limitFloat, _ := req.Params.Arguments["limit"].(float64)
if symbol == "" {
return mcp.NewToolResultError("symbol is required"), nil
}
if period == "" {
period = "1d"
}
limit := int(limitFloat)
if limit <= 0 || limit > 100 {
limit = 20
}
klines := svc.GetKLine(symbol, period, limit)
data, _ := json.MarshalIndent(klines, "", " ")
return mcp.NewToolResultText(fmt.Sprintf(
"股票 %s 的 %s K 线数据(最近 %d 条):\n%s",
symbol, period, limit, string(data),
)), nil
})
// 注册工具:获取财务指标
financialTool := mcp.NewTool("get_financials",
mcp.WithDescription("获取股票的基本财务指标:PE、PB、EPS、ROE、营收增长率"),
mcp.WithString("symbol",
mcp.Required(),
mcp.Description("股票代码"),
),
)
s.AddTool(financialTool, func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
symbol, _ := req.Params.Arguments["symbol"].(string)
if symbol == "" {
return mcp.NewToolResultError("symbol is required"), nil
}
financials := svc.GetFinancials(symbol)
data, _ := json.MarshalIndent(financials, "", " ")
return mcp.NewToolResultText(fmt.Sprintf(
"股票 %s 财务指标:\n%s",
symbol, string(data),
)), nil
})
// 注册工具:板块分析(示例)
sectorTool := mcp.NewTool("search_sector_stocks",
mcp.WithDescription("搜索某个板块的股票列表和涨跌情况"),
mcp.WithString("sector",
mcp.Required(),
mcp.Description("板块名称,如:新能源、银行、消费"),
),
mcp.WithNumber("top_n",
mcp.Description("返回前 N 只股票,默认 10"),
),
)
s.AddTool(sectorTool, func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
sector, _ := req.Params.Arguments["sector"].(string)
topN := 10
if n, ok := req.Params.Arguments["top_n"].(float64); ok && n > 0 {
topN = int(n)
}
// 模拟数据
result := fmt.Sprintf("%s板块前%d只股票:\n", sector, topN)
for i := 0; i < topN; i++ {
change := rand.Float64()*10 - 5
result += fmt.Sprintf("%d. 股票%03d: %.2f%%\n", i+1, rand.Intn(900)+100, change)
}
return mcp.NewToolResultText(result), nil
})
// 启动 Server(stdio 模式,适合 Claude Desktop)
log.Println("Starting MCP Server (stdio mode)...")
if err := server.ServeStdio(s); err != nil {
log.Fatal(err)
}
}配置到 Claude Desktop
编译后,在 Claude Desktop 的配置文件中添加:
macOS: ~/Library/Application Support/Claude/claude_desktop_config.json
{
"mcpServers": {
"financial-data": {
"command": "/path/to/your/financial-mcp-server",
"args": [],
"env": {
"MARKET_API_KEY": "your-api-key"
}
}
}
}重启 Claude Desktop,在对话框左下角就能看到你的工具了。
踩坑实录
踩坑 1:Server 启动后 Claude Desktop 识别不到工具
现象:配置文件写好了,Claude Desktop 重启后工具列表里没有新工具。
原因:我的 Server 二进制文件路径写的是相对路径,Claude Desktop 找不到。另外,首次启动时如果 Server 有任何 stderr 输出(比如 log),会导致协议解析失败。
解法:
- 使用绝对路径
- 把所有
log.Println改成输出到文件,不要输出到 stderr - 用
log.SetOutput(logFile)重定向日志
logFile, _ := os.OpenFile("/tmp/mcp-server.log", os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
log.SetOutput(logFile)踩坑 2:工具参数类型不匹配导致 panic
现象:当用户没有传 limit 参数时,Server 崩溃。
原因:req.Params.Arguments["limit"].(float64) 的类型断言在 key 不存在时返回零值,但如果 value 类型不是 float64(比如是 nil)就会 panic。
解法:用双返回值的类型断言:
limitFloat, ok := req.Params.Arguments["limit"].(float64)
if !ok {
limitFloat = 20 // 默认值
}踩坑 3:HTTP SSE 模式下连接断开没有清理
现象:在 HTTP SSE 模式部署时,客户端断开连接后,Server 端的 goroutine 没有退出,内存缓慢增长。
原因:没有正确监听 HTTP request 的 context 取消信号。
解法:在工具处理函数里检查 ctx.Done(),长时间操作要用 select 监听取消:
s.AddTool(heavyTool, func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
resultCh := make(chan string, 1)
go func() {
// 耗时操作
resultCh <- doHeavyWork()
}()
select {
case result := <-resultCh:
return mcp.NewToolResultText(result), nil
case <-ctx.Done():
return mcp.NewToolResultError("request cancelled"), nil
}
})实现 Resource(可选)
除了 Tools,MCP 还支持 Resources,让 AI 可以"读取"动态数据:
// 注册资源:实时行情数据
s.AddResource("market://realtime/{symbol}", "real-time-quote",
mcp.WithResourceDescription("获取股票实时行情"),
func(ctx context.Context, req mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) {
// 从 URI 中解析 symbol
symbol := extractSymbol(req.Params.URI)
quote := fetchRealTimeQuote(symbol)
return []mcp.ResourceContents{
mcp.TextResourceContents{
URI: req.Params.URI,
MimeType: "application/json",
Text: quote,
},
}, nil
},
)选型建议
| 场景 | 推荐传输方式 |
|---|---|
| 本地工具(Claude Desktop) | stdio |
| 团队共享工具 | HTTP + SSE |
| 需要认证的生产环境 | HTTP + SSE + OAuth |
MCP 是 2024 年底才成熟的协议,Go 的生态还在完善中,但 mcp-go 这个库已经够用了。
