We gave our MCP server an undo button

Letting an AI agent change your product's settings is only reasonable if you can take it all back. So every change our MCP server makes goes into a run that can be undone in one step. This is how it works, and the decisions that turned out to be hard. In 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. 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. We 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. Most MCP servers with write a

We gave our MCP server an undo button

Letting an AI agent change your product's settings is only reasonable if you can take it all back. So every change our MCP server makes goes into a run that can be undone in one step. This is how it works, and the decisions that turned out to be hard.

In 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. 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.

We 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.

Most 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.

Asking first isn't enough

Claude 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.

Setting 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.

What 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.

Undo has to live in the server

Coding 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 say changes made through commands aren't tracked, and Cursor's 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.

MCP doesn't help here either. The specification 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.

One task, one run

The 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?"

Two rules make it reliable:

  • 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.
  • 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.

In 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.

The 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.

The AI assistant changes page in Privatrak's settings, listing the run

Record before and after, in the same transaction

Every 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.

The 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".

One 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.

Undo walks backwards

Undo replays the run's changes newest first, in one transaction:

  • Created becomes deleted.
  • Updated gets back the fields the run changed.
  • Deleted is recreated, with its original id.

Newest 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.

The 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.

Afterwards 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.

Left, the agent's four changes from oldest to newest. Right, what undo does with each, from the newest up: the dashboard is deleted, the renamed funnel is skipped, the rule's old name is put back, the rule is deleted
The undo starts with the agent's last change and works back to its first. The funnel you renamed stays, and everything else goes.

Never overwrite someone else's work

This 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.

So every change is checked before it is reverted:

  • Undoing a creation requires the thing to still be exactly what the run created.
  • 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.
  • A dashboard the run created that now holds someone else's widgets is not deleted.
  • Recreating something that would go over the plan's limits is skipped, not forced through.

When 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.

What 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.

The preview is the real undo, rolled back

You 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.

The

The 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.

Say what can't come back

Some things can't be undone exactly, and pretending otherwise is worse than saying so.

In 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:

  • Undoing a funnel the agent created also deletes the days it has counted since. The preview says how many.
  • 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.

The rule we took from this: when undo can't be exact, the preview has to say what will be lost, before anyone confirms.

Undo is the last line, not the only one

Undo is what makes write access reasonable, but it sits behind other guards:

  • 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.
  • 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.
  • People choose at sign-in. Which projects the connection may see, and whether it may only read or also make changes.
  • Every call names its project. The server keeps no "current project" that one chat could switch for another.
  • The hints are honest. Creating isn't destructive. Updating, deleting and undoing are, so Claude asks before them.

When we submitted Privatrak to Claude's connector directory, 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.

If you build this yourself

  1. Pick the unit: one task, one run. Make every write join a run, even when the agent forgets to start one.
  2. 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.
  3. Undo newest first, in one transaction, and recreate deleted things with their original ids.
  4. Check every change for conflicts. Skip and report; never force, and never give up on the whole run.
  5. Make the preview the real undo, rolled back.
  6. Say what can't come back, before it is lost.
  7. Put undo where people are: in the tool for the agent, and in your product's UI for everyone else.

Privatrak's MCP server works with Claude, Cursor and Codex. Our docs show how to connect it.

Share