版本管理:协议版本协商与向后兼容
摘要:MCP协议版本管理详解,涵盖版本协商机制、向后兼容策略、能力降级处理和版本迁移指南,确保Server升级不破坏现有Client集成。
MCP版本管理 协议版本协商与向后兼容
去年我写了个MCP Server,当时协议版本还是2024-11-05,跑得好好的。今年3月协议升级到2025-03-26,Streamable HTTP替代了HTTP+SSE,我的老客户端连不上了。等6月又出了2025-06-18版本,安全模型和工具返回结构都变了。三个版本一年内迭代,如果不管好版本兼容,每次协议升级就是一次线上事故。这篇讲我怎么处理MCP的版本协商、向后兼容和迁移升级。
MCP协议版本演进
MCP协议从2024年11月发布到现在经历了三个主要版本,每个版本都有实质性的变更。
2024-11-05是初始版本。定义了JSON-RPC 2.0基础上的客户端服务器架构,三大原语(Tools、Resources、Prompts),stdio和HTTP+SSE两种传输方式。这个版本奠定了MCP的基础框架,但很多细节还不完善。
2025-03-26是传输层大改版。最核心的变化是用Streamable HTTP替代了HTTP+SSE。原因是旧方案要求服务器维持长连接,不支持断线恢复,扩展性差。新方案移除了/sse端点,统一用/mcp端点通信,支持无状态服务器模式,可以水平扩展。这一版还引入了结构化工具输出和OAuth 2.1授权框架的初步支持。
2025-06-18是安全与功能大升级。我整理了这一版的九项关键变更。第一,移除JSON-RPC批处理支持,简化规范。第二,工具调用结果新增outputSchema和structuredContent字段,支持结构化输出验证。第三,MCP服务器明确归类为OAuth资源服务器,通过Protected Resource Metadata(RFC 9728)声明授权服务器。第四,强制要求客户端实现RFC 8707的Resource Indicators,防止令牌滥用。第五,新增安全最佳实践指南。第六,支持elicitation功能,服务器可以主动向用户请求补充信息。第七,工具返回新增ResourceLink类型,支持延迟加载大资源。第八,要求HTTP传输时通过MCP-Protocol-Version请求头声明协议版本。第九,生命周期操作从SHOULD升级为MUST,强制双方遵守协商结果。
版本协商机制
版本协商发生在初始化阶段。客户端发initialize请求时带上自己想用的protocolVersion,服务器收到后做判断。如果支持这个版本就原样返回,不支持就返回服务器支持的版本。客户端拿到响应后按服务器返回的版本走后续流程。
这个过程看起来简单,但有个关键细节。2025-06-18版本新增了HTTP传输时的版本头要求。初始化阶段协商完版本后,后续每个HTTP请求都要带MCP-Protocol-Version头,值就是协商确定的版本号。这是为了支持无状态HTTP场景,因为无状态下服务器没法从会话里推断客户端用的版本。
如果不带这个头会怎样。规范说服务器应该返回400错误。但实际中各SDK实现不一,有的容忍有的报错。我的建议是客户端务必带上这个头,服务器做好兼容处理,不带时默认用初始化协商的版本。
向后兼容策略
协议升级最怕的就是老客户端连不上新服务器,或者新客户端连不上老服务器。我的兼容策略分四个层次。
第一层是版本检测。Server启动时声明自己支持的版本列表,Client连接时声明自己支持的版本,取交集。两边都没有交集时给出明确的错误信息,而不是行为异常让人摸不着头脑。
第二层是功能降级。新版本加了新功能,老客户端用不了。这时候Server检测到客户端版本低,自动降级到老版本的行为。比如2025-06-18的structuredContent,老客户端不认识这个字段,Server就额外返回一份纯文本content兜底。
第三层是字段兼容。新版本加的字段,老版本的SDK会忽略。老版本有的字段,新版本保留不删。这保证了JSON结构层面的向前向后兼容。
第四层是传输层兼容。Streamable HTTP虽然替代了HTTP+SSE,但很多SDK还保留了SSE传输作为备选。我的Server同时支持两种传输,根据客户端连接方式自动选择。
弃用管理
功能废弃不能一刀切。我把弃用过程分成三个阶段。
第一阶段是标记弃用。在工具的描述里加上[DEPRECATED]前缀,同时返回一个deprecated: true的字段(对应2025-06-18新增的title和元数据机制)。客户端看到标记就知道这个功能要淘汰了,但仍然能用。
第二阶段是警告期。被弃用的功能仍然工作,但每次调用都返回一个警告提示。同时Server日志记录弃用功能的使用情况,统计还有多少客户端在用。
第三阶段是移除。警告期过后(我一般给3到6个月),如果使用量降到零就彻底移除。如果还有人在用就延长警告期,直到没有人依赖为止。
完整代码
下面是一个完整的多版本兼容MCP Server,支持版本协商、向后兼容和弃用管理。
# version_compat_server.py
# 多版本兼容MCP Server 支持版本协商和向后兼容
# 依赖安装 pip install mcp pydantic
import json
import logging
from dataclasses import dataclass, field
from datetime import datetime
from typing import Any
from mcp.server.fastmcp import FastMCP
# 配置日志 用于记录版本协商和弃用警告
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
logger = logging.getLogger("version-server")
# ============================================================
# 第一部分 版本定义与兼容性矩阵
# ============================================================
# 本Server支持的协议版本列表 从新到旧排列
SUPPORTED_VERSIONS = ["2025-06-18", "2025-03-26", "2024-11-05"]
# 最新版本 当前Server以这个版本的行为为主
LATEST_VERSION = "2025-06-18"
# 版本功能矩阵 记录每个版本支持哪些功能
# 用于决定是否需要降级行为
VERSION_FEATURES: dict[str, set[str]] = {
"2024-11-05": {"basic_tools", "resources", "prompts", "http_sse"},
"2025-03-26": {"basic_tools", "resources", "prompts", "streamable_http", "structured_output"},
"2025-06-18": {
"basic_tools", "resources", "prompts", "streamable_http",
"structured_output", "elicitation", "resource_links",
"oauth_resource_server", "protocol_version_header",
},
}
def negotiate_version(client_version: str) -> str:
"""版本协商逻辑 返回最终使用的协议版本"""
# 客户端版本在支持列表里 直接用
if client_version in SUPPORTED_VERSIONS:
logger.info(f"版本协商成功 客户端{client_version} 服务端支持")
return client_version
# 客户端版本比服务端新 服务端返回自己支持的最高版本
# 客户端需要降级行为
client_idx = _version_index(client_version)
latest_idx = _version_index(LATEST_VERSION)
if client_idx > latest_idx:
logger.warning(f"客户端版本{client_version}高于服务端最高版本 降级到{LATEST_VERSION}")
return LATEST_VERSION
# 客户端版本比服务端最低版本还老 返回最低支持版本
logger.warning(f"客户端版本{client_version}过低 最低支持{SUPPORTED_VERSIONS[-1]}")
return SUPPORTED_VERSIONS[-1]
def _version_index(version: str) -> int:
"""把版本字符串转成可比较的索引 越大越新"""
try:
return SUPPORTED_VERSIONS.index(version)
except ValueError:
# 未知版本 按日期字符串比较
return 0 if version < "2024-11-05" else len(SUPPORTED_VERSIONS)
def has_feature(negotiated_version: str, feature: str) -> bool:
"""检查协商后的版本是否支持某个功能"""
features = VERSION_FEATURES.get(negotiated_version, set())
return feature in features
# ============================================================
# 第二部分 弃用管理
# ============================================================
@dataclass
class DeprecationInfo:
"""弃用信息记录"""
tool_name: str # 被弃用的工具名
deprecated_since: str # 从哪个版本开始弃用
replacement: str # 替代工具名
removal_target: str # 计划移除的版本
call_count: int = 0 # 弃用后的调用次数 用于追踪
# 弃用注册表 记录哪些工具被弃用了
DEPRECATION_REGISTRY: dict[str, DeprecationInfo] = {
"old_search": DeprecationInfo(
tool_name="old_search",
deprecated_since="2025-03-26",
replacement="search",
removal_target="2025-09-18",
),
}
def check_deprecation(tool_name: str) -> DeprecationInfo | None:
"""检查工具是否被弃用 返回弃用信息或None"""
info = DEPRECATION_REGISTRY.get(tool_name)
if info:
# 记录调用次数 用于决定何时安全移除
info.call_count += 1
logger.warning(
f"弃用工具{tool_name}被调用 第{info.call_count}次 "
f"替代工具为{info.replacement} 计划在{info.removal_target}移除"
)
return info
# ============================================================
# 第三部分 MCP Server定义
# ============================================================
mcp = FastMCP("version-compat-server")
# 记录当前客户端协商的版本 模拟会话级存储
# 生产环境用contextvars做请求级隔离
_negotiated_version: str = LATEST_VERSION
@mcp.tool()
def search(query: str, limit: int = 10) -> str:
"""搜索工具 新版本支持结构化输出"""
safe_query = query.strip() if query else ""
# 根据协商版本决定返回格式
if has_feature(_negotiated_version, "structured_output"):
# 2025-03-26及以上版本 返回结构化数据
result = {
"query": safe_query,
"limit": limit,
"results": [
{"id": 1, "title": f"结果{safe_query}1", "score": 0.95},
{"id": 2, "title": f"结果{safe_query}2", "score": 0.87},
],
"total": 2,
}
return json.dumps(result, ensure_ascii=False)
else:
# 老版本降级为纯文本格式
return f"搜索{safe_query}找到2条结果\n1. 结果{safe_query}1 (相关度95%)\n2. 结果{safe_query}2 (相关度87%)"
@mcp.tool()
def old_search(keyword: str) -> str:
"""[已弃用] 旧版搜索 请改用search工具"""
# 检查并记录弃用情况
deprecation = check_deprecation("old_search")
# 弃用工具仍然执行功能 但加上警告前缀
warning = f"[弃用警告 请改用search工具] "
return warning + f"搜索{keyword}找到1条旧结果"
@mcp.tool()
def get_server_info() -> str:
"""返回服务器版本和兼容性信息"""
info = {
"server_name": "version-compat-server",
"server_version": "1.0.0",
"latest_protocol_version": LATEST_VERSION,
"supported_versions": SUPPORTED_VERSIONS,
"current_negotiated_version": _negotiated_version,
"available_features": list(VERSION_FEATURES.get(_negotiated_version, set())),
"deprecated_tools": [
{
"name": d.tool_name,
"since": d.deprecated_since,
"replacement": d.replacement,
"removal_target": d.removal_target,
}
for d in DEPRECATION_REGISTRY.values()
],
}
return json.dumps(info, ensure_ascii=False, indent=2)
@mcp.tool()
def request_user_info(question: str) -> str:
"""elicitation示例 2025-06-18新功能 向用户请求补充信息"""
# 检查当前版本是否支持elicitation
if not has_feature(_negotiated_version, "elicitation"):
# 老版本不支持elicitation 降级为普通提示
return f"需要补充信息但当前协议版本不支持elicitation 请手动提供 {question}"
# 新版本支持elicitation 返回结构化的请求信息
# 实际场景中这里会触发elicitation/create流程
elicitation_request = {
"type": "elicitation",
"message": question,
"requested_schema": {
"type": "object",
"properties": {
"answer": {"type": "string", "description": "用户的回答"}
},
"required": ["answer"],
},
}
return json.dumps(elicitation_request, ensure_ascii=False)
@mcp.tool()
def set_client_version(version: str) -> str:
"""模拟版本协商 设置客户端使用的协议版本"""
global _negotiated_version
# 执行版本协商
negotiated = negotiate_version(version)
_negotiated_version = negotiated
result = {
"client_requested": version,
"server_negotiated": negotiated,
"version_match": version == negotiated,
"available_features": list(VERSION_FEATURES.get(negotiated, set())),
}
return json.dumps(result, ensure_ascii=False, indent=2)
# ============================================================
# 第四部分 版本迁移辅助工具
# ============================================================
# 迁移指南 记录每个版本升级时需要做的改动
MIGRATION_GUIDES: dict[str, list[str]] = {
"2024-11-05_to_2025-03-26": [
"1. 传输层从HTTP+SSE切换到Streamable HTTP",
"2. 移除/sse端点 改用/mcp端点",
"3. 支持无状态服务器模式",
"4. 添加结构化工具输出支持",
"5. 实现OAuth 2.1授权框架初步支持",
],
"2025-03-26_to_2025-06-18": [
"1. 移除JSON-RPC批处理支持",
"2. 工具结果添加outputSchema和structuredContent字段",
"3. 配置OAuth资源服务器元数据端点(RFC 9728)",
"4. 客户端实现RFC 8707 Resource Indicators",
"5. HTTP请求添加MCP-Protocol-Version头",
"6. 生命周期操作从SHOULD改为MUST强制执行",
"7. 可选实现elicitation和ResourceLink功能",
"8. 为工具和资源添加title字段提升展示体验",
],
}
@mcp.tool()
def get_migration_guide(from_version: str, to_version: str) -> str:
"""获取版本迁移指南 指导升级操作"""
key = f"{from_version}_to_{to_version}"
guide = MIGRATION_GUIDES.get(key)
if guide is None:
return f"没有找到从{from_version}到{to_version}的迁移指南 请检查版本号"
return f"迁移指南 {from_version} -> {to_version}\n" + "\n".join(guide)
# ============================================================
# 启动入口
# ============================================================
if __name__ == "__main__":
mcp.run(transport="stdio")
版本协商对比分析
我对比过三种版本协商策略的实际效果。
硬协商策略要求客户端版本必须精确匹配服务端版本,不匹配直接拒绝连接。优点是行为完全确定没有歧义。缺点是灵活性极差,客户端SDK每升一个小版本就得连不上服务器,运维成本高。
软协商策略取客户端和服务端都支持的最高版本。如果客户端版本高于服务端就降级到服务端最高版本,低于就升级到服务端最低版本。优点是兼容性最好,新老客户端都能连上。缺点是行为可能因版本不同而异,测试矩阵大。
范围协商策略允许服务端声明支持的版本范围,客户端版本落在范围内就接受。这其实是软协商的简化版,实现更轻量。
我推荐软协商策略,就是上面代码里negotiate_version函数的实现。它在兼容性和确定性之间取得了平衡,也是MCP协议本身采用的策略。
效果验证
运行Server后按以下步骤验证版本协商和兼容性。
第一步,模拟2025-06-18客户端。调用set_client_version("2025-06-18"),返回显示协商成功,可用功能包含elicitation、structured_output等新特性。调用search("test")返回JSON结构化数据。调用request_user_info("请输入姓名")返回elicitation格式的请求结构。
第二步,模拟2024-11-05老客户端。调用set_client_version("2024-11-05"),返回显示协商成功但功能列表只有basic_tools等基础功能。再调search("test"),这次返回纯文本格式,因为老版本不支持structured_output。调request_user_info返回降级提示,说明不支持elicitation。
第三步,测试弃用工具。调用old_search("keyword"),返回结果带"[弃用警告]"前缀。同时服务端日志记录了弃用工具的调用次数。调get_server_info能看到弃用工具列表和替代方案。
第四步,获取迁移指南。调用get_migration_guide("2024-11-05", "2025-03-26"),返回5条迁移步骤。再调get_migration_guide("2025-03-26", "2025-06-18"),返回8条迁移步骤。
常见问题与避坑
坑一,版本协商后忘了实际降级行为。 我最早的做法是协商了版本但行为没变,老客户端拿到structuredContent字段不认识直接报错。解决办法是用has_feature函数检查协商版本是否支持某功能,不支持就走降级路径。上面代码里search工具就是这么做的,新版本返回JSON老版本返回纯文本。
坑二,MCP-Protocol-Version头丢失导致400错误。 2025-06-18要求每个HTTP请求带这个头。如果你的客户端SDK版本旧不带这个头,又连了严格执行规范的新服务器,就会被拒绝。解决办法是客户端升级到支持新规范的SDK版本,或者服务器端做兼容处理,不带头时默认用初始化协商的版本。两边的配合很重要。
坑三,弃用工具直接删除导致老客户端崩溃。 有次我把一个弃用工具直接删了,结果还有三个老客户端在调用,直接报"工具不存在"错误。正确的做法是先标记弃用,进入警告期,监控调用次数降到零再删除。我给的迁移指南工具能帮用户知道要改什么,配合弃用标记给足过渡时间。
坑四,版本字符串比较用字符串大小判断。 "2025-06-18"和"2025-03-26"用字符串比较确实能比出大小,因为日期格式正好是字典序。但如果以后版本号格式变了就出问题。稳妥的做法是维护一个有序版本列表用index比较,像代码里_version_index函数那样。
坑五,新功能默认开启导致老客户端行为异常。 2025-06-18的elicitation功能,如果服务器默认开启但客户端SDK不支持,调用流程会卡住。解决办法是所有新功能默认关闭,只在检测到客户端版本支持时才开启。has_feature检查就是干这个的,先查能力再用功能。
小结
MCP协议一年迭代三个版本,版本管理是必选项,不能省。核心做四件事。
版本协商让客户端和服务端就协议版本达成一致,取交集保证两边都能理解对方。向后兼容通过功能检测和行为降级,让老客户端能连新服务器,新客户端也能连老服务器。弃用管理通过标记、警告、移除三阶段,平稳淘汰旧功能不引发线上事故。迁移指南把每个版本的变更点列清楚,帮助开发者快速完成升级。
2025-06-18是当前最新版本,安全模型和功能都有重大升级。如果你的Server还停在老版本,建议尽快迁移。先调get_migration_guide看看需要改什么,再逐步实施。协议升级不可怕,可怕的是没有版本管理机制,出了问题都不知道是哪边的版本不对。
相关推荐
更多推荐

所有评论(0)