一、项目概述
前九讲我们逐一攻克了 MCP 的各个模块:工具设计、Transport 选型、Resource/Prompt、认证授权、Client 架构、通知机制、Gateway 网关、可观测性与限流熔断。本讲将它们整合成一个完整的生产级系统——BizMCP。
BizMCP 是一个面向电商业务的 MCP 基础设施,包含三个核心组件:
组件 | 职责 | 端口 |
|---|---|---|
| 订单管理 MCP Server(查询、退款、通知) | 8091 |
| 运维 MCP Server(日志读取、Pod 管理) | 8092 |
| 统一 MCP Gateway(认证、路由、限流、追踪、审计) | 8085 |
二、系统架构
┌─────────────┐ ┌──────────────────────────────────────┐ │ Agent │────▶│ bizmcp-gateway │ │ (Claude │ │ ┌─────────┐ ┌────────┐ ┌────────┐ │ │ Code/ │ │ │ Auth │ │ Router │ │ Rate │ │ │ Custom) │ │ │ (JWT) │ │ (tool │ │ Limit │ │ └─────────────┘ │ │ │ │ → svc)│ │(Token │ │ │ └─────────┘ └────────┘ │ Bucket)│ │ │ ┌─────────┐ ┌────────┐ └────────┘ │ │ │ Trace │ │ Audit │ │ │ │ (OTel) │ │ (JSONL)│ │ │ └─────────┘ └────────┘ │ └──────────────────────────────────────┘ │ ┌───────────────┴───────────────┐ ▼ ▼ ┌───────────────────┐ ┌───────────────────┐ │ bizmcp-orders │ │ bizmcp-ops │ │ :8091 │ │ :8092 │ │ │ │ │ │ Tools: │ │ Tools: │ │ • query_orders │ │ • restart_pod │ │ • create_refund │ │ • check_disk │ │ • track_delivery │ │ │ │ │ │ Resources: │ │ Resources: │ │ • logs://{ns}/{pod}│ │ • orders://{id} │ │ │ └───────────────────┘ └───────────────────┘三、完整代码实现
3.1 项目结构
bizmcp/ ├── cmd/ │ ├── gateway/main.go # Gateway 入口 │ ├── orders/main.go # 订单 Server 入口 │ └── ops/main.go # 运维 Server 入口 ├── internal/ │ ├── mcp/ # MCP 协议核心类型 │ │ ├── message.go # JSON-RPC 消息结构 │ │ ├── server.go # MCP Server 基础框架 │ │ └── transport.go # Transport 抽象 │ ├── auth/ # 认证授权 │ │ └── jwt.go │ ├── gateway/ # Gateway 逻辑 │ │ ├── router.go │ │ ├── ratelimit.go │ │ └── circuitbreaker.go │ └── observability/ # 可观测性 │ ├── tracing.go │ ├── metrics.go │ └── audit.go ├── deploy/ │ ├── docker-compose.yml │ └── k8s/ │ ├── deployment.yaml │ └── service.yaml ├── go.mod └── go.sum3.2 共享 MCP 核心类型(internal/mcp/message.go)
package mcp import "encoding/json" // JSON-RPC 消息 type Request struct { JSONRPC string `json:"jsonrpc"` ID int `json:"id"` Method string `json:"method"` Params json.RawMessage `json:"params,omitempty"` } type Response struct { JSONRPC string `json:"jsonrpc"` ID int `json:"id"` Result json.RawMessage `json:"result,omitempty"` Error *ErrorObj `json:"error,omitempty"` } type ErrorObj struct { Code int `json:"code"` Message string `json:"message"` } // 工具定义 type ToolSpec struct { Name string `json:"name"` Description string `json:"description"` InputSchema interface{} `json:"inputSchema"` } // 资源定义 type Resource struct { URI string `json:"uri"` Name string `json:"name"` Description string `json:"description"` MimeType string `json:"mimeType"` } type ResourceTemplate struct { URITemplate string `json:"uriTemplate"` Name string `json:"name"` Description string `json:"description"` MimeType string `json:"mimeType"` } // 工具调用结果 type ToolResult struct { Content []ContentItem `json:"content"` IsError bool `json:"isError,omitempty"` } type ContentItem struct { Type string `json:"type"` Text string `json:"text"` }3.3 订单 MCP Server(cmd/orders/main.go)
package main import ( "encoding/json" "fmt" "log" "net/http" "regexp" "strings" "sync" "time" "bizmcp/internal/mcp" ) // ---- 订单数据模型 ---- type Order struct { ID string `json:"id"` UserID string `json:"user_id"` Amount float64 `json:"amount"` Status string `json:"status"` CreatedAt string `json:"created_at"` } type OrderStore struct { mu sync.RWMutex orders map[string]Order } func NewOrderStore() *OrderStore { return &OrderStore{ orders: map[string]Order{ "ord_001": {ID: "ord_001", UserID: "u_123", Amount: 299.00, Status: "paid", CreatedAt: "2026-09-01T10:00:00Z"}, "ord_002": {ID: "ord_002", UserID: "u_123", Amount: 59.90, Status: "shipped", CreatedAt: "2026-09-05T14:30:00Z"}, "ord_003": {ID: "ord_003", UserID: "u_456", Amount: 899.00, Status: "pending", CreatedAt: "2026-09-08T08:00:00Z"}, }, } } // ---- 工具 Handlers ---- func handleQueryOrders(store *OrderStore, raw json.RawMessage) *mcp.ToolResult { var args struct { UserID string `json:"user_id"` Status string `json:"status,omitempty"` } json.Unmarshal(raw, &args) // 业务校验 if args.UserID == "" { return &mcp.ToolResult{ IsError: true, Content: []mcp.ContentItem{{Type: "text", Text: "user_id 不能为空"}}, } } if !regexp.MustCompile(`^u_\d+$`).MatchString(args.UserID) { return &mcp.ToolResult{ IsError: true, Content: []mcp.ContentItem{{Type: "text", Text: "user_id 格式无效"}}, } } store.mu.RLock() defer store.mu.RUnlock() var results []Order for _, o := range store.orders { if o.UserID == args.UserID { if args.Status == "" || o.Status == args.Status { results = append(results, o) } } } data, _ := json.Marshal(results) return &mcp.ToolResult{ Content: []mcp.ContentItem{{Type: "text", Text: string(data)}}, } } func handleCreateRefund(store *OrderStore, raw json.RawMessage) *mcp.ToolResult { var args struct { OrderID string `json:"order_id"` Reason string `json:"reason,omitempty"` } json.Unmarshal(raw, &args) if args.OrderID == "" { return &mcp.ToolResult{ IsError: true, Content: []mcp.ContentItem{{Type: "text", Text: "order_id 不能为空"}}, } } store.mu.Lock() defer store.mu.Unlock() order, exists := store.orders[args.OrderID] if !exists { return &mcp.ToolResult{ IsError: true, Content: []mcp.ContentItem{{Type: "text", Text: fmt.Sprintf("订单 %s 不存在", args.OrderID)}}, } } if order.Status == "refunded" { return &mcp.ToolResult{ IsError: true, Content: []mcp.ContentItem{{Type: "text", Text: "订单已退款,不能重复退款"}}, } } order.Status = "refunded" store.orders[args.OrderID] = order return &mcp.ToolResult{ Content: []mcp.ContentItem{{Type: "text", Text: fmt.Sprintf("订单 %s 退款成功", args.OrderID)}}, } } // ---- MCP Server ---- type OrdersMCPServer struct { store *OrderStore tools map[string]func(json.RawMessage) *mcp.ToolResult } func NewOrdersMCPServer() *OrdersMCPServer { store := NewOrderStore() return &OrdersMCPServer{ store: store, tools: map[string]func(json.RawMessage) *mcp.ToolResult{ "query_orders": func(raw json.RawMessage) *mcp.ToolResult { return handleQueryOrders(store, raw) }, "create_refund": func(raw json.RawMessage) *mcp.ToolResult { return handleCreateRefund(store, raw) }, "track_delivery": func(raw json.RawMessage) *mcp.ToolResult { return handleTrackDelivery(store, raw) }, }, } } func handleTrackDelivery(store *OrderStore, raw json.RawMessage) *mcp.ToolResult { var args struct { OrderID string `json:"order_id"` } json.Unmarshal(raw, &args) store.mu.RLock() defer store.mu.RUnlock() order, exists := store.orders[args.OrderID] if !exists { return &mcp.ToolResult{ IsError: true, Content: []mcp.ContentItem{{Type: "text", Text: "订单不存在"}}, } } if order.Status != "shipped" { return &mcp.ToolResult{ IsError: true, Content: []mcp.ContentItem{{Type: "text", Text: "订单尚未发货"}}, } } return &mcp.ToolResult{ Content: []mcp.ContentItem{{Type: "text", Text: fmt.Sprintf("订单 %s 已发货,预计 3-5 个工作日送达", args.OrderID)}}, } } func (s *OrdersMCPServer) Handle(req mcp.Request) mcp.Response { switch req.Method { case "initialize": return mcp.Response{ JSONRPC: "2.0", ID: req.ID, Result: toRaw(map[string]interface{}{ "protocolVersion": "2026-07-28", "capabilities": map[string]interface{}{"tools": map[string]interface{}{}}, }), } case "tools/list": specs := []mcp.ToolSpec{ { Name: "query_orders", Description: "按用户 ID 查询订单列表,支持按状态过滤。只读操作。", InputSchema: map[string]interface{}{ "type": "object", "properties": map[string]interface{}{ "user_id": map[string]interface{}{"type": "string", "description": "用户 UUID,格式 u_xxx"}, "status": map[string]interface{}{"type": "string", "enum": []string{"pending", "paid", "shipped", "refunded"}, "description": "按状态过滤,可选"}, }, "required": []string{"user_id"}, }, }, { Name: "create_refund", Description: "为指定订单发起退款。写操作,不可逆。", InputSchema: map[string]interface{}{ "type": "object", "properties": map[string]interface{}{ "order_id": map[string]interface{}{"type": "string", "description": "订单 ID"}, "reason": map[string]interface{}{"type": "string", "description": "退款原因"}, }, "required": []string{"order_id"}, }, }, { Name: "track_delivery", Description: "查询订单配送状态。只读。", InputSchema: map[string]interface{}{ "type": "object", "properties": map[string]interface{}{ "order_id": map[string]interface{}{"type": "string", "description": "订单 ID"}, }, "required": []string{"order_id"}, }, }, } return mcp.Response{ JSONRPC: "2.0", ID: req.ID, Result: toRaw(map[string]interface{}{"tools": specs}), } case "tools/call": var params struct { Name string `json:"name"` Arguments json.RawMessage `json:"arguments"` } json.Unmarshal(req.Params, ¶ms) handler, exists := s.tools[params.Name] if !exists { return mcp.Response{ JSONRPC: "2.0", ID: req.ID, Error: &mcp.ErrorObj{Code: -32601, Message: "unknown tool: " + params.Name}, } } result := handler(params.Arguments) return mcp.Response{ JSONRPC: "2.0", ID: req.ID, Result: toRaw(result), } default: return mcp.Response{ JSONRPC: "2.0", ID: req.ID, Error: &mcp.ErrorObj{Code: -32601, Message: "unsupported method"}, } } } func toRaw(v interface{}) json.RawMessage { b, _ := json.Marshal(v) return b } func main() { server := NewOrdersMCPServer() mux := http.NewServeMux() mux.HandleFunc("/mcp", func(w http.ResponseWriter, r *http.Request) { var req mcp.Request json.NewDecoder(r.Body).Decode(&req) resp := server.Handle(req) w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(resp) }) log.Println("BizMCP Orders Server 启动于 :8091") log.Fatal(http.ListenAndServe(":8091", mux)) }3.4 运维 MCP Server(cmd/ops/main.go)
package main import ( "encoding/json" "fmt" "log" "net/http" "os" "path/filepath" "regexp" "strings" "time" "bizmcp/internal/mcp" ) // 模拟日志目录 const logDir = "/var/log/bizmcp" type OpsMCPServer struct{} func (s *OpsMCPServer) Handle(req mcp.Request) mcp.Response { switch req.Method { case "initialize": return mcp.Response{ JSONRPC: "2.0", ID: req.ID, Result: toRaw(map[string]interface{}{ "protocolVersion": "2026-07-28", "capabilities": map[string]interface{}{ "tools": map[string]interface{}{}, "resources": map[string]interface{}{}, }, }), } case "tools/list": return mcp.Response{ JSONRPC: "2.0", ID: req.ID, Result: toRaw(map[string]interface{}{ "tools": []mcp.ToolSpec{ { Name: "restart_pod", Description: "重启指定 namespace 下的 Pod。写操作。", InputSchema: map[string]interface{}{ "type": "object", "properties": map[string]interface{}{ "namespace": map[string]interface{}{"type": "string"}, "pod_name": map[string]interface{}{"type": "string"}, }, "required": []string{"namespace", "pod_name"}, }, }, { Name: "check_disk", Description: "检查节点的磁盘使用率。只读。", InputSchema: map[string]interface{}{ "type": "object", "properties": map[string]interface{}{}, }, }, }, "resourceTemplates": []mcp.ResourceTemplate{ { URITemplate: "logs://{namespace}/{pod}", Name: "Pod 日志", Description: "按 namespace 和 pod 名称读取容器日志,只读", MimeType: "text/plain", }, }, }), } case "tools/call": var params struct { Name string `json:"name"` Arguments json.RawMessage `json:"arguments"` } json.Unmarshal(req.Params, ¶ms) switch params.Name { case "restart_pod": var args struct { Namespace string `json:"namespace"` PodName string `json:"pod_name"` } json.Unmarshal(params.Arguments, &args) if args.Namespace == "" || args.PodName == "" { return mcp.Response{ JSONRPC: "2.0", ID: req.ID, Error: &mcp.ErrorObj{Code: -32602, Message: "namespace 和 pod_name 不能为空"}, } } return mcp.Response{ JSONRPC: "2.0", ID: req.ID, Result: toRaw(&mcp.ToolResult{ Content: []mcp.ContentItem{{Type: "text", Text: fmt.Sprintf("Pod %s/%s 重启命令已下发", args.Namespace, args.PodName)}}, }), } case "check_disk": return mcp.Response{ JSONRPC: "2.0", ID: req.ID, Result: toRaw(&mcp.ToolResult{ Content: []mcp.ContentItem{{Type: "text", Text: "磁盘使用率: /data 72%, /var 45%, /tmp 18%"}}, }), } default: return mcp.Response{ JSONRPC: "2.0", ID: req.ID, Error: &mcp.ErrorObj{Code: -32601, Message: "unknown tool"}, } } case "resources/list": return mcp.Response{ JSONRPC: "2.0", ID: req.ID, Result: toRaw(map[string]interface{}{ "resources": []mcp.Resource{}, "resourceTemplates": []mcp.ResourceTemplate{ { URITemplate: "logs://{namespace}/{pod}", Name: "Pod 日志", Description: "按 namespace 和 pod 名称读取容器日志,只读", MimeType: "text/plain", }, }, }), } case "resources/read": var params struct { URI string `json:"uri"` } json.Unmarshal(req.Params, ¶ms) // 解析 URI pattern := regexp.MustCompile(`^logs://([a-z0-9-]+)/([a-z0-9-]+)$`) matches := pattern.FindStringSubmatch(params.URI) if len(matches) != 3 { return mcp.Response{ JSONRPC: "2.0", ID: req.ID, Error: &mcp.ErrorObj{Code: -32602, Message: "URI 格式错误"}, } } namespace, pod := matches[1], matches[2] // 路径遍历防护 if strings.Contains(namespace, "..") || strings.Contains(pod, "..") { return mcp.Response{ JSONRPC: "2.0", ID: req.ID, Error: &mcp.ErrorObj{Code: -32603, Message: "非法路径"}, } } logPath := filepath.Join(logDir, namespace, pod+".log") absPath, _ := filepath.Abs(logPath) absBase, _ := filepath.Abs(logDir) if !strings.HasPrefix(absPath, absBase) { return mcp.Response{ JSONRPC: "2.0", ID: req.ID, Error: &mcp.ErrorObj{Code: -32603, Message: "路径越界"}, } } data, err := os.ReadFile(logPath) if err != nil { return mcp.Response{ JSONRPC: "2.0", ID: req.ID, Error: &mcp.ErrorObj{Code: -32603, Message: "日志不存在"}, } } return mcp.Response{ JSONRPC: "2.0", ID: req.ID, Result: toRaw(map[string]interface{}{ "contents": []map[string]interface{}{ {"uri": params.URI, "mimeType": "text/plain", "text": string(data)}, }, }), } default: return mcp.Response{ JSONRPC: "2.0", ID: req.ID, Error: &mcp.ErrorObj{Code: -32601, Message: "unsupported method"}, } } } func toRaw(v interface{}) json.RawMessage { b, _ := json.Marshal(v) return b } func main() { // 创建模拟日志 os.MkdirAll(filepath.Join(logDir, "production"), 0755) logContent := fmt.Sprintf("[%s] INFO Server started\n[%s] WARN Memory usage 82%%\n", time.Now().Add(-1*time.Hour).Format(time.RFC3339), time.Now().Add(-30*time.Minute).Format(time.RFC3339)) os.WriteFile(filepath.Join(logDir, "production", "web-abc.log"), []byte(logContent), 0644) server := &OpsMCPServer{} mux := http.NewServeMux() mux.HandleFunc("/mcp", func(w http.ResponseWriter, r *http.Request) { var req mcp.Request json.NewDecoder(r.Body).Decode(&req) resp := server.Handle(req) w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(resp) }) log.Println("BizMCP Ops Server 启动于 :8092") log.Fatal(http.ListenAndServe(":8092", mux)) }3.5 MCP Gateway(cmd/gateway/main.go)
完整 Gateway 实现在第9讲基础上精简,保留核心功能:认证、路由、限流、审计。
package main import ( "bytes" "encoding/json" "fmt" "log" "net/http" "os" "strings" "sync" "time" "bizmcp/internal/mcp" ) // ---- 审计日志 ---- type AuditEntry struct { Timestamp string `json:"timestamp"` AgentID string `json:"agent_id"` Method string `json:"method"` ToolName string `json:"tool_name,omitempty"` Backend string `json:"backend,omitempty"` DurationMs int64 `json:"duration_ms"` StatusCode int `json:"status_code"` ErrorMsg string `json:"error_msg,omitempty"` } var auditLogger = log.New(os.Stdout, "", 0) func writeAudit(entry AuditEntry) { entry.Timestamp = time.Now().UTC().Format(time.RFC3339Nano) data, _ := json.Marshal(entry) auditLogger.Println(string(data)) } // ---- 令牌桶限流 ---- type TokenBucket struct { mu sync.Mutex tokens float64 maxTokens float64 refillRate float64 lastRefill time.Time } func NewTokenBucket(maxTokens, refillRate float64) *TokenBucket { return &TokenBucket{tokens: maxTokens, maxTokens: maxTokens, refillRate: refillRate, lastRefill: time.Now()} } func (tb *TokenBucket) Allow() bool { tb.mu.Lock() defer tb.mu.Unlock() now := time.Now() elapsed := now.Sub(tb.lastRefill).Seconds() tb.tokens = min(tb.maxTokens, tb.tokens+elapsed*tb.refillRate) tb.lastRefill = now if tb.tokens >= 1 { tb.tokens-- return true } return false } func min(a, b float64) float64 { if a < b { return a }; return b } // ---- 后端 Server 注册 ---- type Backend struct { Name string URL string Token string Tools []string Client *http.Client } // ---- Gateway ---- type Gateway struct { mu sync.RWMutex backends map[string]*Backend toolRoute map[string]string gwToken string rateLimiter *TokenBucket } func NewGateway(gwToken string) *Gateway { return &Gateway{ backends: make(map[string]*Backend), toolRoute: make(map[string]string), gwToken: gwToken, rateLimiter: NewTokenBucket(100, 50), } } func (g *Gateway) Register(b *Backend) { b.Client = &http.Client{Timeout: 30 * time.Second} g.mu.Lock() defer g.mu.Unlock() g.backends[b.Name] = b for _, tool := range b.Tools { g.toolRoute[tool] = b.Name } } func (g *Gateway) ServeHTTP(w http.ResponseWriter, r *http.Request) { start := time.Now() // 1. 认证 auth := r.Header.Get("Authorization") token := strings.TrimPrefix(auth, "Bearer ") if token != g.gwToken { writeAudit(AuditEntry{AgentID: "unknown", Method: "auth", StatusCode: 401, ErrorMsg: "unauthorized"}) http.Error(w, `{"jsonrpc":"2.0","id":null,"error":{"code":-32001,"message":"unauthorized"}}`, http.StatusUnauthorized) return } // 2. 限流 if !g.rateLimiter.Allow() { writeAudit(AuditEntry{AgentID: token[:8], Method: "rate_limit", StatusCode: 429, ErrorMsg: "rate limited"}) http.Error(w, `{"jsonrpc":"2.0","id":null,"error":{"code":-32000,"message":"too many requests"}}`, http.StatusTooManyRequests) return } var req mcp.Request json.NewDecoder(r.Body).Decode(&req) var resp mcp.Response switch req.Method { case "initialize": resp = mcp.Response{ JSONRPC: "2.0", ID: req.ID, Result: toRaw(map[string]interface{}{ "protocolVersion": "2026-07-28", "capabilities": map[string]interface{}{"tools": map[string]interface{}{}, "resources": map[string]interface{}{}}, }), } case "tools/list": g.mu.RLock() var allTools []map[string]interface{} for name, backend := range g.backends { for _, tool := range backend.Tools { allTools = append(allTools, map[string]interface{}{ "name": name + ":" + tool, "description": fmt.Sprintf("%s 提供的 %s 工具", name, tool), }) } } g.mu.RUnlock() resp = mcp.Response{JSONRPC: "2.0", ID: req.ID, Result: toRaw(map[string]interface{}{"tools": allTools})} case "tools/call": var params struct { Name string `json:"name"` Arguments json.RawMessage `json:"arguments"` } json.Unmarshal(req.Params, ¶ms) parts := strings.SplitN(params.Name, ":", 2) if len(parts) != 2 { resp = mcp.Response{JSONRPC: "2.0", ID: req.ID, Error: &mcp.ErrorObj{Code: -32602, Message: "tool name format: backend:tool"}} break } backendName, toolName := parts[0], parts[1] g.mu.RLock() backend := g.backends[backendName] g.mu.RUnlock() if backend == nil { resp = mcp.Response{JSONRPC: "2.0", ID: req.ID, Error: &mcp.ErrorObj{Code: -32603, Message: fmt.Sprintf("backend %s not found", backendName)}} break } // 转发到后端 forwardReq := mcp.Request{ JSONRPC: req.JSONRPC, ID: req.ID, Method: req.Method, Params: toRaw(map[string]interface{}{"name": toolName, "arguments": params.Arguments}), } bodyBytes, _ := json.Marshal(forwardReq) httpReq, _ := http.NewRequest("POST", backend.URL, bytes.NewReader(bodyBytes)) httpReq.Header.Set("Content-Type", "application/json") httpReq.Header.Set("Authorization", "Bearer "+backend.Token) httpResp, err := backend.Client.Do(httpReq) if err != nil { resp = mcp.Response{JSONRPC: "2.0", ID: req.ID, Error: &mcp.ErrorObj{Code: -32603, Message: err.Error()}} break } defer httpResp.Body.Close() json.NewDecoder(httpResp.Body).Decode(&resp) default: resp = mcp.Response{JSONRPC: "2.0", ID: req.ID, Error: &mcp.ErrorObj{Code: -32601, Message: "unsupported method"}} } // 审计 durationMs := time.Since(start).Milliseconds() statusCode := 200 errorMsg := "" if resp.Error != nil { statusCode = resp.Error.Code errorMsg = resp.Error.Message } writeAudit(AuditEntry{ AgentID: token[:8], Method: req.Method, DurationMs: durationMs, StatusCode: statusCode, ErrorMsg: errorMsg, }) w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(resp) } func toRaw(v interface{}) json.RawMessage { b, _ := json.Marshal(v) return b } func main() { gwToken := os.Getenv("GW_TOKEN") if gwToken == "" { gwToken = "dev-token-change-in-production" } gateway := NewGateway(gwToken) gateway.Register(&Backend{ Name: "orders", URL: "http://localhost:8091/mcp", Token: "orders-backend-token", Tools: []string{"query_orders", "create_refund", "track_delivery"}, }) gateway.Register(&Backend{ Name: "ops", URL: "http://localhost:8092/mcp", Token: "ops-backend-token", Tools: []string{"restart_pod", "check_disk"}, }) mux := http.NewServeMux() mux.HandleFunc("/mcp", gateway.ServeHTTP) log.Printf("BizMCP Gateway 启动于 :8085,Token: %s", gwToken) log.Fatal(http.ListenAndServe(":8085", mux)) }四、Docker Compose 部署
# deploy/docker-compose.yml version: '3.8' services: bizmcp-orders: build: context: .. dockerfile: deploy/Dockerfile args: APP: orders ports: - "8091:8091" environment: - APP_ENV=production networks: - bizmcp-net bizmcp-ops: build: context: .. dockerfile: deploy/Dockerfile args: APP: ops ports: - "8092:8092" volumes: - ./logs:/var/log/bizmcp:ro networks: - bizmcp-net bizmcp-gateway: build: context: .. dockerfile: deploy/Dockerfile args: APP: gateway ports: - "8085:8085" environment: - GW_TOKEN=prod-token-2026 depends_on: - bizmcp-orders - bizmcp-ops networks: - bizmcp-net networks: bizmcp-net: driver: bridge# deploy/Dockerfile FROM golang:1.22 AS builder ARG APP WORKDIR /build COPY go.mod go.sum ./ RUN go mod download COPY . . RUN CGO_ENABLED=0 GOOS=linux go build -o /app ./cmd/$APP FROM alpine:3.19 RUN apk add --no-cache ca-certificates tzdata COPY --from=builder /app /app EXPOSE 8085 8091 8092 ENTRYPOINT ["/app"]五、K8s 部署(简化)
# deploy/k8s/deployment.yaml apiVersion: apps/v1 kind: Deployment metadata: name: bizmcp-gateway spec: replicas: 3 selector: matchLabels: app: bizmcp-gateway template: metadata: labels: app: bizmcp-gateway spec: containers: - name: gateway image: bizmcp/gateway:latest ports: - containerPort: 8085 env: - name: GW_TOKEN valueFrom: secretKeyRef: name: bizmcp-secrets key: gw-token resources: requests: cpu: "500m" memory: "512Mi" limits: cpu: "1000m" memory: "1Gi" --- apiVersion: v1 kind: Service metadata: name: bizmcp-gateway spec: selector: app: bizmcp-gateway ports: - port: 443 targetPort: 8085 type: LoadBalancer六、CI/CD 流水线(GitHub Actions 片段)
# .github/workflows/deploy.yml name: Deploy BizMCP on: push: branches: [main] jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: go-version: '1.22' - run: go test ./... build-and-push: needs: test runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Build images run: | docker compose -f deploy/docker-compose.yml build - name: Push to registry run: | docker tag bizmcp-gateway:latest $REGISTRY/bizmcp-gateway:$GITHUB_SHA docker push $REGISTRY/bizmcp-gateway:$GITHUB_SHA deploy: needs: build-and-push runs-on: ubuntu-latest steps: - name: Deploy to K8s run: | kubectl set image deployment/bizmcp-gateway \ gateway=$REGISTRY/bizmcp-gateway:$GITHUB_SHA七、评估数据集
验证 BizMCP 功能的测试用例:
[ { "name": "正常查询订单", "request": {"method": "tools/call", "params": {"name": "orders:query_orders", "arguments": {"user_id": "u_123"}}}, "expected_status": "success" }, { "name": "参数校验失败 - 空 user_id", "request": {"method": "tools/call", "params": {"name": "orders:query_orders", "arguments": {"user_id": ""}}}, "expected_error": "user_id 不能为空" }, { "name": "权限校验 - 未知后端", "request": {"method": "tools/call", "params": {"name": "unknown:tool", "arguments": {}}}, "expected_error": "backend unknown not found" }, { "name": "认证失败 - 无 Token", "request": {"method": "tools/list"}, "expected_status": 401 }, { "name": "资源读取 - 日志", "request": {"method": "resources/read", "params": {"uri": "logs://production/web-abc"}}, "expected_status": "success" }, { "name": "路径遍历防护", "request": {"method": "resources/read", "params": {"uri": "logs://../etc/passwd"}}, "expected_error": "非法路径" } ]八、课后实践
- 部署运行:在本地用 Docker Compose 启动 BizMCP,用 curl 测试所有端点
- 扩展工具:给 orders Server 添加
cancel_order工具,注册到 Gateway - 写操作确认:给
create_refund和restart_pod添加人工确认步骤(调用前先发通知,等待确认后再执行) - 性能测试:用
hey或wrk对 Gateway 施压,观察限流效果 - 监控集成:配置 Prometheus 抓取 Gateway 的
/metrics,Grafana 展示仪表盘
九、延伸阅读
- MCP Specification 2026-07-28:完整协议规范
- Go net/http 中间件模式:生产级 HTTP 服务的中间件链设计
- Kubernetes Operator Pattern:如何用 Operator 管理 MCP Server 的生命周期
- OpenTelemetry Trace 采样策略:生产环境的全量与采样权衡
十、总结
BizMCP 贯穿了本专题的全部知识点:
知识点 | 对应组件 |
|---|---|
第1讲:MCP 总线概念 | Gateway 统一接入 |
第2讲:Transport 选型 | HTTP Transport |
第3讲:工具设计 | orders Server 的双层校验 |
第4讲:Resource | ops Server 的日志读取 |
第5讲:认证授权 | Gateway JWT 认证 |
第6讲:Client 架构 | 测试用的 curl 客户端 |
第7讲:通知机制 | 可扩展的通知接口 |
第8讲:Gateway 生态 | Gateway 路由与注册 |
第9讲:生产保障 | 限流、审计、可观测性 |
从第一讲到第十讲,我们从零构建了一个生产级的 MCP 系统。MCP 不是银弹,但它提供了一个清晰的协议边界,让 Agent 和后端系统能够以标准化的方式协作。当你下一次需要让 AI 模型安全地调用内部系统时,希望 BizMCP 能成为你的起点。
🧰开发之余的小工具推荐
处理 Base64、JSON 格式化、JWT 解析、Crontab 计算、PDF 合并压缩这些碎片需求,我常用一个纯前端本地工具箱:zz365.top。所有计算在浏览器完成,文件不上服务器,关页即清。免费、无登录、无广告,适合开发者当常驻标签页。