NutriScan的作者做了个能拍照识别营养标签的PWA,用来追踪宏量营养素。然后他发现,自己一整天都在跟Claude报菜名,而不是用自家应用记录。于是他给应用加了一套API,再接一个MCP连接器,让claude.ai的聊天窗口能读当天的数据、也能把饭记进去。
他说,真正难的部分不是把Claude接上,而是想清楚一件事——不管模型那边怎么胡来,服务端必须守住哪些底线。这篇文章用真实代码走了一遍这些保证(FastAPI、Pydantic v2、跑在Neon Postgres上的psycopg2配行级安全、认证交给Supabase)。整套东西是开源的,主要代码在backend/api_v1.py。
![]()
目前的状态是:API和连接器还跑在他自己的账号上做测试,对外开放是下一阶段的事。
17个接口,13个工具,写入全部先预览
接口一共17个,8个读、9个写。MCP工具13个:三个是读(get_context、get_template、search_library),八个写工具只做预览(log_meal、log_template、edit_entry、delete_entry、delete_meal、save_template、update_template、save_to_library),另外还有confirm_change和stop_asking_before_saving。
所有失败长得一模一样
作者给自己定了个规矩:任何功能之前,先让每一种失败都呈现同样的形状。整个服务只抛一种异常Problem,渲染成RFC 9457标准的问题详情,带一个稳定的机器可读error_type,外加可选的扩展字段。
好处很直接:客户端只需要判断error_type,人去看detail就行。被限流的请求会拿到limit、used、resets_at三个扩展字段;幂等冲突则返回in_progress。
请求还没进到Pydantic之前,中间件先卡一道。POST、PATCH、PUT、DELETE这些方法要检查大小和类型:没有Content-Length却带transfer-encoding的,直接411;声明长度超过上限的,413;实际读出来的body超过上限的,也是413;body非空但Content-Type不是application/json的,415。
这里有个细节值得抄:声明长度和真实长度都要查,因为请求头会说谎。
模型幻觉出一个字段,就该当场报错
数据模型走的是严格模式。未知字段是错误,不是被悄悄丢掉;每个数字和字符串都有边界。名字限1到80字符,份数必须大于0且不超过100,日期得匹配YYYY-MM-DD格式,而且拒绝比当前时间晚一天以上的日期。
营养数据同样有上下限:卡路里不小于0,蛋白质不超过1000克,钠最多100000毫克。
作者特别点出extra="forbid"这件事:当客户端是LLM时,它的意义比看上去大得多。模型如果幻觉出一个"meal_type": "brunch"这样的字段,会当场失败,而不是被忽略、同时模型还以为自己存成功了。
令牌只显示一次,猜错要挨罚
个人令牌长这样:nsk_live_前缀加43个URL安全字符。它只展示一次,服务端只存sha256哈希。
数据库是无服务器架构、按唤醒时间计费的,所以一个乱喷随机令牌的机器人不该有能力把它一直叫醒。服务启动时会把所有有效令牌的前12个字符装进一个集合,前缀不在集合里的令牌直接拒绝,连查询都不发。
有效查询会缓存300秒。猜令牌是要付出代价的:同一个IP在五分钟内猜错20次,这个IP会被封十分钟。
权限用FastAPI的依赖按路由划分,写路由还会在批量操作里逐个变更重新检查权限范围。
特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。
Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.