💡 摘要: 麦当劳中国 MCP Server(https://mcp.mcd.cn)提供 35 个 Tool,覆盖菜单、营养、优惠券、算价、点餐全流程。本文基于真实调用与抓包分析,给出三步接入流程(握手/无状态/工具发现)、35 个工具的能力地图、4 个协议级坑(含 600010/600012 报错码)和一套可复用的 MCP 接入方法论,并附完整开源项目(营养规划 + 两阶段组合算法 + 可视化报告)。
难度等级: ⭐⭐⭐(进阶)
前置知识: 了解 JSON-RPC 2.0 与 HTTP/SSE 基本概念,Python 3.9+
🎯 一、背景:为什么值得花半天探查一个 MCP Server
WorkBuddy × 麦当劳的 1024 创意开发大赛让麦当劳中国的 MCP Server 进入了很多人的视野。官方文档写得克制,实际上这个 MCP Server 的能力远比示例丰富:35 个 Tool,覆盖菜单、营养、优惠券、积分、算价、点餐全流程。
我拿它做了一个完整的健身点餐助手(McFit Coach):输入身高、体重、目标(减脂/增肌/均衡),基于真实菜单组合一日三餐,联动官方算价,生成可视化营养报告。开发过程中我对服务端行为做了系统探查,本文沉淀成一份可复用的接入方法论。
项目已开源:https://github.com/dickeryang/mcd-fit-coach

图 1:McFit Coach 生成的可视化营养报告整页截图(Chart.js 雷达图 + 三餐环形图 + 三态进度条)。
📖 二、接入三步:握手、无状态、工具发现
标准 Streamable HTTP 握手如下。我为什么先贴这段最小可运行代码:它是后续所有坑的「参照系」,对照着看每个发现更直观。
python
# Why: MCP 客户端的最小可运行实现------握手一次、解析双格式、无状态调用
def _initialize(self):
payload = {"jsonrpc": "2.0", "id": 1, "method": "initialize",
"params": {"protocolVersion": "2025-03-26", "capabilities": {},
"clientInfo": {"name": "mcd-client", "version": "1.0"}}}
resp = self._post(payload)
# 注意:服务端不返回 mcp-session-id
发现 1:服务端不返回 mcp-session-id。 这意味着每次 tools/call 之前都要重新 initialize。这不是 bug 而是一种取舍------服务端无会话状态,水平扩展友好,但客户端必须封装成无状态设计。如果你的客户端框架默认维持会话,接这个 Server 会莫名报错。
发现 2:响应可能是 SSE 也可能是纯 JSON ,取决于服务端负载。客户端要做双格式解析:先试 json.loads,失败再按 data: 行拆 SSE。
握手与双格式解析的完整时序如下:

图 2:握手与双格式解析时序。注意每次
tools/call前都要重新initialize(无会话)。
📖 三、工具地图:35 个 Tool 的能力分布
| 类别 | 数量 | 代表工具 | 说明 |
|---|---|---|---|
| 营养 | 1 | list-nutrition-foods |
全量 160 餐品,类 CSV,9 字段 |
| 门店 | 1 | query-nearby-stores |
到店/外送双模式 |
| 菜单 | 2 | query-meals / query-meal-detail |
分类+价格 map,约 26KB |
| 优惠 | 4+ | available-coupons / auto-bind-coupons / query-my-coupons |
领券与卡包分离 |
| 算价/下单 | 5+ | calculate-price / create-order |
金额单位:分 |
| 账户/积分/活动 | 余量 | query-my-account 等 |
积分商城、主题活动 |
两个值得注意的数据规模:营养表 160 个餐品去重后 158 个(千卡/蛋白/脂肪/碳水/钠/钙齐全),单店菜单 99 项。这个数据量不需要数据库,本地 JSON 缓存 + 内存计算足够(缓存 TTL:营养 24h / 菜单 1h)。
35 个工具在决策链里的数据流向:

图 3:工具地图与数据链路。35 个 Tool 中真正进入决策链的是 8 个,其余(积分商城/主题活动)未使用。
⚠️ 四、四个协议级坑(附报错码)
1. 门店查询参数是「与」不是「或」
query-nearby-stores 的 schema 里 city 和 keyword 都标注可选,但只传 city 直接报错。必须 city + keyword 同传:
python
# Why: 联动约束只能实测发现------schema 标注 optional ≠ 可单独省略
client.call("query-nearby-stores",
{"beType": 1, "searchType": 2, "city": "上海", "keyword": "麦当劳"})
现象 :只传 city 返回参数错误;根因 :服务端把 city 与 keyword 做联合索引查询;教训:schema 标注 optional ≠ 可单独省略,联动约束只能实测发现。
2. 券 ID 只在领券结果里返回
这是最反直觉的一处。query-my-coupons 返回券标题、价格、有效期、标签------唯独没有 couponId / couponCode。而算价用券恰恰需要这两个字段,它们只在 auto-bind-coupons 的领券结果中出现一次。
结论:引用券必须「领券时刻」保存 ID,卡包接口拿不到。设计上意味着不能做「查询-引用」两段式,只能「领取-即记录」。
3. 用券字段必须成对,缺一报 600010
calculate-price 的 items 中用券:
python
# Why: 600010 是字段成对约束的边界文档------只传一个字段立即触发
{"productCode": "521741", "quantity": 1,
"couponId": "0D94...", "couponCode": "MCD69..."}
现象 :只传 couponId 或只传 couponCode 均报 600010。根因 :变量隔离测试确认两个字段强绑定(服务端按 ID+Code 双因子校验)。教训:600010 这类业务码本身就是最好的边界文档。
4. 成对传入后仍报 600012「当前优惠券不可用」
实测 6 张真实券 × 对应商品 × 换 SKU × 换数量共 8 种排列全部 600012。推测券有隐性条件(时段白名单 / 门店类型 / SKU 映射表未开放)。
现象 :成对传入仍被拒;根因 :券的适用条件(时段/门店/SKU 白名单)未开放文档;解决 :工程上做容错回退------单券失败跳过并回退原价,主流程零影响;教训 :开放问题挂 GitHub Issues。这也是对第三方 MCP 的合理预期:文档永远落后于服务端行为。

图 4:用券算价的失败分支。600010 是字段问题(可修复),600012 是券的隐性条件(只能容错)。
附:金额单位是分
calculate-price 返回 "price": 11450。规划器算出 ¥114.5 与之精确吻合------单位统一后,这个对照本身就是链路正确性的免费断言。
🔧 五、数据链路设计
list-nutrition-foods ──(24h 缓存)──► Food[158] ─┐
├─ 名称归一化对齐 → PlannableItem[33]
query-meals ──────────(1h 缓存)───► MenuItem[99] ─┘
名称对齐是脏活:菜单是简化名(「可乐」vs 营养表「可乐中杯」)、全半角括号差异、规格词差异(中杯/大杯)。策略是三层匹配:归一化相等 → 互为前缀 → 包含关系 + 规格偏好排序,且同一营养项只挂第一个菜单项防止重复。最终 99 项菜单对齐 33 项可规划------剩下的组合项(套餐/四件套)营养无法拆解,明确排除而不是硬猜。
📝 六、可复用的接入方法论
- 先探查后编码 :一次
tools/list+ 每个关键工具一次真实调用,半天换来后续零返工 - 报错码当测试用例:600010/600012 这类业务码是最好的边界文档
- 副作用分级 :只读工具随便试;
auto-bind-coupons这类真实入账的操作必须有显式授权开关(我做成了--bind) - Token 走环境变量 :仓库里只放
${MCD_MCP_TOKEN}占位,两轮 grep 扫描再提交
📝 七、总结
这个 MCP Server 的完成度超出预期------真正制约创意的是「隐性约束」,而探查它们的成本就是参赛者之间的差距。完整项目(营养规划 + 两阶段组合算法 + 可视化报告)开源在:
👉 https://github.com/dickeryang/mcd-fit-coach

图 5:报告页滚动演示 GIF(Playwright 1200x1080 视口真实渲染,5 个滚动位 6 帧循环)。
这是 1024 麦当劳创意开发大赛参赛作品,排名看 Star,觉得有用的话点一个。
💬 互动引导
你接入过的 MCP Server 里踩过最深的坑是什么?评论区见。