tollgate
This server does not currently have a deployable release on Glama. Follow the repository's setup instructions to install and configure it for your MCP client, including any required credentials and local dependencies.
countersigned
Your agent proposes. A human countersigns. Then it runs, verified.
Terraform-style plan / apply for AI agent actions.
Your agent can read anything. When it wants to change something, it proposes a plan: the current state is read, business rules are checked, the exact effects are previewed. A human (or a policy) approves that exact plan. Then apply re-checks that the world has not moved, executes with idempotency keys, verifies every step against the real system, rolls back in reverse if anything fails, and writes it all to a tamper-evident ledger.
Zero dependencies. TypeScript. Speaks MCP out of the box, so it works with Claude, Cursor, VS Code, OpenAI Agents and anything else that talks Model Context Protocol.

agent ──plan──▶ observe state · check rules · preview effects · policy decides
│
human approves the exact plan hash (CLI, UI, chat button) ◀── the agent has no "approve" tool
│
agent ──apply─▶ recheck state (drift? refuse) ─▶ execute(idempotencyKey) ─▶ verify against the system
│ failure
▼
compensate earlier steps, in reverse
every event ─▶ hash-chained ledgerWhy
Agents moved from answering to doing. Writing to real systems (an ERP, a CRM, a billing API, a database) breaks in ways a better prompt cannot fix:
What goes wrong | What countersigned does |
The agent acts on state that changed after the human said yes | Snapshot at plan time, re-observe before apply, refuse on drift ( |
A retry or a crash sends the email twice, books the order twice | Stable idempotency key per step, forwarded to your system; completed steps are never repeated |
"Done!" says the agent. It was not. |
|
Step 3 fails, steps 1 and 2 already changed things | Saga compensation in reverse order; irreversible leftovers are flagged for a human |
The approved plan is not the plan that runs | Approval is pinned to a SHA-256 of the plan; any edit invalidates it |
The agent approves itself | Approval lives outside the agent's process and tools; requester cannot approve |
"What exactly happened at 3am?" | Append-only, hash-chained ledger; |
Terraform solved this for infrastructure years ago. Nobody had generalized it to the rest of the systems agents now touch.
Related MCP server: enterprise-agent-lab
See it
git clone https://github.com/Akiyo974/countersigned && cd countersigned
node examples/erp/demo.ts # Node >= 22.18, no install neededThe GIF above is generated from real runs of examples/erp/agent.ts and the CLI (python3 scripts/render-demo.py . docs/assets/demo.gif). The longer narrated demo runs an agent against a deliberately annoying fake ERP: documents locked by users, writes that report success but do not stick, emails that cannot be unsent. Excerpt:
5. The ERP says "OK" but the write did not stick: verification catches it, earlier steps are rolled back
Plan pl_mw2gf0p3b5f8be729d: COMPENSATED
Step 1 (set_reorder_point) failed: verification failed: ERP shows 3, expected 6.
Every earlier effect was compensated; the system is back to its approved state.
0. reschedule_order: compensated
1. set_reorder_point: failed (verification failed: ERP shows 3, expected 6; compensated)Define an action
An action is five small functions. Writing them forces the questions that matter before the agent runs in production.
import { defineAction, t } from "countersigned";
export const setReorderPoint = defineAction({
name: "set_reorder_point",
description: "Change the reorder point of an inventory item. Reversible.",
reversibility: "reversible", // read | reversible | compensable | irreversible
input: t.object({ sku: t.string(), reorderPoint: t.integer({ min: 0 }) }),
// 1. What state does this depend on? Snapshotted at plan time, re-checked before apply.
observe: ({ sku }) => erp.getItem(sku),
// 2. Business rules. Any violation denies the plan before a human is bothered.
check: (input, item) => (item ? [] : ["item does not exist"]),
// 3. What will change, in words a human can approve.
preview: ({ sku, reorderPoint }, item) => [
{ target: `item:${sku}`, op: "update", before: item.reorderPoint, after: reorderPoint },
],
// 4. Do it. Pass the idempotency key downstream.
execute: ({ sku, reorderPoint }, { idempotencyKey }) => erp.update(sku, { reorderPoint }, idempotencyKey),
// 5. Prove it. Re-read the system; never trust the return value alone.
verify: ({ sku, reorderPoint }) => erp.getItem(sku)?.reorderPoint === reorderPoint,
// The way back. Required for reversible and compensable actions.
compensate: ({ sku }, _result, { before }) => erp.update(sku, { reorderPoint: before.reorderPoint }),
});defineAction refuses, at boot, a writable action without a way back, an irreversible action pretending to have one, or a description too thin for a model to choose correctly.
Already using zod, valibot or ajv? Wrap it with t.custom(jsonSchema, parse).
Use it from code
import { FileStore, rules, Gate } from "countersigned";
const gate = new Gate({
store: new FileStore(".countersigned"),
rules: [rules.irreversibleNeedsApproval(), rules.autoApprove("set_reorder_point"), rules.maxEffects(20)],
}).register(setReorderPoint, rescheduleOrder, emailSupplier);
const plan = await gate.plan(steps, { intent: "absorb the supplier delay", requestedBy: "agent-42" });
// plan.status: "approved" (policy allowed) | "pending_approval" | "denied"
await gate.approve(plan.id, { by: "christen" }); // or from the CLI, or your UI
const report = await gate.apply(plan.id); // safe to call againUse it as an MCP server
import { serveStdio } from "countersigned/mcp";
serveStdio(gate, { agentId: "claude-desktop" });The agent sees countersigned_list_actions, countersigned_plan, countersigned_get_plan, countersigned_apply, plus every read-only action as a direct tool. It never sees an approve tool. Example config (Claude Desktop, Claude Code, Cursor):
{
"mcpServers": {
"erp": { "command": "node", "args": ["/path/to/countersigned/examples/erp/mcp-server.ts"] }
}
}Then, in another terminal, the human side:
npx countersigned list # what is waiting
npx countersigned show pl_... # intent, before/after, why it needs you
npx countersigned approve pl_... # signs this exact plan hash
npx countersigned ledger verify # chain and plan records intact?Storage
import { FileStore, MemoryStore } from "countersigned";
import { SqliteStore } from "countersigned/sqlite";
new MemoryStore(); // tests
new FileStore(".countersigned"); // plain files, zero setup
new SqliteStore("countersigned.db"); // transactional, built on node:sqlite (still zero dependencies)Any database works: a Store is six async methods (createPlan, getPlan, updatePlan, listPlans, appendLedger, readLedger).
Policies
Rules are plain functions (step, plan) => { decision, reason } | undefined. Strictest opinion wins and deny can never be relaxed. With no rules: reads run, every write waits for a human.
Built in: autoApprove, requireApproval, deny, irreversibleNeedsApproval, tagNeedsApproval, maxEffects (blast radius), maxAutoSteps. A custom rule that lets small changes through:
(step) => step.action === "set_reorder_point" && Math.abs(after - before) <= 2
? { decision: "allow", reason: "change of 2 units or less" }
: undefinedGuarantees, and what is not guaranteed
Honest list, because safety tooling that oversells is worse than none.
Drift detection is as good as your
observe(). Observe exactly what the action depends on, from the source of truth.The gate's recheck is time-of-check. A change between the recheck and the write is still possible unless the target system can refuse it. Keep the version (ETag, row version) in
observe()and send it with the write (If-Match): the server then rejects any last-instant change, andexecute()throwsNotAppliedErrorso the gate rolls back cleanly.examples/restshows the full pattern, tested.Compensation is not a transaction. It is a best effort correction. That is why reversibility is declared per action, and irreversible effects that already happened are reported as
unrecoverableinstead of hidden.If
execute()throws, the outcome is unknown. countersigned does not guess: the step is reported for a human, the key lets you look it up. When you know nothing happened (a 409, 412 or 422 from the API), throwNotAppliedErrorinstead.An interrupted apply is never resumed implicitly. If the process dies mid-apply, every further
applyis refused (APPLY_IN_PROGRESS), so an agent cannot crash its way into a re-run. An operator callsgate.resume(id, { by })(not exposed over MCP): ownership is taken atomically (a second resume, or the original process if still alive, stops before its next step), finished steps are skipped, a step that started without a recorded outcome is checked withverify()against the real system and is never blindly re-executed, and steps that never started are re-checked for drift and expiry (overridable withignoreDrift/allowExpiredwhen the operator has looked).FileStore and SqliteStore are single-machine. Both are safe across processes on one host. For several hosts, implement the
Storeinterface on your database (six async methods).The ledger is tamper-evident, not tamper-proof. Edits, insertions and deletions break the chain; entries cut from the end are caught because every plan records the last entry written for it (
countersigned ledger verify). Someone able to rewrite both plans and ledger can still forge history, so publishcountersigned ledger headsomewhere the host cannot rewrite (a git commit, a log service) and verify with--head.
Status
v0.1, working and tested (65 tests, including a real MCP client loop over stdio, a real HTTP API with ETags, multi-process SQLite, and regression tests for every finding of an adversarial security review). APIs may still move before 1.0.
Roadmap
Python SDK with the same plan format, so one ledger serves both ecosystems
SQLite store (
countersigned/sqlite, built onnode:sqlite)Postgres store
Approval channels: Slack / Teams buttons, a small web inbox, email links
Multi-approver and role-based policies (two signatures over 10 000 $)
Adapters: Dynamics 365 Business Central / NAV OData, Odoo, Shopify, Stripe, generic REST with ETags
Remote MCP (Streamable HTTP) transport with OAuth
OpenTelemetry export of ledger events
Ledger anchoring (periodic hash published to git or a transparency log)
Contributing
Issues, adapters and ideas are welcome: see CONTRIBUTING.md and the Code of Conduct. Found a way around a safety guarantee? Please report it privately, see SECURITY.md.
License
This server cannot be deployed
Maintenance
Related MCP Connectors
Runtime permission, approval, and audit layer for AI agent tool execution.
Context and control for any AI. Agent identity, scoped access, and proof across tools.
Human-in-the-loop review and approval for AI agents. Audit trail, approval policies, native MCP.
Security gateway for AI agents: policy, approval, and audited execution, no secrets shared.
Related MCP Servers
- AlicenseNot gradedqualityCmaintenanceSafe, reversible tool execution for AI agents. It sits between an agent and its tool servers, adding contracts, dry-run planning, policy, approvals, saga execution, and rewind.MIT
- FlicenseNot gradedqualityCmaintenanceEnables controlled AI-agent access to enterprise-shaped tools with a deny-by-default gated write path, human approval, dry-run execution, and append-only audit logging.1-
- AlicenseAqualityBmaintenanceEnables AI agents to safely inspect and execute version-controlled operational runbooks with policy checks, dry-run planning, and out-of-band approvals.3MIT
- AlicenseNot gradedqualityBmaintenanceEnables MCP-compatible AI agents to safely act on business backends by enforcing per-agent permissions, autonomy thresholds, human approval with review-and-edit, and full audit trails.MIT