接餐饮 MCP 踩的 4 个坑:前导空格、null ≠ 0、不能 split(','),以及「限流竟是 HTTP 200」
代码取自 github.com/smallchrist...(
src/toon.ts,331 行,可跑)。 说明:为篇幅计,代码块删去了注释与部分类型标注,逻辑与仓库一致;需要逐字对照可看源文件。
先说结论 :TOON 看着像"带表头的 CSV",看着好解析。实际有四个坑, 其中最后一个最阴------被限流时 HTTP 照样返回 200, 只判状态码的代码会把"被拦截"当成"查询成功",然后把空数据当成"这家店没菜"。
一、先看一段真实返回
我用麦当劳中国的 MCP 做了一个营养搭配 CLI。四个工具里,list-nutrition-foods 返回的营养表长这样:
TOON(Token-Oriented Object Notation)------比 JSON 省 token,人也读得懂。第一眼看上去很好解析:表头声明了 [160] 行、9 列,下面就是逗号分隔的行。
我一开始也这么想。然后连踩四个坑。
二、坑 1:每一行首字段带 2 个前导空格
数据行的第一个字段是 " 猪柳麦满分",不是 "猪柳麦满分"。
后果不是报错,是"看起来没数据" :你按名字去查,查不到;打印出来一看,营养表里明明有这条。
这类 bug 的排查成本特别高,因为报错信息会把你带错方向------你会先怀疑"官方营养表里没有这个商品",而不是"我把名字读脏了"。
三、坑 2:null、"null"、0 是三件事,不是两件
营养字段里:
null= 缺失(不知道)
0= 真的是 0(比如矿泉水的热量确实是 0 千卡)
"null"(带引号)= 字面字符串,不是缺失
第三条最容易忽略。所以判断"是不是 null"必须看原始字段有没有被引号包住:
为什么较这个真:如果缺失被填成 0,你的程序不会崩,它会安静地算出一份「0 千卡套餐」------这比崩溃危险得多。崩溃你会去修,错答案你会拿去用。
这条也决定了整个产品的口径:营养数据分 A/B/C/D 四级可信度,缺失一律留白,绝不填 0 。实测 121 项餐品里,可用(非 D 级)的只有 38 项(31.4%) ,其余 83 项就老实标「D 级 · 无法验证」。
四、坑 3:不能 split(',')------字段里可能有逗号
TOON 复用 CSV 的引号规则:字段含逗号、换行、双引号时,整段用双引号包起来,内部的双引号写两遍。
三十行,替换掉一行 split。值不值得?值得------列错位是静默错误,它不会抛异常,只会让"卡路里"那一列装着"蛋白质"的数字。
五、坑 4:被限流时,HTTP 还是 200
这条最阴。
我做过一次压测(305 次请求,2026-10-12)。所有响应都是 HTTP 200,包括被限流的那些:
也就是说:只看 HTTP 状态码,你永远发现不了自己被限了。 你会把限流当成功,把空数据当"这家店没这个菜"。
所以拆包必须检查业务信封,而不是只看有没有 data:
注意最后那个 businessCode 参数------业务码必须带出到上层,否则客户端只能统一当"服务端异常",既不会退避重试,也不会止损。
我在这条上栽过一次:早期版本只检查"有没有 price 字段",于是
{"success":false,"code":500,"data":{"price":0}}被解析成"官方已核实 ¥0"。 失败响应里有残留数据,是这类 API 的常态。
六、结构自检:声明 160 行,就得真的是 160 行
表头写着 [160],那就校验一遍:解析出来的行数必须也是 160,列数必须等于表头列数。
我的默认策略是严格模式直接抛错 ,只有显式传 { strict: false } 才降级为"记警告继续":
列数同理:fields.length !== columns.length ⇒ 严格模式抛 COLUMN_COUNT_MISMATCH,宽松模式记 warnings。
为什么默认严格:解析层的容错,会把"数据变了"这件事悄悄吃掉。宁可让程序在启动时炸给我看,也不要它在三个月后给我一份少了两行的报表。
七、附赠:中文表格在终端里永远是歪的
这是做终端演示时发现的,属于同一个"宽度"家族的问题:
实测修复前,同一列在不同行落到第 35~38 列;修复后全部对齐到第 76 列。
顺带一提 :数值列右对齐时,表头也得右对齐------否则表头和数字会错开一个单元格。这种细节,用户说不出哪里不对,但会觉得"这工具不精致"。
八、怎么自己验
解析器本体:src/toon.ts(331 行)|踩坑记录:docs/API_PROBE_NOTES.md|匹配实测:docs/MATCHING_NOTES.md
九、四条结论
| 坑 | 现象 | 后果 | 修法 |
|---|---|---|---|
| 前导空格 | 名称带缩进 | 匹配全部落空,报错误导 | 每格 trim() |
null / "null" / 0 |
三态混同 | 缺失变 0,算出"0 卡套餐" | 记 quoted,未加引号的才算缺失 |
split(',') |
字段含逗号 | 整行列错位(静默) | 引号感知的状态机 |
| 限流 | HTTP 200 + code:508 |
把限流当成功,把空当"没有" | 校验业务信封,把业务码带出去 |
一句话总结:解析层的每一处"容错",都要想清楚它会不会把"数据变了"变成一个更安静的错误。