rails-mcp — a default-deny governance rail for agents
Python MCP SDK 106 passing tests default-deny CLI-only sign-off public repoThe problem
Every other MCP server I build answers "connect an agent to X." This one answers the opposite question: how do you stop an agent from doing something irreversible without a human noticing? The moment agents can spend money, deploy code, or delete data, the interesting failure mode isn't a bug — it's a fail-open governance layer that quietly approves the thing it was supposed to gate. A rail with a config knob that can flip it permissive is worse than no rail, because it looks like one.
What I built
rails-mcp is a self-hosted, caller-configured default-deny action registry + append-only spend ledger + CLI-only sign-off audit trail, exposed over the Python MCP SDK. Six read-mostly tools, one hard invariant, one hard boundary:
- The invariant that is never configurable —
classify_action(action_type)always returnsGATED. No argument, no config field, no loaded registry can make it return anything permissive. A config-driven fail-open knob would defeat the entire product, so the function simply has no parameter through which a caller could ever soften it. - Honest-empty, not fail-open — with no config loaded the registry is empty, and everything is still unconditionally
GATED. Adopters supply their own action-types; no fleet's specifics ship in the package. - Sign-off is CLI-only, never an MCP tool — the function that records a human blessing the registry's current hash is exposed only as a hand-run CLI command. An agent holding just this server's tool connection cannot self-approve — enforced structurally (an AST-based test proves the server code never imports or calls it), not by convention.
- Append-only ledgers — a spend-intent ledger + rolling-window budget check, and a human sign-off ledger.
record_spend_intentnever calls a vendor or a paid API; it records intent, nothing more.
Evidence you can check yourself
Public on purpose — every claim maps to a file you can open before you reply:
- Repo: github.com/jaimenbell/rails-mcp (MIT, also on PyPI)
- Tests:
.venv/Scripts/python.exe -m pytest -q→106 passed, all hermetic (every ledger/config path goes throughtmp_path; no network, no live-smoke gate — this server has no external API to fake). - The boundary is a test:
test_append_signoff_unreachable_via_any_mcp_toolproves — by AST, not a string grep — that no registered tool can reach the sign-off writer, and a behavioral test confirms the sign-off ledger file is never created by any tool.
Stated plainly in the README: rails-mcp is not an enforcer.
classify_action returning GATED does not block anything by itself —
you still wire it into your own PreToolUse hook, deny-list, or CI gate. And the sign-off ledger
has no cryptographic tamper-evidence: its integrity rests on filesystem ACLs and the self-hosted
model, not on crypto. It stops the narrow, real case of an agent that holds only this
server's tools from self-approving — it is not proof a human reviewed anything, and the README
says so.
What it shows about how I work
This pattern shipped internally against a live multi-bot fleet before it shipped publicly. The discipline it demonstrates is the one that matters for anything with a blast radius: pick the one invariant that must never bend, make it structurally impossible to bend, and prove the boundary with a test rather than asserting it in a comment. That's how I'd build the governance layer in front of your agents' irreversible actions.