mcp-compensator
Click on "Install Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@mcp-compensatorundo my last mistake"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
mcp-compensator
Ctrl+Z for AI agents — an MCP proxy that journals every mutating tool call and can undo them.
mcp-compensator sits in front of the MCP servers you already use. Point
your agent at the proxy instead of at those servers directly, and every
tool call still gets forwarded through unchanged — same tools, same
schemas, same results — but now the agent also has five extra abilities:
start a checkpoint, see what checkpoints exist, list what's
changed since one, undo everything reversible since one, and ask
whether a given tool call would even be undoable before running it.
The idea, in plain language
Picture a concierge who's about to rearrange your apartment. Before they touch anything destructive — say, before they empty a drawer — they take a photo of it. Everything else they do, they jot down a note of the opposite action: "I moved the lamp from the desk to the shelf" becomes a note that says "move it back." When you ask them to undo their work, they don't have a magic rewind button. They walk backwards through their notes, most recent first, and either follow the "opposite action" note or use the photo to put the drawer back the way it was.
That's the whole mechanism. There's no write-ahead log, no transaction, no actual rollback of the downstream server's storage — the proxy can't see inside it. For every mutating call, one of two things happens:
The response already contains enough to build an inverse. A
createreturns an id you candelete. Acompletehas an obviousuncomplete. This is the reversible case — a true inverse exists.The response doesn't contain enough (a
deletereturns nothing useful once the row is gone). So the proxy snapshots state before the call runs, then uses that snapshot afterward to reconstruct it. This is the compensable case — a new, independent write that approximates the old state.
Undo replays these compensations in LIFO order: most recent change undone first, like unwinding a stack.
This is the saga/compensation pattern, not rollback. Say that out loud before using this in anything that matters — see Known limitations below.
Related MCP server: MCP Trust Gateway
Quickstart
Requires Python 3.10+. mcp is pinned to 1.29.0 — see Known
gotchas.
git clone https://github.com/mohithhhh/mcp-compensator
cd mcp-compensator
python3 -m venv .venv
source .venv/bin/activate
pip install -e ".[dev]"Run the end-to-end demo. It spawns the proxy and a toy downstream task-list server as real subprocesses, connects a real MCP client to the proxy (the same way an agent would), and drives a mistake-and-undo scenario:
python demo.pyYou should see something like:
--- checkpoint ---
checkpoint: {"checkpoint_id": 1}
list_tasks (right after checkpoint): {"result": []}
--- add_task x2 ---
add_task (a): {"id": 1, "title": "Write the README", "status": "open"}
add_task (b): {"id": 2, "title": "Ship the demo", "status": "open"}
--- complete_task on task a ---
complete_task: {"id": 1, "title": "Write the README", "status": "completed"}
--- delete_task on task b (the mistake) ---
delete_task: {"id": 2, "deleted": true}
--- undo_to(checkpoint_id=1) ---
undo_to: {"undone": [...4 entries...], "skipped": []}
--- list_tasks (after undo) ---
list_tasks (after undo): {"result": []}
✅ state after undo_to exactly matches state right after checkpointundo_to walked backwards through everything that happened after the
checkpoint — the "mistaken" delete, the complete, and even the two
add_task calls themselves — and compensated all of it, back to the
empty task list that existed right when the checkpoint was taken.
Run the tests:
pytestWiring it up for real
Write a
compensators.yamllisting your downstream servers underservers:and a compensator policy per mutating tool undertools:(see Registry format below;examples/compensators.yamlis a full worked example).Point your MCP client at the proxy instead of at your servers directly:
python -m compensator.proxy --config compensators.yaml --db compensator.dbYour agent now sees every downstream tool renamed to
{server}__{tool}, pluscheckpoint,list_checkpoints,list_changes,undo_to, andexplain_blast_radius.
Configuring in an MCP client
mcp-compensator is a normal stdio MCP server, so any MCP-capable client
can launch it directly — you're just pointing the client at
python -m compensator.proxy instead of at your downstream servers. Use
an absolute path to the interpreter that has this project's
dependencies installed (see Known gotchas — don't rely
on a bare python/python3 resolving correctly, since MCP subprocess
environments don't inherit your shell's activated venv).
Claude Code
Fastest path — one CLI command (-- separates Claude Code's own flags
from the server's command):
claude mcp add --scope project mcp-compensator \
-- /absolute/path/to/mcp-compensator/.venv/bin/python -m compensator.proxy \
--config /absolute/path/to/mcp-compensator/compensators.yaml \
--db /absolute/path/to/mcp-compensator/compensator.db--scope project writes the entry into .mcp.json at your project root
so it's shared via version control; use --scope user instead for a
config available across all your projects, or omit --scope for a
local-only entry. Equivalently, edit .mcp.json by hand:
{
"mcpServers": {
"mcp-compensator": {
"command": "/absolute/path/to/mcp-compensator/.venv/bin/python",
"args": [
"-m", "compensator.proxy",
"--config", "/absolute/path/to/mcp-compensator/compensators.yaml",
"--db", "/absolute/path/to/mcp-compensator/compensator.db"
]
}
}
}Restart Claude Code (or run /mcp to check connection status) and the
proxy's tools — including checkpoint, list_checkpoints,
list_changes, undo_to, explain_blast_radius, and every namespaced
downstream tool — appear alongside your other tools.
GitHub Copilot (VS Code)
VS Code's native MCP support (which Copilot Chat's agent mode uses) reads
.vscode/mcp.json at your workspace root:
{
"servers": {
"mcp-compensator": {
"type": "stdio",
"command": "/absolute/path/to/mcp-compensator/.venv/bin/python",
"args": [
"-m", "compensator.proxy",
"--config", "/absolute/path/to/mcp-compensator/compensators.yaml",
"--db", "/absolute/path/to/mcp-compensator/compensator.db"
]
}
}
}Or use the guided flow instead of hand-editing JSON: open the Command
Palette (Cmd/Ctrl+Shift+P) → MCP: Add Server → choose Command
(stdio) → point it at the interpreter/args above → choose Workspace
to write it into .vscode/mcp.json (or Global for a user-level
config). Then open the Chat view, switch to Agent mode, and the
proxy's tools show up in the tools picker.
Registry format
compensators.yaml has two top-level keys.
servers:
tasks:
command: /path/to/venv/bin/python # absolute path -- see Known gotchas
args: ["examples/tasks_server.py"]
env: {} # optional, passed through explicitly
tools:
tasks__delete_task:
classification: compensable
snapshot_tool: tasks__get_task # called before the real call, to capture prior state
snapshot_args:
id: "{args.id}"
inverse_tool: tasks__restore_task
inverse_args:
id: "{snapshot.id}"
title: "{snapshot.title}"
status: "{snapshot.status}"Every downstream tool is addressed by its namespaced name,
"{server}__{tool}" (double underscore separator). A policy entry can
configure:
Key | Meaning |
| One of |
| Namespaced tool to call before the real call, to capture prior state. |
| Argument template for the snapshot call. |
| Namespaced tool that reverses this one. |
| Argument template for the inverse call. |
Argument templates resolve dotted paths against a context built from
the original call: {args, result, snapshot}. {args.id} reaches into
the arguments the original call was made with; {result.id} reaches into
what it returned; {snapshot.title} reaches into whatever the
snapshot_tool call returned. A template value that is exactly
"{some.path}" resolves with its native type preserved — an int stays an
int, it doesn't get stringified. Anything else (a plain string, a nested
dict/list with templates inside it) is treated as a literal or recursed
into.
Any tool called through the proxy that has no entry in tools: defaults
to the unknown classification: it's still journaled so you can see it
happened, but undo_to will refuse to touch it and tell you why, rather
than silently pretending it wasn't there.
The four classifications
In order of how much you can trust the undo:
read— no side effects. Nothing is journaled; there's nothing to undo.reversible— a true inverse exists (create↔delete,complete↔uncomplete).undo_tocalls the configuredinverse_tooldirectly, built from the original call's own arguments/result.compensable— no inverse can be built from the response alone (adeletedoesn't return the deleted row). The proxy callssnapshot_toolbefore the real call, andundo_touses that snapshot to drive a compensating call that reconstructs prior state.irreversible— no inverse exists at all.explain_blast_radiuswill say so before you run it;undo_toreports it inskippedrather than pretending it did something.
(There's a fifth, implicit tier — unknown, described above — for
anything the registry hasn't been told about.)
API reference
Five meta tools, alongside every namespaced downstream tool:
checkpoint(label?)→ starts a new checkpoint; returns{"checkpoint_id": <int>}. Every mutating call after this point is journaled against it.list_checkpoints()→ lists every checkpoint ever created, newest first, as{id, label, created_at}. Use this to find acheckpoint_idforlist_changes/undo_towhen you don't already have one in hand.list_changes(checkpoint_id?)→ lists not-yet-undone journaled changes, newest first. Omitcheckpoint_idto see every outstanding change; pass one to see only changes at or after it.undo_to(checkpoint_id)→ undoes every reversible/compensable change at or after that checkpoint, most recent first. Returns{"undone": [...], "skipped": [...]}—skippedentries include why (irreversible, unregistered, or the compensator call itself raised; one failure doesn't abort the rest of the undo).explain_blast_radius(tool)→ looks up a namespaced tool and returns its classification plus a plain-language explanation of what undoing it would (or wouldn't) do — meant to be called before the tool itself, so an agent can decide whether it's comfortable proceeding.
Known limitations
Read this before trusting mcp-compensator with anything that matters.
This is compensation, not rollback. A compensator is a new, independent write that approximates prior state — it does not undo side effects the original action had beyond what was snapshotted. A webhook that fired during
delete_task, an email that got sent, a counter decremented somewhere else entirely: none of that unwinds. The proxy only ever sees the downstream MCP tool calls that flow through it.Identity isn't always restorable. The demo's
restore_taskcan reinsert a row under its original id because it's a toy SQLite table under our own control. Real systems that assign ids themselves — autoincrement you don't control, a ticket number, a commit SHA — generally can't be restored under the same identity. Anything that referenced the old id in the meantime is now dangling.undo_tois itself just another mutating call, from each downstream server's point of view — the inverse/compensating calls it makes are real writes with real (if any) side effects of their own. This proxy's chosen policy: compensating calls are not journaled and not themselves undoable.undo_tois terminal. If you need undo-of-undo, you'd need to journal compensations too and decide how deep that stack is allowed to go — this project deliberately doesn't, to keep the semantics of "undo" unambiguous.Undo is best-effort per change, not transactional across changes. If one compensator call fails partway through an
undo_to, the rest still run; you get a mix ofundoneandskippedback, not an all-or-nothing guarantee.A snapshot is only as fresh as the moment it was taken. If something else mutates the same downstream state between the snapshot and the undo (e.g. another agent, another session), the compensating call will happily overwrite that intervening change with the older snapshot.
Known gotchas
Things that will bite you if you don't pin them down:
Pin
mcp==1.29.0.pip install "mcp[cli]"unpinned currently resolves to a2.0.0release with a broken import chain (ImportError: cannot import name 'TASK_STATUS_COMPLETED' from 'mcp.types'). Use a clean venv and pin the version explicitly, as this project'spyproject.tomldoes.MCP subprocess environments are NOT inherited from the parent.
mcp.client.stdio.get_default_environment()only forwards a small safe allowlist (HOME,LOGNAME,PATH,SHELL,TERM,USER). Don't rely on env vars to select an interpreter in a server config — use a literal absolute path to whatever interpreter has that downstream server's dependencies installed, same as any real MCP client config does. If a server genuinely needs extra env vars, pass them explicitly via itsenv:block incompensators.yaml, which does get merged in on top of the default allowlist.
Project layout
compensator/
journal.py # SQLite-backed append-only log of checkpoints + changes
registry.py # loads the YAML compensator registry, resolves arg templates
downstream.py # manages one subprocess connection to a downstream MCP server
results.py # CallToolResult <-> plain dict helpers
proxy.py # the aggregator: list_tools/call_tool handlers, meta tools, undo logic
examples/
tasks_server.py # demo downstream MCP server (FastMCP, SQLite-backed task list)
compensators.yaml # example server list + tool registry for the tasks demo
demo.py # end-to-end script: drives the proxy as a real MCP client would
tests/ # journal, registry template resolution, full demo flowLicense
MIT — see LICENSE.
This server cannot be installed
Maintenance
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Related MCP Servers
- AlicenseAqualityAmaintenanceSecurity-enforcing MCP proxy that sits between an AI agent and any number of downstream MCP servers, intercepting every tool call through a capability-token policy gateway that can allow, deny, or escalate to human approval before the call reaches any real tool. It also exposes built-in operator tools for approval workflows, audit trail queries, token management, voice/HUD output, and hierarchicalLast updated2111Apache 2.0
- Alicense-qualityDmaintenanceEnables trust, reputation, and economic accountability for MCP by proxying between clients and servers, enriching every tool invocation with trust evaluation, KYA tiers, spending limits, and delegation chains.Last updatedMIT
- Flicense-qualityCmaintenanceWraps your existing MCP servers and checks each tool call against policy and live state before it runs. Allow, block, or require a refresh, with a reason the agent can act on.Last updated5
- AlicenseBqualityAmaintenanceA governance proxy for AI tools — every MCP/agent tool call is policy-gated, secret-redacted, and written to a hash-chained, offline-verifiable audit trail.Last updated13MIT
Related MCP Connectors
Hash-chained HMAC-signed audit log MCP for A2A (agent-to-agent) calls. Every tool-call, agent-ha...
Runtime permission, approval, and audit layer for AI agent tool execution.
Control plane for autonomous software labor. Agents claim objectives over MCP with audit trail.
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/mohithhhh/mcp-compensator'
If you have feedback or need assistance with the MCP directory API, please join our Discord server