Announcement

👇Official Account👇

Welcome to join the group & private message

Article first/tail QR code

Skip to content

MCP 2026 无状态协议革命与 OpenAI Secure Tunnel 实战

前言

2026 年 5 月,Model Context Protocol (MCP) 迎来了自发布以来最大的一次协议升级。这次变革的核心可以用四个字概括:全面无状态化。与此同时,OpenAI 推出了 Secure MCP Tunnel,解决了企业级 AI Agent 部署中最棘手的"最后一公里"问题——如何让 AI 安全地访问企业内网数据。

本文将从协议原理出发,带你深入理解这次升级的技术细节,并提供可落地的生产环境部署方案。

一、MCP 协议演进:为什么要无状态?

1.1 旧版协议的设计痛点

旧版 MCP 的设计初衷是简化 AI Agent 与工具之间的连接,但它采用的是有状态长连接模型。让我们看看一个典型的旧版 MCP Server:

python
# 旧版 MCP Server(有状态)
class OldMCPServer:
    def __init__(self):
        self.sessions = {}         # 每个连接都需要存储会话状态
        self.initialized = set()   # 必须追踪初始化状态
        self.capabilities = {}     # 能力协商结果存内存

    async def handle_connect(self, websocket):
        session_id = str(uuid.uuid4())
        self.sessions[session_id] = {
            "ws": websocket,
            "state": "pending",
            "client_caps": None
        }
        # 等待 client 发送 initialize
        await self.wait_for_initialize(session_id)
        return session_id

    async def handle_request(self, session_id, request):
        if session_id not in self.initialized:
            raise ProtocolError("Session not initialized")
        session = self.sessions[session_id]
        return await self.process(session, request)

这带来了三个致命问题:

  1. 水平扩展困难:每个连接粘滞在特定实例上,必须配置 sticky session,无法使用轮询负载均衡
  2. 状态管理复杂:需要共享存储(如 Redis)来在多实例间同步 session 状态
  3. 容错性差:实例宕机后所有 session 丢失,客户端必须重新建连并初始化
┌──────────────────────────────────────────────┐
│              旧版 MCP 架构(有状态)            │
│                                              │
│  Client A ──→ LB (sticky) ──→ Instance 1     │
│              session_abc       ├─ sessions:   │
│                               │  abc: {...}   │
│  Client B ──→ LB (sticky) ──→ Instance 2     │
│              session_xyz       ├─ sessions:   │
│                               │  xyz: {...}   │
│                               │              │
│  ⚠️ 问题:                                   │
│  - Instance 1 宕机 → Client A 全部断开        │
│  - 扩容新实例 → session 无法迁移             │
│  - 需要 Redis 同步 session 状态              │
└──────────────────────────────────────────────┘

1.2 无状态化的六大 SEP

2026 年 5 月的升级通过六个核心 SEP(Specification Enhancement Proposal)实现了全面无状态化:

SEP 编号变更内容实际影响
SEP-2575移除 initialize/initialized 握手客户端信息通过每个请求的 _meta 字段传递
SEP-2567移除 Mcp-Session-Id任意请求可路由到任意服务实例
SEP-2260服务端请求必须关联客户端请求消除无上下文的弹窗,保证请求可追踪
SEP-2243要求 Mcp-MethodMcp-Name负载均衡器无需解析 JSON Body 即可路由
SEP-2549工具列表带 ttlMscacheScope客户端可按策略缓存工具定义,减少重复调用
SEP-414W3C Trace Context 传播跨服务分布式追踪,链路可观测

新架构变为:

┌──────────────────────────────────────────────┐
│              新版 MCP 架构(无状态)            │
│                                              │
│  Client A ──→ LB (round-robin) ──→ Instance 1│
│              基于 Mcp-Method 头路由            │
│  Client B ──→ LB (round-robin) ──→ Instance 2│
│                                              │
│  Client A ──→ LB (round-robin) ──→ Instance 3│  ← 下一个请求可到任意实例
│                                              │
│  ✅ 优势:                                    │
│  - 无需 sticky session                       │
│  - 无需共享状态存储                            │
│  - 实例可随时扩缩容                            │
│  - 任意实例宕机不影响其他请求                    │
└──────────────────────────────────────────────┘

1.3 客户端信息传递的新方式

在无状态协议中,客户端信息不再通过 initialize 握手传递,而是作为每个请求的元数据:

json
// 新版 MCP 请求格式
{
    "jsonrpc": "2.0",
    "method": "tools/call",
    "params": {
        "name": "database_query",
        "arguments": { "sql": "SELECT * FROM users LIMIT 10" }
    },
    "_meta": {
        "clientInfo": {
            "name": "ChatGPT",
            "version": "2026.5"
        },
        "protocolVersion": "2026-05-01",
        "traceContext": {
            "traceparent": "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"
        }
    }
}

二、构建无状态 MCP Server(Go 实战)

让我们用 Go 实现一个生产级的无状态 MCP Server:

go
package main

import (
    "context"
    "encoding/json"
    "fmt"
    "log"
    "net/http"
    "os"
    "time"

    "go.opentelemetry.io/otel"
    "go.opentelemetry.io/otel/propagation"
)

// MCPRequest 代表无状态 MCP JSON-RPC 请求
type MCPRequest struct {
    JSONRPC string          `json:"jsonrpc"`
    ID      int64           `json:"id"`
    Method  string          `json:"method"`
    Params  json.RawMessage `json:"params"`
    Meta    *MCPMeta        `json:"_meta,omitempty"`
}

type MCPMeta struct {
    ClientInfo      ClientInfo `json:"clientInfo"`
    ProtocolVersion string     `json:"protocolVersion"`
}

type ClientInfo struct {
    Name    string `json:"name"`
    Version string `json:"version"`
}

// StatsServer 是一个无状态 MCP Server
type StatsServer struct {
    tools map[string]ToolHandler
}

type ToolHandler func(ctx context.Context, args json.RawMessage) (interface{}, error)

func NewStatsServer() *StatsServer {
    s := &StatsServer{tools: make(map[string]ToolHandler)}
    s.registerTools()
    return s
}

func (s *StatsServer) registerTools() {
    s.tools["get_system_stats"] = s.handleSystemStats
    s.tools["query_metrics"] = s.handleQueryMetrics
    s.tools["health_check"] = s.handleHealthCheck
}

// handleSystemStats 获取系统统计信息
func (s *StatsServer) handleSystemStats(ctx context.Context, args json.RawMessage) (interface{}, error) {
    hostname, _ := os.Hostname()

    return map[string]interface{}{
        "hostname":    hostname,
        "go_version":  "go1.26.0",
        "goroutines":  42,
        "memory_mb":   128.5,
        "cpu_percent": 23.7,
        "uptime_hours": 72.5,
    }, nil
}

// handleQueryMetrics 查询时序指标
func (s *StatsServer) handleQueryMetrics(ctx context.Context, args json.RawMessage) (interface{}, error) {
    var params struct {
        MetricName string `json:"metric_name"`
        TimeRange  string `json:"time_range"`
    }
    if err := json.Unmarshal(args, &params); err != nil {
        return nil, fmt.Errorf("invalid arguments: %w", err)
    }

    return map[string]interface{}{
        "metric":    params.MetricName,
        "range":     params.TimeRange,
        "datapoints": []map[string]interface{}{
            {"timestamp": "2026-06-09T10:00:00Z", "value": 42.5},
            {"timestamp": "2026-06-09T10:01:00Z", "value": 43.1},
            {"timestamp": "2026-06-09T10:02:00Z", "value": 41.8},
        },
    }, nil
}

// handleHealthCheck 健康检查
func (s *StatsServer) handleHealthCheck(ctx context.Context, args json.RawMessage) (interface{}, error) {
    return map[string]interface{}{
        "status":    "healthy",
        "timestamp": time.Now().UTC().Format(time.RFC3339),
        "checks": map[string]string{
            "database": "ok",
            "cache":    "ok",
            "queue":    "ok",
        },
    }, nil
}

// ServeHTTP 处理 MCP JSON-RPC 请求(无状态)
func (s *StatsServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    // 从 W3C Trace Context 恢复追踪上下文
    ctx := otel.GetTextMapPropagator().Extract(r.Context(),
        propagation.HeaderCarrier(r.Header))

    // 验证必要头
    if r.Header.Get("Mcp-Method") == "" || r.Header.Get("Mcp-Name") == "" {
        http.Error(w, "missing Mcp-Method or Mcp-Name header", http.StatusBadRequest)
        return
    }

    var req MCPRequest
    if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
        writeJSON(w, http.StatusBadRequest, MCPError{
            Code:    -32700,
            Message: "Parse error",
        })
        return
    }

    // 记录客户端信息(无状态日志)
    if req.Meta != nil {
        log.Printf("[INFO] request from %s v%s, method=%s",
            req.Meta.ClientInfo.Name,
            req.Meta.ClientInfo.Version,
            req.Method)
    }

    var result interface{}
    var err error

    switch req.Method {
    case "tools/list":
        result = s.listTools()
    case "tools/call":
        var params struct {
            Name      string          `json:"name"`
            Arguments json.RawMessage `json:"arguments"`
        }
        if e := json.Unmarshal(req.Params, &params); e != nil {
            writeJSON(w, http.StatusBadRequest, MCPError{Code: -32602, Message: "Invalid params"})
            return
        }
        handler, ok := s.tools[params.Name]
        if !ok {
            writeJSON(w, http.StatusNotFound, MCPError{Code: -32601, Message: "Tool not found"})
            return
        }
        result, err = handler(ctx, params.Arguments)
    case "ping":
        result = map[string]string{"status": "ok"}
    default:
        writeJSON(w, http.StatusMethodNotAllowed, MCPError{Code: -32601, Message: "Method not found"})
        return
    }

    if err != nil {
        writeJSON(w, http.StatusInternalServerError, MCPError{Code: -32000, Message: err.Error()})
        return
    }

    writeJSON(w, http.StatusOK, MCPResponse{
        JSONRPC: "2.0",
        ID:      req.ID,
        Result:  result,
    })
}

func (s *StatsServer) listTools() []MCPTool {
    return []MCPTool{
        {
            Name:        "get_system_stats",
            Description: "获取当前系统运行统计信息",
            InputSchema: map[string]interface{}{
                "type":       "object",
                "properties": map[string]interface{}{},
            },
        },
        {
            Name:        "query_metrics",
            Description: "查询指定指标的时序数据",
            InputSchema: map[string]interface{}{
                "type": "object",
                "properties": map[string]interface{}{
                    "metric_name": map[string]string{"type": "string"},
                    "time_range":  map[string]string{"type": "string"},
                },
                "required": []string{"metric_name"},
            },
        },
    }
}

type MCPTool struct {
    Name        string      `json:"name"`
    Description string      `json:"description"`
    InputSchema interface{} `json:"inputSchema"`
}

type MCPResponse struct {
    JSONRPC string      `json:"jsonrpc"`
    ID      int64       `json:"id"`
    Result  interface{} `json:"result,omitempty"`
    Error   *MCPError   `json:"error,omitempty"`
}

type MCPError struct {
    Code    int    `json:"code"`
    Message string `json:"message"`
}

func writeJSON(w http.ResponseWriter, status int, data interface{}) {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(status)
    json.NewEncoder(w).Encode(data)
}

func main() {
    server := NewStatsServer()

    mux := http.NewServeMux()
    mux.HandleFunc("/mcp", server.ServeHTTP)
    mux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
        w.WriteHeader(http.StatusOK)
        w.Write([]byte("ok"))
    })

    log.Println("MCP Server starting on :8080 (stateless mode)")
    log.Fatal(http.ListenAndServe(":8080", mux))
}

2.1 关键设计要点

  1. 零会话状态StatsServer 结构体不维护任何 session 相关的 map,每个请求独立处理
  2. W3C Trace Context:通过 OpenTelemetry 的 propagation.HeaderCarrier 恢复追踪上下文
  3. Mcp-Method 头路由:请求必须携带 Mcp-Method,负载均衡器可据此做基于内容的路由
  4. 每个请求自包含:客户端信息通过 _meta 字段随每个请求一起传递

三、MCP Apps:Server 端渲染交互界面

3.1 概念与原理

MCP Apps(SEP-1865)允许 MCP Server 提供交互式 HTML 界面,由 AI Host(如 ChatGPT、Claude Desktop)在沙盒 iframe 中渲染。这意味着:

  • Server 端可以定义复杂 UI 组件(图表、表单、数据表格)
  • 所有用户操作经过与直接工具调用相同的审计和授权流程
  • UI 运行在沙盒环境,保证安全隔离
json
// MCP Server 的 tools/list 响应
{
  "tools": [
    {
      "name": "database_explorer",
      "description": "交互式数据库浏览器",
      "inputSchema": {
        "type": "object",
        "properties": {
          "query": { "type": "string", "description": "初始查询语句" }
        }
      },
      "ui": {
        "templateUrl": "https://mcp.example.com/templates/explorer.html",
        "sandbox": true,
        "permissions": ["clipboard-read", "clipboard-write"]
      }
    }
  ]
}

3.2 MCP App UI 模板示例

html
<!-- templates/explorer.html -->
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Database Explorer</title>
    <style>
        body { font-family: system-ui; padding: 16px; }
        .result-table { width: 100%; border-collapse: collapse; }
        .result-table th, .result-table td {
            border: 1px solid #ddd; padding: 8px; text-align: left;
        }
        .result-table th { background: #f5f5f5; }
    </style>
</head>
<body>
    <div>
        <textarea id="sql-input" rows="4" style="width:100%; font-family: monospace;">
SELECT * FROM users LIMIT 5
        </textarea>
        <button onclick="executeQuery()">执行查询</button>
    </div>
    <div id="result" style="margin-top: 16px;"></div>

    <script>
        // 通过 MCP 的 JSON-RPC 接口与 Host 通信
        async function executeQuery() {
            const sql = document.getElementById('sql-input').value;

            // 向 Host 发送数据(沙盒内使用 postMessage 与父窗口通信)
            window.parent.postMessage({
                type: 'mcp:tool:call',
                tool: 'database_query',
                arguments: { sql: sql }
            }, '*');
        }

        // 接收 Host 的响应
        window.addEventListener('message', (event) => {
            if (event.data.type === 'mcp:tool:result') {
                renderResult(event.data.result);
            }
        });

        function renderResult(data) {
            if (!data || data.length === 0) {
                document.getElementById('result').innerHTML = '<p>无结果</p>';
                return;
            }
            const columns = Object.keys(data[0]);
            let html = '<table class="result-table"><tr>';
            columns.forEach(col => html += `<th>${col}</th>`);
            html += '</tr>';
            data.forEach(row => {
                html += '<tr>';
                columns.forEach(col => html += `<td>${row[col]}</td>`);
                html += '</tr>';
            });
            html += '</table>';
            document.getElementById('result').innerHTML = html;
        }
    </script>
</body>
</html>

四、Tasks 扩展:异步长任务处理

当一个 MCP Tool 需要长时间运行(例如模型训练、大数据 ETL),Tasks 扩展提供了优雅的异步处理方案:

go
// 长任务工具 - 立即返回任务句柄
func (s *StatsServer) handleLongRunningTask(ctx context.Context, args json.RawMessage) (interface{}, error) {
    // 创建异步任务
    taskID := fmt.Sprintf("task_%d", time.Now().UnixNano())
    
    // 启动后台处理
    go func() {
        log.Printf("[TASK] starting task %s", taskID)
        // 模拟长任务
        time.Sleep(30 * time.Second)
        s.taskResults.Store(taskID, TaskResult{
            Status: "completed",
            Data:   map[string]string{"output": "处理完成,共处理 10000 条记录"},
        })
        log.Printf("[TASK] completed task %s", taskID)
    }()

    // 立即返回任务句柄
    return map[string]interface{}{
        "type":   "task",
        "taskId": taskID,
        "status": "running",
        "estimatedSeconds": 30,
    }, nil
}

客户端侧通过轮询获取结果:

python
import asyncio
import aiohttp

async def wait_for_task(task_id: str, poll_interval: float = 2.0):
    """轮询等待 MCP Task 完成"""
    async with aiohttp.ClientSession() as session:
        while True:
            async with session.post(
                "https://mcp.example.com/mcp",
                json={
                    "jsonrpc": "2.0",
                    "method": "tasks/get",
                    "params": {"taskId": task_id},
                    "id": 1,
                    "_meta": {
                        "clientInfo": {"name": "Python-Agent", "version": "1.0"},
                        "protocolVersion": "2026-05-01"
                    }
                }
            ) as resp:
                result = await resp.json()
                status = result["result"]["status"]
                
                if status == "completed":
                    return result["result"]["data"]
                elif status == "failed":
                    raise Exception(result["result"]["error"])
                
            await asyncio.sleep(poll_interval)

五、OpenAI Secure MCP Tunnel:企业内网的安全桥梁

5.1 解决的核心矛盾

企业级 AI Agent 部署面临一个经典的安全困境:

业务团队想让 AI Agent 访问 Jira、Confluence、数据库等内部系统
安全团队绝不允许将内部服务暴露到公网

Secure MCP Tunnel 通过纯出站连接架构完美解决了这一问题:

┌──────────────────────────────────────────────────────┐
│                     企业内网                           │
│                                                      │
│   ┌──────────────┐          ┌────────────────────┐   │
│   │ tunnel-client│──出站──→│  Jira MCP Server    │   │
│   │              │  local  │  (10.0.1.100:8080)  │   │
│   └──────┬───────┘          └────────────────────┘   │
│          │ 出站 HTTPS(无需任何入站端口)               │
│          │ api.openai.com:443                        │
└──────────┼──────────────────────────────────────────┘


┌──────────────────────────────────────────────────────┐
│              OpenAI 控制平面                           │
│                                                      │
│   ┌──────────────┐     ┌────────────────────────┐    │
│   │  任务队列     │←───│  ChatGPT / Codex / API  │    │
│   │  + 路由层     │    │                        │    │
│   └──────────────┘     └────────────────────────┘    │
└──────────────────────────────────────────────────────┘

5.2 请求流转四步走

Step 1: tunnel-client 向 OpenAI 发起出站 HTTPS 长轮询
        GET /v1/tunnel/poll?tunnel_id=tnl_xxx

Step 2: ChatGPT 触发工具调用
        → OpenAI 将请求放入任务队列

Step 3: tunnel-client 获取到请求
        → 转发给本地 MCP Server (http://localhost:8080/mcp)

Step 4: 本地 MCP Server 处理 → 响应沿同一隧道返回
        → tunnel-client → OpenAI → ChatGPT

5.3 实战部署:Kubernetes Sidecar 模式

yaml
apiVersion: v1
kind: Namespace
metadata:
  name: mcp-gateway
---
# Secret 存储隧道凭证
apiVersion: v1
kind: Secret
metadata:
  name: tunnel-secret
  namespace: mcp-gateway
type: Opaque
stringData:
  tunnel-id: "tnl_dev_your_unique_tunnel_id"
  api-key: "sk-proj-your-runtime-api-key"
---
# ConfigMap 存储隧道配置
apiVersion: v1
kind: ConfigMap
metadata:
  name: tunnel-config
  namespace: mcp-gateway
data:
  proxy.yaml: |
    # 如果企业使用出站代理
    http_proxy: ""
    https_proxy: ""
    no_proxy: "localhost,127.0.0.1,.internal"
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: mcp-gateway
  namespace: mcp-gateway
  labels:
    app: mcp-gateway
spec:
  replicas: 2
  selector:
    matchLabels:
      app: mcp-gateway
  template:
    metadata:
      labels:
        app: mcp-gateway
    spec:
      containers:
        # 主容器:无状态 MCP Server
        - name: mcp-server
          image: registry.internal.com/mcp-server:v2.0.0
          ports:
            - containerPort: 8080
              name: http
          env:
            - name: DATABASE_URL
              valueFrom:
                secretKeyRef:
                  name: db-secret
                  key: url
          livenessProbe:
            httpGet:
              path: /healthz
              port: 8080
            initialDelaySeconds: 5
            periodSeconds: 10
          readinessProbe:
            httpGet:
              path: /readyz
              port: 8080
            initialDelaySeconds: 3
            periodSeconds: 5
          resources:
            requests:
              memory: "256Mi"
              cpu: "250m"
            limits:
              memory: "512Mi"
              cpu: "500m"

        # Sidecar:Tunnel Client
        - name: tunnel-client
          image: ghcr.io/openai/tunnel-client:v2.1.0
          args:
            - "run"
            - "--mcp-server-url"
            - "http://localhost:8080/mcp"
          env:
            - name: CONTROL_PLANE_TUNNEL_ID
              valueFrom:
                secretKeyRef:
                  name: tunnel-secret
                  key: tunnel-id
            - name: CONTROL_PLANE_API_KEY
              valueFrom:
                secretKeyRef:
                  name: tunnel-secret
                  key: api-key
            - name: CONTROL_PLANE_MTLS
              value: "true"
          lifecycle:
            preStop:
              exec:
                command: ["/bin/sh", "-c", "sleep 5"]
          resources:
            requests:
              memory: "128Mi"
              cpu: "100m"
            limits:
              memory: "256Mi"
              cpu: "200m"
---
apiVersion: v1
kind: Service
metadata:
  name: mcp-server-svc
  namespace: mcp-gateway
spec:
  selector:
    app: mcp-gateway
  ports:
    - port: 8080
      targetPort: 8080
      name: http

5.4 安全特性一览

特性说明
零入站暴露MCP Server 地址完全私有,仅在 Pod 内部使用
纯出站连接防火墙只需允许到 api.openai.com:443 的出站流量
mTLS 支持连接至 mtls.api.openai.com:443,双向证书验证
权限隔离隧道遵循 OpenAI 组织/工作区权限模型
日志脱敏HTTP 日志默认禁用,导出文件经过脱敏处理
管理 UI 隔离管理界面默认仅监听 loopback 地址

5.5 从 ChatGPT 连接隧道

1. 打开 ChatGPT → 设置 → 连接器
2. 点击"创建自定义连接器"
3. 在"连接方式"下选择"隧道"
4. 输入你的 tunnel_id,或从下拉列表选择
5. 点击连接,完成!

提示:连接器发现和工具调用都需要 tunnel-client run 保持运行。如果 ChatGPT 中看不到工具,请先确认 tunnel-client 状态。

六、生产环境运维检查清单

6.1 健康检查

bash
# Tunnel Client 健康检查
curl http://localhost:8080/healthz    # 健康状态
curl http://localhost:8080/readyz     # 就绪状态
curl http://localhost:8080/metrics    # Prometheus 指标

# 诊断模式
tunnel-client doctor --profile production --explain

6.2 Prometheus 告警规则

yaml
groups:
  - name: mcp-tunnel
    rules:
      - alert: TunnelDisconnected
        expr: tunnel_connected == 0
        for: 2m
        labels:
          severity: critical
        annotations:
          summary: "MCP Tunnel {{ $labels.tunnel_id }} 断开连接"

      - alert: TunnelHighLatency
        expr: histogram_quantile(0.95, tunnel_request_duration_seconds) > 5
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "MCP Tunnel P95 延迟 > 5s"

      - alert: TunnelRequestFailures
        expr: rate(tunnel_requests_failed_total[5m]) > 0.1
        for: 5m
        labels:
          severity: critical
        annotations:
          summary: "MCP Tunnel 请求失败率 > 10%"

七、总结与展望

MCP 2026 的无状态协议变革是一次架构层面的飞跃

维度旧版(2025)新版(2026.05)
协议模型有状态长连接无状态短请求
负载均衡需要 sticky session标准 round-robin
横向扩展需共享状态存储天然支持
交互界面纯文本MCP Apps (HTML)
长任务阻塞等待Tasks 异步句柄
企业部署需开放入站端口Secure Tunnel 纯出站

对于技术团队而言,现在就是用 Go 或 Python 构建生产级 MCP 基础设施的最佳时机。无状态协议消除了水平扩展的障碍,Secure Tunnel 解决了企业安全合规的最后关卡,MCP Apps 和 Tasks 扩展则为更丰富的应用场景打开了大门。


参考资料

上次更新于: