MCP 终于无状态了,这意味着什么?
slashslashdev·

MCP 2026-07-28 版本里,一个对 Remote MCP 部署影响很大的变化是:HTTP 传输层不再依赖 MCP Session。
如果用过旧版 Streamable HTTP,下面几个概念应该很熟:
initialize
initialized
Mcp-Session-Id
Sticky Session
Session Store客户端先 initialize,服务端创建 Session,再返回 Mcp-Session-Id。之后的请求都带着这个 ID。
本地开发没什么感觉,但一旦 MCP 服务部署到 Kubernetes、Serverless,或者放到负载均衡后面,问题就出来了。
第一次请求到了 Server A:
Load Balancer
│
▼
Server A
│
▼
MCP Session会话状态存在 A 上,后续请求也最好继续到 A。
于是原本普通的 HTTP 请求:
Request → 任意实例 → Response变成了:
Request
↓
找到 Session
↓
找到对应实例
↓
处理请求这也是旧版 Remote MCP 经常需要 Sticky Session,甚至额外维护 Session Store 的原因。
旧版为什么依赖 Session
在单实例环境里,Session 的存在不太容易带来问题。
但到了多实例环境,请求和 Session 状态就产生了绑定关系:
Request
↓
Session
↓
Server A如果下一次请求到了 Server B,它拿不到 Server A 上的 Session 状态,处理就会出现问题。
所以部署 Remote MCP 时,通常需要:
Load Balancer
↓
Sticky Session
↓
MCP Server或者把 Session 状态放到 Redis 之类的共享存储中。
对于一个本来可以无状态扩展的 HTTP 服务来说,这多了一层基础设施。
2026-07-28 改了什么
新版本移除了:
initialize/initialized握手Mcp-Session-Id
请求不再需要先建立一个 MCP Session。
例如调用工具时,直接发送请求:
POST /mcp
MCP-Protocol-Version: 2026-07-28
Mcp-Method: tools/call
Mcp-Name: search
Content-Type: application/json请求体仍然使用 JSON-RPC:
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "search",
"arguments": {
"q": "MCP"
}
}
}如果需要了解服务端有哪些能力,可以调用 server/discover。
这样部署结构就简单很多:
Load Balancer
/ | \
▼ ▼ ▼
MCP-1 MCP-2 MCP-3第一次请求到 MCP-1,下一次到 MCP-3,也不需要考虑 Session 路由。
对于 Remote MCP 来说,最直接的变化就是:MCP 服务可以直接放到普通的无状态 HTTP 基础设施后面。
业务状态放到哪里
传输层无状态,并不意味着业务系统不能保存状态。
需要改变的是状态的归属:不要把业务状态放进 MCP Session。
比如一个数据导出工具,旧的写法可能是:
async def export(ctx):
job = await start_export()
ctx.session.export_job = job
return "export started"状态关系变成:
export_job
↓
MCP Session
↓
某个 Server请求换到另一个实例后,就需要找到原来的 Session。
更合适的方式是直接返回业务 ID:
async def export():
job = await start_export()
return {
"job_id": job.id,
"status": "running"
}客户端拿到:
{
"job_id": "job_123",
"status": "running"
}之后查询任务:
async def export_status(job_id: str):
job = await get_job(job_id)
return {
"job_id": job.id,
"status": job.status
}真正的状态放在 Redis、数据库或者任务系统里:
MCP Server
│
job_id
│
▼
┌──────────────┐
│ Redis / DB │
└──────────────┘这样请求无论落到 Server A 还是 Server C,都能通过 job_id 找到同一个业务对象。
以前是:
MCP Session → 找状态现在是:
Business ID → 找状态长任务和用户确认怎么处理
长任务也可以使用同样的方式。
例如生成报表、导出数据库这类任务,可以返回任务 ID:
tools/call
↓
task_id
↓
tasks/get任务状态由业务系统保存,MCP 负责提供调用接口。
用户确认也不需要依赖一条长期存在的 Session。
比如删除文件时需要先问用户:
Client
│
│ tools/call
▼
Server
│
│ input_required
▼
Client
│
│ 用户确认
│
│ inputResponses
▼
Server
│
▼
Result整个过程可以通过多次 HTTP 请求完成。
2026-07-28 版本里的 Tasks 也被调整为扩展能力,不再是核心协议的一部分。
网关层还有两个变化
新版本还增加了:
Mcp-Method: tools/call
Mcp-Name: search以前网关想知道 MCP 请求调用的是哪个工具,需要解析 JSON 请求体。现在可以直接从 Header 获取方法和工具名称,在网关层做路由、鉴权、限流等处理。
另外,服务发现也从过去的初始化流程中独立出来,需要了解服务能力时可以使用 server/discover。
这些改动和 Session 移除放在一起看,都是在减少传输层对请求状态的依赖。
现有 Remote MCP 检查这三处
如果已经有 MCP 服务,不需要因为这次规范更新就重写一遍。可以先检查三个地方。
1. 业务状态有没有放进 Session
看看代码里有没有类似:
session.user
session.job
session.cart
session.context如果有,可以改成:
user_id
job_id
cart_id
workspace_id让业务层负责状态的存取。
2. 部署是不是依赖 Sticky Session
如果架构是:
Load Balancer
↓
Sticky Session
↓
MCP Server检查一下这个依赖是不是只为维护 MCP Session。
如果业务状态已经独立存储,那么这层粘性路由可能就不再需要。
3. 长任务有没有独立的业务 ID
如果现在还是:
tools/call
↓
一直等待
↓
最终返回可以考虑改成:
tools/call
↓
task_id
↓
tasks/get让任务生命周期和 HTTP 连接分开。
从 Session 到业务 ID
如果只看规范变化,是删掉了 initialize、initialized 和 Mcp-Session-Id。
从部署方式看,则是把请求和 Session 状态拆开:
旧版:
Request
↓
Session
↓
Server
↓
Application State变成:
新版:
Request
↓
任意 Server
↓
Application State业务状态依然存在,只是它不再属于 MCP Session,而属于 job_id、user_id、workspace_id 这样的业务对象。
对 Remote MCP 来说,这意味着 HTTP 请求可以自由地在多个实例之间流动,业务状态则交给 Redis、数据库或者其他业务系统管理。
这也是这次更新对实际部署最直接的影响。