1. 从零搭一个 Go MCP Server,到底难在哪
MCP 全称 Model Context Protocol,说白了就是给大模型装一个“万能插座”:模型本身只会生成文本,但通过 MCP 它就能调用你写的工具函数、查数据库、发 HTTP 请求。你如果用过 Cursor、Claude Desktop 或者各类 IDE 里的 Agent 功能,背后十有八九就是 MCP 在干活。
那为什么用 Go 来写 MCP Server?我自己的体感是三点:一是编译出来是单个二进制,扔到服务器上就能跑,不用装 Python 环境;二是并发模型天然适合处理多个工具调用;三是 mcp-go 这个库把协议细节封装得比较干净,你只需要关心“工具怎么实现”。
这篇要解决的核心问题是:很多人照着 demo 能跑通 stdio,但一到 HTTP 传输、一到 tools/list 和 tools/call 的完整闭环、一到把 endpoint 换成统一通道联调,就开始报错。我会带你从 go.mod 开始,先跑通 stdio,再切 HTTP,然后把请求打到 TaoToken 的统一通道上做验证。全程给可复制的代码和 curl 命令,你跟着敲就行。
适合谁看:有 Go 基础、想给自己的 AI 工作流加自定义工具的开发者;正在用 Cursor 或 Claude Code、想把本地工具接进 Agent 的人;以及想理解 MCP 协议请求/响应长什么样的后端同学。
先说清楚 MCP 的角色分工,不然后面配置容易懵。MCP Host 是启动连接的应用,比如 Cursor、Claude Desktop;MCP Client 是 Host 内部维护一对一连接的协议客户端;MCP Server 就是你用 Go 写的这个进程,对外暴露 tools。本地数据源和远程服务都是 Server 背后去调的东西。整个链路是:用户在 Host 输入 → LLM 判断要不要调工具 → Client 发 tools/call → 你的 Go Server 执行 → 结果回传给 LLM 作为上下文。
理解了这条链路,你就知道为什么调试时既要看 Server 日志,也要看 Client 侧的请求。下面进入实操。
2. TaoToken 前置准备:把统一通道的 Key 和 Base URL 拿到手
在写代码之前,先把“出口”准备好。因为最后我们要把 MCP Server 里调用的模型 endpoint 统一指向 TaoToken 的通道,这样你本地工具和线上 Agent 用的是同一套鉴权和模型 ID,联调时不会出现“本地能跑、线上 401”的割裂。
第一步,打开官网 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,注册并登录。登录后进控制台,地址是 https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_content=console 。在控制台里你能看到账户余额、调用统计,以及最关键的 API Keys 入口。
第二步,创建 API Key。进 https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api-keys ,点新建,复制那串以 sk- 开头的 Key。注意:这个 Key 只在创建时完整显示一次,关掉页面就看不到了,先存到你的密码管理器或者本地 .env 文件里,别直接硬编码进 main.go 提交到 Git。
第三步,确认 Base URL。TaoToken 的 API 入口是 https://taotoken.net/api ,注意这个地址后面不加任何 UTM 参数,代码里配置就用这个干净的。它兼容 OpenAI 风格的 /v1/chat/completions 路径,所以你在 Go 里用任何 OpenAI SDK 或者自己发 HTTP 请求都能对接。
这里有个概念要区分清楚:MCP Server 本身不负责“调用大模型”,它负责“暴露工具给大模型调用”。但你的工具内部如果要再调模型(比如做一个“总结文本”的工具),那这个内部调用就应该走 TaoToken 的统一通道。所以 §3 里我会给你两段配置:一段是 MCP Server 的传输配置,一段是工具内部调模型时的 Base URL + Key + Model ID 三件套。
如果你打算长期跑编码类 Agent,可以了解下 Coding Plan,地址是 https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding-plan ,它更适合高频调用场景。只是想先验证模型通不通,用模型对话页面 https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_content=chat 手动发一条消息最快。接入细节和参数说明都在文档里: https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc 。
Key 拿到后,先别急着写 MCP 代码,用一条 curl 确认通道是通的:
curl https://taotoken.net/api/v1/chat/completions \ -H "Content-Type: application/json" \ -H "Authorization: Bearer sk-你的Key" \ -d '{ "model": "gpt-4o-mini", "messages": [{"role": "user", "content": "ping"}] }'返回里能看到 choices 数组和 content 字段,就说明 Key 和 Base URL 都没问题。这一步过了,后面 MCP 联调才有意义。如果这里就 401,先回控制台检查 Key 有没有复制全、有没有多余空格。
3. 可复制配置:go.mod、main.go 骨架与传输切换
现在开始写代码。先建目录和初始化模块:
mkdir mcp-server && cd mcp-server go mod init mcp-server go get github.com/mark3labs/mcp-gogo.mod 里最终会有这么一行依赖(版本号以你 go get 拉到的为准):
module mcp-server go 1.24 require github.com/mark3labs/mcp-go v0.27.0接下来是 main.go 的完整骨架。我把它拆成“工具定义”和“传输启动”两块,方便你后面切换 stdio 和 HTTP。
package main import ( "context" "errors" "fmt" "os" "github.com/mark3labs/mcp-go/mcp" "github.com/mark3labs/mcp-go/server" ) func main() { s := server.NewMCPServer( "Calculator Demo", "1.0.0", server.WithResourceCapabilities(true, true), server.WithLogging(), server.WithRecovery(), ) calculatorTool := mcp.NewTool("calculate", mcp.WithDescription("Perform basic arithmetic operations"), mcp.WithString("operation", mcp.Required(), mcp.Description("The operation to perform (add, subtract, multiply, divide)"), mcp.Enum("add", "subtract", "multiply", "divide"), ), mcp.WithNumber("x", mcp.Required(), mcp.Description("First number")), mcp.WithNumber("y", mcp.Required(), mcp.Description("Second number")), ) s.AddTool(calculatorTool, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { op := request.Params.Arguments["operation"].(string) x := request.Params.Arguments["x"].(float64) y := request.Params.Arguments["y"].(float64) var result float64 switch op { case "add": result = x + y case "subtract": result = x - y case "multiply": result = x * y case "divide": if y == 0 { return nil, errors.New("cannot divide by zero") } result = x / y } return mcp.NewToolResultText(fmt.Sprintf("%.2f", result)), nil }) // 传输方式一:stdio,给 Cursor / Claude Desktop 本地调用 if os.Getenv("MCP_TRANSPORT") != "http" { if err := server.ServeStdio(s); err != nil { fmt.Printf("stdio server error: %v\n", err) } return } // 传输方式二:HTTP,方便 curl 和远程联调 httpServer := server.NewStreamableHTTPServer(s) fmt.Println("HTTP MCP server listening on :8080/mcp") if err := httpServer.Start(":8080"); err != nil { fmt.Printf("http server error: %v\n", err) } }编译:
go mod tidy go build -o mcpServer main.go这里有个关键点:stdio 模式下,Server 通过标准输入输出和 Client 通信,你没法直接用 curl 测;HTTP 模式下才有 /mcp 这个 endpoint。所以我用环境变量 MCP_TRANSPORT 来切换,默认 stdio,设成 http 就走 HTTP。
如果你要在工具内部调用模型,配置三件套长这样,建议放 .env 里:
TAOTOKEN_BASE_URL=https://taotoken.net/api TAOTOKEN_API_KEY=sk-你的Key TAOTOKEN_MODEL_ID=gpt-4o-mini在 Go 里读环境变量拼请求:
baseURL := os.Getenv("TAOTOKEN_BASE_URL") apiKey := os.Getenv("TAOTOKEN_API_KEY") modelID := os.Getenv("TAOTOKEN_MODEL_ID") // 请求路径:baseURL + "/v1/chat/completions" // Header:Authorization: Bearer + apiKey // Body 里 model 字段填 modelIDBase URL、Key、Model ID 这三个必须成对出现,缺一个就会报错。很多人只配了 Key 忘了 Model ID,结果请求发出去返回 model not found。
4. 验证请求:curl 与 MCP Inspector 跑通 tools/list 和 tools/call
代码编译完,先启动 HTTP 模式:
MCP_TRANSPORT=http ./mcpServer看到 “HTTP MCP server listening on :8080/mcp” 就说明起来了。MCP 的 HTTP 传输用的是 JSON-RPC 2.0,请求体里要带 method 和 params。先测 tools/list,看工具注册成功没有:
curl -s http://localhost:8080/mcp \ -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -d '{ "jsonrpc": "2.0", "id": 1, "method": "tools/list", "params": {} }'正常返回里 tools 数组会包含 calculate,inputSchema 里能看到 operation、x、y 三个参数。如果 tools 是空数组,说明 AddTool 没执行到,检查一下工具定义是不是在 ServeStdio 之前。
再测 tools/call,实际调用一次加法:
curl -s http://localhost:8080/mcp \ -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -d '{ "jsonrpc": "2.0", "id": 2, "method": "tools/call", "params": { "name": "calculate", "arguments": { "operation": "add", "x": 12, "y": 30 } } }'返回的 result.content 里应该有 text 字段等于 "42.00"。到这一步,tools/list 和 tools/call 的最小闭环就跑通了。
如果你更习惯图形化调试,用 MCP Inspector:
npx @modelcontextprotocol/inspector它会起一个本地页面,在 Transport 里选 Streamable HTTP,URL 填 http://localhost:8080/mcp,点 Connect。连上后左侧能看到 Tools 列表,点 calculate 填参数点 Run,右侧直接出结果。Inspector 的好处是它会帮你格式化 JSON-RPC 的请求和响应,排查参数类型错误特别直观。
stdio 模式的验证方式不一样,它不能 curl。你要在 Cursor 里配。打开 Cursor 设置(Command + Shift + J),在 mcp.json 里加:
{ "mcpServers": { "calculate": { "command": "/Users/你的路径/mcp-server/mcpServer" } } }注意 command 要填编译出来的二进制的绝对路径,不是目录。保存后 Cursor 会自动拉起这个进程,你在对话里说“帮我算 12 加 30”,它就会走 tools/call 调你的 calculate。
这里插一句关于统一通道的联调:如果你的工具内部要调模型,把 baseURL 指向 https://taotoken.net/api ,Key 用前面创建的,Model ID 填你控制台里可用的模型。这样本地 MCP Server 和线上 Agent 走的是同一个出口,日志和额度统计都在一个地方看,排查问题不用两头对。
5. 本篇常见错排查:401、local proxy failed、reading choices、OAuth
这一节按真实报错来对。你照着上面敲,大概率会撞上下面几个。
401 Unauthorized。两种场景。一是 curl 测 TaoToken 通道时 401,八成是 Key 复制时带了空格,或者 Authorization 头写成了 "Bearer: sk-xxx"(冒号是错的,正确是空格)。二是 MCP Server 内部调模型时 401,检查 .env 有没有被正确加载,Go 里 os.Getenv 读不到就是空字符串,请求头会变成 "Bearer ",服务端直接拒。用echo $TAOTOKEN_API_KEY确认环境变量在当前 shell 里可见。
local proxy failed / connection refused。这个通常出现在 Cursor 配了 stdio 但二进制路径不对,或者二进制没有执行权限。先chmod +x ./mcpServer,再用绝对路径填 command。还有一种情况是你把 HTTP 模式的进程当 stdio 配了,Cursor 拉起后进程立刻退出,日志里能看到 “stdio server error”。确认 MCP_TRANSPORT 没被设成 http。
reading choices: unexpected end of JSON input。这是解析模型响应时挂了,常见原因是请求打到了错误的路径。TaoToken 的入口是 https://taotoken.net/api ,完整路径要拼成 /api/v1/chat/completions。如果你只写了 https://taotoken.net 然后拼 /v1/chat/completions,会打到别的路由上返回 HTML,JSON 解析自然失败。用 curl -v 看实际请求 URL 和返回的 Content-Type,是 text/html 就说明路径错了。
OAuth 相关报错。有些 MCP Client 在连接远程 Server 时会尝试 OAuth 流程,如果你只是本地调试,不需要 OAuth,报错里出现 invalid_client 或者 redirect_uri mismatch,先确认你用的是 stdio 或者无鉴权的 HTTP。生产环境要加鉴权的话,在 HTTP Server 前面套一层反向代理做 Bearer 校验,别在 MCP 协议层硬塞 OAuth。
tools/call 返回 isError: true。这不是协议错误,是你的工具函数返回了 error。比如 divide 时 y=0,我代码里返回了 errors.New,MCP 会把它包成 isError。看 result.content 里的 text 就知道具体原因。调试时在工具函数里多打日志,log.Printf("call calculate op=%s x=%v y=%v", op, x, y),比猜快得多。
参数类型断言 panic。request.Params.Arguments 是 map[string]interface{},JSON 里的数字反序列化后是 float64,但如果你在 schema 里写的是 integer,客户端可能传 int,断言 .(float64) 就 panic。统一用 float64 接,或者用 mcp 库提供的类型转换辅助函数。server.WithRecovery() 能兜住 panic 不让进程挂掉,但错误还是要在日志里看。
排查顺序建议:先 curl 确认通道通 → 再 curl 确认 MCP HTTP 通 → 再 Inspector 确认工具注册 → 最后 Cursor 里确认 stdio。一层一层来,别跳步。
6. 把 endpoint 收口到统一通道,继续往下做
走到这里,你已经有了一个能跑 stdio 和 HTTP 两种传输、tools/list 和 tools/call 都通的 Go MCP Server。接下来最有价值的动作是把工具内部的模型调用 endpoint 统一收口到 TaoToken,这样你新增工具时不用每个都配一遍鉴权。
具体做法:在项目里加一个 config.go,从环境变量读 Base URL、Key、Model ID,封装一个 callModel 函数。所有需要调模型的工具都走这个函数。这样你换模型、换额度、看调用统计,都只在 TaoToken 控制台一个地方操作。
如果你要接 Claude Code 这类工具,Anthropic 兼容的接入方式在文档里有说明: https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc ,ClaudeCodeAnthropic 的配置入口是 https://taotoken.net/claude-code-anthropic?utm_source=taotoken_aicg_blog_end&utm_content=claudecode-anthropic 。核心还是那三件套:Base URL 填 https://taotoken.net/api ,Key 用你创建的,Model ID 按控制台可用列表填。
下一步可以做的事:给 calculate 加更多工具,比如查天气、读本地文件、调你自己的业务 API;把 HTTP 模式加上 Bearer 鉴权中间件;用 Docker 把二进制打包成镜像部署到内网。每加一个工具,记得先在 Inspector 里单独测通,再放进 Cursor 里让 Agent 调,这样出问题能快速定位是工具本身还是 Agent 的调用逻辑。
最后留一个我踩过的坑:stdio 模式下不要往 stdout 打任何非协议内容。你加一句 fmt.Println("debug"),Client 解析就会失败,因为 stdout 是协议通道。调试信息一律走 stderr,用 log 包默认就是 stderr,别用 fmt.Println。这个坑我调了半小时才反应过来。