首页 / 资讯中心 / 文章详情

MCP协议大改:Session移除与Sampling废弃后的迁移指南

MCP协议大改:Session移除与Sampling废弃后的迁移指南 ★ FEATURED ARTICLE
你手机里收藏的那篇《MCP Server 从零写起》《MCP Protocol 详解》可以清空了。上个月官方协议做了一轮大改我生产环境里跑得好好的 MCP 服务SDK 一升级直接开始报错初始化握手失败、Sampling 返回 method not found、旧的 SSE 长连接通道彻底失效。我一开始以为是依赖版本没对齐翻了半天 changelog 才确认不是我的代码坏了是 MCP 协议把自己推倒重写了一遍——Session 管理被移除Sampling 被正式废弃。这事的杀伤力在于网上大量教程还停留在 2025 年那个“有状态 双通道 可反向采样”的旧模型上照着写等于直接踩坑。这篇文章就围绕这次改版把 Session 为什么没、Sampling 为什么废、老代码怎么迁移、怎么快速识别过期教程全部讲清楚。适合正在写 MCP client/server、准备把工具链接入 MCP 的同学也适合只想搞明白“协议怎么又变了”的围观群众。1. 先说结论这次协议改动到底动了哪些骨头1.1 一份改动清单省得你被各种术语绕晕网上讨论这次改版时经常把 Transport、Session、Sampling、Roots 混在一起说其实核心改动就三件事模块旧模型2025 年教程新模型传输层HTTP SSE 双通道客户端 POST、服务端经 SSE 推流Streamable HTTP 单端点请求和流式响应走同一通道会话管理初始化握手后生成 session id后续请求带着它维持有状态会话去掉 session id每次请求自包含、无状态模型采样服务端通过 sampling/createMessage 反向请求客户端调用 LLM正式废弃改由客户端侧编排或工具回环实现注意这不是“新版本加了几个字段”那种小迭代而是动了协议的基本假设。老的教程如果还在教你“先建立 session再在这个 session 里传工具调用”那它描述的东西在现在的协议版本里已经不存在了。你照着旧教程写完的代码能编译能启动但一跑初始化就会在协议协商阶段被对方拒绝因为两边对“一个 MCP 会话长什么样”的理解已经完全不同。1.2 “协议版本号”才是这一切的钥匙MCP 的版本号长得和普通软件不一样它直接拿日期当版本号2024-11-05、2025-03-26、2025-06-18后面可能还会有更新的。这就有个非常坑的地方——初始化握手时客户端和服务端必须在 protocolVersion 上达成一致两边版本不兼容就直接拒绝。很多人的 SDK 自动更新到了新版本但代码里还在申请旧版本号于是出现“SDK 支持新特性但服务端不认”的尴尬局面。我建议你今天就去看一下自己项目里的 protocolVersion 写的是哪个日期。如果还是 2024-11-05你大概率还在旧模型上跑随时可能踩到 Session/Sampling 的雷。这个字段就藏在 initialize 请求的 params 里不管是官方 TypeScript SDK、Python SDK 还是自己手写的 JSON-RPC 客户端都能一眼看到。1.3 为什么说“你学的教程还停在 2025”这个说法其实挺准确。MCP 从 2024 年底开始火2025 年上半年是教程爆发期那时候大家写的都是基于 2024-11-05 或 2025-03-26 规范的玩法教你怎么维护 session、怎么用 sampling 让服务端“自己调用大模型”。这些内容放到现在的协议版本里要么失效要么被官方标记为 deprecated。如果你是从教程入门的你脑子里那套 MCP 模型和现在协议想表达的模型已经是两个东西了。这一点特别容易让人产生挫败感因为你在网上搜 MCP 教程搜出来的前几页很可能还是这些旧文章。搜索引擎不会替你判断版本新旧标题写着“2025 最新”的文章可能字里行间全是老 API。所以学会自己判断版本比收藏更多教程重要得多。2. Session 退场从“保持连接”到“无状态请求”的底层逻辑2.1 老协议里 Session 是怎么工作的在旧模型里MCP 的处理流程大概是这样的客户端先发一个 initialize 请求服务端回一个包含 session id 的响应客户端从响应头里拿到这个 id之后所有的请求都要在 header 里带上它。服务端根据这个 id 找到对应的会话状态知道“这个客户端是谁、它声明过什么能力、之前调用到哪一步了”。这套设计和 Web 里 cookie session 的思路一脉相承学过后端的人一看就懂本质上是服务端开了个“抽屉”每个客户端占一个状态全存在服务端。有状态设计不是错它很适合早期 MCP“一个客户端对一个服务端、跑个长任务”的场景。一个 MCP server 被一个 IDE 插件用连接数不多状态放在内存里也没啥问题。我最早写的几个工具 server 都是这个套路逻辑直观调试也方便。2.2 有状态为什么扛不住真实部署问题出在 MCP 的生态很快从“IDE 连本地服务”长成了“网关、多客户端、多租户”的复杂形态。一旦服务端要水平扩展session 就成了灾难连接被负载均衡转发到另一台机器状态不在那里请求直接失败你得额外搞 session 粘滞或者把状态塞进 Redis。更别提 session 超时、断线重连、服务端重启后 session 失效这些老问题。拿我自己的经历举个例。我的服务之前用的是会话式长连接上线两周后就开始出现奇怪现象客户端抽风重连服务端还留着旧 session 的定时器结果内存里堆了一堆僵尸会话连接数看着正常实际可用会话没几个。排查了半天才想起来这是有状态服务的老毛病——状态没人清理。新版协议把这套全砍了服务端不需要再维护任何会话上下文每个请求都自包含这不只是简化而是把一整类运维问题连根拔了。这里插一句session 这个话题在传统 Web 里也被讨论烂了cookie 和 session 和 token 的对比文章一搜一大把。MCP 这次其实是把后端领域已经走过的路又走了一遍从状态集中管理走向无状态、水平扩展友好。只不过因为 MCP 是协议层面的东西改起来比应用层更伤筋动骨所以感觉特别剧烈。2.3 没有 Session 之后“状态”去哪了那聊天类、多轮工具调用这种天然要状态的功能怎么办新模型的答案是状态归客户端管服务端只处理“此时此刻的这一个请求”。你把上下文、对话历史、工具调用的中间产物都放到请求参数里传给服务端服务端处理完就忘。就像以前你打电话接线员记住你是谁现在改成你每次写信都把上次的往来内容附上对方看完就扔。这个转变在写服务端代码时感觉特别明显以前要维护“当前用户的会话状态”现在只需要写纯函数式的请求处理逻辑一个请求进来算完返回完事。对于要做无状态网关、要水平扩展的团队来说这是质的简化。代价是客户端要变得“啰嗦”一点每次都要带足上下文但对现代大模型应用来说这恰恰是更自然的交互方式——本来 prompt 就是要把背景讲清楚的。另外提醒一句session 没了不代表连接不能复用。Streamable HTTP 底层仍然支持把多个请求放在同一个连接上只是不再用 session id 区分用户状态了。别把“无状态”理解成“必须每次新建连接”这两回事。3. Sampling 被废的真正原因不是多此一举而是权限模型兜不住3.1 Sampling 原本解决什么问题Sampling 是旧协议里我很喜欢的一个设计。MCP server 本身不接大模型但它有时候需要“让某个大模型帮忙想一下”。比如代码审查工具扫描完代码发现潜在问题它想生成一句解释怎么办旧协议下服务端可以发一个 sampling/createMessage 请求给客户端帮我调用你那边配置好的 LLM把这段代码的问题讲一下结果回传给我。这相当于把客户端的模型当成了服务端的“远程大脑”。好处是服务端不用自己接模型密钥直接借用客户端的模型资源这在智能 IDE 插件、自动化工具链里非常实用。我早期写 MCP server 时就靠这套接口让工具自动生成错误修复建议体验很顺。两个原本需要各自配 API key 的服务通过 sampling 一次打通开发效率确实高。3.2 为什么最终被砍权限链路崩了Sampling 被废核心问题不是“不好用”而是它把权限模型搞崩了。你想想这条链路服务端可能是一段不可信的第三方代码越过用户直接向客户端要一次模型调用。它既不需要用户确认也不需要单独授权等于一个外部程序借着你的模型额度、你的 API key 在说话。开发者写个 MCP server 发布出去它想在背后调用多少次模型、往 prompt 里塞什么内容用户全不知情。这在单机、单用户、全可信的环境里没毛病但 MCP 的愿景是开放生态谁都能写 server谁都能装 server。一旦 server 数量多起来sampling 就变成了天然的 prompt injection 通道和算力消耗口。我见过有人写 demo server 时在里面偷偷循环调用 client 的模型用来做向量化一次工具调用能吃掉几十次采样额度用户还以为是插件正常功能。从管理员视角看这就是“不可控的成本出口”。官方选择废弃而不是修补说明他们判断这个口子在安全模型上兜不住。更关键的是审计问题。像数据合规、成本核算这种场景你得能说清楚“这个模型调用是谁发起的、为什么要发起、花了多少钱”。旧 sampling 模式下调用方是服务端但资源属于客户端这笔账挂在谁头上都尴尬。废掉之后模型调用链路重新变得干净可追踪。3.3 现在的替代方案工具回环与客户端编排那么服务端再想让大模型干活怎么办现在的答案是把“需要模型理解”的逻辑上移到客户端。服务端只负责提供结构化的工具和数据好不好用、怎么用由客户端的代理模型自己决策。这实际上是把编排的职责明确划给了 client。服务端真的需要 LLM 能力时不直接“借嘴”而是通过一个显式的工具入口由客户端完成模型调用后再把结果写回流做成“工具回环”。每一步都有清晰的调用边界谁调用谁、花谁的额度链路一目了然。如果你的服务端自己有模型资源也可以直接在服务端自建一个模型调用入口通过资源接口把结果传回去完全不依赖对方的模型资源。我迁移完最大的体会是旧 sampling 是“协议替你偷偷做事”新模型是“协议逼你光明正大地分工”。虽然代码变多了但整条链路的可审计性完全不一样了。现在任何一个 server 想花我的模型额度都会暴露在工具调用的日志里我能看到是谁、什么时候、调了什么。对做企业级集成的团队来说这个改动其实是加分项。4. 迁移实操老代码哪些要改怎么改4.1 客户端初始化去掉 session id改版本号如果你用官方 Python SDK升级后第一件事就是把初始化逻辑改成无状态版本。老写法是 POST 之后从响应头解析 session id新写法不用管这个头了# 老写法已失效 resp requests.post( http://localhost:8000/mcp, headers{mcp-session-id: old_session_id}, json{ jsonrpc: 2.0, id: 1, method: initialize, params: { protocolVersion: 2024-11-05, capabilities: {}, clientInfo: {name: demo, version: 1.0.0}, }, }, ) session_id resp.headers.get(mcp-session-id) # 新版没有这个头了# 新写法无状态协议版本对齐后直接干 resp requests.post( http://localhost:8000/mcp, json{ jsonrpc: 2.0, id: 1, method: initialize, params: { protocolVersion: 2025-06-18, # 与服务端支持的最高版本对齐 capabilities: {}, clientInfo: {name: demo, version: 2.0.0}, }, }, )顺手验一下响应里的 protocolVersion 是不是你期望的版本不一致立刻打日志别闷头往下跑。很多诡异问题都是版本没对齐引发的先确认这个字段能省掉一半排查时间。4.2 Transport 层从 HTTPSSE 换成 Streamable HTTP老教程里那一套“客户端先 GET /sse 建立事件流再往另一个 POST 端点发消息”的玩法已经下线了。现在是一个端点POST 请求直接返回响应需要流式输出时走同一个连接的 SSE 分块。对于用官方 SDK 的人来说改动其实很小就是把 transport 的构造参数换掉from mcp import ClientSession from mcp.client.streamable_http import streamablehttp_client async with streamablehttp_client(http://localhost:8000/mcp) as (read, write, _): async with ClientSession(read, write) as session: await session.initialize() result await session.call_tool(analyze_code, {path: ./main.py})如果你是手写的 HTTP 客户端比如在 Go、Rust 里手搓 JSON-RPC注意要把原来的双通道逻辑删掉改成单端点、POST、头部不再带任何 mcp-session-id。状态和上下文全部塞进请求的 params 里。服务端也一样路由表里只留一个 /mcp 端点所有方法都走它。4.3 Sampling 调用点怎么重写老代码里如果有类似这样的调用# 老协议服务端请求客户端做 LLM 补全 result await session.send_request( sampling/createMessage, { messages: [{role: user, content: {type: text, text: 解释这段代码的问题}}], maxTokens: 500, }, )迁移时不要说“改用新 sampling 版本”没有这回事。正确改法是重新设计这一环把 LLM 调用挪到客户端服务端只暴露工具。例如代码审查服务拆成两步服务端 scan_code 返回问题列表客户端用自己的模型根据问题列表生成解释需要时再调 server 的 get_fix_suggestion 获取详细建议。所有模型调用都发生在 client 侧server 端的“想说话”变成了“提供数据”由客户端决定说不说、怎么说。4.4 一个容易漏的点capabilities 声明别乱吹协议大改后服务端在 initialize 响应里声明的 capabilities 会被客户端严格校验。旧代码里如果还声明了 sampling: {}新客户端可能直接拒绝握手或者忽略该能力。把所有过时能力从声明里清掉只留实际支持的工具、资源、提示这三类核心能力。我自己当时就是漏了这个初始化响应里带着一坨旧字段导致客户端和服务端在能力协商阶段互相猜忌连接建立失败。4.5 stdio 场景反而最省事如果你只是本地跑一个 stdio transport 的 MCP server这是很多 IDE 插件和本地工具链的常态这次改动对你影响很小。stdio 本来就是进程级管道天然无状态session 那种东西在 stdio 模式下一直比较多余。我迁移时唯一要做的就是把初始化的版本号改到新日期capabilities 清理一遍测试一把跑通就完了。所以先确认自己的 transport 类型别一看标题就慌不是所有场景都被波及。5. 教程失效的根源版本号和“语义化版本陷阱”5.1 MCP 的日期版本号到底什么意思MCP 用日期做版本号本意是“协议以发布日定版”但它没有语义化版本里那种严格的兼容性承诺。2025-03-26 到 2025-06-18 之间的差异比很多项目 1.x 到 2.x 的差异还大。这就导致一个现象一篇 2025 年 4 月写的教程很可能基于 2025-03-26 规范一个月后协议更新教程内容就半废了。你跟着老教程学学到一半发现 API 对不上不是你蠢是版本漂移太狠。判断教程是否过期有个偷懒技巧先看它有没有提到 protocolVersion 的具体日期再看它讲的是 SSE 双通道还是 Streamable HTTP。如果一篇“最新教程”还在大谈 session id、还在教 sampling/createMessage那它写于旧协议时代当历史资料看看可以照着做要谨慎。5.2 动手前先确认三件事协议版本、SDK 版本、transport 类型我整理了一个检查清单接入任何 MCP 项目之前先过一遍服务端支持的最高 protocolVersion 是多少客户端声明的版本是否在兼容范围内SDK 锁定的版本是哪个是否跟上了 transport 层调整代码里有没有 mcp-session-id、sampling/createMessage、HTTPSSE 双通道这类旧痕迹有没有依赖第三方教程的“轮子代码”轮子代码更新了吗这几条只要有一条不过接入的时候大概率出事。特别是第三、四条很多项目一升级就炸就是因为旧痕迹藏在深层调用链里没被发现。5.3 靠谱的信息源在哪别再收藏公众号二次加工的教程了。MCP 的官方 spec 仓库和 SDK 的 CHANGELOG 才是唯一可靠的信息源每次版本变更都会写清楚哪些废弃、哪些删除、哪些替换。SDK 发布说明里的 BREAKING CHANGES 段落尤其值得读几次协议大改SDK 都在第一时间同步了迁移提示。遇到报错先看这两处比自己盲猜快得多。另外GitHub 上那些“awesome-mcp”之类的精选列表建议也定期看更新。插件生态跟着协议走像 IDA MCP、x32dbg MCP 这类逆向工程插件还有蓝湖、Figma 这类设计工具的 MCP 接入每次协议大改它们都会快速跟进。如果一个知名插件长期没更新多半是被协议变化卡住了你接入前要多留个心眼。6. 我在迁移过程中踩过的坑6.1 坑一capabilities 返回了不存在的能力我最早踩的坑是服务端初始化响应的 capabilities 里还留着旧协议的字段。当时用的 SDK 已经支持新协议客户端拿到“服务端声称支持 sampling”这个信号后试图做能力协商结果两边语义不一致连接直接挂了。排查方式很笨但也最有效把 initialize 的请求和响应完整打日志逐字段比对新旧协议的差异。清理掉过期能力声明后握手恢复正常。6.2 坑二旧的 /sse 端点直接 404上线前我把 transport 换成了新写法但服务端路由表里还留着老的 /sse 路径。结果就是SDK 走新端点请求打到 /mcp路由没配404。顺着错误信息翻日志才发现问题根本不在协议而在 URL 路径。新协议只有一个统一端点迁移时记得把所有旧路径配置清干净别留着给自己挖坑。6.3 坑三无状态后“多轮对话断片”这是纯功能性坑。以前客户端和服务端共享会话上下文多轮工具调用很自然改成无状态后服务端不再记上下文第二轮请求里没带历史信息工具就“忘”了之前的事。一开始我以为是协议 bug后来才意识到无状态服务端本来就该由客户端全权负责上下文组装。修正方式简单粗暴——在客户端把每轮的目标状态显式放到请求参数里自己维护上下文。这也让我把服务端代码重构得更干净了函数完全无副作用每个请求都可以独立测试回归测试做起来特别舒服。6.4 排查心法抓住 JSON-RPC 报文就赢了一半MCP 再复杂底层还是 JSON-RPC。我迁移那几天几乎全靠抓报文定位问题。把每次请求、响应、通知都按 jsonrpc、method、params、result/error 格式化打印很快就能看出是协议版本不匹配、transport 串了、还是方法名不存在。强烈建议你本地开发时开一个 JSON-RPC 报文日志开关问题定位效率能翻一倍。网上有些教程喜欢直接给“标准答案”但真正能帮你在生产环境活下来的是你自己看报文、对规范的能力。最后说点个人体会。这一轮协议改版表面上是砍功能实际上是 MCP 在从一个“demo 友好的协议”长成“生产可用的协议”。无状态化换来了扩展性和可审计性sampling 的废弃换来了清晰的权限边界短期看是麻烦长期看是健康。我的建议是别再依赖过时教程了把官方 spec 的 CHANGELOG 加入你的周更阅读清单动手前先确认协议版本、SDK 版本和 transport 类型三者对齐生产环境里把 SDK 版本锁死升级走测试流程别让“上个月还能跑”变成你项目里的惊悚故事。
阅读完成 · 觉得有帮助?
咨询建站