1. 先把两条“路”的底细摸清楚最近在对接MCP服务的时候,经常有人拿着同样一个问题来找我:MCP的stdio和Streamable HTTP到底怎么选,折腾半天标了“流式消息”的东西最后又传到哪里去了?说实话,这两个问题看着基础,但真到了写代码、定架构的时候,踩坑的人一点不少。MCP(Model Context Protocol)本身只解决“AI应用怎么调用工具、怎么取数据”这件事,但传输层选错了,后面所有联调和部署都会跟着别扭。先解释一下背景。MCP的传输层目前主流是两条路,一条叫stdio,另一条叫Streamable HTTP。stdio的意思很直白:客户端以子进程方式拉起MCP server,两者通过标准输入、标准输出交换JSON-RPC消息。server跑在本地,client往server的stdin里写请求,server把响应写到自己的stdout,client再从管道里读回来。整个过程是一个典型的“本地进程间通信”,没有IP、没有端口、没有网络。Streamable HTTP则是把MCP搬到了HTTP之上。client通过POST向一个HTTP端点发送JSON-RPC消息,server可以立即返回结果,也可以因为处理时间较长,在POST响应里直接返回一个SSE(Server-Sent Events)流,后续的消息通过这个流持续推送。此外,client还可以用GET请求另开一条SSE连接,专门接收服务端主动下发的消息。所以“流式消息传到哪里”这个问题,答案其实就藏在这两条链路的差异里。要想选得明白,得先把这两条路各自的脾气摸清楚。stdio的最大优势是简单、直接、零网络开销,缺点也明显:server必须和client在同一台机器上,没法远程部署,没法被多个客户端共享。Streamable HTTP则恰恰补上了这个短板,但代价是一大堆网络协议、会话管理、认证、超时、代理的破事。1.1 stdio:本地进程的“直连通道”stdio模式最适合的场景,是那些“本地工具类”的MCP server。比如我给你写一个MCP server,读取本地某个目录下的文件、跑一个本地脚本、查一下系统状态,这类能力用stdio天然合适。client拉起server进程,双方通过stdin/stdout对话,不需要任何网络配置。有朋友问过:为什么MCP要支持stdio?其实道理很简单。在本地场景下,你去引入一整套HTTP服务、端口监听、网络权限,是纯属给自己加戏。stdio的开销几乎为零,进程启动、管道读写,延迟可以做到忽略不计,这对那些“工具调用要快、要稳”的场景非常关键。你在命令行工具或桌面App里嵌一个MCP客户端,意图就是让server和client共享同一个生命周期,client启动时拉起server,client退出时server也结束,谁都不需要额外操心连接回收。开发调试阶段,stdio还有个隐形好处:你可以用最朴素的手段验证server逻辑。直接在终端里启动server,手动把JSON-RPC请求打进stdin,观察stdout的输出,就能知道协议交互对不对。不需要起端口、不需要带鉴权、不需要关心跨域。stdio的坑也让人印象深刻,最大的一个就是“日志不能乱打”。因为协议对话走的是stdout,你的server代码如果往stdout里print一条调试日志,这条日志会直接混进MCP的JSON-RPC消息流里,导致client解析失败。正确做法是日志一律走stderr,或者落到文件。我见过不止一次,线上排查问题半天找不到原因,最后发现就是一行print把管道搅乱了。1.2 Streamable HTTP:越过进程边界,把服务变成“端点”Streamable HTTP解决的是另外一个问题:当MCP server要做成一个常驻服务、给多个客户端共享,或者部署在远处机房、云环境里,stdio就没法用了。你得把它暴露成一个HTTP端点,让client通过网络访问。在Streamable HTTP的模型里,server端只有一个核心端点,比如/mcp。client向这个端点POST请求,请求体是JSON-RPC消息。服务端返回有两种情况:第一种是处理很快,直接在一次POST响应里返回application/json格式的JSON-RPC响应;第二种是需要持续处理,比如工具执行中途要发进度通知、异步任务要推结果,server就会在POST响应里返回text/event-stream,后续消息一条条通过这个流推给client。所以“流式消息传到哪里”这个问题,放到Streamable HTTP场景下就有了明确的答案:要么传到“POST请求的SSE响应流”里,要么传到“client主动用GET建立的SSE连接”里。这两个通道是服务端向客户端推送消息的两条路。初看有点绕,但一旦理解了MCP对实时性、双向通信的需求,就会明白这个设计是经过取舍的。和stdio相比,Streamable HTTP最大的收益是拓扑灵活。server可以部署在任意有网络可达的地方,前面随便挂负载均衡、加鉴权、做灰度。多个client可以同时连同一个server实例,资源利用率明显更高。代价是协议复杂度上了一个台阶:要处理session、要处理超时与重连、要处理SSE流的生命周期、还要解决代理缓冲、网关长连接等一堆基础设施问题。2. 怎么选:别先看技术,先回答四个问题选stdio还是Streamable HTTP,不存在“哪个更好”的说法,只有“哪个更符合当前场景”。我在实际项目里一般按四个问题来排队判断,每一条都能直接导向选择结果。2.1 客户端和服务端在不在同一台机器这是第一顺位判断,也是最硬的条件。MCP server如果只能跑在client本机,比如是个读取本地文件、操作本地桌面的工具,那直接用stdio,没有任何犹豫。你要是非把它套成HTTP端点,等于白白引入网络开销,还得处理“localhost端口占用”这种本地问题,纯属自找麻烦。反过来,只要server和client不在同一台机器上,stdio直接出局。你不可能让client从一个远端的Python程序去“拉起一个本地子进程”,除非你用SSH再套一层,但那就已经不是MCP传输层该操心的事了。跨机器、跨网络,就只剩Streamable HTTP这条现实路径。2.2 连接的生命周期归谁管stdio的生命周期和client进程绑定。client启动server、client退出server退出。这种模式适合那些“一次会话一个进程”的用法,状态天然隔离,进程退出后一切干净,不会有残余连接或者泄漏问题。缺点是,如果多个client想复用同一个常驻server,对不起,stdio做不到。Streamable HTTP的生命周期则由server自己掌控。server是一个常驻进程,理论上可以服务N个client。对做平台型、中心化服务场景来说,这是刚需。你要做一个MCP网关,后端接统一的工具服务,前面让多个AI Agent接入,那Streamable HTTP就是唯一合理的选择。这里需要额外提醒一句:别以为HTTP常驻服务就不用管生命周期。会话超时、空闲清理、重连握手,这些工作比stdio复杂得多。MCP的session机制要求client和server先做一次initialize握手,后续所有请求都带着session标识,server端要维护这些状态、处理过期。选HTTP,就等于默认接受这一堆额外的运维责任。2.3 有没有现成的HTTP基础设施这一条经常被忽略。如果公司内部已经有成熟的API网关、统一鉴权、流量监控体系,把MCP server挂进去只需要一个路由配置,那Streamable HTTP的成本会显著降低。你也更容易把MCP能力纳入已有的可观测性体系,服务健康检查、告警、日志收集都复用已有设施。反过来,如果完全是空地上起楼,没有任何HTTP基础设施,连Nginx都没人维护,那优先选stdio反而更务实。它不依赖任何外部组件,一个进程、两根管道就能跑。我见过一些小团队,原本只需要一个本地MCP工具,非要上个HTTP server,结果还要去配HTTPS证书、处理跨域、设置反向代理,折腾两周,本质上一点收益都没有。2.4 调试和维护的成本差异调试维度上,stdio基本碾压HTTP。stdio可以在命令行里直接喂数据,所见即所得。HTTP场景下,你要开着代理抓包、看SSE流有没有被缓冲、检查session头有没有正确传递,调试链路长一大截。但维护维度上,HTTP又反过来占优。stdio进程挂在client进程里,出了故障你没有一个独立的服务维度去排查;客户端崩溃,server也一起没了,日志也难分离,排障边界很模糊。HTTP常驻服务则有独立日志、指标、告警,出了事情能快速定位是client侧问题还是server侧问题。一句话总结:本地、单机、快速集成的活,选stdio;远程、共享、面向多client的活,选Streamable HTTP。也可以做个简单的决策表格,方便对照:判断维度stdioStreamable HTTP部署位置要求client与server同机允许远程部署多客户端共享不支持支持连接生命周期由client进程管理由server常驻管理网络依赖无需要认证与安全本地直连,默认可信必须显式处理调试便利性高较低运维可观测性较弱较强流式消息路径子进程stdoutSSE流典型场景本地工具、桌面端网关、云服务、多Agent接入3. 流式消息到底传到哪里很多人卡住的点,其实是没看懂Streamable HTTP里的“流式”两个字。平时我们说的流式响应,一般是AI生成内容那种打字机式输出——客户端发一个请求,服务端一段一段地吐token。MCP里的流式消息也类似,但范围更广,不只有文本token,还包括进度通知、工具执行状态、异步结果等等。3.1 一个典型Streamable HTTP会话的完整生命周期先把标准流程走一遍。第一步,client向server的HTTP端点发一个POST请求,内容是initialize请求,属于JSON-RPC消息。server返回一个JSON响应,包含协议版本、server能力、以及一个新的session id。这个session id很重要,后续所有请求都要带上它,相当于服务端给这个连接发的临时通行证。第二步,client再发一个notifications/initialized通知,告诉server“我已经完成初始化”。这一步是通知,不需要回应。之后client就可以正常发工具的调用请求了。第三步,也就是关键区别所在。client发送一个工具调用请求,如果server处理很快,比如只是查个配置表,那server直接在POST请求的HTTP响应里返回JSON即可——整个过程和普通REST接口没区别。如果这个工具执行很慢,比如要跑一个长时间的数据任务,server不可能让HTTP请求一直挂着直到任务完成,它会选择立刻让POST响应返回一个SSE流,然后在这条流里持续推进度通知、最终结果。还有一种情况:server需要主动给client推消息,而client当前并没有挂着的POST请求。这就需要第四条通道,client主动用GET方法请求同一个端点,建立一条SSE连接,服务端把异步通知、任务结果都推到这条流里。这就是“流式消息传到哪里”的完整答案——传到这两条SSE消息通道里,而不是一个预先定死的“流地址”。这里有个容易混淆的细节:有人会问,POST请求的SSE响应和GET建立的SSE连接,是同一条流吗?答案是不是一回事。POST的SSE流是“这次请求的响应流”,生命周期随请求走;GET的SSE流是“独立的长连接”,可供服务端多次推送。两者都能承载流式消息,但语义不同。真的做客户端实现的时候,SDK通常会把两个通道组合起来,作为一整套事件流来消费。3.2 流式消息与普通响应的区别MCP把消息定义为JSON-RPC协议里的三类:请求(Request)、响应(Response)、通知(Notification)。流式消息机制主要针对“响应不是一次到位”的情况。普通响应是同步的:client发请求,server一次性返回结果,连接关闭。流式响应是异步的:server先回一个“收到请求、正在处理”的信号,然后通过SSE流逐步推送。client不需要一直挂着HTTP连接,处理完成的事件会在流里“带回来”。这一点和WebSocket的异步通信很像,但实现上更轻,因为SSE只是单向推送,反向链路依然走普通的HTTP POST。需要特别注意:MCP的协议版本存在兼容差异,不同版本对streamable HTTP的细节要求略有不同。老版本对“服务端能否在POST响应里直接返回SSE流”这件事,定义得比较弱;新版规范进一步明确了“不以流响应替代普通响应的SDK兼容”问题。做跨版本支持时,最好先确认两端的协议版本,避免服务端发了SSE流,老SDK却只当普通JSON响应去解析,结果整个客户端卡死。3.3 需要特别留意的“双通道”问题既然流式消息有两条路径,那就必然存在“消息从哪个通道来”的归属问题。实践中,我建议在服务端实现里把两个通道当成“同一个事件源的两种投递方式”,不要为它们维护两套业务逻辑。POST的SSE流适合那些“请求响应连续对话”的场景:client发一个长耗时任务,server在这条流的上下文里回复最终结果。GET的SSE连接适合“服务端主动推送”的场景:任务已经开始,但client之后又发了别的请求,结果回来后,通过这条独立连接推给client。服务端代码里,通常是把事件写到一条内部消息管道,由分发器决定走哪条SSE流,而不是让业务代码感知到底层通道差异。对客户端而言,核心就是处理“同一请求的响应可能出现在不同通道”这件事。我在项目里遇到的绝大多数诡异问题,都是客户端实现只监听POST响应的SSE流、却没有单独消费GET建立的SSE流,导致异步任务结果永远等不到。4. 迁移与联调实战:从stdio走向Streamable HTTP如果你现在用的是stdio,因为业务需要必须迁移到Streamable HTTP,这里有一份可以直接抄作业的路径,以及我踩过的几个坑。4.1 从stdio迁到HTTP的步骤第一步,明确存量逻辑。列出当前MCP server暴露了哪些工具、哪些资源、哪些提示(Prompts),看哪些是本地强依赖、哪些能抽成独立能力。如果server里有大量依赖本地环境的逻辑,比如读某个本机路径,直接搬到HTTP服务需谨慎,要么隔离成一跳转发,要么明确能力边界。第二步,引入MCP的HTTP传输能力。官方SDK目前都内置了Streamable HTTP transport,不用自己手写SSE解析。读一下版本说明,确认SDK支持的是哪个MCP协议版本。第三步,选择部署形态。最省事的做法是用SDK自带的HTTP服务能力,把transport挂到现有的Web框架上。注意路径设置,client配置的URL要和服务端路由严格一致,多一个前缀、少一个前缀都可能握手失败。第四步,接入鉴权。stdio天然本地可信,但一上HTTP,你的服务就暴露在网络里。建议至少接一层Bearer Token,进阶再用OAuth流程换取临时的access token。这一步别省,否则你的MCP服务就相当于裸奔在公网上,任何能访问你端点的人都能调用你的工具。第五步,配置基础设施。如果前面挂了Nginx或网关,下面两个参数是重点:一是代理的读取超时要调大,SSE长连接会挂很久,默认几十秒的超时会导致连接被切;二是关闭代理缓冲,Nginx默认会对响应做缓冲,而SSE要求边产生边推送,缓冲会造成数据“憋住”不吐。开发阶段我建议先用本地端口跑通,确保client能握手、能初始化、能调用工具;再在测试环境加鉴权和代理,逐步往外推。4.2 常见报错和排查思路迁移中最容易踩的几个坑,我做成一个速查表,每个都是身边真实发生过的案件:现象原因排查思路client连接后报“协议版本不支持”客户端与SDK版本跨度过大查看握手响应里的协议版本,与SDK支持的版本对齐工具调用无响应,界面一直转圈client没有消费GET建立的SSE流检查代码是否监听服务端事件流,或请求是否被代理缓冲偶尔断连,长任务总是中断代理服务器等中介掐断空闲SSE连接调大proxy_read_timeout,开启长连接心跳server日志有调用记录,client收不到结果负载均衡轮询把多次请求散到不同实例,session状态丢失给HTTP端点配置会话保持(sticky session),或改用全局共享session存储调试打印内容混入消息流服务端代码往stdout打印了日志日志改走stderr或文件,stdio模式尤其严格POST响应带SSE头,client却按JSON解析使用了只支持一次性响应的老SDK升级SDK到支持streamable response的版本排查工具方面,我强烈建议学会直接观察原始网络流。开一个抓包工具,或者直接用curl手动发POST请求、看响应头,就能快速判断是服务端没返回流、还是客户端没消费流。另外分享一个我自己的排查经验:遇到“连不上”的问题,先看握手阶段有没有拿到session id,再查后续请求有没有把session id带在请求头里。这个头字段在不同版本的SDK里命名略有差异,但原则是一致的——服务端要能唯一标识你这个client,才能把消息推给你。Session丢失,是远程部署时最大的坑,没有之一。5. 选型体会与实操心态做了一段时间MCP接入后,我个人最大的感受是:stdio和Streamable HTTP不是竞争对手,而是两个完全不同的使用场景。它们解决的问题不同,对应的心智模型也不同。本地工具型MCP server,天然适合stdio。你写一个小插件、一个桌面端的工具联动,根本没必要引入网络层。调试方便、启动快、生命周期简单,这些优点对本地场景来说是碾压级的。反过来说,如果你的目标是让多个AI Agent共享一套工具网关、或者把MCP能力对外开放,那Streamable HTTP是绕不开的选择,它背后的session、SSE、鉴权、超时这些复杂问题,都是远程化的必然代价。关于流式消息,我用一句话总结:在Streamable HTTP里,消息不是传到某个固定端口,而是传到两条可被订阅的SSE事件流里——一条是POST请求的响应流,一条是GET建立的长连接流。理解了这一点,你对MCP传输层的认知基本就到位了。最后再分享一个小技巧:无论选哪种传输方式,都把MCP消息日志单独留一份。stdio模式用stderr记录,HTTP模式打印请求体和事件类型。别嫌麻烦,排障的时候,一份完整的消息轨迹能帮你省下大半天的排查时间。协议这东西,理解归理解,真正让人踏实的,还是手里能抓到的那份原始记录。
1. 先把两条“路”的底细摸清楚最近在对接MCP服务的时候,经常有人拿着同样一个问题来找我:MCP的stdio和Streamable HTTP到底怎么选,折腾半天标了“流式消息”的东西最后又传到哪里去了?说实话,这两个问题看着基础,但真到了写代码、定架构的时候,踩坑的人一点不少。MCP(Model Context Protocol)本身只解决“AI应用怎么调用工具、怎么取数据”这件事,但传输层选错了,后面所有联调和部署都会跟着别扭。先解释一下背景。MCP的传输层目前主流是两条路,一条叫stdio,另一条叫Streamable HTTP。stdio的意思很直白:客户端以子进程方式拉起MCP server,两者通过标准输入、标准输出交换JSON-RPC消息。server跑在本地,client往server的stdin里写请求,server把响应写到自己的stdout,client再从管道里读回来。整个过程是一个典型的“本地进程间通信”,没有IP、没有端口、没有网络。Streamable HTTP则是把MCP搬到了HTTP之上。client通过POST向一个HTTP端点发送JSON-RPC消息,server可以立即返回结果,也可以因为处理时间较长,在POST响应里直接返回一个SSE(Server-Sent Events)流,后续的消息通过这个流持续推送。此外,client还可以用GET请求另开一条SSE连接,专门接收服务端主动下发的消息。所以“流式消息传到哪里”这个问题,答案其实就藏在这两条链路的差异里。要想选得明白,得先把这两条路各自的脾气摸清楚。stdio的最大优势是简单、直接、零网络开销,缺点也明显:server必须和client在同一台机器上,没法远程部署,没法被多个客户端共享。Streamable HTTP则恰恰补上了这个短板,但代价是一大堆网络协议、会话管理、认证、超时、代理的破事。1.1 stdio:本地进程的“直连通道”stdio模式最适合的场景,是那些“本地工具类”的MCP server。比如我给你写一个MCP server,读取本地某个目录下的文件、跑一个本地脚本、查一下系统状态,这类能力用stdio天然合适。client拉起server进程,双方通过stdin/stdout对话,不需要任何网络配置。有朋友问过:为什么MCP要支持stdio?其实道理很简单。在本地场景下,你去引入一整套HTTP服务、端口监听、网络权限,是纯属给自己加戏。stdio的开销几乎为零,进程启动、管道读写,延迟可以做到忽略不计,这对那些“工具调用要快、要稳”的场景非常关键。你在命令行工具或桌面App里嵌一个MCP客户端,意图就是让server和client共享同一个生命周期,client启动时拉起server,client退出时server也结束,谁都不需要额外操心连接回收。开发调试阶段,stdio还有个隐形好处:你可以用最朴素的手段验证server逻辑。直接在终端里启动server,手动把JSON-RPC请求打进stdin,观察stdout的输出,就能知道协议交互对不对。不需要起端口、不需要带鉴权、不需要关心跨域。stdio的坑也让人印象深刻,最大的一个就是“日志不能乱打”。因为协议对话走的是stdout,你的server代码如果往stdout里print一条调试日志,这条日志会直接混进MCP的JSON-RPC消息流里,导致client解析失败。正确做法是日志一律走stderr,或者落到文件。我见过不止一次,线上排查问题半天找不到原因,最后发现就是一行print把管道搅乱了。1.2 Streamable HTTP:越过进程边界,把服务变成“端点”Streamable HTTP解决的是另外一个问题:当MCP server要做成一个常驻服务、给多个客户端共享,或者部署在远处机房、云环境里,stdio就没法用了。你得把它暴露成一个HTTP端点,让client通过网络访问。在Streamable HTTP的模型里,server端只有一个核心端点,比如/mcp。client向这个端点POST请求,请求体是JSON-RPC消息。服务端返回有两种情况:第一种是处理很快,直接在一次POST响应里返回application/json格式的JSON-RPC响应;第二种是需要持续处理,比如工具执行中途要发进度通知、异步任务要推结果,server就会在POST响应里返回text/event-stream,后续消息一条条通过这个流推给client。所以“流式消息传到哪里”这个问题,放到Streamable HTTP场景下就有了明确的答案:要么传到“POST请求的SSE响应流”里,要么传到“client主动用GET建立的SSE连接”里。这两个通道是服务端向客户端推送消息的两条路。初看有点绕,但一旦理解了MCP对实时性、双向通信的需求,就会明白这个设计是经过取舍的。和stdio相比,Streamable HTTP最大的收益是拓扑灵活。server可以部署在任意有网络可达的地方,前面随便挂负载均衡、加鉴权、做灰度。多个client可以同时连同一个server实例,资源利用率明显更高。代价是协议复杂度上了一个台阶:要处理session、要处理超时与重连、要处理SSE流的生命周期、还要解决代理缓冲、网关长连接等一堆基础设施问题。2. 怎么选:别先看技术,先回答四个问题选stdio还是Streamable HTTP,不存在“哪个更好”的说法,只有“哪个更符合当前场景”。我在实际项目里一般按四个问题来排队判断,每一条都能直接导向选择结果。2.1 客户端和服务端在不在同一台机器这是第一顺位判断,也是最硬的条件。MCP server如果只能跑在client本机,比如是个读取本地文件、操作本地桌面的工具,那直接用stdio,没有任何犹豫。你要是非把它套成HTTP端点,等于白白引入网络开销,还得处理“localhost端口占用”这种本地问题,纯属自找麻烦。反过来,只要server和client不在同一台机器上,stdio直接出局。你不可能让client从一个远端的Python程序去“拉起一个本地子进程”,除非你用SSH再套一层,但那就已经不是MCP传输层该操心的事了。跨机器、跨网络,就只剩Streamable HTTP这条现实路径。2.2 连接的生命周期归谁管stdio的生命周期和client进程绑定。client启动server、client退出server退出。这种模式适合那些“一次会话一个进程”的用法,状态天然隔离,进程退出后一切干净,不会有残余连接或者泄漏问题。缺点是,如果多个client想复用同一个常驻server,对不起,stdio做不到。Streamable HTTP的生命周期则由server自己掌控。server是一个常驻进程,理论上可以服务N个client。对做平台型、中心化服务场景来说,这是刚需。你要做一个MCP网关,后端接统一的工具服务,前面让多个AI Agent接入,那Streamable HTTP就是唯一合理的选择。这里需要额外提醒一句:别以为HTTP常驻服务就不用管生命周期。会话超时、空闲清理、重连握手,这些工作比stdio复杂得多。MCP的session机制要求client和server先做一次initialize握手,后续所有请求都带着session标识,server端要维护这些状态、处理过期。选HTTP,就等于默认接受这一堆额外的运维责任。2.3 有没有现成的HTTP基础设施这一条经常被忽略。如果公司内部已经有成熟的API网关、统一鉴权、流量监控体系,把MCP server挂进去只需要一个路由配置,那Streamable HTTP的成本会显著降低。你也更容易把MCP能力纳入已有的可观测性体系,服务健康检查、告警、日志收集都复用已有设施。反过来,如果完全是空地上起楼,没有任何HTTP基础设施,连Nginx都没人维护,那优先选stdio反而更务实。它不依赖任何外部组件,一个进程、两根管道就能跑。我见过一些小团队,原本只需要一个本地MCP工具,非要上个HTTP server,结果还要去配HTTPS证书、处理跨域、设置反向代理,折腾两周,本质上一点收益都没有。2.4 调试和维护的成本差异调试维度上,stdio基本碾压HTTP。stdio可以在命令行里直接喂数据,所见即所得。HTTP场景下,你要开着代理抓包、看SSE流有没有被缓冲、检查session头有没有正确传递,调试链路长一大截。但维护维度上,HTTP又反过来占优。stdio进程挂在client进程里,出了故障你没有一个独立的服务维度去排查;客户端崩溃,server也一起没了,日志也难分离,排障边界很模糊。HTTP常驻服务则有独立日志、指标、告警,出了事情能快速定位是client侧问题还是server侧问题。一句话总结:本地、单机、快速集成的活,选stdio;远程、共享、面向多client的活,选Streamable HTTP。也可以做个简单的决策表格,方便对照:判断维度stdioStreamable HTTP部署位置要求client与server同机允许远程部署多客户端共享不支持支持连接生命周期由client进程管理由server常驻管理网络依赖无需要认证与安全本地直连,默认可信必须显式处理调试便利性高较低运维可观测性较弱较强流式消息路径子进程stdoutSSE流典型场景本地工具、桌面端网关、云服务、多Agent接入3. 流式消息到底传到哪里很多人卡住的点,其实是没看懂Streamable HTTP里的“流式”两个字。平时我们说的流式响应,一般是AI生成内容那种打字机式输出——客户端发一个请求,服务端一段一段地吐token。MCP里的流式消息也类似,但范围更广,不只有文本token,还包括进度通知、工具执行状态、异步结果等等。3.1 一个典型Streamable HTTP会话的完整生命周期先把标准流程走一遍。第一步,client向server的HTTP端点发一个POST请求,内容是initialize请求,属于JSON-RPC消息。server返回一个JSON响应,包含协议版本、server能力、以及一个新的session id。这个session id很重要,后续所有请求都要带上它,相当于服务端给这个连接发的临时通行证。第二步,client再发一个notifications/initialized通知,告诉server“我已经完成初始化”。这一步是通知,不需要回应。之后client就可以正常发工具的调用请求了。第三步,也就是关键区别所在。client发送一个工具调用请求,如果server处理很快,比如只是查个配置表,那server直接在POST请求的HTTP响应里返回JSON即可——整个过程和普通REST接口没区别。如果这个工具执行很慢,比如要跑一个长时间的数据任务,server不可能让HTTP请求一直挂着直到任务完成,它会选择立刻让POST响应返回一个SSE流,然后在这条流里持续推进度通知、最终结果。还有一种情况:server需要主动给client推消息,而client当前并没有挂着的POST请求。这就需要第四条通道,client主动用GET方法请求同一个端点,建立一条SSE连接,服务端把异步通知、任务结果都推到这条流里。这就是“流式消息传到哪里”的完整答案——传到这两条SSE消息通道里,而不是一个预先定死的“流地址”。这里有个容易混淆的细节:有人会问,POST请求的SSE响应和GET建立的SSE连接,是同一条流吗?答案是不是一回事。POST的SSE流是“这次请求的响应流”,生命周期随请求走;GET的SSE流是“独立的长连接”,可供服务端多次推送。两者都能承载流式消息,但语义不同。真的做客户端实现的时候,SDK通常会把两个通道组合起来,作为一整套事件流来消费。3.2 流式消息与普通响应的区别MCP把消息定义为JSON-RPC协议里的三类:请求(Request)、响应(Response)、通知(Notification)。流式消息机制主要针对“响应不是一次到位”的情况。普通响应是同步的:client发请求,server一次性返回结果,连接关闭。流式响应是异步的:server先回一个“收到请求、正在处理”的信号,然后通过SSE流逐步推送。client不需要一直挂着HTTP连接,处理完成的事件会在流里“带回来”。这一点和WebSocket的异步通信很像,但实现上更轻,因为SSE只是单向推送,反向链路依然走普通的HTTP POST。需要特别注意:MCP的协议版本存在兼容差异,不同版本对streamable HTTP的细节要求略有不同。老版本对“服务端能否在POST响应里直接返回SSE流”这件事,定义得比较弱;新版规范进一步明确了“不以流响应替代普通响应的SDK兼容”问题。做跨版本支持时,最好先确认两端的协议版本,避免服务端发了SSE流,老SDK却只当普通JSON响应去解析,结果整个客户端卡死。3.3 需要特别留意的“双通道”问题既然流式消息有两条路径,那就必然存在“消息从哪个通道来”的归属问题。实践中,我建议在服务端实现里把两个通道当成“同一个事件源的两种投递方式”,不要为它们维护两套业务逻辑。POST的SSE流适合那些“请求响应连续对话”的场景:client发一个长耗时任务,server在这条流的上下文里回复最终结果。GET的SSE连接适合“服务端主动推送”的场景:任务已经开始,但client之后又发了别的请求,结果回来后,通过这条独立连接推给client。服务端代码里,通常是把事件写到一条内部消息管道,由分发器决定走哪条SSE流,而不是让业务代码感知到底层通道差异。对客户端而言,核心就是处理“同一请求的响应可能出现在不同通道”这件事。我在项目里遇到的绝大多数诡异问题,都是客户端实现只监听POST响应的SSE流、却没有单独消费GET建立的SSE流,导致异步任务结果永远等不到。4. 迁移与联调实战:从stdio走向Streamable HTTP如果你现在用的是stdio,因为业务需要必须迁移到Streamable HTTP,这里有一份可以直接抄作业的路径,以及我踩过的几个坑。4.1 从stdio迁到HTTP的步骤第一步,明确存量逻辑。列出当前MCP server暴露了哪些工具、哪些资源、哪些提示(Prompts),看哪些是本地强依赖、哪些能抽成独立能力。如果server里有大量依赖本地环境的逻辑,比如读某个本机路径,直接搬到HTTP服务需谨慎,要么隔离成一跳转发,要么明确能力边界。第二步,引入MCP的HTTP传输能力。官方SDK目前都内置了Streamable HTTP transport,不用自己手写SSE解析。读一下版本说明,确认SDK支持的是哪个MCP协议版本。第三步,选择部署形态。最省事的做法是用SDK自带的HTTP服务能力,把transport挂到现有的Web框架上。注意路径设置,client配置的URL要和服务端路由严格一致,多一个前缀、少一个前缀都可能握手失败。第四步,接入鉴权。stdio天然本地可信,但一上HTTP,你的服务就暴露在网络里。建议至少接一层Bearer Token,进阶再用OAuth流程换取临时的access token。这一步别省,否则你的MCP服务就相当于裸奔在公网上,任何能访问你端点的人都能调用你的工具。第五步,配置基础设施。如果前面挂了Nginx或网关,下面两个参数是重点:一是代理的读取超时要调大,SSE长连接会挂很久,默认几十秒的超时会导致连接被切;二是关闭代理缓冲,Nginx默认会对响应做缓冲,而SSE要求边产生边推送,缓冲会造成数据“憋住”不吐。开发阶段我建议先用本地端口跑通,确保client能握手、能初始化、能调用工具;再在测试环境加鉴权和代理,逐步往外推。4.2 常见报错和排查思路迁移中最容易踩的几个坑,我做成一个速查表,每个都是身边真实发生过的案件:现象原因排查思路client连接后报“协议版本不支持”客户端与SDK版本跨度过大查看握手响应里的协议版本,与SDK支持的版本对齐工具调用无响应,界面一直转圈client没有消费GET建立的SSE流检查代码是否监听服务端事件流,或请求是否被代理缓冲偶尔断连,长任务总是中断代理服务器等中介掐断空闲SSE连接调大proxy_read_timeout,开启长连接心跳server日志有调用记录,client收不到结果负载均衡轮询把多次请求散到不同实例,session状态丢失给HTTP端点配置会话保持(sticky session),或改用全局共享session存储调试打印内容混入消息流服务端代码往stdout打印了日志日志改走stderr或文件,stdio模式尤其严格POST响应带SSE头,client却按JSON解析使用了只支持一次性响应的老SDK升级SDK到支持streamable response的版本排查工具方面,我强烈建议学会直接观察原始网络流。开一个抓包工具,或者直接用curl手动发POST请求、看响应头,就能快速判断是服务端没返回流、还是客户端没消费流。另外分享一个我自己的排查经验:遇到“连不上”的问题,先看握手阶段有没有拿到session id,再查后续请求有没有把session id带在请求头里。这个头字段在不同版本的SDK里命名略有差异,但原则是一致的——服务端要能唯一标识你这个client,才能把消息推给你。Session丢失,是远程部署时最大的坑,没有之一。5. 选型体会与实操心态做了一段时间MCP接入后,我个人最大的感受是:stdio和Streamable HTTP不是竞争对手,而是两个完全不同的使用场景。它们解决的问题不同,对应的心智模型也不同。本地工具型MCP server,天然适合stdio。你写一个小插件、一个桌面端的工具联动,根本没必要引入网络层。调试方便、启动快、生命周期简单,这些优点对本地场景来说是碾压级的。反过来说,如果你的目标是让多个AI Agent共享一套工具网关、或者把MCP能力对外开放,那Streamable HTTP是绕不开的选择,它背后的session、SSE、鉴权、超时这些复杂问题,都是远程化的必然代价。关于流式消息,我用一句话总结:在Streamable HTTP里,消息不是传到某个固定端口,而是传到两条可被订阅的SSE事件流里——一条是POST请求的响应流,一条是GET建立的长连接流。理解了这一点,你对MCP传输层的认知基本就到位了。最后再分享一个小技巧:无论选哪种传输方式,都把MCP消息日志单独留一份。stdio模式用stderr记录,HTTP模式打印请求体和事件类型。别嫌麻烦,排障的时候,一份完整的消息轨迹能帮你省下大半天的排查时间。协议这东西,理解归理解,真正让人踏实的,还是手里能抓到的那份原始记录。