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

slashslashdev·

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

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

如果只看规范变化,是删掉了 initializeinitializedMcp-Session-Id

从部署方式看,则是把请求和 Session 状态拆开:

旧版:

Request

Session

Server

Application State

变成:

新版:

Request

任意 Server

Application State

业务状态依然存在,只是它不再属于 MCP Session,而属于 job_iduser_idworkspace_id 这样的业务对象。

对 Remote MCP 来说,这意味着 HTTP 请求可以自由地在多个实例之间流动,业务状态则交给 Redis、数据库或者其他业务系统管理。

这也是这次更新对实际部署最直接的影响。

MCPAILLMDistributed Systems
🧑‍💻

开发者周报

关注技术的新变化