返回列表
工程 14 分钟阅读

MCP 取消 Session 之后,状态去了哪里

MCP 2026-07-28 移除了协议级 Session,但没有让 Agent 应用变成“没有状态”的系统。它真正做的是拆开过去混在长连接里的状态,并把它们交给更明确的所有者:

  • MCP
  • Stateless Protocol
  • Domain Handle
  • MRTR
  • Agent Runtime
  • Task State
  • 单次调用所需的上下文,随每个请求传递;
  • 跨调用的业务状态,通过显式 Domain Handle 引用;
  • 中途确认和补充输入,由 MRTR 组织;
  • 长任务状态,由 Tasks Extension 承载;
  • 身份、审批、轨迹和任务编排,继续由 Agent Runtime 管理。

这次变更的核心不是“删除状态”,而是让每一种状态都能被看见、命名、授权、恢复和审计

原始资料:Model Context Protocol,The 2026-07-28 Specification,2026-07-28。
正式规范:MCP Specification 2026-07-28
实现参考:TypeScript SDK 2026-07-28 Migration GuideSEP-2663: Tasks Extension

MCP 究竟取消了什么

旧版 MCP 使用 initialize/initialized 完成能力协商,并通过 Mcp-Session-Id 把之后的请求绑定到同一个协议会话。Server 还可以借助保持打开的连接向 Client 发起 Sampling、Elicitation 或 Roots 请求。

这意味着协议状态与应用状态容易混在一起:

连接属于哪个 Client
Client 支持什么能力
前一次调用创建了什么对象
当前流程等待什么输入
任务执行到哪里

这些信息可能都依赖同一条连接或同一个 Session ID。Server 扩容时,要么保持 Session Affinity,要么把会话状态放入共享存储;实例失效时,客户端还要判断连接与业务状态分别丢失了什么。

2026-07-28 规范取消 initialize/initialized 和 Mcp-Session-Id。每个请求携带协议版本,并通过 _meta 提供客户端身份和能力信息。需要提前了解 Server 能力的客户端可以调用可选的 server/discover。

旧模式
Client ── initialize ──> Server A
Client <== protocol session ==> Server A

新模式
Self-contained Request ──> Server A
Self-contained Request ──> Server B

任意请求可以落到任意健康实例。无状态首先是一项协议属性:Server 不再依赖前一条连接,才能理解当前 MCP 请求。

状态没有消失,而是重新分配

新版协议可以用一张状态所有权表概括:

状态类型 过去常见位置 2026-07-28 的主要归属
请求上下文 initialize 结果与 Session 每个自描述请求
业务对象状态 Server 隐式 Session 显式 Domain Handle
交互状态 保持打开的双向流 MRTR 与 Agent Runtime
长任务状态 长连接或实验性任务机制 Tasks Extension 与任务存储
治理状态 分散在 Client、Gateway、Server Runtime / Gateway 的身份、策略与轨迹

这张表也给出了迁移时最重要的判断方法:任何原来放在 Session 中的字段,都应该先判断它属于哪一种状态,再选择新的承载方式。

请求上下文:让每次调用独立成立

无状态请求必须携带完成当前调用所需的协议信息。规范将协议版本放在 MCP-Protocol-Version Header 中,并把 Client Info 与 Client Capabilities 放入请求 _meta。

它解决的是“这个请求该怎样解释”,而不是“整个任务进行到哪里”。

适合放入请求上下文的内容包括:

  • 协议版本;
  • 客户端产品与版本;
  • 当前请求需要声明的能力;
  • Trace、Idempotency 或 Correlation 标识;
  • 与本次调用直接相关的 Locale、格式或 Feature Flag。

不适合放入请求上下文的内容包括大型工作区状态、完整任务历史、长期凭证和可变业务对象。把所有状态复制到每个请求会形成另一种耦合:Payload 持续膨胀,敏感信息反复传播,状态并发更新也更难控制。

自描述请求的原则是:携带解释当前调用所需的最小上下文,把可变状态留在拥有它的系统中。

代码对比一:握手加 Session,变成单次自描述请求

旧版 2025-11-25 要先初始化:

POST /mcp HTTP/1.1
Content-Type: application/json

{"jsonrpc":"2.0","id":1,"method":"initialize",
 "params":{"protocolVersion":"2025-11-25","capabilities":{},
           "clientInfo":{"name":"my-app","version":"1.0"}}}

Server 签发 Mcp-Session-Id 后,后续调用必须带回它:

POST /mcp HTTP/1.1
Mcp-Session-Id: 1868a90c-3a3f-4f5b
Content-Type: application/json

{"jsonrpc":"2.0","id":2,"method":"tools/call",
 "params":{"name":"search","arguments":{"q":"otters"}}}

新版 2026-07-28 直接发送完整调用:

POST /mcp HTTP/1.1
MCP-Protocol-Version: 2026-07-28
Mcp-Method: tools/call
Mcp-Name: search
Content-Type: application/json

{"jsonrpc":"2.0","id":1,"method":"tools/call",
 "params":{"name":"search","arguments":{"q":"otters"},
           "_meta":{"io.modelcontextprotocol/clientInfo":
                    {"name":"my-app","version":"1.0"}}}}

对负载均衡器而言,差别非常具体:旧请求的正确处理依赖 Mcp-Session-Id 对应的历史;新请求只依赖当前 Header、Body 与外部授权上下文。Mcp-MethodMcp-Name 让 Gateway 无需解析完整 JSON 即可路由,但它们必须与 Body 一致,否则应拒绝请求。

clientInfo 是客户端自报信息,只适合日志、显示与调试,不能代替 Access Token、Principal 或租户校验。把它用于授权,会把“自描述”误实现成“自证明”。

业务状态:用显式 Domain Handle 取代隐藏 Session

官方说明明确指出,取消协议 Session 不要求应用保持无状态。Server 需要跨调用保存业务状态时,可以由 Tool 创建一个显式 Handle,再让模型或 Runtime 在下一次调用中把它作为参数传回。

例如,一次分析任务先创建工作对象:

{
  "name": "analysis/create",
  "arguments": {
    "dataset": "sales-2026-q2"
  }
}

Server 返回:

{
  "analysis_handle": "anl_7f92",
  "expires_at": "2026-08-17T18:00:00Z"
}

下一次 Tool Call 显式引用该对象:

{
  "name": "analysis/run",
  "arguments": {
    "analysis_handle": "anl_7f92",
    "metric": "retention"
  }
}

关键变化在于:过去由传输层暗中关联的状态,现在成为 Tool Schema 中可见的领域对象。

代码对比二:进程内 Session Map,变成显式领域对象

下面是旧实现中常见的写法。业务对象被挂在协议 Session 下,并保存在单实例内存中:

type Basket = { items: Array<{ sku: string; quantity: number }> };

const sessions = new Map<
  string,
  { tenantId: string; basket: Basket }
>();

async function addItem(req: Request, args: { sku: string; quantity: number }) {
  const sessionId = req.headers.get("Mcp-Session-Id");
  const state = sessionId ? sessions.get(sessionId) : undefined;

  if (!state) throw new Error("SESSION_NOT_FOUND");

  state.basket.items.push({ sku: args.sku, quantity: args.quantity });
  return { item_count: state.basket.items.length };
}

这个函数看似简单,实际隐含了三个前提:请求必须回到能访问该 Map 的实例;Session 同时承担连接关联和业务对象定位;并发写入没有版本冲突语义。

新版应把 Basket 设计成 Tool 明确创建、持久化并返回的领域对象。以下是与 MCP SDK 无关的应用层 TypeScript:

type Principal = { tenantId: string; subjectId: string };

type BasketRecord = {
  basketId: string;
  tenantId: string;
  ownerId: string;
  revision: number;
  expiresAt: string;
  items: Array<{ sku: string; quantity: number }>;
};

async function createBasket(principal: Principal) {
  const basket: BasketRecord = {
    basketId: `bsk_${crypto.randomUUID()}`,
    tenantId: principal.tenantId,
    ownerId: principal.subjectId,
    revision: 1,
    expiresAt: new Date(Date.now() + 30 * 60_000).toISOString(),
    items: []
  };

  await basketStore.insert(basket);
  return {
    basket_id: basket.basketId,
    revision: basket.revision,
    expires_at: basket.expiresAt
  };
}

async function addItem(
  principal: Principal,
  args: {
    basket_id: string;
    expected_revision: number;
    idempotency_key: string;
    sku: string;
    quantity: number;
  }
) {
  const replay = await idempotencyStore.get(
    principal.tenantId,
    args.idempotency_key
  );
  if (replay) return replay;

  const current = await basketStore.get(args.basket_id);
  if (!current) throw new Error("BASKET_NOT_FOUND");
  if (
    current.tenantId !== principal.tenantId ||
    current.ownerId !== principal.subjectId
  ) {
    throw new Error("BASKET_FORBIDDEN");
  }
  if (Date.parse(current.expiresAt) <= Date.now()) {
    throw new Error("BASKET_EXPIRED");
  }
  if (current.revision !== args.expected_revision) {
    throw new Error("REVISION_CONFLICT");
  }

  const next: BasketRecord = {
    ...current,
    revision: current.revision + 1,
    items: [...current.items, { sku: args.sku, quantity: args.quantity }]
  };

  await basketStore.compareAndSwap(
    current.basketId,
    current.revision,
    next
  );

  const result = {
    basket_id: next.basketId,
    revision: next.revision,
    item_count: next.items.length
  };
  await idempotencyStore.put(
    principal.tenantId,
    args.idempotency_key,
    result
  );
  return result;
}

这里的 basket_id 只负责定位对象;当前 Principal 决定能否访问,expected_revision 处理并发,idempotency_key 处理网络重试。四项责任彼此独立,任一 Server 实例都可以执行同一调用。

一个合格的 Handle 需要哪些约束

Handle 不能只是一段能够猜测或无限复用的字符串。生产实现至少需要定义:

约束 需要回答的问题
类型与签发方 Handle 指向什么对象,由哪个 Server 签发
Subject Binding 它属于哪个用户、租户、Agent 或授权主体
Scope 允许读取、修改、执行还是结束对象
生命周期 何时过期,能否续期、撤销和回收
Revision 并发调用时基于哪个对象版本执行
可转移性 能否跨 Session、设备、Agent 或 Client 使用
审计关系 哪次 Tool Call 创建、读取或修改了它

Handle 最适合作为不透明引用。模型负责在调用间传递它,Server 负责解释和验证它。Handle 本身不应自动等同于完整授权;Server 仍需校验当前身份、租户和操作 Scope,防止获得 Handle 的其他主体直接继承权限。

并发和重试不能依赖运气

无状态请求可以同时落到多个实例,因此 Domain Handle 指向的对象必须具备明确的一致性语义:

  • 只读调用可以并发;
  • 写调用使用 Revision、ETag 或 Expected Version 检测冲突;
  • 重复请求通过 Idempotency Key 返回同一结果;
  • 已过期或撤销的 Handle 返回可区分错误;
  • Server 故障不能让 Handle 指向无法恢复的单实例内存。

无状态协议降低了连接层复杂度,却会把隐藏的一致性问题暴露出来。这是架构收益:状态越显式,越容易定义恢复和冲突规则。

交互状态:MRTR 把反向调用改成可重试流程

旧版 Elicitation、Sampling 和 Roots 可以通过保持打开的流,由 Server 向 Client 发起请求。新版 Multi Round-Trip Requests 将它改成普通请求/响应序列:

sequenceDiagram participant R as Agent Runtime participant S as MCP Server actor U as User R->>S: tools/call + operation_id S-->>R: input_required + requests R->>U: 展示目标、影响和待确认内容 U-->>R: 批准、拒绝或补充输入 R->>S: 重试原调用 + inputResponses S-->>R: result

Server 返回 resultType: input_required 以及所需输入;Client 收集答案后,把 inputResponses 附加到原调用并重新请求。协议不再要求 Server 为等待用户保持一条双向连接。

代码对比三:等待反向调用,变成显式返回与重入

旧版 Handler 可以在一次调用内部暂停,主动向 Client 请求确认:

const answer = await ctx.mcpReq.elicitInput({
  mode: "form",
  message: "确认删除 3 个文件?",
  requestedSchema: {
    type: "object",
    properties: { approved: { type: "boolean" } },
    required: ["approved"]
  }
});

if (answer.action !== "accept" || !answer.content?.approved) {
  return textResult("已取消");
}
return deleteFiles(args.files);

await 背后需要可用的 Server→Client 通道,以及仍然存活的原始请求。新版在线路上拆成三步。

第一步,Client 发起普通 Tool Call:

{
  "jsonrpc": "2.0",
  "id": 41,
  "method": "tools/call",
  "params": {
    "name": "files/delete",
    "arguments": {
      "operation_id": "op_817",
      "files": ["a.csv", "b.csv", "c.csv"]
    }
  }
}

第二步,Server 不删除文件,只返回待确认输入和一个不透明的 requestState

{
  "jsonrpc": "2.0",
  "id": 41,
  "result": {
    "resultType": "input_required",
    "inputRequests": {
      "confirm_delete": {
        "type": "elicitation",
        "message": "确认删除 3 个文件?",
        "schema": {
          "type": "object",
          "properties": { "approved": { "type": "boolean" } },
          "required": ["approved"]
        }
      }
    },
    "requestState": "signed-opaque-state"
  }
}

第三步,Client 使用新的 JSON-RPC ID 重试原方法和原参数,并附上本轮答案:

{
  "jsonrpc": "2.0",
  "id": 42,
  "method": "tools/call",
  "params": {
    "name": "files/delete",
    "arguments": {
      "operation_id": "op_817",
      "files": ["a.csv", "b.csv", "c.csv"]
    },
    "inputResponses": {
      "confirm_delete": {
        "action": "accept",
        "content": { "approved": true }
      }
    },
    "requestState": "signed-opaque-state"
  }
}

用 TypeScript SDK v2 实现时,Handler 变成一个显式状态机:

import {
  acceptedContent,
  createRequestStateCodec,
  inputRequired
} from "@modelcontextprotocol/server";
import * as z from "zod/v4";

const CONFIRM_SCHEMA = z.object({ approved: z.boolean() });
const CONFIRM_REQUEST = {
  type: "object" as const,
  properties: { approved: { type: "boolean" as const } },
  required: ["approved"]
};

type DeleteState = {
  step: "awaiting-confirmation";
  operationId: string;
  argsHash: string;
};

const REQUEST_STATE_KEY = Buffer.from(
  process.env.MCP_REQUEST_STATE_SECRET!,
  "base64url"
);
if (REQUEST_STATE_KEY.length < 32) {
  throw new Error("MCP_REQUEST_STATE_SECRET_TOO_SHORT");
}

const stateCodec = createRequestStateCodec<DeleteState>({
  key: REQUEST_STATE_KEY,
  ttlSeconds: 300
});

// 创建 Server 时配置:
// { requestState: { verify: stateCodec.verify } }

async function deleteFilesTool(args: DeleteArgs, ctx: ToolContext) {
  const state = ctx.mcpReq.requestState<DeleteState>();
  const argsHash = await stableHash(args.files);

  if (!state) {
    return inputRequired({
      inputRequests: {
        confirm_delete: inputRequired.elicit({
          message: `确认删除 ${args.files.length} 个文件?`,
          requestedSchema: CONFIRM_REQUEST
        })
      },
      requestState: await stateCodec.mint({
        step: "awaiting-confirmation",
        operationId: args.operation_id,
        argsHash
      })
    });
  }

  if (
    state.operationId !== args.operation_id ||
    state.argsHash !== argsHash
  ) {
    throw new Error("APPROVAL_CONTEXT_CHANGED");
  }

  const answer = acceptedContent(
    ctx.mcpReq.inputResponses,
    "confirm_delete",
    CONFIRM_SCHEMA
  );
  if (!answer?.approved) return textResult("已取消");

  return idempotencyStore.runOnce(
    args.operation_id,
    () => deleteFiles(args.files)
  );
}

这段代码表达了四个边界:input_required 前不产生删除副作用;requestState 只保存续跑所需的最小状态;确认绑定原参数摘要;真正执行仍以 operation_id 保证幂等。

requestState 会经过 Client,因此必须视为不可信输入。SDK 的 Codec 提供 HMAC 完整性保护,但不加密 Payload;敏感内容应存入服务端状态库,只在 requestState 中放短期 Operation Handle。多轮交互中,inputResponses 只包含当前一轮答案,前几轮已收集的信息需要进入签名后的 requestState 或服务端持久化对象。

MRTR 的关键不是多一次请求,而是重试语义

实现 MRTR 时需要明确四件事:

  1. 输入返回前是否允许产生副作用。
    最安全的设计是在 input_required 前只完成检查和报价,不创建资源、不发送消息、不删除数据。

  2. 第二次请求怎样识别为同一次操作。
    Client 应保持稳定的 Operation ID 或 Idempotency Key,Server 应避免把重试解释成新的业务动作。

  3. 用户批准的对象是什么。
    Approval 应绑定具体 Tool、参数、目标、费用、作用域和有效期。参数发生变化时重新确认。

  4. 拒绝、取消、超时怎样结束流程。
    三者需要可区分状态,方便 Runtime 决定终止、重规划或再次询问。

如果首次请求必须产生准备性状态,Server 应返回显式 Continuation Handle,并为其定义过期与清理规则。这样,中间状态仍然是领域对象,而不是重新藏回连接。

长任务状态:Tasks Extension 取代连接生命周期

长时间运行的数据处理、代码构建或外部工作流不能依赖 HTTP 连接一直存活。新版把 Tasks 从 2025-11-25 的实验性核心能力移入 io.modelcontextprotocol/tasks 扩展。

旧方案要求 Client 先通过 tools/list 了解方法与具体 Tool 是否支持 Task,再决定是否给请求增加 Task 参数。遇到 input_required 时,Client 还要提前调用会阻塞的 tasks/result,让 Server 获得可发送 Elicitation 或 Sampling 的 SSE 通道。能力发现、执行和连接生命周期因此相互耦合。

新版只保留三项任务操作:

  • tasks/get:读取任务当前状态、待处理输入或最终结果;
  • tasks/update:Client 回传任务等待的 inputResponses
  • tasks/cancel:表达取消意图。

tasks/list 被移除,因为无 Session 架构下很难为“列出当前调用者的全部任务”定义天然且安全的作用域。Task ID 应作为不可猜测的 Handle,并继续绑定当前授权主体。

代码对比四:阻塞等待,变成可恢复轮询

Client 在当前 tools/call 中声明 Tasks Extension。Server 可以自行决定立即返回结果,还是创建 Task:

{
  "jsonrpc": "2.0",
  "id": 101,
  "method": "tools/call",
  "params": {
    "name": "dataset/import",
    "arguments": { "source_uri": "s3://tenant-a/orders.parquet" },
    "_meta": {
      "io.modelcontextprotocol/clientCapabilities": {
        "extensions": { "io.modelcontextprotocol/tasks": {} }
      }
    }
  }
}

Task 必须在 Server 返回 Handle 前持久化完成,保证 Client 随即调用 tasks/get 时已经能够读到:

{
  "jsonrpc": "2.0",
  "id": 101,
  "result": {
    "resultType": "task",
    "taskId": "task_42",
    "status": "working",
    "statusMessage": "正在导入数据",
    "createdAt": "2026-08-17T10:00:00Z",
    "lastUpdatedAt": "2026-08-17T10:00:00Z",
    "ttlMs": 86400000,
    "pollIntervalMs": 5000
  }
}

Client 按 Server 建议的间隔轮询,而不是保持原始 Tool Call:

{
  "jsonrpc": "2.0",
  "id": 102,
  "method": "tasks/get",
  "params": { "taskId": "task_42" }
}

任务完成时,tasks/get 在同一对象中返回终态和原始 tools/call 对应的结果:

{
  "jsonrpc": "2.0",
  "id": 102,
  "result": {
    "resultType": "complete",
    "taskId": "task_42",
    "status": "completed",
    "statusMessage": "导入完成",
    "createdAt": "2026-08-17T10:00:00Z",
    "lastUpdatedAt": "2026-08-17T10:04:12Z",
    "ttlMs": 86400000,
    "result": {
      "content": [
        { "type": "text", "text": "已导入 12,480,331 行" }
      ]
    }
  }
}

如果任务进入 input_requiredtasks/get 会带回 inputRequests;Client 收集答案后调用 tasks/update

{
  "jsonrpc": "2.0",
  "id": 103,
  "method": "tasks/update",
  "params": {
    "taskId": "task_42",
    "inputResponses": {
      "resolve_schema_conflict": {
        "action": "accept",
        "content": { "strategy": "create_nullable_column" }
      }
    }
  }
}

tasks/update 只确认接收输入,不直接宣告任务已经推进到某个状态。Worker 更新状态库,Client 再通过 tasks/get 读取新的事实。这个区分避免 Client 越权修改服务端任务状态,也允许执行系统采用最终一致的 Worker 与队列。

协议只定义交互边界,任务状态仍需要可靠存储。一个可运营的任务对象至少应具备:

task:
  task_id: task_42
  owner: tenant-a
  operation: dataset.import
  status: running
  progress_ref: progress_18
  created_at: 2026-08-17T10:00:00Z
  expires_at: 2026-08-18T10:00:00Z
  result_ref: null

具体字段由实现决定,但责任不能模糊:

  • Task ID 绑定用户和租户,不能仅凭 ID 越权查询;
  • 状态迁移保持单调或具备明确回退原因;
  • Worker 重启后任务可以继续、重试或进入确定失败;
  • 任务取消与业务回滚分别建模;
  • 最终结果引用对应 Artifact、外部状态或完成证据;
  • 轮询和订阅看到同一个事实源。

Tasks 的意义不是为长连接换一个名称,而是让长任务的生命周期独立于网络连接和单个 Server 实例。

其他规范变化只承担辅助作用

Header、缓存和 OAuth 变化很重要,但它们服务于无状态核心,无需再次展开成完整 Gateway 架构。

变化 直接作用
Mcp-Method、Mcp-Name Header Gateway 可直接路由、限流和计量
ttlMs、cacheScope、确定性排序 Tool、Prompt 与 Resource 结果可以稳定缓存
RFC 9207 iss 校验 防止 Authorization Server Mix-up
Credential 与 Issuer 绑定 限制凭证跨授权服务器复用
CIMD 替代 DCR 让 Client Metadata 管理更可控
正式 Extensions Framework 让 Tasks、Apps、企业授权独立演进

Header 与 JSON-RPC Body 必须保持一致;Cache Key 必须包含身份与 Scope;OAuth Credential 必须绑定正确 Issuer。这些是无状态部署的配套约束。

Tool Catalog、Top-K Discovery、Registry 和 Gateway 适用边界已在《Agent 工具规模化之后,为什么需要 MCP Gateway》中展开。本文只讨论协议状态迁移。

迁移时不要直接删除 Session Store

从旧协议迁移时,最危险的做法是先删除 Session,再逐个修复出现的错误。更可靠的顺序是先完成状态盘点。

第一步:列出 Session 中的全部字段

记录字段的写入者、读取者、更新频率、敏感度、过期规则和故障恢复方式。

第二步:为每个字段重新分类

字段性质 新位置
解释单次请求所需 Request Header 或 _meta
指向业务对象 Domain Handle
等待用户补充输入 MRTR
描述长任务进度 Tasks Store
身份、审批、策略与轨迹 Agent Runtime / Gateway
只为连接维护 删除

第三步:定义失败语义

重点明确:

  • 请求超时后是否可以安全重试;
  • Handle 过期后能否恢复;
  • 两个请求同时修改同一对象时如何裁决;
  • input_required 前是否已经发生副作用;
  • Task Worker 失效后由谁接管;
  • Client 重连后从哪里恢复任务状态。

第四步:用故障场景验收

迁移测试至少覆盖:

  1. 连续请求被分配到不同 Server 实例;
  2. 请求在得到响应前超时并重试;
  3. 同一 Idempotency Key 被重复提交;
  4. Handle 过期、撤销、跨租户和版本冲突;
  5. MRTR 的批准、拒绝、修改参数和超时;
  6. 长任务期间 Worker 与 Gateway 分别重启;
  7. Polling 与 Subscription 返回一致状态;
  8. 旧 Client 与新 Server 的版本协商和明确失败。

只有这些场景通过,无状态化才真正带来可靠性,而不是把原来的 Session Bug 转移成分布式状态 Bug。

新规范没有解决什么

2026-07-28 规范明确了通信方式,但仍有几项责任留给 Agent 平台:

  • 哪些工具应该对当前 Agent 可见;
  • 多个工具和 Skill 如何编排;
  • 高风险动作由谁批准;
  • Domain Handle 应采用怎样的一致性模型;
  • Task 完成需要哪些证据;
  • 执行轨迹怎样关联用户、规则和外部副作用。

因此,无状态 MCP Server 不能替代 Agent Runtime。协议负责让请求独立、状态显式;Runtime 负责把这些能力组织成可恢复、可授权、可验证的任务。

结语

MCP 取消 Session 之后,状态去了五个地方:

请求上下文 → 自描述 Request
业务状态   → Domain Handle
交互状态   → MRTR
长任务状态 → Tasks Extension
治理状态   → Agent Runtime / Gateway

这比“有状态还是无状态”的二选一更准确。新版 MCP 移除的是协议对隐式连接状态的依赖,并没有消除业务状态。它要求系统为每一种状态明确所有者、标识、权限、生命周期、并发和恢复语义。

当这些责任被正确分配,MCP Server 才能真正实现任意实例处理、普通负载均衡和故障恢复;Agent Runtime 也获得更清晰的状态边界。

交互式图表

放大查看

使用 + / − 缩放,按 0 适应窗口;放大后可拖动或滚动查看,Esc 关闭。