fix(mcp)★★: JSON-RPC 帧一直是坏的 —— 任何 MCP 客户端都收不到事件
## 一个生产上一直存在的 bug
`Frame` 里 `"params":` 后面**少一个左花括号**:
{"jsonrpc":"2.0",...,"params":"event_id":"1","event_type":"connected",...}
↑ params 变成了字符串
这不是非法 JSON 能靠编译发现的那种错 —— 它**编译通过**、连接是活的、
字节在流动、服务端日志里有帧,但**任何 MCP 客户端都解不开**。
症状完全像「没有人给我发信」。
## 上一轮我的生产实测为什么没抓到
head -c 160 | cut -c1-160 ← 截断了,我没看全
当时只确认了「有帧到达」,没检查结构。**看片段不算验证。**
## 判据为什么一直是绿的
`TestFrameRendersJSONRPCNotification` 只断言「以 `{"jsonrpc"` 开头」——
坏帧恰好也满足。前缀检查、`HasSuffix("\n\n")`、payload 检查**全部通过**。
⇒ 唯一能抓到它的办法是把整帧当 JSON 解一遍。现在补上了,
连同 SSE 三行格式(id: / event: message / data:)一起断言。
两个变异(去掉 `{`、去掉 `data:` 前缀)都验证转红。
## 顺带修:SSE 方言
之前写的是**裸 JSON**(无任何 SSE 行前缀),而 Content-Type 却是
text/event-stream。标准 MCP 客户端用 EventSource / eventsource-parser
按 `data:` 取 payload,遇到裸 JSON 会读到空 payload 然后丢掉每个事件。
现在输出标准三行。
`id:` 行一并补上:SSE 规范据此维护 Last-Event-ID,断线重连时客户端
自动带上 —— 与服务端那份事件缓冲区回放是两条互补路径。
## 新增:完整生命周期 E2E(TestMCPFullLifecycleEndToEnd)
已有的 40 格判据全是**单点**(握手对了、工具列表对了、某个鉴权对了)。
它们各自通过,却不保证「一个真实客户端从头走到尾不撞墙」。
MCP 出问题时往往不在单点:
initialize → tools/list → 订阅 SSE → 收到信 → **断开** → 重连(Last-Event-ID)
→ 回放补发 → 发信 → 读回 → 作用域延续
走真实 HTTP + 真实 SSE + 真实 AgentAuth,不是直接调 Run。
★ 这一格一写出来就抓到了上面那个坏帧(它按 data: 解析,读到 0 帧)。
**判据抓不到的东西,真实客户端同样抓不到。**
## E2E 过程中判据自己错了三次
| 现象 | 真因(都不是产品缺陷) |
|---|---|
| `Unauthorized` | 漏挂 `AgentAuth` —— 少一层中间件 = 测的不是生产形状 |
| SSE 405 | 只挂了 `HandleHTTP`,漏了 `HandleGET`(POST/GET 是两个 handler) |
| 「回放没生效」 | `repoInsertMail` 是裸 INSERT,**不触发 broadcast**,事件从未进 ring |
第三个尤其阴险 —— 判据报出来的是一句**产品缺陷**的措辞
(「回放没生效,客户端会静默漏信」),而真实投递一定经过
`notify.Recipients`。判据数据错了会伪装成被测代码坏了。
## 同时把工具面对齐判据从手抄清单改成真读源码
`TestToolsExposeParamsOtherBridgesHave` 原来是一份手抄
`map[string][]string`:会漂移,且只查"不缺"不查"不多"。
改为每次现读 plugins/pi-mail-bridge/src/tools.mjs 做双向比对。
★ 解析器第一版用行正则判块结束,而 properties 里每行参数长得就是
`xxx: { … },` —— 首行就把块关掉,**解析出 0 个参数却全绿**。
改按花括号深度计,守卫从 `len(out)==0` 改成**参数总数**
(漏掉的正是那个检查)。两个方向的变异都验证转红。
14 包全绿。