🔌 第7章 MCP开发进阶
📄 一句话概括

换传输:stdio 走管道,SSE 走两条 HTTP 连接。协议那层一个字没改。 SSE 的固定顺序是先 GET 建流、再 POST 发请求,POST 只回 202,结果从流里出来。 2025 年 3 月那版已经把它换成了 Streamable HTTP。做新东西别再用 SSE。

📝 本章和前面几章的关系

上一节在本地调通了,这一节讲两件事:换传输、搬到线上。 教材这一节讲的是基于 SSE 的开发,那是当年的做法;现在新写的东西应该用 Streamable HTTP。 所以这一节把 SSE 讲清,同时说明它为什么被换掉——理解旧方案,才知道新方案好在哪。

一、换传输:只换搬运方式

stdio 版的入口是「从标准输入读一行」,SSE 版的入口是「一个 HTTP 处理器」。 读一条消息:stdio 是读一行,SSE 是从请求体里取。 写一条消息:stdio 是写一行加 flush,SSE 是塞进队列,由流推回去。 握手:stdio 在同一根管道里,SSE 先 GET 建流再 POST 发。 断线:stdio 是进程退出,SSE 是连接关掉、进程还在。

协议那一层一个字没改:还是 JSON-RPC,还是那四个方法。 【换的只有「这一行 JSON 怎么送过去」】。这句话是这一节的主干,记住它,剩下都是细节。

二、先建流,再发请求

SSE 版的连接分两步。第一步 GET 那条流,服务端立刻推一帧,告诉客户端「以后往哪 POST」。 演示里那一帧是 event 加 endpoint,data 里是一个地址,带一个 session 参数。 这个顺序不能反。没有那条流,POST 出去的结果就没地方回。

session 参数也很关键:一个服务端可能同时接着好几个客户端,靠这个参数分辨「这一条结果该推给谁」。 麻烦也就从这儿开始:要维护会话、要处理流断开、中间代理还可能把长连接掐掉。

三、POST 只回 202,结果从流里出来

演示里 POST 之后状态码是 202,正文长度 0。 202 的意思是「收到了,正在办」,它不是结果。 结果在另一条连接上,就是刚才那条 GET 流。 新手最容易在这里卡住:POST 回了个空,以为失败,其实成功了。

读流的时候会看到两类帧:endpoint 帧(每次连上都推一遍)和 message 帧(真正的结果)。 所以一个完整的调用要读两处:POST 的回执告诉你「收到了」,流里的 message 告诉你「算完了」。

四、新传输把两条连接合成一条

老传输的三个麻烦:要两条连接、要维护会话、流会断。 Streamable HTTP 的做法是:合成一个端点,POST 直接回结果;需要推送时再升级成流。 会话靠请求头里的 ID 带,不再靠一条长连接维持。断了重发就行,不用重连整条流。

所以 2025 年 3 月那一版把 SSE 换掉了。现在做新东西,直接用 Streamable HTTP。 但读老代码、看老文档的时候还是会遇到 SSE,所以这一节还是得看懂它——看见 event 加 endpoint 那种帧, 就知道是这一版。

五、搬到线上要补的五件事

端口:本地可以随机分配,线上要固定,还要让防火墙放过。 鉴权:本地不需要,线上必须有,至少一个令牌。 健康检查:本地不需要,线上要一个不干活的接口,让负载均衡知道你还活着。

日志:本地 print 到屏幕就行,线上要写文件、带请求 id。 限流:本地不需要,线上要,否则一个人能把服务打满。 五条里三条和协议无关,是任何网络服务都要做的事。 所以上线不难, 难的是把它当成一个正经服务来对待。

还有一条最要命的:stdio 版的 Key 写在本机配置里,SSE 版的 Key 要在服务端的密钥库里。 【别把它写进代码再推上去】——这是最容易犯的一个错。

六、动手 Demo:换成 SSE,再搬到线上

同一个 Server,先用 stdio 版跑,再换成 SSE 版跑,对比两条路。 SSE 那一段是真起了一个 HTTPServer,真 GET 了流、真 POST 了请求、真从流里读到结果。 最后把上线要补的五件事列成一张表。

# 一、SSE 版:先 GET 建流,服务端先推一帧
base, stop = serve_mcp_sse(srv)
code, body = get_text(base + '/sse', accept='text/event-stream')
print(body)                     # event: endpoint / data: /messages?session=...

# 二、POST 只回 202,正文长度 0
code, body = post_json(base + '/messages?session=' + sid,
                        jr('tools/call', {'name': 'note.add',
                                          'arguments': {'text': 'SSE 里结果不走 POST'}}, 2))
print(code, len(body))          # 202 0

# 三、结果在流里
print(get_text(base + '/sse', accept='text/event-stream')[1])
stop()

实跑输出:

--------------------------------------------------------------------
换成 SSE,再搬到线上
--------------------------------------------------------------------


  ① stdio 版和 SSE 版差在哪
  --------------------------------------------------------------------

|  | stdio 版 | SSE 版 |
|---|---|---|
| 入口 | for line in sys.stdin | 一个 HTTP 处理器 |
| 读一条消息 | 读一行 | 从请求体里取 |
| 写一条消息 | 写一行加 flush | 塞进队列,由流推回去 |
| 握手 | 同一根管道 | 先 GET 建流,再 POST 发 |
| 断线 | 进程退出 | 连接关掉,进程还在 |

  协议那一层一个字没改:还是 JSON-RPC,还是那四个方法。
  【换的只有「这一行 JSON 怎么送过去」。】

  ② 先建流,再发请求
  --------------------------------------------------------------------
  服务起在 http://127.0.0.1:51415

第一步,GET 打开流。服务端先推一帧,告诉你「以后往哪 POST」:
  event: endpoint
  data : /messages?session=sess-5822

  ③ POST 只回 202,结果从流里出来
  --------------------------------------------------------------------
  POST /messages?session=sess-5822 -> 状态码 202,正文长度 0
  202 意思是「收到了,正在办」,它不是结果。
  【结果在另一条连接上】——就是刚才那条 GET 流,去那里读。

再读一次流,里面除了新的 endpoint 帧,还多了一条 message 帧:
  event: endpoint
  data : /messages?session=sess-5822
  event: message
  data : {"jsonrpc":"2.0","id":2,"result":{"content":[{"type":"text",
"text":"{\"ok\": true, \"text\": \"SSE 里结果不走 POST\"}"}],
"isError":false}}

这一来一回暴露了老传输最麻烦的地方:

| 问题 | 为什么 | 新传输怎么解决 |
|---|---|---|
| 要两条连接 | 一条收、一条发 | 合成一个端点,POST 直接回 |
| 要维护会话 | 哪个流对应哪个客户端 | 请求头里带会话 ID |
| 流会断 | 中间代理超时会掐掉 | 改成普通请求,断了重发 |

  所以 2025 年 3 月那一版把 SSE 换掉了。【现在做新东西,直接用 Streamable HTTP】。

  ④ 搬到线上要补的五件事
  --------------------------------------------------------------------

| 项 | 本地跑 | 线上 |
|---|---|---|
| 端口 | 随机分配 | 固定,且要让防火墙放过 |
| 鉴权 | 不需要 | 必须要,至少一个令牌 |
| 健康检查 | 不需要 | 要一个不干活的接口 |
| 日志 | print 到屏幕 | 写文件,带请求 id |
| 限流 | 不需要 | 要,否则一个人能打满 |

  五条里三条和协议无关,是【任何网络服务都要做的事】。
  所以上线不难,难的是把它当成一个正经服务来对待。
  ! 还有一条:stdio 版的 Key 写在本机配置里,SSE 版的 Key 要在服务端的密钥库里。
  【别把它写进代码再推上去】——这是最容易犯的一个错。

①第一张表五行,逐行对比两种传输。 注意最后一行「断线」:stdio 是进程退出,SSE 是连接关掉、进程还活着。 ②第二段那一帧 endpoint 是 SSE 版的入口。没有它,客户端不知道该往哪 POST。 ③第三段是本节的坑:状态码 202、正文长度 0。看着像失败,其实是「收到了,正在办」。 ④第四段读流的时候能看到两类帧。真正的结果在 message 帧里,它出现的时间可能比 POST 的回执晚。 ⑤最后那张五行的表,三条和协议无关。这句话能解释为什么「上线」这件事总比想象中麻烦。

30 秒实验

  1. 把演示里的 POST 地址改成不带 session 参数,看服务端怎么回。
  2. 列出你手上任何一个本地跑着的东西,判断它搬到线上要补五件事里的哪几件。
  3. 查一下 Streamable HTTP 和 SSE 的差别,用一句话说清为什么新方案更省事。

常见误区

  1. 「SSE 是现在推荐的传输。」2025 年 3 月已被 Streamable HTTP 取代。 新写的东西别用 SSE,但老代码还得看得懂。
  2. 「POST 回 202 说明出错了。」202 是「收到了,正在办」。结果在流里。
  3. 「本地能跑,上线就只是换个地址。」端口、鉴权、健康检查、日志、限流,五件事里有三件跟协议无关。
自测三问(点开对答案) 1. SSE 版为什么必须先 GET 再 POST? —— 结果要从那条流里推回来。没有流,POST 出去的结果没地方回。 2. POST 回 202、正文长度 0,说明什么? —— 说明服务端收到了,正在办。结果稍后从 GET 那条流里以 message 帧推回来。 3. 为什么现在不推荐 SSE 了? —— 要两条连接、要维护会话、流还可能被中间代理掐断。Streamable HTTP 把这些合成一个端点。

传输和上线都讲完了,这一章还剩最后一个话题:把「记忆」也做成一个 Server,让谁都接得上。

相关笔记