如何用 Go 语言搭 MCP 服务:从零实现一个可调试的 MCP Server 并接入 TaoToken 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 DesktopMCP 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_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 注册并登录。登录后进控制台地址是 https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsole 。在控制台里你能看到账户余额、调用统计以及最关键的 API Keys 入口。第二步创建 API Key。进 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-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_sourcetaotoken_aicg_blog_endutm_contentcoding-plan 它更适合高频调用场景。只是想先验证模型通不通用模型对话页面 https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_contentchat 手动发一条消息最快。接入细节和参数说明都在文档里 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdoc 。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_URLhttps://taotoken.net/api TAOTOKEN_API_KEYsk-你的Key TAOTOKEN_MODEL_IDgpt-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 // HeaderAuthorization: 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_TRANSPORThttp ./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 数组会包含 calculateinputSchema 里能看到 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 Inspectornpx modelcontextprotocol/inspector它会起一个本地页面在 Transport 里选 Streamable HTTPURL 填 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会打到别的路由上返回 HTMLJSON 解析自然失败。用 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 时 y0我代码里返回了 errors.NewMCP 会把它包成 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_sourcetaotoken_aicg_blog_endutm_contentdoc ClaudeCodeAnthropic 的配置入口是 https://taotoken.net/claude-code-anthropic?utm_sourcetaotoken_aicg_blog_endutm_contentclaudecode-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。这个坑我调了半小时才反应过来。