MCP 2026-07-28 简明指南:从本地连接到无状态 HTTP

MCP 最早让人容易理解的地方,是它把 AI 应用连接外部系统这件事标准化了。一个 Host 不必分别理解 GitHub、Slack、数据库、内部工单系统各自的 API;外部系统实现 MCP server,Host 通过 MCP client 去发现能力、读取上下文、调用工具。

在本地开发环境里,这个模型很顺。Host 启动一个本地 server 进程,通过 stdio 交换 JSON-RPC 消息;连接从哪里来、什么时候结束、能力怎么协商,都可以围绕这一条进程连接来组织。

但 MCP 现在面对的已经不只是本地工具。远程 MCP server、企业 connector、serverless、edge、网关后的内部服务,都会把同一个问题放大:如果协议把上下文藏在连接或 session 里,部署和恢复就会越来越重。

2026-07-28 这版规格的主线,就是把 MCP 的核心协议重新整理成更适合远程 HTTP 服务的形态:连接只负责传输消息,请求自己说明上下文;长任务、交互 UI、企业授权这类复杂能力,则放到更明确的扩展和授权机制里。

MCP 的基本结构:Host、Client 和 Server

MCP 的基本关系仍然是:

Host → Client → Server

这三个角色分别做不同的事:

  • Host 是用户正在使用的 AI 应用,比如 Claude、IDE、agent 平台。
  • Client 是 Host 里面负责连接某个 MCP server 的连接器。
  • Server 是外部能力提供方,可以是 GitHub、Slack、数据库,也可以是企业内部系统。

Server 暴露给 Host 的基础能力也没有变:

能力 更自然的理解 典型用途
Tools 让模型执行动作 查 issue、创建 PR、部署项目、调用业务 API
Resources 给模型读取上下文 文件、schema、业务记录、文档片段
Prompts 给用户一个工作流入口 /review-pr/write-release-notes/debug-test

一个 GitHub MCP server 可以暴露 search_issuescreate_pull_requestread_file 等工具。Host 不需要写死 GitHub API 的每个端点,只需要通过 MCP 发现这些工具,理解参数 schema,再在合适的时候调用。

所以这次更新不是推翻 MCP 的抽象,而是调整这些抽象在远程部署时如何被承载。

本地连接模型为什么不适合远程服务

早期 MCP 很适合本地连接。典型流程是:Host 启动一个 server,双方先做初始化握手,再在同一个 session 里继续通信。

client → server: initialize
server → client: capabilities
client → server: notifications/initialized
后续请求都发生在这个 session 里

在本地进程里,这样写很自然。进程通常由 Host 拉起,生命周期也比较清楚;一条连接对应一个使用场景,server 在连接上保存一点状态,问题不大。

远程 HTTP 服务就不一样了。server 可能跑在多副本后面,也可能跑在 serverless 或 edge 环境里,请求之间不一定落到同一个实例。网络断开、负载均衡、重试、超时,都会让“这条连接以前初始化过”变成一个需要额外维护的事实。

如果协议层依赖 session,server 就要不断追问:

  • 这个 client 初始化过没有?
  • 它协商的是哪个协议版本?
  • 它支持哪些 capabilities?
  • 这次请求是不是属于之前那个 session?
  • 连接断了以后,旧的 stream 还要不要恢复?
  • 多副本部署时,session 状态应该存在谁那里?

这些问题当然可以解决,但代价是 MCP server 会被推向更复杂的长连接服务。对标准 HTTP、serverless、edge 和企业网关来说,这不是最轻的形态。

无状态核心:请求自己说明上下文

2026-07-28 的核心变化,是让 MCP 的协议层变成更明确的无状态核心。

这里的“无状态”不是说业务系统不能有状态,也不是说不能有长任务、文件句柄、用户会话或审批流程。它强调的是另一件事:协议层不要把连接身份当成上下文来源;server 处理请求时,应该从当前请求本身拿到必要的协议信息。

因此新版移除了 initialize / notifications/initialized 握手,也移除了 Streamable HTTP 里的 Mcp-Session-Id。每个请求都要携带协议版本和 client capabilities,放在 _meta 里:

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/list",
  "params": {
    "_meta": {
      "io.modelcontextprotocol/protocolVersion": "2026-07-28",
      "io.modelcontextprotocol/clientCapabilities": {},
      "io.modelcontextprotocol/clientInfo": {
        "name": "ExampleClient",
        "version": "1.0.0"
      }
    }
  }
}

这样 server 收到请求时,不需要先检查“这条连接以前初始化过没有”。它直接看当前请求里的 _meta,就能知道 client 使用的协议版本、能力声明和基本身份信息。

如果业务确实需要跨请求状态,也不是不能做,而是要显式表达。例如长任务返回 taskId,后续请求再带着 taskId 查询进度;某份上下文用 resource URI 标识;某个业务流程用 server 自己签发的 handle 表达。

第一次请求:创建部署任务,返回 taskId
后续请求:tasks/get(taskId) 查询状态或结果

状态还在,但它从“藏在连接里”变成了“由应用层明确管理”。这就是新版无状态核心最重要的边界。

Streamable HTTP:每条消息都是一次 POST

无状态核心落到 HTTP 上,形态就很直接:每条 MCP 消息都是一次新的 HTTP POST。

client --POST /mcp--> server
server --JSON response 或 request-scoped SSE--> client

如果请求很快完成,server 直接返回 JSON-RPC response。
如果请求执行过程中需要进度或请求相关通知,server 可以在这一次请求的响应里返回 SSE stream,最后再给出最终响应。

这次规格同时去掉了旧的几类传输机制:

  • 不再有 HTTP GET stream endpoint;
  • 不再有协议层 session;
  • 不再使用 Mcp-Session-Id
  • 不再用 Last-Event-ID 恢复断掉的 SSE stream。

如果 response stream 中断了,client 不能指望从旧 event ID 继续接上,而是要用新的 JSON-RPC ID 重新发起请求。

新版 Streamable HTTP 还要求 POST 请求带标准 MCP HTTP header,例如 MCP-Protocol-VersionMcp-Method,在 tools/callresources/readprompts/get 这类请求里还会带 Mcp-Name。这些 header 不是用来恢复 session,而是让 HTTP 层、代理、网关和日志系统能更清楚地识别请求。

也就是说,连接仍然可以承载流式响应,但连接不再代表会话本身。

没有初始化握手后,版本怎么协商

移除 initialize 后,版本和能力协商不能再依赖“先建立一条会话”。新版给了一个更直接的入口:

server/discover

Server 必须实现 server/discover,用来告诉 client:

  • server 支持哪些 protocol version;
  • server 暴露哪些 capabilities;
  • server 的名称、版本等身份信息。

Client 可以在正式调用前先请求 server/discover,提前选出双方都支持的版本。它也可以不先 discover,直接发业务请求;如果版本不匹配,server 返回 UnsupportedProtocolVersionError,并列出自己支持的版本,client 再选择共同版本重试。

这个设计和无状态核心是一致的:协商不再绑定到某条连接,而是变成“请求声明版本;不兼容就明确报错并重试”。

一次请求不够时:MRTR 如何保持多轮交互

无状态请求不等于所有事情都必须一轮完成。很多真实流程会在中途缺信息。

比如用户让 agent 预订机票,server 发现缺少出发日期。旧思路可能是 server 反过来向 client 发起一个请求,让 client 去问用户。这样协议就会变成双方都能主动请求,复杂度很快上升。

新版用 MRTR,也就是 Multi Round-Trip Requests,把这类流程仍然放回清楚的请求-响应模式里。

Server 可以先返回一个中间结果:

{
  "resultType": "input_required",
  "inputRequests": {
    "date": {
      "type": "elicitation",
      "message": "请选择出发日期"
    }
  }
}

Client 收集到用户输入后,再重试原来的请求,并带上 inputResponses

client request → server: 需要更多输入
client request + inputResponses → server: 完成处理

这样看起来是多轮,但每一轮仍然是 client 发起请求、server 返回结果。MRTR 的意义就在这里:需要补充信息时,不把协议重新拉回复杂的双向请求模型。

新版还要求所有结果都带 resultType。普通完成结果是 "complete",需要补充输入时是 "input_required";兼容旧 server 时,没有 resultType 的结果可以按 "complete" 理解。

长期通知:subscriptions/listen 取代旧订阅机制

有些信息不是某一次工具调用的结果,而是长期变化。比如工具列表变了、prompt 列表变了、某个 resource 更新了。

旧机制里有 HTTP GET stream,也有 resources/subscribe / resources/unsubscribe。新版把这些收敛成一个统一入口:

subscriptions/listen

Client 发起一个长期 POST 请求,声明自己想接收哪些通知。Server 在这一次请求的 response stream 上推送变化。

它看起来像长连接,但语义上仍然很清楚:client 发起订阅请求,server 在这个请求的响应流里发送通知。它不是 MCP 的隐藏 session,也不要求所有后续请求都依附在这条连接上。

长任务:用 Tasks 返回可持久追踪的 handle

部署、CI、批处理、人工审批、模型训练,这类动作可能持续很久。如果普通 tools/call 一直阻塞到结束,就会遇到 HTTP 超时、client 重启、网络中断等问题。

2026-07-28 把 Tasks 从核心协议移到了官方扩展 io.modelcontextprotocol/tasks。它的基本思路是:如果一个操作会跑很久,server 不必阻塞当前请求,而是返回一个 durable handle。

client → server: tools/call deploy_project
server → client: resultType = "task", taskId

client → server: tasks/get(taskId)
server → client: working

client → server: tasks/get(taskId)
server → client: completed + result

如果任务中途需要用户确认,也可以进入 input_required 状态,tasks/get 返回需要的输入,client 再通过 tasks/update 提交。

这里和无状态核心并不冲突。任务状态可以长期存在,但每次访问都用 taskId 明确指向它,而不是依赖某条连接还活着。

交互界面:MCP Apps 如何把 UI 放进 Host

不是所有结果都适合用文本表达。复杂报表、筛选器、审批表单、部署配置,如果都塞进聊天文字里,会让用户和模型都很累。

MCP Apps 解决的是这个问题:server 可以提供一个交互式 UI,让 Host 把它渲染在对话里。

要理解它怎么做,可以先把它拆成两部分:一个 tool 负责触发动作,一个 UI resource 负责提供界面

Server 侧先注册 tool,并在 _meta.ui.resourceUri 里指向对应的 ui://... resource。下面是官方 MCP Apps 文档中 get-time 示例的核心关系,省略了 imports、server 初始化和 HTTP 启动代码:

const resourceUri = "ui://get-time/mcp-app.html";

registerAppTool(server, "get-time", {
  title: "Get Time",
  description: "Returns the current server time.",
  inputSchema: {},
  _meta: { ui: { resourceUri } }
}, async () => ({
  content: [{ type: "text", text: new Date().toISOString() }]
}));

然后 server 再注册同一个 ui://... resource,真正返回要被渲染的 HTML:

registerAppResource(server, resourceUri, resourceUri, {
  mimeType: RESOURCE_MIME_TYPE
}, async () => ({
  contents: [{ uri: resourceUri, mimeType: RESOURCE_MIME_TYPE, text: html }]
}));

UI 侧则通过 MCP Apps SDK 和 Host 通信:

const app = new App({ name: "Get Time App", version: "1.0.0" });
app.connect();

app.ontoolresult = render;

button.onclick = async () => {
  const result = await app.callServerTool({ name: "get-time", arguments: {} });
  render(result);
};

完整链路是:模型调用 tool,Host 看到 tool 关联了 _meta.ui.resourceUri,就读取对应的 ui://... resource,把 HTML 放进 sandboxed iframe;tool 的结果会被推给 UI,UI 也可以通过 app.callServerTool() 请求 Host 代它调用 MCP tool。

所以 MCP App 不是普通网页直接访问后端,也不是把 UI 状态塞进文本让模型读。它是在 Host 受控环境里运行的交互式界面,适合图表、表单、配置、筛选和需要用户反复操作的结果。

远程服务让授权边界变重要

本地 MCP server 常常从环境变量里拿 token,风险边界比较窄。远程 MCP server 面向真实用户、企业组织和外部网络时,授权问题就不能再靠简单约定解决。

需要回答的问题包括:

  • 用户是谁;
  • client 能不能代表这个用户访问某个 server;
  • token 是发给哪个 resource server 的;
  • scope 是否足够;
  • 企业管理员如何统一授权、审计和撤销;
  • 恶意 server 能不能诱导 client 拿到不该拿的 token。

新版授权体系更贴近生产 OAuth / OIDC 部署。HTTP MCP server 被明确放在 OAuth Resource Server 的位置;client 必须支持 Resource Indicators,把 token 请求明确绑定到目标 resource;Client ID Metadata Documents 成为推荐方向,Dynamic Client Registration 被标为弃用但保留兼容。

Claude 侧提到的 enterprise-managed auth,也属于这个方向:企业可以通过 Okta、Entra 这类 IdP 统一管理 connector 的访问权限,员工用组织身份登录后继承企业配置,而不是每个人分别手动授权每个 server。

迁移检查:旧 server 需要改哪些地方

如果你要写新的 MCP server,建议直接按新版边界设计:

场景 推荐做法
新 server 直接按无状态请求模型实现
需要版本协商 实现 server/discover,并处理 UnsupportedProtocolVersionError
远程 HTTP 部署 使用单一 POST MCP endpoint,返回 JSON 或 request-scoped SSE
HTTP 可观测性 正确带 MCP-Protocol-VersionMcp-Method、必要时带 Mcp-Name
需要跨请求状态 taskId、resource URI 或业务 handle 显式传递
长时间任务 用 Tasks 扩展,不要长期阻塞普通工具调用
长期通知 subscriptions/listen,不要恢复旧 GET stream 思路
复杂交互结果 用 MCP Apps,而不是把界面状态塞进文本
企业环境 按 OAuth/OIDC、Resource Indicators 和企业 IdP 设计

如果你在迁移旧 server,先查三类依赖:

  1. 是否还依赖 initialize 后保存的 session 状态;
  2. 是否把协议版本、capabilities、client 身份保存在连接层,而不是每个请求的 _meta
  3. 是否有长任务、订阅、server 主动请求 client 的逻辑,需要改成 Tasks、subscriptions/listen 或 MRTR。

另外,Roots、Sampling、Logging 这些旧能力在新版里也进入了弃用或迁移路径。更自然的方向分别是:用显式参数、resource URI 或配置表达 roots;需要模型调用时直接集成 LLM provider;日志则走 stderr、OpenTelemetry 或每请求 _meta 里的日志级别,而不是依赖旧的协议通知。

这次更新的核心:连接不再代表会话

MCP 2026-07-28 不是简单的字段增删,而是一次边界整理。

过去 MCP 从本地工具连接起步,连接本身承担了不少上下文含义。现在 MCP 要进入远程服务、企业 connector、serverless 和 edge 场景,协议就必须把这些隐含状态拆开:请求自己声明版本和能力,跨请求状态用明确 handle 表达,长任务和交互 UI 用扩展承载,授权交给更标准的 OAuth / OIDC 模型。

这样做会让某些旧实现需要迁移,但方向更清楚:MCP server 不再像一个必须维持会话的长连接进程,而更像一个标准 HTTP 服务;复杂能力仍然存在,只是从核心协议里移到更明确、更可组合的位置。