摘要: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批处理支持,简化规范。第二,工具调用结果新增outputSchemastructuredContent字段,支持结构化输出验证。第三,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看看需要改什么,再逐步实施。协议升级不可怕,可怕的是没有版本管理机制,出了问题都不知道是哪边的版本不对。


相关推荐

Logo

欢迎加入 MCP 技术社区!与志同道合者携手前行,一同解锁 MCP 技术的无限可能!

更多推荐