团队把内部工具平台封装成 MCP Server 给 Agent 用,一开始走的是 stdio,本地跑得挺好。要上生产让多个 Agent 通过 URL 连进来,才发现远程传输这块的水比想象中深:SSE 已经被正式标记弃用,换成 Streamable HTTP 不是改个导入的事;OAuth 2.1 的发现链路在客户端实现上有一堆隐蔽的假设,authorization_servers 里带路径,有的客户端直接给你把路径 strip 掉;accept 头没同时声明两种 content-type,服务端回 SSE 的时候客户端直接挂。这篇文章把传输换血、鉴权链路和 SDK 迁移三个环节的坑拆开,每一步给出可复现的现象和可操作的配置。

一、Streamable HTTP 到底变了什么:先理解协议,再动手迁移

Streamable HTTP 在 protocol version 2025-03-26 引入,替代 2024-11-05 的 HTTP+SSE 传输。2026-07-28 的修订进一步改了行为,移除的东西比很多人以为的多:GET 流端点没了,协议级会话没了,服务器只暴露一个 POST 端点,客户端每条 JSON-RPC 消息都是独立的 HTTP POST。

和旧 HTTP+SSE 的根本区别在于:

维度HTTP+SSE(已弃用)Streamable HTTP
端点数量/sse + /messages 两个单一 MCP 端点
连接模型长连接 SSE 通道 + 独立 POST每条请求独立 POST,响应可升级为请求作用域 SSE
恢复能力依赖 Last-Event-ID(已在 2026-07-28 移除)无协议级恢复,靠请求重试
无状态支持必须维持长连接天然支持 stateless

旧 SSE 的三个致命缺陷直接导致了替换:连接中断无法从端点恢复、服务器必须维持高可用长连接、服务器只能通过 SSE 通道单向推送,不能在请求上下文之外主动发消息。Streamable HTTP 保留了“响应可流式”的能力,但把“长连接”从必选项变成了可选项:服务端可以选择回一个普通 JSON,也可以回一个限定于该请求的 SSE 流。

第一个认知坑:SSE 弃用不等于“现在就必须删代码”。 正式标记 deprecated 是 2026-07-28 的事,但 spec 会给 12 个月以上的过渡期,SseServerTransport 不会在某天早上突然罢工。真正会先坏的是恢复机制:Last-Event-ID 在 2026-07-28 无论用哪种 transport 都已经移除了。如果你的业务依赖断线续传,换到 Streamable HTTP 之后需要自己实现请求级重试和幂等,不能指望协议帮你兜底。

二、传输迁移:改导入只是第一步,路由和 accept 头才是硬仗

2.1 SseServerTransport 的端点路径参数没有等价物

旧的代码长这样:

from mcp.server.sse import SseServerTransport
transport = SseServerTransport("/messages")

迁移后的代码:

from mcp.server.streamable_http import StreamableHTTPServerTransport
transport = StreamableHTTPServerTransport()

看起来只是换了个类名、删了个参数。但 SseServerTransport 的 "/messages" 参数没有对应物——Streamable HTTP 只有一个端点,不再有独立的 message POST 路径需要配置。如果你的反向代理规则、ALB target group 或者 API gateway 的路由硬编码了 /messages 这个路径,光改 Python 代码不够,基础设施侧也得动。这就是为什么自动化迁移工具把这条标记为 review 而不是 safe:导入能自动修,路由不能。

2.2 accept 头必须同时声明两种 content-type

官方 spec 写得很明确:客户端发 POST 时,Accept 头必须同时列出 application/json 和 text/event-stream,服务器可能返回其中任何一种,客户端必须两种都支持。

踩坑现象:客户端只发了 Accept: application/json,服务端选择用 SSE 回,客户端因为 content-type 不匹配直接丢弃响应或者报解析错误。你以为服务端挂了,实际上是自己的 accept 头没写全。

2.3 notification 的 202 响应别当成错误

如果 POST 的 body 是 JSON-RPC notification,服务器接受后返回 202 Accepted 且无 body;不接受才返回 400 级别错误。很多 HTTP 客户端封装习惯把“无 body 的 2xx”当成异常或者空响应处理,结果 notification 发出去看似成功,实际上后续状态全乱了。拿到 202 之后不要尝试解析 body,直接当成功处理。

三、鉴权链路:OAuth 2.1 的“标准”在客户端实现里不一定标准

Streamable HTTP 的远程部署几乎绕不开 OAuth 2.1。阿里云那篇实践把链路拆得很清楚:401 不是故障,是发现的起点。

服务端返回:

HTTP/2 401
WWW-Authenticate: Bearer resource_metadata="https://your-server/.well-known/oauth-protected-resource"

客户端读取受保护资源元数据,拿到 authorization_servers 列表,再去拉授权服务器元数据,走 PKCE S256 授权码流程。

这里有一个真实存在、且不是配置错误的坑:authorization_servers 里的 URL 带路径时,部分客户端会忽略路径部分。 微软 Q&A 上有人用 Azure APIM 做 resource server,authorization_servers 返回的是 https://example.azure-api.net/my-path,按 RFC 8414 和 RFC 9728 应该去拉 https://example.azure-api.net/my-path/.well-known/oauth-authorization-server。但 VS Code(GitHub Copilot MCP)和 Claude Desktop 实际发出的是 https://example.azure-api.net/.well-known/oauth-authorization-server,路径被 strip 掉了。

这不是服务端配错了。微软的回复也确认了这是客户端实现层面的 quirk。如果你的授权服务器部署在子路径下,要么把 well-known 端点代理到根路径,要么改用无路径的授权服务器 URL,否则换一个客户端就挂一次。

另一个被 spec 明确禁止、但很容易顺手写出来的做法:把客户端拿到的 bearer token 直接透传给上游业务 API。MCP 规范把 MCP Server 定义为 OAuth 2.1 resource server,转发客户端 token 给上游是 forbidden 的,会引入 confused-deputy 问题。正确的路径是 token exchange 或代表用户发起独立的上游认证,而不是把同一个 token 一路传下去。

四、SDK 侧的实操:FastMCP 跑通不等于生产可用

FastMCP 3.x 对 transport 的切换给了一行 API:

mcp.run(transport="streamable-http", host="0.0.0.0", port=8000)

如果还需要兼容旧客户端,可以保留 transport="sse" 分支。但能跑起来只是起点,远程部署有几个默认值会咬人。

第一,本地绑定的安全默认值。 spec 建议本地运行时只绑 127.0.0.1,不要绑 0.0.0.0;同时服务端必须校验 Origin 头,不合法直接回 403,防 DNS rebinding。容器化部署时绑 0.0.0.0 是必要的,但 Origin 校验不能省,否则等于把内网端点暴露给任意网页发起的请求。

第二,middleware 和 lifespan 的顺序。 FastMCP 的 middleware 必须在 server 实例化之前定义,lifespan 里 yield 的 dict 通过 ctx.lifespan_context 访问。迁移时如果把旧 SSE 时代的中间件逻辑直接搬过来,容易因为注册顺序导致工具调用日志丢失或者异常吞掉。

第三,transport="sse" 字符串字面量是独立的调用点。 如果你的 CLI flag 或者 config 文件默认值里写的是 "sse",改类名不会自动改这个字符串。迁移工具把这一步作为独立步骤处理,说明它足够容易被漏掉。

五、决策速查

你的情况建议关键动作
新建远程 MCP Server直接用 Streamable HTTPtransport="streamable-http";accept 头同时声明 application/json 和 text/event-stream
已有 SSE Server,暂不迁移可以等,但别等太久确认没有依赖 Last-Event-ID 恢复;12 个月后 SSE 离开 spec
反向代理硬编码了 /messages基础设施和代码一起改Streamable HTTP 没有独立的 message POST 路径,路由规则需要重写
授权服务器部署在子路径优先把 well-known 放到根路径部分客户端会 strip authorization_servers 的路径,VS Code 和 Claude Desktop 已复现
需要把 MCP 接入上游业务 API不要透传 bearer token用 token exchange 或独立的上游认证,透传违反 spec 且引入 confused-deputy
容器化绑 0.0.0.0不要省 Origin 校验不合法 Origin 必须回 403,防 DNS rebinding

最后一句大实话:Streamable HTTP 的设计比 SSE 干净得多,单一端点、可无状态、兼容普通 HTTP 基础设施,这些都是实打实的改进。但“干净”不等于“零成本”——/messages 路由的拆除、accept 头的双声明、202 响应的特殊处理、OAuth 发现链路里客户端对 authorization_servers 路径的擅自裁剪,每一个都能让你在本地跑通、上生产才发现问题的地方卡上一阵。先把这几个边界确认一遍,再决定是把旧 SSE 服务原地改掉,还是新起一个 Streamable HTTP 端点并行过渡。

Logo

更多推荐