资讯详情

资讯详情

MCP 2026-07-28 规范拆解:无状态协议核心怎么让 MCP 服务器跑在普通负载均衡后面

2025年12月9日Anthropic把Model Context Protocol捐赠给Linux Foundation旗下新成立的Agentic AI Foundation与Block的goose和OpenAI的AGENTS.md并列三大创始项目。八个月后2026年7月28日MCP发布了自诞生以来最大的一次规范修订——版本号直接定为2026-07-28。维护者称其为协议启动以来最大的修订这不是营销话术整个会话模型被删除初始化握手被移除三项核心特性被废弃授权机制全面重写扩展框架正式成为一等公民。这次修订的核心可以用一句话概括MCP从一个有状态协议变成了完全无状态协议。这意味着每个请求自带全部上下文任何服务器实例都能处理任何请求不再需要粘性路由或共享会话存储。对于在负载均衡后面跑多个MCP服务器实例的团队来说这一变化直接消灭了一整层只为绕开协议限制而存在的基础设施。本文拆解2026-07-28规范的六个关键技术变化从旧模型为什么撑不住讲起到无状态核心的实现机制、Handle替代session的范式转换、OAuth 2.1安全加固、扩展框架与废弃清单最后给出一份可以直接跑起来的迁移代码。旧session模型为什么撑不住要理解为什么去掉session得先看清旧模型到底在哪里断裂。MCP从2024年11月5日的初始规范到2025-11-25版本一直采用有状态连接模型。客户端连接服务器时先执行两步握手第一步发initialize请求交换协议版本、客户端信息和能力声明第二步发initialized通知确认握手完成。服务器在握手期间返回一个Mcp-Session-Id头此后客户端的每一个请求都必须回到同一个服务器实例——因为只有那个实例认识这个session票据。用一个具体场景说明这个模型为什么脆弱。假设你搭了一个MCP服务器叫Slotly让AI Agent替用户预订会议室。一个进程跑了几个月没问题。然后流量上来你把Slotly扩到三个实例放在负载均衡后面。实例A发出了session票据但下一个请求被负载均衡路由到实例B实例B从未见过这个票据直接报错。对用户来说预订流程莫名其妙失败了对团队来说排查这个bug要花一整个下午才能追溯到又是session的问题。现有团队解决这个问题的办法有两种。第一种是粘性路由负载均衡器记住哪个客户端绑在哪个实例上后续请求一律送到同一个实例。第二种是共享会话存储所有实例都能查到所有session票据通常用Redis。两种方案都能跑但它们都是纯粹为了绕开协议限制而搭建的额外基础设施。更麻烦的是在API网关层做深度包检查来路由请求不仅要解析JSON-RPC请求体找到方法名还要维护session到实例的映射表——这些全是不应在协议层面存在的运维负担。无状态核心三件套2026-07-28规范用三个机制组合在一起彻底消灭了session模型。第一件_meta字段。每个请求现在在JSON-RPC的_meta字段里自带协议版本、客户端信息和客户端能力声明。以前这些信息只在initialize握手中交换一次服务器存在session上下文里。现在每个请求都携带任何实例拿到请求就能立刻知道对方说什么版本的协议、客户端是谁、支持哪些能力。不需要握手不需要session不需要记住之前聊过什么。第二件Mcp-Method和Mcp-Name两个新HTTP头。客户端每次调用都带上这两个头Mcp-Method标明JSON-RPC方法名如tools/callMcp-Name标明工具或资源名称如search。这两个头让API网关可以不解析请求体就能路由流量这比旧的深度包检查方案高效得多。网关读HTTP头做路由决策是标准HTTP基础设施早就擅长的事情。第三件server/discover方法。旧的initialize握手被移除后客户端需要一个新方式获取服务器能力声明。新规范提供server/discover方法客户端按需调用服务器返回自己的能力清单。这个调用本身也是无状态的——每次调用都返回当前能力不依赖之前的连接历史。特性旧规范2025-11-25新规范2026-07-28连接初始化两步initialize/initialized握手无握手直接发请求会话标识Mcp-Session-Id头已删除协议版本传递握手时交换一次每请求_meta字段携带网关路由深度包检查JSON-RPC体读Mcp-Method/Mcp-Name头多实例部署粘性路由或共享session存储普通round-robin负载均衡服务器能力发现initialize响应中返回按需调用server/discoverHandle替代session state去掉session不意味着应用本身不能有状态。MCP服务器仍然需要记住跨调用的中间状态——比如Agent正在分步骤预订会议室选了时间还没选餐饮。新规范的要求是状态从协议层的隐藏管道里拿出来变成显式的Handle。具体做法是工具调用返回一个显式标识符。比如Slotly的预订工具返回一个booking_idAgent在后续调用中把这个booking_id作为普通参数传回来。服务器自己决定怎么存这些状态——Postgres、Redis、内存里都行——booking_id只是一个指针。这种模式比隐藏的session状态更强大因为Agent能看到、能推理自己持有哪些Handle能在不同工具之间组合传递能在步骤之间交接。但这也带来一个之前由协议代为处理的责任那个booking_id必须绑定到创建它的用户并且必须有过期机制。一个永不过期、任何人拿到字符串就能用的Handle本质上是一个穿了马甲的永久访问令牌。新规范在授权部分专门处理了这个问题。从实现角度仔细看这个范式转换。旧session模型下客户端连上服务器后服务器在内存里维护一个session对象存着这个连接的所有中间状态——当前预订的房间、选了什么时间、有没有加餐饮、流程走到了哪一步。客户端不需要知道这些细节它只管发请求服务器在session上下文里找到之前的数据接着往下走。这种模式看似方便但有个根本缺陷Agent无法看见自己持有什么状态也无法推理这些状态之间的关系。如果Agent同时操作两个预订——一个在选时间、一个在等确认——旧session模型下这两个流程的状态分别绑定在两个连接上Agent无法把它们交叉组合因为它们在不同的session管道里。Handle模式完全改变了这个局面。每次create_booking返回的booking_id是一个Agent可以看见、可以持有、可以传递的显式标识符。Agent拿到两个booking_id后可以自由地把一个预订的时间信息传递给另一个工具可以在不同预订之间交叉引用。状态不再是隐藏在协议管道里的隐形线索而是Agent可以像操作普通数据一样操作的显式输入输出。这种可见性也让审计变得简单——你只需要记录哪个Handle被传给了哪个工具就能完整重建Agent的操作路径。session模型下要重建这种审计轨迹你得去翻服务器的session日志而且不同实例的session日志可能散落在不同机器上。OAuth 2.1安全加固旧版MCP规范的授权模型可以说是自带令牌——规范定义了基本框架但把所有安全细节留给实现者自行处理导致每个MCP服务器的授权实现都不一样。2026-07-28规范用六个SEPSpecification Enhancement Proposal把授权从理论上可行拉到照着RFC做就行。第一MCP服务器正式成为OAuth 2.1资源服务器。服务器必须实现OAuth 2.0 Protected Resource MetadataRFC 9728暴露.well-known/oauth-protected-resource端点让客户端自动发现正确的授权服务器。客户端必须实现Resource IndicatorsRFC 8707在令牌请求中明确指定令牌是给哪个MCP服务器用的。这直接解决了confused deputy问题为服务器A签发的令牌无法被重放到服务器B因为令牌里绑定了目标服务器标识。第二Client ID Metadata DocumentsCIMD替代Dynamic Client RegistrationDCR。DCRRFC 7591被废弃只保留向后兼容。CIMD让客户端注册变成一个URL就能搞定的操作——客户端把自己的元数据文档发布到一个HTTPS URL上授权服务器直接读取这个URL完成注册。这消除了DCR的无边界客户端注册问题也让客户端身份可以被独立验证。第三发行方验证变为强制要求。客户端必须验证授权响应来自哪个授权服务器基于RFC 9207必须把注册凭据绑定到发行授权服务器的issuer标识。如果一个资源在授权服务器之间迁移客户端必须重新注册。这防止了一类混合攻击当一个客户端同时与多个MCP服务器通信时这恰好是MCP鼓励的部署模式令牌不能在服务器之间混用。第四刷新令牌处理被正式文档化SEP-2207step-up授权的scope累积行为也被明确SEP-2350。之前刷新令牌行为未定义每个实现各做各的。第五客户端注册时必须声明OpenID Connect的application_typeSEP-837解决了授权服务器把桌面或CLI客户端默认当成web应用然后拒绝localhost回调的常见问题。这六项SEP叠加起来的效果是把MCP授权从理论上可行你自己接变成照着这些RFC做就行。之前一个团队想给MCP服务器加企业身份认证要从头设计令牌签发、验证、刷新逻辑每个实现都不一样互操作性很差。新规范直接对齐OAuth 2.1和OpenID Connect的标准实践RFC 9728定义资源服务器元数据发现RFC 8707定义令牌绑定到特定服务器RFC 9207定义发行方验证CIMD定义客户端注册。对于在Okta、Azure AD、Google Workspace后面跑MCP服务器的企业团队来说从未认证的MCP服务器到规范级安全的MCP服务器的路径现在在规范层面就画清楚了不再是留作练习。EMAEnterprise-Managed Authorization作为配套特性在7月28日发布前就已经先行落地它让企业的身份提供商集中控制谁能访问哪些MCP服务器不需要每个人每个服务器点一次同意——这对两百人规模以上的组织来说是刚需。扩展框架与废弃清单2026-07-28规范引入了正式的扩展框架。扩展获得反向DNS标识符、独立仓库、委托维护者和独立于主规范的版本号。客户端和服务器通过capabilities中的extensions映射协商扩展支持。两个扩展随发布候选一起推出。MCP Apps让服务器直接在客户端渲染交互式HTML界面。渲染出来的UI运行在沙箱iframe里与宿主通过标准JSON-RPC通信。这意味着UI发起的每次操作都走与普通工具调用相同的审计和同意路径。服务器可以声明工具附带一个日历选择器UI客户端预取、缓存、审查后渲染用户在日历里的每次点击都经过同意层。MCP Apps可能是把MCP从开发者集成层推向用户生态系统的关键原语。Tasks为长时间运行的异步工作提供一等支持。Tasks在2025-11-25规范中是实验性核心特性但生产使用暴露了足够多设计问题于是被移到扩展并重新设计。服务器用tools/call返回一个task handle客户端用tasks/get轮询结果、tasks/update发送输入、tasks/cancel停止任务。旧的tasks/list端点被移除因为没有session它无法安全地做范围限定。任何基于旧实验性Tasks API构建的代码都需要迁移到新生命周期。三项核心特性在此版本中被废弃Roots被Resource URI普通URL替代Sampling从核心规范中移除可能作为扩展回归Logging也从核心规范中移除。规范首次引入正式废弃策略——被废弃的特性会被文档化在过渡期内继续工作最终按明确时间线被移除。这给了实现者至少十二个月的迁移窗口。另外两个值得注意的变化工具输入schema现在支持完整的JSON Schema 2020-12替代之前受限的子集——工具可以声明接受room ID或room name这样的oneOf结构了。错误码Resource not found从自定义的-32002改为标准JSON-RPC的-32602。正式废弃策略本身也值得展开说。之前的MCP规范没有结构化的废弃流程——特性有时被默默移除有时被改了行为但没有迁移窗口实现者经常在升级SDK后才发现某个功能已经不工作了。2026-07-28引入的废弃策略分三个阶段首先被废弃的特性在规范文档中被明确标注为deprecated附带替代方案和迁移指南其次在过渡期内至少十二个月继续正常工作给实现者足够的时间迁移最后在明确的时间线到期后被正式移除。这三步让协议可以有序演化而不制造混乱。对于Roots来说替代方案是Resource URI——就是普通URL本来就用Roots做文件系统路径映射的团队只需要把路径改成URL格式。Sampling从核心规范移除后可能作为扩展回归因为它在实际使用中有场景让服务器请求客户端做模型推理但放进核心会增加所有实现者的负担。Logging同样移出核心因为它和JSON-RPC的日志能力有重叠放在扩展里更合适。完整迁移代码示例下面是一个完整的无状态MCP服务器实现用Python和FastAPI搭建展示了2026-07-28规范的全部关键模式从_meta读取协议版本、用Mcp-Method/Mcp-Name头做路由、返回显式Handle、暴露OAuth 2.1 Protected Resource Metadata端点。这个服务器可以直接跑起来用curl测试。#!/usr/bin/env python3 无状态 MCP 服务器示例 — 符合 2026-07-28 规范 依赖: pip install fastapi uvicorn pydantic 运行: python mcp_stateless_server.py 测试: curl -X POST http://localhost:8000/mcp \ -H MCP-Protocol-Version: 2026-07-28 \ -H Mcp-Method: tools/call \ -H Mcp-Name: create_booking \ -H Content-Type: application/json \ -d {jsonrpc:2.0,id:1,method:tools/call,params:{name:create_booking,arguments:{room:A301,time:2026-10-09T14:00}}} import json import time import uuid from datetime import datetime, timezone, timedelta from typing import Any from fastapi import FastAPI, Request, Response from fastapi.responses import JSONResponse from pydantic import BaseModel app FastAPI(titleStateless MCP Server) # ---- Handle 存储生产环境换成 Redis 或 Postgres ---- # 每个 handle 绑定用户、30分钟过期防止变成永久令牌 _HANDLES: dict[str, dict[str, Any]] {} HANDLE_TTL 1800 # 30 分钟 def create_handle(user_id: str, data: dict) - str: 创建显式 Handle绑定用户设置过期时间。 handle_id fbooking_{uuid.uuid4().hex[:12]} _HANDLES[handle_id] { user_id: user_id, data: data, expires_at: datetime.now(timezone.utc) timedelta(secondsHANDLE_TTL), } return handle_id def resolve_handle(handle_id: str, user_id: str) - dict | None: 解析 Handle验证归属和过期。 record _HANDLES.get(handle_id) if record is None: return None if datetime.now(timezone.utc) record[expires_at]: _HANDLES.pop(handle_id, None) return None if record[user_id] ! user_id: return None # Handle 不属于此用户 return record[data] # ---- OAuth 2.1 Protected Resource Metadata (RFC 9728) ---- app.get(/.well-known/oauth-protected-resource) async def protected_resource_metadata() - dict: RFC 9728: 让客户端自动发现授权服务器。 return { resource: https://mcp-slotly.example.com, authorization_servers: [https://auth.example.com], bearer_methods_supported: [header], resource_signing_alg_values_supported: [RS256], } # ---- server/discover 方法 ---- app.get(/.well-known/mcp-server) async def server_metadata() - dict: server/discover 的 HTTP GET 形式返回服务器能力。 return { protocol_version: 2026-07-28, server_info: {name: slotly-mcp, version: 1.0.0}, capabilities: { tools: {listChanged: False}, extensions: { io.github.mcp.apps: {version: 1.0.0}, }, }, extensions: [ {id: io.github.mcp.apps, version: 1.0.0}, ], } # ---- 主 MCP 端点无状态处理 ---- app.post(/mcp) async def handle_mcp(request: Request) - Response: 无状态 MCP 端点。 每个请求从 _meta 和 HTTP 头读取全部上下文 不依赖 session任何实例都能处理任何请求。 # 从 HTTP 头读取路由信息网关也可用这些头做路由 protocol_version request.headers.get(MCP-Protocol-Version, unknown) method request.headers.get(Mcp-Method, ) name request.headers.get(Mcp-Name, ) # 从 _meta 读取客户端信息和能力替代旧的 initialize 握手 body await request.json() meta body.get(_meta, {}) client_info meta.get(client, {}) client_capabilities meta.get(capabilities, {}) # 从 Authorization 头提取用户身份OAuth 2.1 Bearer Token auth_header request.headers.get(Authorization, ) user_id extract_user_from_token(auth_header) if not user_id: return JSONResponse( status_code401, content{ jsonrpc: 2.0, id: body.get(id), error: {code: -32602, message: Unauthorized}, }, headers{WWW-Authenticate: Bearer realmmcp}, ) # 根据 Mcp-Method 路由网关层也可用同样逻辑 if method server/discover: return _build_response(body, { protocolVersion: protocol_version, serverInfo: {name: slotly-mcp, version: 1.0.0}, capabilities: {tools: {}, extensions: {}}, }) if method tools/list: return _build_response(body, {tools: _get_tool_list()}) if method tools/call: tool_name name or body.get(params, {}).get(name, ) args body.get(params, {}).get(arguments, {}) result _dispatch_tool(tool_name, args, user_id) return _build_response(body, result) return _build_response(body, { error: {code: -32601, message: fMethod not found: {method}} }) def extract_user_from_token(auth_header: str) - str | None: 从 Bearer token 提取用户标识生产环境用 JWT 验证。 if not auth_header.startswith(Bearer ): return None token auth_header[7:] # 生产环境验证 JWT、检查 Resource Indicator (RFC 8707) # 示例直接用 token 前缀当 user_id return fuser_{token[:8]} if len(token) 8 else None def _get_tool_list() - list[dict]: return [ { name: create_booking, description: 创建会议室预订返回 booking handle, inputSchema: { type: object, properties: { room: {type: string, description: 会议室编号}, time: {type: string, description: ISO 8601 时间}, }, required: [room, time], }, }, { name: add_catering, description: 为已有预订添加餐饮需传入 booking handle, inputSchema: { type: object, properties: { booking_id: {type: string, description: create_booking 返回的 handle}, menu: {type: string, description: 餐单选项}, }, required: [booking_id, menu], }, }, { name: confirm_booking, description: 确认预订完成流程, inputSchema: { type: object, properties: { booking_id: {type: string, description: booking handle}, }, required: [booking_id], }, }, ] def _dispatch_tool(tool_name: str, args: dict, user_id: str) - dict: if tool_name create_booking: handle create_handle(user_id, { room: args.get(room), time: args.get(time), catering: None, status: draft, }) return {content: [{type: text, text: json.dumps({ booking_id: handle, room: args.get(room), time: args.get(time), status: draft, expires_in: HANDLE_TTL, }, ensure_asciiFalse)}]} if tool_name add_catering: booking_id args.get(booking_id) data resolve_handle(booking_id, user_id) if data is None: return {content: [{type: text, text: Handle 无效或已过期}]} data[catering] args.get(menu) return {content: [{type: text, text: json.dumps({ booking_id: booking_id, catering: data[catering], }, ensure_asciiFalse)}]} if tool_name confirm_booking: booking_id args.get(booking_id) data resolve_handle(booking_id, user_id) if data is None: return {content: [{type: text, text: Handle 无效或已过期}]} data[status] confirmed return {content: [{type: text, text: json.dumps({ booking_id: booking_id, status: confirmed, room: data[room], time: data[time], catering: data.get(catering), }, ensure_asciiFalse)}]} return {content: [{type: text, text: f未知工具: {tool_name}}]} def _build_response(request_body: dict, result: dict) - Response: return Response( contentjson.dumps({ jsonrpc: 2.0, id: request_body.get(id), result: result, }, ensure_asciiFalse), media_typeapplication/json, headers{ MCP-Protocol-Version: 2026-07-28, Cache-Control: public, max-age300, }, ) if __name__ __main__: import uvicorn print(无状态 MCP 服务器启动在 http://localhost:8000) print(OAuth 元数据: http://localhost:8000/.well-known/oauth-protected-resource) print(服务器发现: http://localhost:8000/.well-known/mcp-server) print(MCP 端点: POST http://localhost:8000/mcp) uvicorn.run(app, host0.0.0.0, port8000)这段代码展示的无状态模式有几个关键点值得逐条说明。handle_mcp函数从HTTP头和_meta字段读取全部上下文没有任何地方依赖session或全局连接状态。create_handle函数创建的每个Handle都绑定了user_id并设了30分钟过期——这就是新规范要求的Handle不能变成永久令牌。protected_resource_metadata端点暴露RFC 9728要求的元数据让客户端自动发现授权服务器。tools/list响应带了Cache-Control头和max-age客户端可以在ttlMs允许的窗口内缓存列表减少重复请求。用curl测试这个服务器的完整流程分三步。第一步创建预订POST到/mcp带Mcp-Method: tools/call和Mcp-Name: create_booking头Authorization头带Bearer tokenbody里的arguments包含room和time。服务器返回一个booking_id handle。第二步添加餐饮同样的端点Mcp-Name改成add_cateringarguments里把第一步拿到的booking_id传回来附上menu选项。第三步确认预订Mcp-Name改成confirm_booking只传booking_id。整个流程中三个请求可以打到三个不同的服务器实例——因为没有session任何实例都能处理任何请求。迁移检查清单如果你的团队正在运行生产环境的MCP服务器在迁移到2026-07-28规范时下面这些是必须完成的工作项。第一拆除session初始化逻辑。initialize/initialized握手已经不存在不需要等待它完成。从_meta字段读取协议版本、客户端信息和能力替代从initialize响应中读取。第二客户端侧添加Mcp-Method和Mcp-Name头到每个请求确保负载均衡器或API网关能用这两个头做路由。第三把任何依赖Mcp-Session-Id的状态迁移到显式Handle。找到服务器代码里所有存储或读取session绑定状态的地方替换为工具返回的显式标识符确保Handle绑定了正确的用户并设置了过期时间。第四把Tasks代码从旧的阻塞模式迁移到新的轮询扩展模式。第五审计OAuth实现检查是否暴露了.well-known/oauth-protected-resource端点客户端是否实现了Resource Indicatorsiss字段验证是否到位。第六在代码库中搜索字面量-32002替换为-32602。第七在多实例部署下跑完整测试套件部署多个服务器实例放在round-robin负载均衡后面跑测试任何失败的测试都指向你遗漏的隐藏session依赖。一级SDK——Python和TypeScript——预计在7月28日最终规范发布前就具备完整支持。二级SDK包括Go、C#等会稍后跟进。DCR被废弃但保留向后兼容所以现有基于DCR的客户端不会立刻断掉但应该尽快迁移到CIMD。Roots、Sampling和Logging三项废弃特性至少有十二个月的过渡期在此期间继续工作但应该开始规划迁移。Roots变成Resource URI就是普通URLSampling可能作为扩展回归Logging移出核心规范。这里值得单独展开说说无状态化对生产部署的实际影响。Cloudflare在2026年8月5日专门发了一篇文章标题就叫Scaling AI Agent Infrastructure with the MCP Stateless updates——用Google的工程师写的这篇文章的题目就点明了核心变化无状态化是MCP能大规模扩展的前提。之前的session模型要求sticky routing这意味着负载均衡器要维护客户端到实例的映射表实例重启时映射表清空需要重新握手期间所有请求都会失败。无状态后任何实例都能处理任何请求实例可以随时重启、随时扩缩容、随时被替换不需要担心session状态丢失。这也让MCP服务器可以直接跑在Cloudflare Workers或任何普通HTTP基础设施上不需要专门的MCP托管服务。从更宏观的视角看2026-07-28规范标志着MCP从一个实验性的Agent工具连接协议转变为可以支撑企业级生产部署的基础设施。无状态核心让它能跑在标准的HTTP负载均衡基础设施上OAuth 2.1对齐让它能接入企业身份提供商Okta、Azure AD、Google Workspace正式废弃策略让协议可以有序演化而不破坏现有实现。MCP加入AAIF后第一次大版本修订就交出了一份消除自身技术债同时加固安全模型的答卷。对于在这个协议上构建产品的团队来说迁移窗口已经打开该动起来了。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →