MCP 2026-07-28 简明指南:从本地连接到无状态 HTTP
---
title: MCP 2026-07-28 简明指南:从本地连接到无状态 HTTP
published_at: 2026-07-29
language: zh
---
# 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 的基本关系仍然是:
```text
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_issues`、`create_pull_request`、`read_file` 等工具。Host 不需要写死 GitHub API 的每个端点,只需要通过 MCP 发现这些工具,理解参数 schema,再在合适的时候调用。
所以这次更新不是推翻 MCP 的抽象,而是调整这些抽象在远程部署时如何被承载。
## 本地连接模型为什么不适合远程服务
早期 MCP 很适合本地连接。典型流程是:Host 启动一个 server,双方先做初始化握手,再在同一个 session 里继续通信。
```text
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` 里:
```json
{
"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 表达。
```text
第一次请求:创建部署任务,返回 taskId
后续请求:tasks/get(taskId) 查询状态或结果
```
状态还在,但它从“藏在连接里”变成了“由应用层明确管理”。这就是新版无状态核心最重要的边界。
## Streamable HTTP:每条消息都是一次 POST
无状态核心落到 HTTP 上,形态就很直接:每条 MCP 消息都是一次新的 HTTP POST。
```text
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-Version`、`Mcp-Method`,在 `tools/call`、`resources/read`、`prompts/get` 这类请求里还会带 `Mcp-Name`。这些 header 不是用来恢复 session,而是让 HTTP 层、代理、网关和日志系统能更清楚地识别请求。
也就是说,连接仍然可以承载流式响应,但连接不再代表会话本身。
## 没有初始化握手后,版本怎么协商
移除 `initialize` 后,版本和能力协商不能再依赖“先建立一条会话”。新版给了一个更直接的入口:
```text
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 可以先返回一个中间结果:
```json
{
"resultType": "input_required",
"inputRequests": {
"date": {
"type": "elicitation",
"message": "请选择出发日期"
}
}
}
```
Client 收集到用户输入后,再重试原来的请求,并带上 `inputResponses`。
```text
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`。新版把这些收敛成一个统一入口:
```text
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。
```text
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 启动代码:
```typescript
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:
```typescript
registerAppResource(server, resourceUri, resourceUri, {
mimeType: RESOURCE_MIME_TYPE
}, async () => ({
contents: [{ uri: resourceUri, mimeType: RESOURCE_MIME_TYPE, text: html }]
}));
```
UI 侧则通过 MCP Apps SDK 和 Host 通信:
```typescript
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-Version`、`Mcp-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 服务;复杂能力仍然存在,只是从核心协议里移到更明确、更可组合的位置。