I run a small MCP server that wraps my DEV.to account — list articles, create one, update one, that kind of thing. I also have a completely separate script, publish_devto.py, that a scheduled task calls directly to publish drafts twice a day. Same account, same API, two different code paths to the same endpoint.

Five days ago I fixed a real bug in publish_devto.py: its idempotency guard — the check that stops a retried publish from creating a duplicate live article — only ever looked at the 30 most recent published posts. My account has 91. Today, going back through the MCP server to sanity-check things before writing something new, I found the identical bug sitting untouched in the server's create_article tool. Same shape, same root cause, never fixed there. This is a short post about why fixing a bug in one place doesn't fix it in the other place that has the same bug, even when you know they're twins.

The guard, and why it exists

publish_devto.py's whole idempotency check exists because of a specific failure mode: a POST to /articles can succeed on DEV.to's end and still leave the client with nothing but a timeout. urllib's timeout=30 doesn't distinguish "the server never got the request" from "the server processed it and the ack just got lost." If something upstream — my own scheduled task's own "if 429, wait and retry" instruction, or an MCP client retrying a tool call that looked like it failed — fires the same publish again, you get two live articles with the same title and body.