1. 为什么你的 Go 服务需要挂上 MCP Server大模型能写代码、能查文档但一到“帮我查一下订单表里那个超时未支付的单子”就卡住了——它没有手伸不进你的内网服务。MCPModel 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一块是模型侧客户端如何找到并调用这个 Serversettings.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.gopackage 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 。真正让链路稳的不是工具写得多而是鉴权和超时这两处别偷懒。我踩过的坑基本都在这两块配置对齐了后面加工具就是体力活。