{"slug": "we-gave-our-mcp-server-an-undo-button", "title": "We gave our MCP server an undo button", "summary": "Privatrak, a cookieless analytics tool, built undo functionality directly into its MCP server so that agents such as Claude, Cursor and Codex can group their writes into a single \"setup run\" that a person can review and reverse in one step. The design addresses confirmation fatigue and the limits of client-side checkpoints: 17 of Privatrak's 44 MCP tools change data, and the MCP specification has nothing for transactions or rollback, so writes join a run via an X-Setup-Run-ID header and any write made without a run id starts one automatically. The work follows Replit's July 2025 incident, in which an AI agent deleted a company's production database during a code freeze and told the user the data could not be recovered, though a checkpoint restored it.", "body_md": "[Blog](https://privatrak.com/blog)\n\n# We gave our MCP server an undo button\n\nHow to undo what an AI agent changed through an MCP server: one run per task, before and after records, conflict checks and a real preview.\n\nPublished\n\nIn July 2025, Replit’s AI agent deleted a company’s production database during a code freeze, then told the user the data couldn’t be recovered. It could. Replit had a rollback, and the user restored everything from a checkpoint. The agent just [didn’t know the feature existed](https://incidentdatabase.ai/cite/1152/). Two lessons from that story shaped what we built: the agent has to know that undo exists, and people have to be able to undo without asking the agent.\n\nWe build Privatrak, a cookieless analytics tool. Its MCP server lets Claude, Cursor or Codex do more than read your analytics. They can set them up: feature rules that name what people click, funnels, dashboards, and marking click problems as fixed. 17 of its 44 tools change something.\n\nMost MCP servers with write access rely on the client asking before each call. We wanted one more thing: after an agent has made twenty changes, you see all of them in one list and undo them in one step, without losing what you or a colleague changed in between.\n\n## Asking first isn’t enough\n\nClaude asks before it runs a tool marked destructive. That’s good, and we set our tool annotations carefully for it. But it depends on the server labelling its tools honestly, which is why the MCP specification tells clients to treat those labels as untrusted unless the server is trusted. And it protects one call at a time, while an agent’s work isn’t one call.\n\nSetting up a project takes a dozen or twenty writes: rules, a funnel built on those rules, a dashboard whose widgets query them. Nobody reads the fifteenth confirmation as carefully as the first. Security people call this confirmation fatigue, and OWASP’s list of risks for AI agents includes it. And the mistakes in a setup rarely show in a single call. They show later, when a dashboard counts the wrong thing.\n\nWhat you need then isn’t another confirmation. You need to see everything the agent did as one unit, and to take that unit back. Developers have that for code in version control. Product settings usually have nothing like it.\n\n## Undo has to live in the server\n\nCoding agents already have undo. Claude Code and Cursor keep checkpoints you can go back to. But both only cover files the agent edited. [Claude Code’s docs](https://code.claude.com/docs/en/checkpointing) say changes made through commands aren’t tracked, and [Cursor’s](https://docs.cursor.com/agent/chat/checkpoints) say restoring a checkpoint doesn’t undo a database migration the agent ran. Once an agent calls an API, the client can’t take it back. Only the service behind the API can.\n\nMCP doesn’t help here either. The [specification](https://modelcontextprotocol.io/specification/2025-11-25/server/tools) asks clients to keep a person in the loop and to confirm tool calls, but it has nothing for transactions or rollback. So if you want undo for what an agent does in your product, you build it into your MCP server. That’s what the rest of this article is about.\n\n## One task, one run\n\nThe unit of undo is a **setup run**. An agent starts one at the beginning of a task with `start_setup_run` and a label such as “Checkout tracking”, then passes its id on every write. The run answers the question “what did the assistant do?”\n\nTwo rules make it reliable:\n\n- **Nothing happens outside a run.** A write tool called without a run id starts a run itself and returns its id. Even an agent that ignores the instructions leaves a trail that can be undone.\n- **Runs that changed nothing don’t clutter the list.** If the API rejects a write, the tool deletes the run it had just started for it.\n\nIn the API, a write joins a run through a header, `X-Setup-Run-ID`, not a body field. DELETE has no body, and all fourteen write operations should join the same way. A write without the header takes exactly the code path it always took, so adding runs changed nothing that already worked.\n\nThe runs also show up in the dashboard under Settings, as **AI assistant changes**. The person who connected the assistant doesn’t need the assistant to undo what it did.\n\n## Record before and after, in the same transaction\n\nEvery write that belongs to a run is saved together with a record of what it changed: the state before and the state after. Both are saved in one database transaction, so either both happen or neither does. The run is locked meanwhile, so nobody can undo it halfway through a write.\n\nThe recorded state is the whole definition: a rule’s name and conditions, a funnel’s steps, a dashboard with its widgets. Everything undo needs to rebuild it. The database enforces the shape: a creation has no “before”, a deletion has no “after”.\n\nOne detail cost us more time than the rest. Undo later compares these records with what is in the database now. If the same thing can be saved in two ways that mean the same, the comparison fails, and every undo looks like a conflict. So every write saves its input in exactly one form.\n\n## Undo walks backwards\n\nUndo replays the run’s changes newest first, in one transaction:\n\n- **Created** becomes deleted.\n- **Updated** gets back the fields the run changed.\n- **Deleted** is recreated, with its original id.\n\nNewest first matters. An agent that created a rule and then renamed it has to unwind the rename before the creation. And if the run created something and later deleted it itself, there is nothing to do for it at all.\n\nThe original id matters more than it sounds. Dashboards, funnels and links refer to things by id. A recreated rule even gets its original creation time back, because the order of rules decides which name the journey view shows for a click. Undo should leave the project reading exactly as before, not just something equivalent.\n\nAfterwards the run is marked as undone and its report is stored. Asking again returns the same report, and a write that tries to join an undone run is refused.\n\n## Never overwrite someone else’s work\n\nThis was the hardest decision. Between the agent’s run and the undo, people keep working. Someone renames a widget the agent made, or fixes a rule’s condition. Undo must not destroy that.\n\nSo every change is checked before it is reverted:\n\n- Undoing a creation requires the thing to still be exactly what the run created.\n- Undoing an update requires every field the run changed to still hold the run’s value. Fields the run didn’t touch may have changed since. They are none of its business.\n- A dashboard the run created that now holds someone else’s widgets is not deleted.\n- Recreating something that would go over the plan’s limits is skipped, not forced through.\n\nWhen a check fails, that one change is skipped and reported with the reason, and the rest of the run is still undone. We considered stopping the whole undo instead. But then one widget someone renamed would keep an agent’s entire setup in place. Skipping and reporting gives people the undo they asked for, minus exactly the things they have since made their own.\n\nWhat counts as someone’s work was a decision too. Moving or resizing a widget doesn’t count. Someone who drags an agent’s widget aside hasn’t made it theirs, and deleting it destroys nothing they did.\n\n## The preview is the real undo, rolled back\n\nYou want to see an undo before it happens. The MCP tool `undo_setup_run` does a dry run unless told otherwise, and the server’s instructions tell the agent to show you the result first. In the dashboard, the dialog says “This is what undoing will do. Nothing has been changed yet.” before the button.\n\nThe preview isn’t a separate prediction. It runs the actual undo, every check and every write, inside the transaction, and then rolls the transaction back. A prediction written as separate code drifts away from what undo really does, and then the preview is wrong exactly when it matters. One code path can’t disagree with itself.\n\n## Say what can’t come back\n\nSome things can’t be undone exactly, and pretending otherwise is worse than saying so.\n\nIn Privatrak, a funnel counts from the day it is created. The per-visitor data that could recount earlier days is deleted every night, for privacy. That has two consequences for undo:\n\n- Undoing a funnel the agent created also deletes the days it has counted since. The preview says how many.\n- A funnel the agent deleted comes back with its name and steps, but it counts again from the undo. Its old days are gone, and the report says so.\n\nThe rule we took from this: when undo can’t be exact, the preview has to say what will be lost, before anyone confirms.\n\n## Undo is the last line, not the only one\n\nUndo is what makes write access reasonable, but it sits behind other guards:\n\n- **Tools go through our own API.** Each tool sends an ordinary request to our existing API, with the same authentication, validation, plan limits and project scoping as any other client. There is no second path to the data, so there is no second set of rules to get wrong.\n- **Write access is a fixed list.** A read-only connection can reach a fixed list of API endpoints. Write access adds exactly the endpoints the write tools need, and nothing else. Tests fail if the tools and the lists disagree, in either direction.\n- **People choose at sign-in.** Which projects the connection may see, and whether it may only read or also make changes.\n- **Every call names its project.** The server keeps no “current project” that one chat could switch for another.\n- **The hints are honest.** Creating isn’t destructive. Updating, deleting and undoing are, so Claude asks before them.\n\nWhen we submitted Privatrak to [Claude’s connector directory](https://claude.ai/directory/privatrak), the undo made one more thing possible: a test plan that calls all 44 tools on a shared review account and ends by undoing everything it changed.\n\n## If you build this yourself\n\n1. Pick the unit: one task, one run. Make every write join a run, even when the agent forgets to start one.\n2. Record the whole state before and after, in the same transaction as the write, saved in one fixed form so you can compare it later.\n3. Undo newest first, in one transaction, and recreate deleted things with their original ids.\n4. Check every change for conflicts. Skip and report; never force, and never give up on the whole run.\n5. Make the preview the real undo, rolled back.\n6. Say what can’t come back, before it is lost.\n7. Put undo where people are: in the tool for the agent, and in your product’s UI for everyone else.\n\nPrivatrak’s MCP server works with Claude, Cursor and Codex. [Our docs](https://privatrak.com/docs/ai-assistant) show how to connect it.", "url": "https://wpnews.pro/news/we-gave-our-mcp-server-an-undo-button", "canonical_source": "https://privatrak.com/blog/mcp-server-undo", "published_at": "2026-10-21 04:02:23+00:00", "updated_at": "2026-10-21 04:19:13.382603+00:00", "lang": "en", "topics": ["ai-agents", "agent-protocols", "ai-tools", "ai-safety"], "entities": ["Privatrak", "Model Context Protocol", "Replit", "Claude", "Cursor", "Codex", "OWASP"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/we-gave-our-mcp-server-an-undo-button", "markdown": "https://wpnews.pro/news/we-gave-our-mcp-server-an-undo-button.md", "text": "https://wpnews.pro/news/we-gave-our-mcp-server-an-undo-button.txt", "jsonld": "https://wpnews.pro/news/we-gave-our-mcp-server-an-undo-button.jsonld"}}