构建 MCP 写工具时,仅检查空字段不够,需要 GET-before-write diff 模式判断值是否真正变化,避免无实际修改时产生虚假变更日志。
Claude Code 用户应当在 MCP 工具的守卫中加入值变更检查,而不仅仅做空载荷检查,以避免无意义写入和虚假审计记录。请使用 GET-before-write 差分模式。
当构建向外部 API 写入数据的 MCP 工具时,你的本能反应是对空载荷做守卫——如果没有字段传入,就不发送请求。这是一个好的开始,但还远远不够。真正的问题是:这次写入是否会实际改变任何东西?
一位开发者最近在他们的 DEV.to MCP 服务器的 update_article 工具中发现了这个漏洞。守卫检查了 title、body_markdown 或 published 是否为 None。如果全为 None,就抛出错误。但如果调用方传入 title="Same Title It Already Has"——与当前值完全相同——守卫通过了,工具向一篇线上文章发送了 GET 和 PUT 请求。
更糟的是,审计日志将其记录为一次变更:fields_changed: ["title"],且 title_before 和 title_after 完全一致。这条假阳性看起来和一次真实的编辑一模一样。
这不仅仅关乎节省 API 调用——而是关乎信任。审计日志的存在是为了追踪恶意写入。如果它无法区分真正的编辑和防御性的重发,它就毫无用处。无意义写入的代价不仅仅是延迟和 token 消耗;还有对你后续调试能力的侵蚀。
对于 Claude Code 用户来说,这一点尤为关键。Agent 经常在决定行动前重新读取当前状态。它们可能会把刚获取的值传回来,以为是在做一次更新。如果你的 MCP 工具盲目写入这些值,审计日志中就会出现噪音,还可能产生副作用(比如触发 webhook 或递增版本计数器)。
以下是在 MCP 工具中实现的模式:
在构建载荷之前获取当前状态(你可能已经这样做了,用于差分计算)。
将每个传入字段与当前值进行比较。
从载荷中丢弃未变更的字段。如果什么都没有剩余,抛出 ValueError 或返回一个 "no-op" 标志。
before = _dev(f"/articles/{article_id}")
article = {}
if title is not None and title != before.get("title"):
article["title"] = title
if body_markdown is not None and body_markdown != before.get("body_markdown"):
article["body_markdown"] = body_markdown
if published is not None and published != before.get("published"):
article["published"] = published
if not article:
raise ValueError("update_article called with no fields that would change")
result = _dev(f"/articles/{article_id}", method="PUT", data={"article": article})
_log_article_update(article_id, before, article.keys(), result)
这是一个判断问题:抛出异常 vs. 静默丢弃 vs. 提前返回。对于 MCP 工具,抛出异常通常是最佳选择——它告诉 Agent 其输入是冗余的,促使它完全避免这次调用。
检查你的 MCP 服务器。对于每个有写入操作的工具,问自己:
它在写入前是否获取了当前状态?
它是否过滤掉了未变更的值?
它的审计日志是否只记录实际存在差异的字段?
如果没有,请应用上面的差分模式。这是一个小改动,却能防止虚假日志和浪费的调用——正是这种细节打磨让 MCP 工具在生产环境中可靠运行。
Originally published on gentic.news