参加 1024 程序员节 · 麦当劳程序员创意开发大赛的参赛作品记录。 项目:麦麦请客卡 McTreat · https://github.com/hu-Angie/mcd-treat-card
618 之后麦当劳陆续把能力开放了出来,到 2025 年 12 月 25 日正式上线 MCP Server。 官方 README 列了 33 个工具,业务范围是麦乐送点餐、到店取餐、团餐、积分兑换券、活动日历。
把工具摊开看会发现,它把三件事挂到了一起:
读你的会员数据 → 替你写订单 → 把支付链接递回来这在消费品牌里不常见。星巴克、瑞幸、肯德基的会员体系都还在。
但真正让我决定做这个项目的,是另一个观察。 我把当时已经上榜的 106 个作品按赛道拆了一遍:
赛道 | 项目数 | 最高 Star |
|---|---|---|
省钱 / 优惠券 / 比价 | 24 | 843 |
趣味 / 人格 / 身份卡 | 26 | 676 |
综合助手 / 管家 | 16 | 592 |
营养 / 健身 | 10 | 636 |
团餐 / 派对预约 | 5 | 703 |
门店地图 / 时空 | 5 | 72 |
积分 / 商城 / 临期 | 4 | 195 |
前三名清一色在做省钱。这不难理解——「真的能省钱」是最强的转发理由。
于是我退了一步问自己:除了让自己这顿更便宜,这 33 个接口还能干嘛?
答案藏在工具列表里:唯一同时具备「跨人」和「写操作」属性的组合, 只有 delivery-create-address(新增收货地址)+ create-order(建单返回支付链接)这一条。 而当时 106 个作品里,没有人在用它。
那就用它做"请客",而不是"省钱"。
用户在 WorkBuddy 里说一句话:
给在上海张江上班的同事点份夜宵,预算 40,他今晚加班改 bug然后:
你名下还没有可配送地址,我帮你加一个。请给我:省市区 + 详细地址 + 收件人 + 手机号
(用户提供后)
张江科苑店可配送,给你三套:
A|深夜续命组 · ¥38.00
麦香鱼 1 + 中薯条 1 + 美式咖啡(大)1
提神扛饿,改完 bug 正好垫一口
B|体面饱腹组 · ¥42.00
板烧鸡腿堡 1 + 麦辣鸡翅 2 块 + 可乐(中)1
热食送到手还是热的,吃得饱
C|轻量夜宵组 · ¥26.00
麦旋风 1 + 薯条(小)1 + 热牛奶 1
不重,睡前吃没负担
(本单不为你凑满减——送人送得干净比省 8 块重要。)确认后建单、给支付链接,钱由用户自己扫。最后给一张 1080×1440 的卡片,直接发微信。
这是整个项目我最想讲的一点。
省钱类工具的目标函数是总价最小。所以它们的典型做法是: 满 40 减 8,你只想吃 30 的,它让你凑到 40——多买 10 块钱的东西填满减。
请客不能这么算。
因为收卡的人看得见卡片上的订单明细。 为了用掉一张券,硬塞两个甜筒进去—— 送的人省了 8 块,收的人看到的是"敷衍"。
所以引擎里有一条硬规则:
若用某张券需要加购,只有当加购物本身是合理组成部分(小食/饮料)时才保留该券, 否则弃券,保持组合干净。
同样的逻辑还带来另外两条:
第 2 条一开始我没想到,是真实菜单数据打醒我的: 114 个菜品里有一大半是"XX 三件套""XX 四件套"这类套餐, 如果照搬"主食 + 小食 + 饮料"的公式往套餐上套,送出去的东西会荒谬地重叠。
这部分是我觉得最值钱的。
官方 README 说 33 个。实测 tools/list 返回 35 个。 文档没收录的有:query-promotions、query-survey-coupon。
文档写的 | 实际是 |
|---|---|
query-partystore-date | query-party-store-date |
query-partystore-session | query-party-store-session |
照着文档抄,必然失败。
query-nearby-stores 的 city 和 keyword 必须同时传# 两个都报 600058「城市名或者关键词不能为空」
{'beType':1,'searchType':2,'city':'上海'}
{'beType':1,'searchType':2,'keyword':'上海'}
# 只有这样才能成功
{'beType':1,'searchType':2,'city':'上海','keyword':'上海'}beCode 必传,虽然 schema 标着"选填"入参 schema 里 beCode 是 required: [] 里的,但 description 写着 「得来速(beType=5)/外送(orderType=2)/团餐场景必传,到店自取(beType=1)场景不传」。
只看必填标记就传参,外送会挂。
这条最要命。
我默认 tools/call 会返回结构化数据,结果拿到的是一整个 Markdown 文档:
每个门店的基本信息按单行展示:
- **门店名称**: data[].storeName
- **门店编号**: data[].storeCode
...
{"success":true,"code":200,"data":{...}}先给模型看展示指令,再把真 JSON 塞在后面。
更麻烦的是:JSON 的位置不固定。 有的响应有 ## Original Response 标记,有的没有,直接跟在指令后面。
所以我最后没用 split,而是写了括号平衡扫描:
def _scan_json_object(text: str) -> dict | None:
start = text.find('{"')
while start >= 0:
depth = 0
in_str = False
esc = False
for i in range(start, len(text)):
...
start = text.find('{"', start + 1)
return None外层还统一裹着 {"success","code","message","traceId","data"}, success:false 时 message 是中文原因,traceId 可追溯。
query-meals:currentPrice 是字符串、单位「元」,如 "34.5"calculate-price:返回「分」混用会差 100 倍。
initialize 不返回 Mcp-Session-Id。每次请求独立,别依赖会话头。
全流程只有两个写操作:delivery-create-address 和 create-order。
create-order 我把它放在了一道闸门后面:
def create_order(self, confirmed: bool = False):
if not confirmed:
raise TreatSafetyError(
"下单需要用户明确确认。请在用户说出「确认」「下单」等明确指令后再调用。"
)Agent 只建单,付款永远是人扫。
这不是技术限制,是刻意的设计。支付一旦交给 Agent, 误下单、比价跑偏、prompt 注入导致的非预期消费,全都会变成平台的责任。 现在的切分很干净:单是 Agent 建的,钱是人付的。
没有真实可调的环境也能验证传输层。我起了个本地假 MCP 服务, 把握手、会话 ID 透传、JSON/TOON/SSE 三种响应形态、401 中止、429 退避都验了一遍。
最终 43 项离线自测全绿,不消耗任何调用额度,随时可重跑。
python -m tests.test_mcp_client_offline # 传输层 21 项
python -m tests.test_recommender # 组单引擎 15 项
python -m tests.test_pipeline # 端到端 7 项顺带说一句,这套 mock 测试当场抓出了我三个 bug: _biz_error 缩进掉进类里、_extract_mcd_envelope 漏 self、 以及 initialize 把 401 当普通错误白试 3 个协议版本。
这个比赛比的表面是代码,实际是传播——排名只认 GitHub Star。
一个 Star 都没有,代码写得再好也等于没参赛。这个我是在报名成功之后才查明白的, 规则里写着「Star数为0的项目不进入排行榜」。
所以如果你看到这里觉得这个项目还有点意思, 帮我点个 Star 吧,这可能是整场比赛里投入产出比最高的一次点击。
https://github.com/hu-Angie/mcd-treat-card
本项目为独立参赛作品,非麦当劳官方产品。文中数据均来自麦当劳中国 MCP 接口返回, 价格与供应状态以官方渠道实时结果为准。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。