MCP状态与会话
4589 字约 15 分钟
AIAgentMCP状态管理
2026-07-24
MCP 协议本身定义了连接建立、能力协商和消息交换的标准流程,但对"状态应该存放在哪里、如何管理、如何恢复"并没有给出统一规定。状态管理是 MCP Server 和 Client 设计中最容易出问题的环节——设计不当会导致数据不一致、会话泄漏、断线后无法恢复等问题。
1. 无状态 Server vs 有状态 Server
MCP Server 可以选择两种基本的状态策略:
| 维度 | 无状态 Server | 有状态 Server |
|---|---|---|
| 请求处理 | 每次请求独立,不依赖上下文 | 维护会话信息,请求间有依赖 |
| 扩展性 | 水平扩展简单,任意实例可处理任意请求 | 需要会话粘性(sticky session)或共享状态 |
| 故障恢复 | 天然容错,重试即可 | 需要恢复机制,否则丢失上下文 |
| 复杂度 | 低 | 高,需要管理生命周期、过期、清理 |
| 适用场景 | 查询、独立操作、计算类工具 | 交互式编辑、多步向导、流式处理 |
优先选择无状态。如果可以通过参数传递上下文(如把文件路径、游标位置作为参数传入),就不要在 Server 端维护会话状态。有状态设计只在以下情况必要:
- 操作天然有顺序依赖(如事务性多步写入)
- 状态体积大,不适合每次由 Client 传递
- 需要服务端主动推送通知(如进度更新)
2. Client 会话管理
MCP Client 需要同时管理与多个 Server 的连接会话。每个连接是一个独立的会话单元。
Client 侧的会话管理职责:
- 连接池管理:维护与每个 Server 的连接,处理重连和超时
- 能力缓存:缓存 Server 声明的能力列表,减少重复发现请求
- 请求追踪:将异步请求与响应正确匹配(通过 JSON-RPC 的
id字段) - 会话生命周期:初始化握手 → 正常运行 → 优雅关闭
3. 用户会话 vs 业务会话 vs 连接会话
MCP 系统中存在三种不同层次的"会话",混淆它们是常见的设计错误:
| 会话类型 | 生命周期 | 绑定对象 | 典型内容 |
|---|---|---|---|
| 连接会话 | 随连接建立和断开 | Client ↔ Server 连接 | 传输状态、协议版本、能力协商结果 |
| 用户会话 | 用户登录到登出 | 用户身份 | 认证 token、用户偏好、权限范围 |
| 业务会话 | 业务操作开始到结束 | 具体业务任务 | 多步操作的中间状态、事务上下文 |
关键区分:
- 连接断开 ≠ 用户会话结束。用户重新连接后应能恢复业务会话
- 一个用户会话可能跨越多个连接会话(断线重连场景)
- 一个用户会话可能包含多个并行的业务会话
- 业务会话的状态不应绑死在连接上,否则断线即丢失
4. 临时状态 vs 持久状态
| 状态类型 | 存储位置 | 生命周期 | 示例 |
|---|---|---|---|
| 临时状态 | 内存 | 单次请求或单次会话 | 请求上下文、中间计算结果、分页游标 |
| 半持久状态 | 内存 + 过期机制 | 会话级别,有 TTL | 交互式编辑上下文、多步向导进度 |
| 持久状态 | 数据库或文件 | 跨会话保留 | 用户配置、操作历史、已创建的资源 |
设计原则:
- 临时状态随请求结束自动清理,不泄漏
- 半持久状态必须设置 TTL(Time-To-Live),避免无限累积
- 持久状态需要明确的存储、索引和清理策略
- 能从已有数据推导的状态不要额外存储
5. 连接状态
连接状态是 MCP 协议层面需要维护的信息:
初始化阶段状态:
- 协议版本(
protocolVersion) - 双方能力声明(
capabilities) - 服务器信息(
serverInfo)
运行阶段状态:
- 活跃请求的 ID 映射
- SSE 流的连接状态(HTTP+SSE 传输方式)
- 通知的订阅和分发状态
连接状态的特点:
- 完全属于传输层,不应泄漏到业务逻辑
- 连接断开后,这些状态应全部可丢弃
- 重连后需要重新初始化握手,不能假设旧连接状态仍然有效
6. 任务状态
当工具调用涉及长时间运行的操作时,需要管理任务状态:
任务状态生命周期:
提交 → 排队 → 执行中 → [成功 | 失败 | 取消]任务状态管理的关键问题:
- 进度追踪:Server 通过通知(notification)向 Client 推送进度更新
- 取消机制:Client 发送取消请求,Server 需要能够中断正在执行的任务
- 超时控制:设置合理的超时时间,超时后自动清理任务资源
- 结果保留:任务完成后,结果应保留一段时间供 Client 获取,而非立即丢弃
对于跨多次工具调用的复杂任务,考虑将任务状态编码为 Resource URI,而不是在 Server 内存中维护:
# 不推荐:Server 内存中维护任务状态
# Client 断开后状态丢失
# 推荐:用 Resource URI 标识任务状态
# task://import-job/abc123/status
# Client 可以随时通过 URI 查询任务进度,Server 可以无状态处理7. 断线恢复
断线是分布式系统中的常态,MCP 设计必须考虑断线后的状态恢复。
| 状态类型 | 能否恢复 | 恢复方式 |
|---|---|---|
| 连接会话状态 | ❌ 不可恢复 | 重新初始化握手 |
| 能力协商结果 | ✅ 可恢复 | 重新执行能力发现 |
| 用户认证状态 | ✅ 可恢复 | 使用持久化的 token 重新认证 |
| 业务会话状态 | ⚠️ 取决于设计 | 需要持久化到存储层 |
| 正在执行的任务 | ⚠️ 部分可恢复 | 幂等操作可重试;非幂等操作需要检查中间状态 |
| 内存中的临时状态 | ❌ 不可恢复 | 需要 Client 重新提供上下文 |
设计建议:
- 把可恢复的状态和不可恢复的状态明确分开
- 对于不可恢复的状态,在断线时向 Client 返回明确的错误信息
- 对于幂等操作,支持安全重试
- 对于非幂等操作,提供状态查询接口让 Client 判断操作是否已完成
8. 多实例一致性
当 MCP Server 部署多个实例时,状态管理面临一致性挑战:
无状态 Server 的多实例:
- 天然支持多实例部署
- 任意实例可处理任意请求
- 无需实例间状态同步
有状态 Server 的多实例:
| 策略 | 做法 | 代价 |
|---|---|---|
| 会话粘性 | 同一用户的请求路由到同一实例 | 实例故障时该用户不可用 |
| 共享存储 | 状态存入 Redis 或数据库 | 增加延迟和基础设施复杂度 |
| 状态广播 | 实例间同步状态变更 | 网络开销大,一致性窗口长 |
实践建议:
- 优先设计为无状态,通过外部存储(数据库、Redis)管理需要共享的状态
- 如果必须使用内存状态,使用会话粘性 + 故障转移作为降级方案
- 避免在实例间直接同步状态,复杂度高且容易出错
9. 状态过期和清理
状态不会自动消失,必须有明确的清理机制。
过期策略:
| 状态类型 | 建议 TTL | 清理方式 |
|---|---|---|
| 请求级临时状态 | 请求结束即清理 | finally 块或上下文管理器 |
| 会话级状态 | 5-30 分钟(视业务而定) | 定时扫描 + 惰性检查 |
| 任务结果 | 5-60 分钟 | 定时任务清理过期结果 |
| 用户缓存 | 1-24 小时 | LRU 淘汰 + TTL |
清理机制:
- 惰性清理:访问时检查是否过期,过期则清理。简单但可能导致过期状态堆积
- 定时清理:后台任务定期扫描并清理过期状态。及时但增加系统复杂度
- 事件驱动清理:监听连接断开、会话结束等事件,触发即时清理。最精确但需要完善的事件处理
必须清理的状态:
- 已断开连接的会话数据
- 超时未完成的任务状态
- 临时文件和子进程
- 外部系统的锁或租约
- 内存中的缓存条目
不清理的后果:内存泄漏、文件句柄耗尽、外部系统资源占用、安全风险(过期 token 残留)。
10. 幂等键(Idempotency Key)
幂等键是解决"重复请求产生重复副作用"问题的标准方案。
工作原理:
1. Client 生成唯一键(如 UUID),附加到请求中
2. Server 收到请求,先检查是否已处理过该键
3. 如果已处理:直接返回之前的结果
4. 如果未处理:正常执行,记录键和结果适用场景:
- 创建资源(防止重复创建)
- 支付或转账(防止重复扣款)
- 提交操作(防止重复提交)
设计要点:
| 要素 | 说明 |
|---|---|
| 键的生成 | 由 Client 生成,使用 UUID v4 或类似方案 |
| 键的传递 | 通过请求参数或请求头传递 |
| 键的存储 | Server 端存储已处理的键,设置 TTL(建议 24 小时) |
| 冲突处理 | 同一个键的不同参数,以首次请求为准 |
| 结果返回 | 重复请求返回首次执行的结果,不重新执行 |
在 MCP Tool 定义中,建议在参数 Schema 中包含可选的 idempotency_key 字段,并在工具描述中说明该工具是否支持幂等。
11. 分布式锁
当多个 Client 或 Server 实例可能并发操作同一资源时,需要分布式锁来保证一致性。
常见场景:
- 多个 Client 同时编辑同一文件
- 多个 Server 实例同时处理同一用户的请求
- 并发创建同名资源
锁的设计选择:
| 方案 | 适用场景 | 特点 |
|---|---|---|
| 数据库行锁 | 已有数据库的场景 | 简单可靠,性能受数据库限制 |
| Redis 分布式锁 | 高并发场景 | 性能好,需要处理锁过期和续期 |
| 文件锁 | 本地单实例场景 | 简单,不适合分布式环境 |
| 乐观锁 | 冲突概率低的场景 | 无锁等待,冲突时重试 |
注意事项:
- 锁必须设置超时时间,防止死锁
- 持有锁的实例崩溃时,锁必须能自动释放(TTL 机制)
- 锁的粒度要合理——太粗影响并发,太细增加复杂度
- 在 MCP Server 中,优先通过工具设计避免冲突(如使用唯一 ID 创建资源),而非依赖锁
12. 会话隔离
不同用户、不同连接、不同任务之间的状态必须隔离。
隔离维度:
- 用户隔离:用户 A 不能访问用户 B 的会话状态
- 连接隔离:同一用户的不同连接不互相干扰
- 任务隔离:同一连接中的不同任务有独立的状态空间
隔离失败的风险:
- 数据泄漏:一个用户看到另一个用户的数据
- 状态污染:一个任务的操作影响另一个任务的结果
- 权限越界:低权限用户获取高权限用户的上下文
实现方式:
- 使用命名空间或前缀隔离不同维度的状态
- 每次访问状态时验证当前上下文是否有权访问
- 在异步操作中传递上下文(context),避免使用全局变量存储会话状态
13. 多租户场景
当同一个 MCP Server 服务于多个租户(如不同团队、不同客户)时,状态管理需要额外考虑:
隔离级别选择:
| 级别 | 隔离方式 | 适用场景 |
|---|---|---|
| 数据库级隔离 | 每个租户独立数据库 | 强隔离需求,合规要求 |
| Schema 级隔离 | 同一数据库不同 Schema | 中等隔离,便于管理 |
| 行级隔离 | 同一表通过租户 ID 区分 | 成本最低,需要严格的查询过滤 |
多租户状态管理要点:
- 每个租户的能力配置可能不同(不同的工具集、不同的权限)
- 租户间的状态必须严格隔离,任何跨租户访问都是安全漏洞
- 限流和配额应基于租户维度,而非全局
- 租户数据清理需要完整,不能残留
14. 状态与权限绑定
状态不是中性的——状态的访问必须受权限约束。
关键原则:
- 状态的存在不代表可以被任意访问。即使状态在内存中,访问前也要检查权限
- 用户 A 创建的会话状态,用户 B 不能通过猜测 ID 来访问
- 权限变更应立即反映到状态访问上。用户被降级后,不应继续访问高权限状态
- 敏感状态(如包含凭证或个人信息)需要加密存储
权限检查时机:
创建状态 → 检查创建权限
读取状态 → 检查读取权限
修改状态 → 检查修改权限
跨用户访问 → 拒绝(除非明确授权)
状态过期 → 自动清理,无需权限检查15. 状态管理层全景
蓝色 = Client 侧管理 | 橙色 = Server 侧管理 | 绿色 = 持久层存储
16. 设计原则
- 优先无状态:能通过参数传递的上下文,就不要在 Server 端维护
- 状态分层:区分连接状态、用户状态、业务状态,各自独立管理
- 明确生命周期:每种状态都要有创建、使用、过期的完整路径
- 为断线而设计:假设连接随时可能断开,确保关键状态可恢复
- 隔离是底线:不同用户、不同租户、不同任务的状态必须隔离
- 权限绑定状态:访问状态前必须检查权限,权限变更即时生效
- 清理不可省略:每个创建状态的地方都要有对应的清理逻辑
- 幂等优于锁:优先通过幂等设计避免并发问题,锁是最后手段
17. 常见误区
| 误区 | 问题 | 正确做法 |
|---|---|---|
| 把所有状态放在内存 | 重启即丢失,无法多实例 | 关键状态持久化到外部存储 |
| 连接断开 = 会话结束 | 用户体验差,丢失业务上下文 | 连接状态和业务状态分开管理 |
| 不设 TTL | 内存无限增长,最终 OOM | 所有临时状态设置合理的过期时间 |
| 忽略状态隔离 | 数据泄漏,安全事故 | 每次状态访问都做权限校验 |
| 用全局变量存会话状态 | 并发场景下状态互相覆盖 | 使用请求级上下文传递状态 |
| 重复请求不做幂等 | 网络重试导致重复创建 | 关键写操作支持幂等键 |
| 依赖连接维持业务状态 | 断线即丢失所有进度 | 业务状态独立于连接,支持查询和恢复 |
18. 检查清单
设计 MCP 状态管理时,逐项确认: