Skip to main content
Glama
mohithhhh

mcp-compensator

by mohithhhh

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 create returns an id you can delete. A complete has an obvious uncomplete. This is the reversible case — a true inverse exists.

  • The response doesn't contain enough (a delete returns 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.py

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

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

pytest

Wiring it up for real

  1. Write a compensators.yaml listing your downstream servers under servers: and a compensator policy per mutating tool under tools: (see Registry format below; examples/compensators.yaml is a full worked example).

  2. Point your MCP client at the proxy instead of at your servers directly:

    python -m compensator.proxy --config compensators.yaml --db compensator.db
  3. Your agent now sees every downstream tool renamed to {server}__{tool}, plus checkpoint, list_checkpoints, list_changes, undo_to, and explain_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

classification

One of read, reversible, compensable, irreversible (see below).

snapshot_tool

Namespaced tool to call before the real call, to capture prior state.

snapshot_args

Argument template for the snapshot call.

inverse_tool

Namespaced tool that reverses this one.

inverse_args

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 (createdelete, completeuncomplete). undo_to calls the configured inverse_tool directly, built from the original call's own arguments/result.

  • compensable — no inverse can be built from the response alone (a delete doesn't return the deleted row). The proxy calls snapshot_tool before the real call, and undo_to uses that snapshot to drive a compensating call that reconstructs prior state.

  • irreversible — no inverse exists at all. explain_blast_radius will say so before you run it; undo_to reports it in skipped rather 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 a checkpoint_id for list_changes/undo_to when you don't already have one in hand.

  • list_changes(checkpoint_id?) → lists not-yet-undone journaled changes, newest first. Omit checkpoint_id to 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": [...]}skipped entries 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_task can 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_to is 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_to is 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 of undone and skipped back, 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 a 2.0.0 release 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's pyproject.toml does.

  • 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 its env: block in compensators.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 flow

License

MIT — see LICENSE.

A
license - permissive license
-
quality - not tested
C
maintenance

Maintenance

Maintainers
Response time
Release cycle
Releases (12mo)
Commit activity

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

  • A
    license
    A
    quality
    A
    maintenance
    Security-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 hierarchical
    Last updated
    21
    11
    Apache 2.0
  • A
    license
    -
    quality
    D
    maintenance
    Enables 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 updated
    MIT
  • A
    license
    B
    quality
    A
    maintenance
    A 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 updated
    13
    MIT

View all related MCP servers

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.

View all MCP Connectors

Latest Blog Posts

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