1. 这回真把地基拆了:Session 和 Sampling 到底改了什么
1.1 一句话看懂新版 MCP 的变化
MCP 协议在 2025 年这轮更新里,把 Session 这个概念从核心流程里摘掉了,Sampling 也正式进入废弃通道。我刚从官方 SDK 的 changelog 里看到这消息时,第一反应是“完了,我手里几套教程全白写了”。如果你现在打开那些 2025 年初发布的、标题写着“从零玩转 MCP”的文章,对照最新规范去跑代码,大概率会遇到方法不存在、字段被移除、握手流程对不上的情况。
我说的不是某个第三方插件的小调整,是协议层的结构性改动。原来你学的那套“先建 Session,再通过 Session 发请求”的思路,在新规范里基本不成立;原来你调用的 Sampling 接口,现在要么被宿主应用接管,要么就得换成别的方式实现。很多朋友可能觉得“协议更新关我什么事,我跟着 demo 写就行”,但协议一变,所有基于它的 SDK、框架、平台服务都会跟着变,你手上的代码迟早要适配。
这篇文章不打算给你背一遍官方文档,而是想把这次变化讲清楚:Session 为什么被拿掉,Sampling 为什么被废,老教程的写法该怎么迁移,以及以后怎么快速判断一套教程是否已经过期。适合正准备学 MCP、或者已经在生产环境里用 MCP 的开发者看,尤其是那些依赖网上教程而不是官方规范的人。
1.2 为什么大家会误以为“2025 年教程没过期”
MCP 自打 2024 年底爆火以来,迭代速度比大部分人的学习速度还快。最典型的情况是:你跟着 B 站或掘金的一篇热门文章写好了 server,三个月后官方 SDK 一升级,代码直接编译不过。为什么这么容易踩坑?因为很多教程是照着一两个稳定版本写的,但协议本身并没有“稳定”到可以保证长时间不变。
我复盘了一下自己的学习路径,发现一个规律:网上九成教程都在讲“MCP 是什么、怎么配一个文件服务器、怎么接大模型”,很少人提醒你“去读当前版本规范”。而协议恰恰是最该看规范的地方。你只要在教程里看到initialize之后还要处理session/create这类旧字段,或者看到sampling/createMessage这种旧接口,基本可以判断这篇教程写的是老版本了。
更麻烦的是,搜索引擎会把旧文章排在最前面。2025 年发布的内容,在 2025 年下半年看起来当然不算“旧”,但协议恰恰在“上个月”发生了破坏性变化。所以最稳妥的办法不是收藏教程,而是直接看官方仓库的 CHANGELOG 和 SDK 的版本更新说明。实际操作中,我每次开始一个新项目前,都会花十分钟确认三件事:当前协议版本号、我用的 SDK 版本号、官方示例里有没有出现被废弃的关键字。这十分钟能省掉后续几小时的排查时间。
2. Session 删除背后的设计逻辑
2.1 从“建立连接”到“请求即上下文”
旧版 MCP 的核心流程里,客户端和服务器要先完成一次握手,建立一个长期有效的 Session,后续所有 JSON-RPC 请求都挂在 Session 下面。这种设计很符合直觉:先打电话,再说事。可它带来的问题是状态维护成本太高。服务器要记得每个 Session 的创建时间、权限、能力集、消息历史,一旦 Session 失联还要做超时清理。
新版的做法更干脆:不维护全局 Session,每个请求本身就是完整的上下文。你发一个tools/call,里面就把客户端标识、能力列表、需要的数据全部带上;服务器处理完就返回,不保留中间状态。用生活化类比,旧版是“你跟饭店订了个包间,服务员全程只服务这个包间”,新版是“你每次点菜都用同一个菜单,但不需要固定座位”。
这个改动让 MCP 彻底变得无状态。无状态意味着更容易做水平扩展,服务器挂了可以随时换一台,请求重发也不会有副作用。代价是什么呢?开发者的心智要改过来:不能再靠“把上下文存在 session 里”偷懒,必须在请求参数里设计好完整的上下文信息。我实际改代码时最深的感受是,以前偷懒写在 session 临时变量里的东西,现在全部得显式传参。看着啰嗦,但对于分布式场景和边缘计算来说,这反而是最合理的取舍。
2.2 对开发者的实际影响:少写状态,多写幂等
Session 从协议核心消失,影响最大的不是纯客户端调用,而是那些“带状态”的服务端实现。比如老代码里常见这种逻辑:用户登录后把 token 放进 session,后续每个请求从 session 里取 token。新版协议不再帮你存这些,你得在请求头或参数里自己带身份信息,每次都携带。
我遇到的一个实际项目里,旧服务端用 session 缓存了一份很大的上下文,每次请求都基于这份上下文做推理。改成无状态后,缓存策略得挪到外部存储,比如 Redis 或数据库,请求进来时显式加载。这其实对系统健壮性更友好:不再依赖单个进程的内存,服务重启也不丢上下文了。
另一个要注意的点是幂等性。以前请求在 Session 内按顺序执行,重复发送的风险比较低;现在每个请求独立处理,同一个操作可能被客户端重试多次。所以服务端最好给每个请求设计request_id或者幂等键,保证重复请求不会重复扣款、重复写库。坦白讲,这个习惯在任何 API 设计里都该有,只是 MCP 这次更新把它变成了硬性要求。
2.3 迁移检查清单
我整理了一份从旧 Session 思路迁移到新无状态写法的检查清单,照着过一遍,基本就能把老工程的坑踩平:
- 把代码里所有“从 session 取东西”的逻辑,改成“从请求参数或上下文对象里取”。
- 检查有没有依赖 session 的鉴权方式,比如
session.get("user_id"),改成 JWT 或显式 Authorization 头。 - 如果你用的是官方 SDK,直接升级到最新版,老的 Session API 很可能已经删了。
- 给所有写操作加上幂等键,至少留一个重试去重的字段。
- 测试时故意把连接断开重连,看看客户端是否能无感续传,这是无状态服务必须通过的一项验证。
按这个清单走一遍,你会发现大部分迁移工作其实不复杂,复杂的是心态:以前习惯有状态地思考,现在要换成“每次请求都是全新的”。用熟了以后,调试起来反而更清爽,因为一个请求一个响应,日志链路清清楚楚。
3. Sampling 被弃用以后,我们还能怎么“让模型说话”
3.1 老 Sampling 的诱人之处和它的坑
Sampling 是什么?简单说,它允许 MCP 服务器在运行过程中反向要求客户端调用大模型,生成一段文本或补全。这个能力听起来很爽:服务器不再只是被动地提供工具,而是能主动“让模型帮个忙”。比如你写了个文档分析服务器,分析到一半发现需要给某个术语配个解释,于是发起一次 sampling,让大模型生成一段解释,再继续分析。
我第一次看到 Sampling 时觉得它是整个协议里最有想象力的设计,但实际用起来问题一堆。首先是权限边界模糊。服务器只是通过 MCP 连接到了客户端,客户端背后到底是什么模型、有没有权限、要不要花钱,全都没有清晰的授权机制。这就好比一个外卖员进了你家厨房,虽然没有钥匙,但他可以直接从冰箱里拿食材做饭,你不知道他哪次拿多了。
其次是递归风险。服务器发起 sampling,模型返回内容,服务器可能又基于这个内容发起新的 sampling,一不小心就变成无限循环。我在一个实验项目里真遇到过,一次请求触发了几十次模型调用,账单刷刷往上涨。协议设计者显然比我早吃过这些亏,所以在新版里干脆把 Sampling 从标准流程里剥出去,只保留“由宿主应用自行决定是否支持”的扩展口。表面上看是功能变弱了,实际上是安全模型变清晰了:模型调用必须由用户或宿主应用发起,服务器不能偷偷调用。
3.2 替代方案:工具、资源、宿主能力
弃用了 Sampling 不代表这个需求消失了。服务器想让模型帮忙,在新规范里至少有三种正规路径:
第一条路径是把“让模型做某事”建模成工具。服务器自己定义好工具名和参数,客户端调用模型后把结果以工具返回值的形式回传给服务器。这样整个流程是显式的、可审计的,用户能看到模型在做什么。
第二条路径是使用资源引用。如果服务器只需要给模型提供一段资料,不要求模型生成结果,完全可以用 resource 的方式暴露给客户端,让客户端自己决定什么时候加载。这比偷偷调用 sampling 更透明。
第三条路径是依赖宿主应用提供的原生能力。很多 MCP 客户端本身就有“把模型输出给你”的能力,比如 Claude Desktop、各类 Agent 框架,它们会按自己的权限规则决定是否调用模型。服务器不应该越俎代庖。
我个人的建议是:能用工具就用工具,能传资源就传资源,别老想着让服务器直接调模型。模型调用应该掌握在客户端手里,这既是安全边界,也是责任边界。新版规范等于替大家把这个边界画清楚了。
3.3 权限模型的变化
Sampling 被弃用,本质上是权限模型从“服务器可主动发起动作”变成“一切动作都由客户端控制和委托”。这对生态建设是好事。
以前服务器只要获得连接资格,就能间接动用客户端的模型资源,这是很危险的安全漏洞。现在新版要求每个敏感动作都要经过授权,而且更强调 OAuth 和设备授权码这类标准流程。做企业级应用的朋友应该深有体会:权限这东西,宁可绕路也不能省。MCP 在这一点上终于想明白了。
我写授权代码时踩过一个坑:老版本里只要在 initialize 时声明支持 sampling,客户端就无条件放行;现在新版本里服务器如果还想请求模型能力,必须先声明需要哪些 scope,再由用户或管理员审批。流程变重了,但至少每个人都知道“谁在什么时候用了什么能力”。
所以你如果看到网上教程里还在教人用 sampling 实现“服务器自动写摘要”,请自动把这段内容替换成“定义工具、声明权限、回传结果”。功能上能做到九成相似,安全性却高了一个量级。
4. 实操:按新规范搭一个 MCP Server 和 Client
4.1 先确认版本
实操之前,第一步永远是确认自己手里的依赖版本。我通常看三个地方:
首先是官方协议仓库的 README 和 CHANGELOG,看当前主线版本是多少,有没有标记 breaking change。其次是 SDK 的package.json或pyproject.toml,看装上的是不是最新版。最后是跑一个官方示例,看编译是否有废弃警告。
以 Python 为例,如果你还在用旧版 SDK,代码里很可能出现from mcp.server.session import ServerSession这种导入。新版本 SDK 里这个路径基本已经移除了。正确做法是去官方仓库的 examples 目录里复制一份最新代码,确认 import 路径和初始化方式再动手。
我常用的判断方法是搜索自己代码里有没有session和sampling这两个词。如果有,再看它是不是业务自建术语。如果发现是直接调用 SDK 的 Session API 或create_message,那基本可以确定该升级了。升级不过是pip install --upgrade mcp或npm update @modelcontextprotocol/sdk一条命令,但升级之后要跑一遍回归测试,因为接口变动会用编译错误直接给你提示。
4.2 最小 server 实现
新规范下,一个最小的 MCP Server 其实比旧版更像“一个普通 JSON-RPC 服务”。下面我用伪代码演示结构,真实项目里请换成官方的 FastMCP 或同类封装:
from mcp.server.fastmcp import FastMCP mcp = FastMCP("demo-server") @mcp.tool() def add(a: int, b: int) -> int: """两数相加,纯函数,无状态""" return a + b mcp.run(transport="stdio")注意这里完全没有session相关的初始化,也没有声明支持sampling。工具函数只接收参数、返回结果,状态全部在调用方手里。
如果要用 JSON-RPC 裸协议理解也行。新版里的请求大概长这样:
{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "add", "arguments": {"a": 1, "b": 2} }, "meta": { "client_id": "abc-123" } }响应则是标准的 JSON-RPC 返回,不需要关联任何会话 ID。这种设计让抓包调试变得特别舒服,你可以把每个请求独立地发给任意一个后端实例,返回结果不受历史请求影响。
4.3 Client 侧适配:不要再用 session/sampling
客户端适配同样简单。下面是一个发送工具调用的最小示例:
import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"; const transport = new StdioClientTransport({ command: "python", args: ["server.py"] }); const client = new Client({ name: "demo-client", version: "1.0.0" }); await client.connect(transport); const result = await client.callTool({ name: "add", arguments: { a: 1, b: 2 } }); console.log(result);关键点在于,连接建立后不需要向服务器索取 session id,也不要在后续调用里带 session 字段。你可能会在旧文档里看到类似client.session的属性,新版里我已经不推荐再用了。若代码里出现.session的链式调用,最好先查一下 SDK 版本。
还要注意,旧版客户端最常见的报错是“session not initialized”。新版里这个错误会变成“transport not connected”或“server capabilities not found”。意思类似,但排查路径变了:你不是去查 session 初始化顺序,而是去查传输层有没有真正连上、服务器有没有声明对应能力。
4.4 如何把老教程改造成新写法
假设你手头有一篇老教程写得很好,只是用的旧 API,怎么快速改造成新规范?我一般分四步:
第一步,把所有session相关的初始化代码删掉。别想留个兼容层,新协议下 Session 已经被移除,兼容层等于自己造了个没人认的伪协议。
第二步,把sampling调用改成普通工具调用。比如旧代码里await session.sample(prompt)改成await client.callTool({ name: "ask_model", arguments: { prompt } })。服务器侧实现一个ask_model工具,内部再走自己的模型通道,并在工具说明里标注权限。
第三步,重启所有能力协商流程。新版 initialize 里会交换 capabilities,你看看服务器有没有声明tools、resources、prompts。如果服务器没有声明,客户端根本不会发对应请求,这是很多“工具调不通”的根源。
第四步,跑官方 example 做对照组。我几乎每次改完都会用一个官方最小 demo 验证环境没问题,再逐步把我的逻辑加上去。这样能把“我的 bug”和“协议理解错误”严格区分开。
改造过程听起来琐碎,但真花不了太多时间。我最近帮一个同事迁移老项目,总共也就两个晚上。真正花时间的不是改代码,而是说服自己“旧思路已经不适用了”。
5. 从老教程迁移的故障排查速查表
5.1 常见报错一览
我把这轮升级过程中容易遇到的报错整理成了表格,方便大家直接对照:
| 报错信息 | 常见原因 | 新规下的处理方式 |
|---|---|---|
Session not found | 服务器还在按旧协议维护 Session | 去掉 Session 管理,改为无状态处理,检查请求是否自带完整上下文 |
Unknown method: session/create | 客户端调用了旧协议方法 | 升级 SDK,删除session/create、session/close等调用 |
Sampling not supported | 服务器声明了旧能力,但客户端不再支持 | 改用工具或资源来传递模型调用需求,不要依赖 sampling |
Capabilities not negotiated | initialize 阶段能力声明不完整 | 在 initialize 响应里里明确声明 tools/resources/prompts 支持 |
Timeout waiting for response | 服务器在处理长任务但没有流式反馈 | 改用tools/call的流式模式或分步轮询,请求里带上_meta提示 |
Deprecated: createMessage | 代码中仍使用旧 sampling API | 移除该调用,改走工具流程 |
Client transport already connected | 重复调用 connect 且旧连接未关闭 | 每次连接前先 close,或复用同一个 client 实例 |
别小看这些报错,很多都是“照着旧教程写,然后报错查了半天”的典型场景。我把它们写下来,是因为我本人至少踩过其中五个。
5.2 排查方法论:规范版本、SDK 版本、能力协商
遇到 MCP 相关的问题,我通常不急着看堆栈,而是按顺序检查三个东西:
首先看规范版本。打开官方文档,看当前最新版本号,再对比你代码里的协议版本协商参数。如果 initialize 里写的版本和服务器支持版本对不上,后面所有请求都会异常。
其次看 SDK 版本。这一步最简单也最容易忽略。很多人用的是 IDE 自动补全出来的旧版本 SDK,或者项目里 lock 文件锁了一个老版本。升级前建议先读一下官方变更日志,确认你要用的功能没有被改名或删除。
最后看能力协商。新版协议里,客户端和服务器像两个人见面先互报技能树:你说你会工具,我说我会流式输出,那么后面才有可能协作。如果能力没对上,哪怕代码写得再对,请求也会石沉大海。我遇到过不少次“代码完全没错但就是不响应”的情况,最后查出来是 capabilities 没带上,简直气死人。
这三板斧看着简单,能解决我遇到过的百分之八十的问题,而且不依赖任何魔法,就是老老实实核对版本、核对能力、核对日志。
5.3 一个实战排查案例
上个月我给一个内部工具升级,遇到一个诡异现象:客户端能连上服务器,但调用任何工具都返回空结果。我一开始怀疑是业务代码问题,后来抓了原始报文才发现,服务器返回的内容里 capabilities 只有resources,没声明tools。客户端一看你不支持工具,就直接把 tools 相关的请求拦下了,根本不发给服务器。
原因是服务器在 initialize 时没有把tools列进能力列表,而代码里确实定义了工具函数。改法特别简单,在能力声明里补上"tools": {}或者用框架默认值。但排查过程花了两个小时。如果早点检查能力协商,一分钟就能定位。这件事之后,我给自己定了个规矩:凡是用框架搭的 MCP Server,启动后先打一条日志,把实际声明的能力打印出来,和预期比对一次。
这个案例给我们的启示是:协议升级后,很多“旧方法报错”其实都变成了“能力没对上”的静默失败。报错还好查,最怕的就是不报错但行为不对。所以排查时一定不要只看应用层,要下到协议层看原始消息。
5.4 防止下一次被过时教程坑的经验
最后分享几个我这几年总结出来的实在经验:
第一个经验是,永远把官方文档放在浏览器收藏夹第一位。你可以不看教程,但一定要会看规范。规范里看到Deprecated字样时,说明这个功能已经进入倒计时了,别再用它写新代码。
第二个经验是,学 MCP 不要只学一个 SDK,最好同时看一下协议层是怎么定义消息的。很多教程直接把 API 包装成“神秘黑盒”,遇到版本升级你就完全傻眼。但如果你理解 JSON-RPC 的基础结构,版本再变,你也能自己推断出新写法。
第三个经验是,每季度固定花半天做一次依赖升级和回归测试。协议类依赖特别容易出现连锁变更,晚升不如早升。我在计划任务里加了这条,已经救了我好几次。
第四个经验是,遇到不确定的写法,先问官方示例,再问搜索引擎。搜索引擎只会给你一堆复制粘贴的旧代码,而官方示例永远是紧跟当前版本的。两者冲突时,以官方示例为准。
说实话,MCP 这次把 Session 和 Sampling 拿掉,短期确实让很多人难受,但这恰恰说明协议在往更成熟的方向走。那些还停在 2025 年的教程不是没用,它们教会了我们基础概念;但要真正把 MCP 用好,还是得学会跟着协议版本往前走,而不是守着旧 API 过日子。
我个人的体会是,能不能跟上协议变化,本质上取决于你是在“学一个工具”还是在“学一种思想”。工具会过期,思想不会。新版 MCP 用无状态的请求模型和显式的工具调用,把 Agent 之间的协作方式定义得更干净了。顺着这个思路走,你不仅能看懂新版代码,还能预判下一次协议更新会朝哪个方向去。这比多记几个 API 重要得多。