The agent that
asks first
Project #2 built a loop that calls every tool the instant the model picks it, keeps no record, and forgets everything on refresh. That's fine for dice and cookies. It is not fine the moment a tool can delete, send, or charge something.
This one stops and asks — and writes down what happened either way.
The uncomfortable question
Your agent works. It rolls dice, adds cookies, chains tool calls, shows every step. Now connect one more server — one with a delete_file tool.
Project #2's loop
The model picks the tool. The loop runs it. Those are the same instant — there is no gap between them where anything could intervene. No pause, no confirmation, no record. Refresh the page and it never happened.
This loop
Between “the model picked a tool” and “the tool ran” there is now a gap, and a human standing in it. Everything that happens on either side is written down.
Four things, in plain words
Forget the jargon for a second. This project gives the agent a notebook, an are-you-sure, a report card, and a rewind button. Everything else is detail.
A notebook
It writes down everything it does — every conversation, every run, every step, every approve and deny — in Postgres. A refresh doesn't wipe it, and neither does a different machine picking up the next request.
An "are you sure?"
Before anything it can't undo, it stops and shows you the exact tool and the exact arguments. You click Approve or Deny. Deny feeds a refusal back into the loop and the agent adapts.
A report card
A list of prompts and what should happen for each. Run them, score a pass rate, diff against last time. It turns “feels better” into a number that moved or didn't.
A rewind button
Because the notebook recorded every step in order, you can pull up any past run and watch it again — including the calls that failed and the ones you refused.
Try it. It will stop you.
A real loop against two real MCP servers — this repo's cookie jar (backed by a real Postgres database) and project #1's live server. Every tool call, argument, result and token count is shown as it happens.
Ask it to empty the jar and watch the run stop dead. The HTTP request that started it has already finished by then — the agent is sitting in Postgres waiting for you. You could close the tab and come back tomorrow.
Pick one, or ask your own.
Start with the first one. It stops and asks you before it does anything.
Who decides what's dangerous?
MCP lets a server advertise destructiveHint: true on a tool. This host sets that flag honestly on its own tools — and then never reads it.
That flag is a claim, sent over a network, by a machine you don't control. If your gate consults it, then any server that wants to bypass your gate simply sets it to false. You'd have built a lock whose key is kept by the person outside the door.
A hint from the other side of a network boundary is not a permission model.
So the list lives here, on the host side
cookiejar__smash_jarwhen alwayssmash_jar permanently deletes every cookie AND erases the jar's entire history. There is no undo and no backup.
cookiejar__cookie_jarwhen action is "eat"Eating removes cookies from the jar. Cookies cannot be un-eaten, so a human should see the number first.
cookiejar__cookie_jarwhen action is "refill"Refill resets the jar to exactly 12 cookies. If the jar currently holds more than 12, this silently destroys the difference.
Everything else runs without asking. Rules read the arguments, not just the name — because cookie_jar isn't dangerous, but cookie_jar { action: "eat" } is. The whole file is lib/approval.ts.
The rewind button
Every run above was written to Postgres as it happened — one row per event, in order. So replaying one is a select. No model is called and nothing re-runs; you are reading back what actually happened, including the calls that failed and the ones somebody refused.
This project ships a server too
The cookie jar from project #1, with its memory finally fixed. Same tools, same protocol — but the count lives in Postgres instead of a variable, so it survives restarts and agrees across machines. Plug it into Claude Code like any other server:
claude mcp add --transport http cookie-jar-durable https://learn-mcp-agent-guard.vercel.app/api/jar
It offers cookie_jar, jar_history, secret_code and smash_jar — that last one genuinely, permanently deletes data, which is exactly why the gate exists.