☰
手把手用 Go 写一个 MCP Server:让大模型真正“调得动”你的服务(TaoToken 统一 Key 接入版)
2026/9/27 22:25:57 网站建设 项目流程

1. 为什么你的 Go 服务需要挂上 MCP Server

大模型能写代码、能查文档,但一到“帮我查一下订单表里那个超时未支付的单子”就卡住了——它没有手,伸不进你的内网服务。MCP(Model Context Protocol)就是给模型装手的协议:你按规范暴露一个工具接口,模型侧通过标准消息发起调用,你的 Go 服务执行完把结果塞回去。整条链路里,模型不需要知道你的数据库密码,也不需要你写一堆胶水代码去适配每家模型厂商的 SDK。

这篇要做的,是用 Go 从零写一个能跑起来的 MCP Server,把“查订单状态”这种内部能力注册成工具,再通过 TaoToken 的统一 Key 通道让模型真正调得动它。适合谁:手里有 Go 后端服务、想让 AI Agent 直接调用自有接口的工程师;或者你已经看过 MCP 协议文档,但还没跑通一次端到端调用。读完你能拿到可复制的config.toml、settings.json骨架,一条启动命令,以及一次完整的“模型发起调用 → 服务响应”验证动作。

我试过把工具注册、上下文管理、超时重试这几块拆开写,最后发现最影响跑通速度的其实是配置和鉴权这两步,所以下面会把篇幅压在能直接抄的部分。

2. TaoToken 前置:统一 Key 与 API 通道准备

MCP Server 本身不负责“连模型”,它只负责“被调用”。真正让模型发起调用的那一侧,需要一个能访问模型的通道。TaoToken 在这里的角色是统一 Key 和 API 入口:你不需要为每个模型厂商单独维护一套鉴权,工具侧接入时只认一个 Key。

先到控制台拿 Key。打开 https://taotoken.net/api-keys ,登录后创建一个 API Key,复制出来形如sk-...的字符串。这个 Key 后面会写进 MCP Server 的配置里,用于校验来自模型侧的调用请求。

如果你还没决定用哪个模型来发起工具调用,可以先到模型对话页面 https://taotoken.net/model-chat 试一下,确认通道可用。长期跑编码类 Agent 的话,Coding Plan 页面 https://taotoken.net/coding-plan 有更细的套餐说明,这里不展开。

注意:API Key 只放在服务端配置文件或环境变量里,不要提交到 Git,也不要写进前端代码。MCP Server 的鉴权是“模型侧 → 你的服务”,Key 泄露等于别人能调你的内部工具。

接入文档在 https://taotoken.net/doc ,里面列了请求头格式和错误码,排障时会用到。

3. 可复制配置:config.toml 与 settings.json 骨架

MCP Server 的配置分两块:一块是服务自身运行参数(config.toml),一块是模型侧客户端如何找到并调用这个 Server(settings.json)。两块都要对,链路才通。

3.1 config.toml:服务端运行参数

在项目根目录建config.toml:

[server] host = "127.0.0.1" port = 8080 read_timeout_seconds = 15 write_timeout_seconds = 15 [auth] # 从 https://taotoken.net/api-keys 获取 api_key = "sk-替换成你自己的Key" header_name = "Authorization" header_prefix = "Bearer " [context] # 会话级状态存储,生产环境换成 redis backend = "memory" ttl_seconds = 1800 [tools] # 工具注册表,启动时加载 enabled = ["query_order_status", "list_recent_orders"]

api_key这一项就是 TaoToken 统一 Key。服务启动时会读它,用来校验每个进来的/mcp请求。header_prefix保持Bearer带一个空格,这是标准写法,少空格会导致 401。

3.2 settings.json:模型侧客户端配置

模型侧(比如支持 MCP 的客户端或 Agent 框架)需要知道你的 Server 地址和调用方式。建settings.json:

{ "mcpServers": { "go-order-service": { "url": "http://127.0.0.1:8080/mcp", "headers": { "Authorization": "Bearer sk-替换成你自己的Key" }, "timeout": 15000 } } }

url指向你 Go 服务的/mcp路由,headers里的 Key 必须和config.toml里的一致。timeout单位毫秒,设 15000 对应服务端 15 秒读超时,两边对齐能避免“服务端还在跑、客户端已断开”的假失败。

3.3 工具注册表:把内部能力变成原子工具

MCP 的核心是工具。下面这段是工具注册与执行的核心,放在internal/tool/registry.go:

package tool import ( "context" "encoding/json" "errors" "sync" ) type Tool struct { Name string `json:"name"` Description string `json:"description"` Parameters json.RawMessage `json:"parameters"` Exec func(args map[string]interface{}, ctx context.Context) (interface{}, error) } type Registry struct { mu sync.RWMutex tools map[string]*Tool } func NewRegistry() *Registry { return &Registry{tools: make(map[string]*Tool)} } func (r *Registry) Register(t *Tool) error { r.mu.Lock() defer r.mu.Unlock() if _, exists := r.tools[t.Name]; exists { return errors.New("tool already exists: " + t.Name) } r.tools[t.Name] = t return nil } func (r *Registry) Get(name string) (*Tool, bool) { r.mu.RLock() defer r.mu.RUnlock() t, ok := r.tools[name] return t, ok }

Parameters用 JSON Schema 描述入参,模型侧靠它决定怎么填参数。Exec是真正干活的函数,查订单、调内部 API、跑脚本都行,只要签名一致。

3.4 HTTP 处理:解析 → 鉴权 → 执行 → 响应

internal/handler/mcp.go里处理/mcp路由:

package handler import ( "context" "encoding/json" "net/http" "strings" "time" "mcp-server/internal/tool" ) type MCPHandler struct { registry *tool.Registry apiKey string } func NewMCPHandler(r *tool.Registry, apiKey string) *MCPHandler { return &MCPHandler{registry: r, apiKey: apiKey} } type ToolRequest struct { RequestID string `json:"request_id"` ToolName string `json:"tool_name"` Arguments map[string]interface{} `json:"arguments"` ContextID string `json:"context_id,omitempty"` } type ToolResponse struct { RequestID string `json:"request_id"` Status string `json:"status"` Result interface{} `json:"result,omitempty"` Error string `json:"error,omitempty"` } func (h *MCPHandler) Handle(w http.ResponseWriter, r *http.Request) { token := strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ") if token != h.apiKey { http.Error(w, "unauthorized", http.StatusUnauthorized) return } var req ToolRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "invalid JSON", http.StatusBadRequest) return } t, ok := h.registry.Get(req.ToolName) if !ok { http.Error(w, "tool not found", http.StatusNotFound) return } ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second) defer cancel() result, err := t.Exec(req.Arguments, ctx) resp := ToolResponse{RequestID: req.RequestID, Status: "success", Result: result} if err != nil { resp.Status = "error" resp.Error = err.Error() } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(resp) }

鉴权放在最前面,未授权直接 401,不进入工具执行。超时用context.WithTimeout控制,防止某个工具卡死拖垮整个服务。

4. 启动与端到端验证:模型发起调用 → 服务响应

配置和代码就位后,启动服务:

go mod tidy go build -o mcp-server ./cmd/server ./mcp-server --config ./config.toml

看到MCP server listening on 127.0.0.1:8080就说明起来了。

4.1 先本地验证工具能跑

用 curl 模拟一次模型侧调用:

curl -X POST http://127.0.0.1:8080/mcp \ -H "Authorization: Bearer sk-替换成你自己的Key" \ -H "Content-Type: application/json" \ -d '{ "request_id": "req-001", "tool_name": "query_order_status", "arguments": {"order_id": "20240517001"} }'

预期返回:

{ "request_id": "req-001", "status": "success", "result": {"order_id": "20240517001", "status": "unpaid", "amount": 299.00} }

如果返回unauthorized,检查 Key 是否和config.toml一致;返回tool not found,检查enabled列表里有没有注册这个工具名。

4.2 再让模型真正发起调用

把settings.json放到模型侧客户端的配置目录,重启客户端。在对话里输入“帮我查一下订单 20240517001 的状态”,模型会解析出工具名和参数,向http://127.0.0.1:8080/mcp发请求。你的 Go 服务执行query_order_status,把结果返回,模型再组织成自然语言回复。

这一步跑通,意味着“模型发起调用 → 服务响应”的完整链路成立。你可以在服务端加一行日志,打印request_id和tool_name,确认请求确实来自模型侧而不是你手动 curl 的。

4.3 上下文管理:跨工具协作

如果模型需要先验证用户、再查订单,就要用到context_id。在internal/context/store.go里用内存 map 存会话状态:

package contextstore import "sync" type Store struct { mu sync.RWMutex data map[string]map[string]interface{} } func NewStore() *Store { return &Store{data: make(map[string]map[string]interface{})} } func (s *Store) Get(ctxID string) (map[string]interface{}, bool) { s.mu.RLock() defer s.mu.RUnlock() v, ok := s.data[ctxID] return v, ok } func (s *Store) Update(ctxID string, updates map[string]interface{}) { s.mu.Lock() defer s.mu.Unlock() if _, exists := s.data[ctxID]; !exists { s.data[ctxID] = make(map[string]interface{}) } for k, v := range updates { s.data[ctxID][k] = v } }

工具执行时通过context_id读写状态,第一步存user_id,第二步查订单时读出来做权限过滤。生产环境把backend换成 Redis,多实例部署时状态才不丢。

5. 本篇常见错排查

401 unauthorized:最常见。三个地方对一遍——config.toml的api_key、settings.json的Authorization、curl 命令里的Bearer后面有没有空格。Key 前后有换行也会导致不匹配,复制时注意。

tool not found:工具名拼写不一致,或者enabled列表里没加。MCP 工具名区分大小写,query_order_status和QueryOrderStatus是两个东西。

连接被拒绝:服务没起来,或者host写成了0.0.0.0但客户端连的是127.0.0.1。本地调试统一用127.0.0.1。

超时但服务端日志显示执行成功:客户端timeout比服务端read_timeout_seconds小。两边对齐,客户端设 15000 毫秒,服务端设 15 秒。

模型不调用工具:Parameters的 JSON Schema 写错了,模型解析不出入参格式。用在线 JSON Schema 校验器过一遍,确保type、properties、required字段完整。

上下文丢失:context_id没在请求里传,或者服务重启后内存存储清空。调试阶段可以在响应里回显context_id,确认模型侧有没有带上。

排障时优先看接入文档 https://taotoken.net/doc 里的错误码说明,比盲猜快。

6. 下一步:把更多内部能力挂上去

跑通一次调用之后,加新工具就是复制粘贴的事:写一个Exec函数,注册进Registry,在config.toml的enabled里加上名字。数据库查询、内部 API、Shell 脚本,只要签名一致都能挂。

长期跑编码类 Agent 的话,Coding Plan https://taotoken.net/coding-plan 里有更细的通道说明;需要管理多个 Key 或查看调用量,控制台 https://taotoken.net/console 能看。模型对话验证通道在 https://taotoken.net/model-chat ,接入文档在 https://taotoken.net/doc ,API 入口是 https://taotoken.net/api 。

真正让链路稳的,不是工具写得多,而是鉴权和超时这两处别偷懒。我踩过的坑基本都在这两块,配置对齐了,后面加工具就是体力活。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询