四款 MCP 服务被报告存在更新丢失
一份商业审计机构发布的报告称,GitHub、Grafana 与 PostHog 的 MCP 服务出现并发写入覆盖,且双方均收到成功响应。Kubernetes MCP 案例则涉及缺少 resourceVersion 的对象回写,将其他写入者的修改还原。
AI 深度解读
两个 AI agent 同时修改同一条 GitHub issue 的不同字段,都收到成功回复,最后却有一处修改消失。作者通过 GitHub 官方 MCP server 做了 30 组并发测试,19 组出现这种情况;直接调用 GitHub 的字段更新 API,28 组没有丢失。
这组审计想提醒开发者:工具返回成功,不代表并发修改被完整保留。GitHub、Grafana、PostHog 和 Kubernetes 都测出了问题,但原因不同,有的是 MCP 扩大了写入范围,有的是后端 API 存在竞争窗口,还有的是主动选择了强制覆盖。
只改一个字段,为什么会覆盖别人的修改
丢失更新(lost update)常见于「先读、再改、最后整份写回」。
假设两个 agent 读到同一份旧数据,一个改标题,一个改描述。如果各自提交时都附带其他字段的旧值,后提交的请求就可能把前一个人的修改覆盖掉。两个请求都执行成功,最终数据却没有同时包含两人的改动。
GitHub 的 MCP 工具就是这种情况:设置一个 issue 字段时,先读取所有字段,合并修改,再整组写回,而且没有版本检查。直接调用 GitHub 的字段更新 API 没有复现丢失,说明这次风险由 MCP 层引入。
用户要求修改的范围,应该尽量等于工具实际写入的范围。
Grafana 多了一层问题。它的 MCP patch 模式看起来只是局部修改,实际仍然是读取整个 dashboard、应用补丁、保存整份 dashboard,并强制开启 overwrite,导致版本检查不执行。
允许版本检查运行时,Grafana 在 10 次测试中全部拒绝了过期保存。但旧版保存端点仍有竞争窗口:两次保存恰好同时到达,即使开启检查,也可能双双通过。Grafana 13 的 Kubernetes 风格 API 则在 30 组同时保存中,每组都拒绝了其中一次。
这里要区分「能发现旧版本」和「检查、写入之间不会被插队」。前者通过了,不代表后者也成立。
有些问题要修 API,有些要重新考虑强制覆盖
PostHog 的结果与 GitHub 相反:直接调用产品 API,丢失更新的频率与通过 MCP 调用差不多。MCP 没有制造额外损失,也没有消除它。
修复位置因此应该在 API。把 MCP 工具换个名字、补几句提示词,都不能修掉后端的并发缺陷。作者称,PostHog 的小范围修复在次日上午完成审查并合并,但输入没有提供修复后的完整对照数据。
Kubernetes 的情况又不同。开源 containers/kubernetes-mcp-server 的 resources_create_or_update 使用 server-side apply,并设置 force: true。这是上游在 issue 799 中主动选择的行为,作者将其作为已测量的设计取舍,而非新提交的缺陷。
原本有两道保护:不强制 apply 时,其他 manager 拥有的字段会受到所有权冲突检查;请求体携带 resourceVersion 时,过期写入会失败。force: true 取消了第一道,第二道则依赖客户端保留读取时的版本。
审计中的两个原生 agent 会话都没有发送 resourceVersion。并发编辑任务里,另一个写入者在 agent 读取 ConfigMap 约半秒后,把 MAX_CONNECTIONS 从 100 改成 200;约四秒后,agent 用旧副本强制 apply,又把它改回 100,还告诉用户其他键没有变化。
同一服务器的 resources_scale 提供了对照:它会发送读到的 resourceVersion。注入冲突时,3 次全部返回错误且没有改动;真实竞争中,10 次全部暴露了冲突。
另一个问题 K8S-04 出在 resources_delete:请求没有 uid 或 resourceVersion 前置条件。对象在读取后被删除、重建,工具仍可能删掉这个新对象,10 次测试全部发生。原文也指出,kubectl 同样没有 uid 前置条件。
怎样验证自己的工具有没有丢失更新
作者的方法可以直接用于内部 MCP 验收:
选择一个只修改对象局部的工具,让两个独立服务器进程同时修改同一对象的不同部分,至少重复 10 次。随后重新读取对象,检查两处修改是否都在。如果两个调用都报告成功,但一处修改消失,就确认了一次丢失更新。
接着,把相同修改直接发给产品 API,重复测试。API 不丢、MCP 丢,优先检查工具的读写转换;两边都丢,则继续查后端。后端提供冲突信号时,工具应把它传出来。
判断写入是否可靠,要核对最终状态,不能只数成功响应。
不过,这些数字不能当成生产事故率。GitHub、Grafana 和 PostHog 每个测试单元只有 11 至 30 组,而且刻意同时释放请求。Kubernetes 多数测试每组 10 次,API 对照和注入 scale 冲突各 3 次,环境也仅覆盖单台机器、单个集群或测试账号。
四份审计都有真实 agent 任务,但丢失更新的计数来自脚本测试,不是真实会话的自然碰撞统计。四个项目也是按广泛使用、开放源码挑选,没有随机抽样。输入提到的汇总图表未提供具体内容或图片 URL,因此无法补出其他丢失比例。
复现材料也有边界。GitHub 实验仓库 isokron-ai/github-mcp-writepath-experiment 基于 v2.1.0,修复以独立提交提供;go test ./... 运行上游和新增测试,但并发测试程序依赖私有仓库,尚未公开。PostHog 实验仓库 isokron-ai/posthog-mcp-writepath-experiment 的测试程序在 harness/。Kubernetes 实验仓库 isokron-ai/kubernetes-mcp-writepath-experiment 提供固定测试程序,可用 for b in baseline fixed research; do harness/build/build.sh $b; done 构建三个二进制。Grafana 修复对应上游 pull request 1518。
我的判断
我赞同作者最关键的做法:用产品原生 API 做对照。否则,一句「MCP 会丢数据」容易把工具实现、后端缺陷和强制写入策略混成同一个问题,最后修错地方。
对前端和全栈团队,这也不陌生。表单拿着旧快照提交整个对象,或 Node 服务做读取、合并、写回,同样需要处理并发。MCP 值得额外检查,是因为多个 agent 可以同时操作同一份共享数据。
但这篇没有证明多 agent 在日常任务中究竟多容易碰撞,也没有比较串行队列、字段更新和版本控制的性能成本。根据这些结果调整具体写入路径有依据,直接认定所有 MCP 写工具都不可靠则超出了证据。
三个同时写入的案例里,丢失的修改可以重新写回,Grafana 还保留 dashboard 版本历史。麻烦在于系统没有告诉用户需要恢复。冲突报错会打断任务,静默覆盖却可能让后续任务继续使用错误状态。
作者经营销售 MCP 审计服务的 IsoKron,研究自费、使用 AI 辅助,也声明由本人在另一台机器复测。商业关系不自动推翻结果,但仍应结合公开方法和可复现程度判断,不能把作者的复核声明当成独立验证。
能拿来做什么
- 给团队实际使用的 MCP 写工具补一轮并发验收,让两个独立进程修改不同字段,并在成功返回后核对最终数据。
- 检查 Node 工具封装是否为了修改一个字段而回写整个对象;后端支持字段更新或版本条件时,把这些能力保留到工具调用中。
- 使用 Kubernetes 写工具时,确认 agent 是否携带
resourceVersion,并明确force: true是否符合这次任务的覆盖意图。 - 给 AI 编程任务保留冲突处理分支;遇到版本冲突后重新读取、核对差异,再决定如何重试,避免原样重放旧副本。