ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

第8讲:MCP 生态——Hub、网关与联邦

第8讲:MCP 生态——Hub、网关与联邦 一、当 MCP Server 从几个变成几百个前七讲都在围绕单个 MCP Server 展开。但在真实生产环境中一个中等规模的公司很快会遇到订单团队维护 3 个 MCP Server订单查询、退款、物流运维团队维护 5 个 MCP Server日志、监控、部署、告警、配置数据团队维护 2 个 MCP Server数据查询、报表生成AI 团队自己又写了几个实验性的 ServerAgent 不可能在 system prompt 里硬编码 20 个 Server 地址。需要一个中间层来统一管理这些 Server 的注册、发现、路由、鉴权和限流。这就是 MCP Hub 和 MCP Gateway 要解决的问题。二、三个核心概念组件职责类比MCP Server​提供具体的工具/资源/提示微服务中的 providerMCP Hub​Server 的注册中心 目录服务API 网关 服务发现MCP Gateway​统一的接入层代理请求到正确的 ServerAPI 网关的反向代理在实际部署中Hub 和 Gateway 经常合并为一个组件称为MCP Gateway​ 或MCP Proxy。三、Gateway 的核心功能一个生产级的 MCP Gateway 需要做四件事功能说明统一接入​Agent 只连 Gateway不直接连各个 Server路由分发​根据工具名/资源 URI 前缀把请求转发到对应的 Server统一鉴权​在 Gateway 层做一次认证向下游 Server 传递 Agent Identity治理能力​限流、审计日志、超时控制、熔断四、Go 实现轻量 MCP Gateway下面实现一个 MCP Gateway支持注册多个下游 Server每个 Server 声明自己提供的工具列表Agent 统一连 GatewayGateway 根据工具名路由到正确的 Server统一的 Bearer Token 认证请求级别的审计日志package main import ( bytes encoding/json fmt log net/http strings sync time ) // ---- MCP 消息结构 ---- type JSONRPCReq struct { JSONRPC string json:jsonrpc ID int json:id Method string json:method Params json.RawMessage json:params,omitempty } type JSONRPCResp struct { JSONRPC string json:jsonrpc ID int json:id Result json.RawMessage json:result,omitempty Error *RPCError json:error,omitempty } type RPCError struct { Code int json:code Message string json:message } // ---- 下游 Server 注册信息 ---- type BackendServer struct { Name string json:name URL string json:url Token string json:- // 调用下游 Server 的凭证 Tools []string json:tools // 该 Server 提供的工具名列表 Healthy bool json:healthy } // ---- MCP Gateway ---- type MCPGateway struct { mu sync.RWMutex backends map[string]*BackendServer // key: server name // 工具名 - Server 名的映射路由表 toolRoute map[string]string // Gateway 自身的 TokenAgent 调用 Gateway 时需要 gwToken string client *http.Client } func NewMCPGateway(gwToken string) *MCPGateway { return MCPGateway{ backends: make(map[string]*BackendServer), toolRoute: make(map[string]string), gwToken: gwToken, client: http.Client{ Timeout: 30 * time.Second, }, } } // 注册下游 Server func (g *MCPGateway) Register(server *BackendServer) { g.mu.Lock() defer g.mu.Unlock() g.backends[server.Name] server for _, tool : range server.Tools { g.toolRoute[tool] server.Name } server.Healthy true log.Printf(注册后端 Server: %s (%s)提供 %d 个工具, server.Name, server.URL, len(server.Tools)) } // 根据工具名找到对应的后端 Server func (g *MCPGateway) findBackend(toolName string) *BackendServer { g.mu.RLock() defer g.mu.RUnlock() serverName, ok : g.toolRoute[toolName] if !ok { return nil } backend : g.backends[serverName] if backend nil || !backend.Healthy { return nil } return backend } // 向后端 Server 转发 JSON-RPC 请求 func (g *MCPGateway) forwardToBackend(backend *BackendServer, req JSONRPCReq) (*JSONRPCResp, error) { bodyBytes, _ : json.Marshal(req) httpReq, err : http.NewRequest(POST, backend.URL, bytes.NewReader(bodyBytes)) if err ! nil { return nil, fmt.Errorf(创建请求失败: %w, err) } httpReq.Header.Set(Content-Type, application/json) httpReq.Header.Set(Authorization, Bearer backend.Token) resp, err : g.client.Do(httpReq) if err ! nil { return nil, fmt.Errorf(转发请求失败: %w, err) } defer resp.Body.Close() var rpcResp JSONRPCResp json.NewDecoder(resp.Body).Decode(rpcResp) return rpcResp, nil } // 收集所有后端的工具列表聚合 func (g *MCPGateway) collectAllTools() ([]map[string]interface{}, error) { g.mu.RLock() defer g.mu.RUnlock() var allTools []map[string]interface{} for name, backend : range g.backends { if !backend.Healthy { continue } // 向后端请求 tools/list req : JSONRPCReq{ JSONRPC: 2.0, ID: 1, Method: tools/list, } resp, err : g.forwardToBackend(backend, req) if err ! nil { log.Printf(从 %s 获取工具列表失败: %v, name, err) continue } if resp.Error ! nil { log.Printf(从 %s 获取工具列表返回错误: %s, name, resp.Error.Message) continue } var listResult struct { Tools []map[string]interface{} json:tools } json.Unmarshal(resp.Result, listResult) // 给工具名加 Server 前缀避免冲突 for _, tool : range listResult.Tools { tool[name] name : tool[name] tool[_backend] name // 内部标记路由用 allTools append(allTools, tool) } } return allTools, nil } // ---- HTTP Handler ---- func (g *MCPGateway) ServeHTTP(w http.ResponseWriter, r *http.Request) { // 1. 验证 Gateway 层的 Token auth : r.Header.Get(Authorization) if !strings.HasPrefix(auth, Bearer ) || strings.TrimPrefix(auth, Bearer ) ! g.gwToken { http.Error(w, {jsonrpc:2.0,id:null,error:{code:-32001,message:unauthorized}}, http.StatusUnauthorized) return } var req JSONRPCReq if err : json.NewDecoder(r.Body).Decode(req); err ! nil { http.Error(w, {jsonrpc:2.0,id:null,error:{code:-32700,message:parse error}}, http.StatusBadRequest) return } // 审计日志 log.Printf([AUDIT] method%s agent%s, req.Method, extractAgent(r)) var resp JSONRPCResp switch req.Method { case initialize: // Gateway 自身的能力声明 resp JSONRPCResp{ JSONRPC: 2.0, ID: req.ID, Result: map[string]interface{}{ protocolVersion: 2026-07-28, capabilities: map[string]interface{}{ tools: map[string]interface{}{}, resources: map[string]interface{}{}, }, }, } case tools/list: // 聚合所有后端的工具列表 tools, err : g.collectAllTools() if err ! nil { resp JSONRPCResp{ JSONRPC: 2.0, ID: req.ID, Error: RPCError{Code: -32603, Message: 无法获取工具列表}, } } else { resp JSONRPCResp{ JSONRPC: 2.0, ID: req.ID, Result: map[string]interface{}{tools: tools}, } } case tools/call: var params struct { Name string json:name Arguments json.RawMessage json:arguments } json.Unmarshal(req.Params, params) // 解析工具名格式为 server_name:tool_name parts : strings.SplitN(params.Name, :, 2) if len(parts) ! 2 { resp JSONRPCResp{ JSONRPC: 2.0, ID: req.ID, Error: RPCError{Code: -32602, Message: 工具名格式错误应为 server:tool}, } break } serverName, toolName : parts[0], parts[1] // 查找后端 Server g.mu.RLock() backend : g.backends[serverName] g.mu.RUnlock() if backend nil || !backend.Healthy { resp JSONRPCResp{ JSONRPC: 2.0, ID: req.ID, Error: RPCError{Code: -32603, Message: fmt.Sprintf(后端 %s 不可用, serverName)}, } break } // 改写工具名为原始名称去掉前缀转发到后端 forwardReq : req forwardParams : struct { Name string json:name Arguments json.RawMessage json:arguments }{ Name: toolName, Arguments: params.Arguments, } forwardParamsBytes, _ : json.Marshal(forwardParams) forwardReq.Params forwardParamsBytes forwardResp, err : g.forwardToBackend(backend, forwardReq) if err ! nil { resp JSONRPCResp{ JSONRPC: 2.0, ID: req.ID, Error: RPCError{Code: -32603, Message: fmt.Sprintf(转发到 %s 失败: %s, serverName, err.Error())}, } } else { resp *forwardResp } default: resp JSONRPCResp{ JSONRPC: 2.0, ID: req.ID, Error: RPCError{Code: -32601, Message: 不支持的方法}, } } w.Header().Set(Content-Type, application/json) json.NewEncoder(w).Encode(resp) } func extractAgent(r *http.Request) string { auth : r.Header.Get(Authorization) if len(auth) 20 { return auth[:20] ... } return unknown } // ---- 主函数 ---- func main() { gateway : NewMCPGateway(gw-secret-token-2026) // 注册两个模拟的后端 Server gateway.Register(BackendServer{ Name: orders, URL: http://localhost:8091/mcp, Token: orders-backend-token, Tools: []string{query_orders, create_refund, track_delivery}, Healthy: true, }) gateway.Register(BackendServer{ Name: ops, URL: http://localhost:8092/mcp, Token: ops-backend-token, Tools: []string{restart_pod, check_disk, tail_log}, Healthy: true, }) // 启动 Gateway mux : http.NewServeMux() mux.HandleFunc(/mcp, gateway.ServeHTTP) log.Println(MCP Gateway 启动于 :8084/mcp) log.Println(Agent 使用 Token: gw-secret-token-2026) log.Fatal(http.ListenAndServe(:8084, mux)) }五、模拟后端 Server 测试为了测试 Gateway启动两个模拟后端// 模拟 orders 后端:8091 func mockOrdersBackend() { mux : http.NewServeMux() mux.HandleFunc(/mcp, func(w http.ResponseWriter, r *http.Request) { var req JSONRPCReq json.NewDecoder(r.Body).Decode(req) var resp JSONRPCResp switch req.Method { case initialize: resp JSONRPCResp{JSONRPC: 2.0, ID: req.ID, Result: map[string]interface{}{protocolVersion: 2026-07-28, capabilities: map[string]interface{}{tools: map[string]interface{}{}}}} case tools/list: resp JSONRPCResp{JSONRPC: 2.0, ID: req.ID, Result: map[string]interface{}{ tools: []map[string]interface{}{ {name: query_orders, description: 查询订单, inputSchema: map[string]interface{}{type: object, properties: map[string]interface{}{}}}, {name: create_refund, description: 发起退款, inputSchema: map[string]interface{}{type: object, properties: map[string]interface{}{}}}, }, }} case tools/call: resp JSONRPCResp{JSONRPC: 2.0, ID: req.ID, Result: map[string]interface{}{ content: []map[string]interface{}{{type: text, text: orders 后端执行成功}}, }} } w.Header().Set(Content-Type, application/json) json.NewEncoder(w).Encode(resp) }) log.Fatal(http.ListenAndServe(:8091, mux)) }测试命令# 1. 列出所有工具Gateway 聚合了两个后端的工具 curl -X POST localhost:8084/mcp \ -H Authorization: Bearer gw-secret-token-2026 \ -H Content-Type: application/json \ -d {jsonrpc:2.0,id:1,method:tools/list} # 2. 调用 orders 后端的 query_orders工具名加前缀 orders: curl -X POST localhost:8084/mcp \ -H Authorization: Bearer gw-secret-token-2026 \ -H Content-Type: application/json \ -d {jsonrpc:2.0,id:2,method:tools/call,params:{name:orders:query_orders,arguments:{}}} # 3. 不带 Token 调用应被拒绝 curl -X POST localhost:8084/mcp \ -H Content-Type: application/json \ -d {jsonrpc:2.0,id:3,method:tools/list}六、Gateway 的高级设计考虑6.1 健康检查Gateway 应定期探测后端 Server 的健康状态func (g *MCPGateway) healthCheckLoop(interval time.Duration) { for { time.Sleep(interval) g.mu.RLock() for name, backend : range g.backends { // 发送一个简单的 initialize 请求探测 req : JSONRPCReq{JSONRPC: 2.0, ID: 1, Method: initialize} _, err : g.forwardToBackend(backend, req) g.mu.RUnlock() g.mu.Lock() if err ! nil { backend.Healthy false log.Printf(后端 %s 不健康标记为下线, name) } else { backend.Healthy true } g.mu.Unlock() g.mu.RLock() } g.mu.RUnlock() } }6.2 限流每个 Agent 或每个后端 Server 的调用频率限制type RateLimiter struct { mu sync.Mutex requests map[string][]time.Time limit int // 窗口内最大请求数 window time.Duration // 时间窗口 } func (rl *RateLimiter) Allow(key string) bool { rl.mu.Lock() defer rl.mu.Unlock() now : time.Now() windowStart : now.Add(-rl.window) // 清理过期记录 times : rl.requests[key] var valid []time.Time for _, t : range times { if t.After(windowStart) { valid append(valid, t) } } if len(valid) rl.limit { rl.requests[key] valid return false } valid append(valid, now) rl.requests[key] valid return true }七、安全分层L7本讲在 L1-L6 基础上补充 Gateway 层的安全关注点L7Gateway 安全Gateway 是统一的安全边界所有外部请求必须经过 Gateway不允许 Agent 直连后端 ServerGateway 到后端 Server 使用独立的 Token不同于 Agent 的 Token实现凭证隔离Gateway 应记录完整的审计日志谁、什么时候、调用了哪个工具、参数摘要、结果状态Gateway 应做请求体大小限制默认 1MB防止恶意大请求压垮后端Gateway 应做超时控制默认 30s防止慢后端拖垮整个 Gateway后端 Server 的网络隔离只允许 Gateway 所在网段访问后端后端不对外暴露八、延伸阅读Envoy 作为 MCP Gateway用 Envoy 的 Lua filter 或 Wasm 插件实现 MCP 协议的路由和鉴权Apache APISIX MCP 插件开源 API 网关对 MCP 的原生支持MCP Hub 规范草案MCP 社区的 Server 注册与发现标准九、下一讲预告第9讲生产级保障——可观测性、限流、熔断与审计Gateway 解决了统一接入的问题但生产环境还需要知道工具调用成功了吗花了多久谁调的有没有异常下一讲会为整个 MCP 系统加上可观测性OpenTelemetry 追踪 Prometheus 指标 结构化日志、限流熔断、以及完整的审计链路。开发之余的小工具推荐处理 Base64、JSON 格式化、JWT 解析、Crontab 计算、PDF 合并压缩这些碎片需求我常用一个纯前端本地工具箱zz365.top。所有计算在浏览器完成文件不上服务器关页即清。免费、无登录、无广告适合开发者当常驻标签页。
返回列表