Server data from the Official MCP Registry
Default-deny action registry, append-only spend ledger, and human sign-off audit trail (MCP tools).
Default-deny action registry, append-only spend ledger, and human sign-off audit trail (MCP tools).
Valid MCP server (1 strong, 5 medium validity signals). No known CVEs in dependencies. Imported from the Official MCP Registry. 1 finding(s) downgraded by scanner intelligence.
12 files analyzed · 1 issue found
Security scores are indicators to help you make informed decisions, not guarantees. Always review permissions before connecting any MCP server.
This plugin requests these system permissions. Most are normal for its category.
Set these up before or after installing:
Environment variable: RAILS_MCP_CONFIG_PATH
Environment variable: RAILS_MCP_SIGNOFF_LEDGER_PATH
Environment variable: RAILS_MCP_SPEND_LEDGER_PATH
Add this to your MCP configuration file:
{
"mcpServers": {
"io-github-jaimenbell-rails-mcp": {
"args": [
"rails-mcp"
],
"command": "uvx"
}
}
}From the project's GitHub README.
A self-hosted, caller-configured default-deny action registry + append-only spend ledger + CLI-only sign-off audit trail, exposed as MCP tools. Different category from this author's other six MCP servers (mcp-factory, rag-mcp, bus-mcp, desktop-mcp, github-mcp, discord-mcp) -- those are devtools ("connect an agent to X"); this one is governance and safety: "stop an agent from doing something irreversible without a human noticing."
Ported and generalized from a live internal registry (shared/rails/,
31 tests, running against a real multi-bot fleet since 2026-07-06) --
this pattern shipped internally before it shipped publicly.
Is:
action_type strings as
unconditionally GATED (default-deny), registered or not.enforcement_layer, enforcement_pointer, ceremony) --
informational, never permissive.Is NOT:
classify_action returning GATED does not block
anything by itself. You still wire it into your own PreToolUse hook,
permission deny-list, or CI gate -- rails-mcp gives you the schema and
the audit trail, not the interceptor. record_spend_intent records
intent to spend; it never calls a vendor, a paid API, or a broker, and
nothing here stops an over-budget spend from happening.classify_action(action_type) always returns "GATED" -- registered
or not, whatever config was loaded, no argument or config field can change
it. This is the whole product. A config-driven fail-open knob would defeat
the entire pitch, so classify() (rails_mcp/registry.py) takes only an
action_type argument: there is no parameter through which a caller could
ever make it return anything permissive. is_action_registered answers a
separate, purely informational question -- "do I know something about this
action-type's enforcement?" -- and never feeds back into the GATED verdict.
pip install rails-mcp
Add to your Claude Desktop/Code MCP config:
{
"mcpServers": {
"rails-mcp": {
"command": "rails-mcp"
}
}
}
No console script on PATH? Fall back to "command": "python", "args": ["-m", "rails_mcp"].
By default the registry loads empty (honest-empty, not fail-open --
classify_action is still unconditionally GATED for everything). Point
it at your own action-type config:
{
"mcpServers": {
"rails-mcp": {
"command": "rails-mcp",
"env": { "RAILS_MCP_CONFIG_PATH": "C:\\path\\to\\rails.config.yaml" }
}
}
}
See examples/rails.config.example.yaml (or .example.json) for the
config shape.
All six are read-mostly -- none of them can write to the sign-off ledger.
| Tool | Purpose |
|---|---|
classify_action(action_type) | Default-deny verdict: always "GATED". Implemented in rails_mcp/registry.py::classify, tested in tests/test_registry.py + tests/test_server.py. |
is_action_registered(action_type) | Whether the loaded registry has an entry, plus enforcement_layer/enforcement_pointer/ceremony when present. rails_mcp/routes.py::is_action_registered, tested in tests/test_routes.py. |
get_rails_hash() | 12-hex sha256 digest of the loaded registry + entry count -- the value a human sign-off records. rails_mcp/registry.py::rails_hash, tested in tests/test_registry.py. |
get_signoff_state() | Current active human sign-off, or null. Read-only. rails_mcp/registry.py::load_signoff_state, tested in tests/test_registry.py + tests/test_routes.py. |
record_spend_intent(amount_usd, vendor, purpose, actor) | Append one spend-intent record. Never calls a vendor or paid API. rails_mcp/spend_ledger.py::record_spend_intent, tested in tests/test_spend_ledger.py. |
evaluate_budget(limit_usd, window_days=30.0) | Rolling-window spend total vs. limit. Never raises. rails_mcp/spend_ledger.py::evaluate_budget, tested in tests/test_spend_ledger.py. |
append_signoff -- the function that records a human blessing the
registry's current hash -- is deliberately not an MCP tool, and never
will be. It is exposed only as a CLI command a human runs by hand:
rails-mcp sign --operator "jaime" --note "reviewed 2026-07-16 config"
Why: the boundary exists to prevent an agent holding only this
server's MCP tool connection from self-approving an irreversible action.
If append_signoff were reachable as an MCP tool, any agent holding this
server's connection could sign its own registry -- silently defeating the
one thing the boundary exists to enforce. This mirrors the internal design
rule the original shared/rails/ implementation was built around: the
lane that builds the auditor never signs the registry it ships. An
auditor that can also sign isn't an auditor.
What this boundary does not prove: the sign-off ledger has no
cryptographic tamper-evidence and no binding to a real human identity --
its integrity rests entirely on filesystem ACLs and the self-hosted
deployment model, not on cryptography. An agent (or anyone) with shell or
file-write access to the ledger's path can run rails-mcp sign itself, or
hand-append a forged {"type": "signoff", ...} JSONL line straight into
the file -- the ledger has no way to tell that apart from a real CLI
invocation. The MCP-only boundary stops the narrower case of an agent that
has only this server's MCP tool connection; it is not proof that a human
reviewed anything, and shouldn't be read as one.
This boundary is enforced structurally, not just by convention:
rails_mcp/server.py and rails_mcp/routes.py never import or call
append_signoff, anywhere -- proven by an AST-based check (not a naive
string grep, which would false-positive on this very explanation
appearing in their docstrings) in
tests/test_server.py::test_append_signoff_unreachable_via_any_mcp_tool.sign/append_signoff/revoke_signoff tool exists, checked in
tests/test_server.py::test_all_six_rails_tools_registered.test_no_registered_tool_can_create_a_signoff_record).run_server.py (the entrypoint ~/.claude.json invokes) imports only
rails_mcp.server, never rails_mcp.cli -- so even the process that
serves MCP tools has no code path to the sign subcommand.| Var | Default | Purpose |
|---|---|---|
RAILS_MCP_CONFIG_PATH | unset | Path to your rails.config.{yaml,yml,json}. Unset = honest-empty registry (nothing registered, classify_action still unconditionally GATED). |
RAILS_MCP_SIGNOFF_LEDGER_PATH | ./rails_data/signoff.jsonl | Where the append-only sign-off ledger lives. |
RAILS_MCP_SPEND_LEDGER_PATH | ./rails_data/spend.jsonl | Where the append-only spend-intent ledger lives. |
actions:
deploy_prod:
enforcement_layer: "L1"
enforcement_pointer: "CI gate requires a passing e2e suite + a manual approve step"
ceremony: "operator hand"
Or the more compact 3-element form (matches the internal registry's native shape):
actions:
deploy_prod: ["L1", "CI gate requires a passing e2e suite + a manual approve step", "operator hand"]
JSON works identically ({"actions": {"deploy_prod": [...]}}). See
examples/ for full examples of both.
enforcement_layer should be honest, not aspirational -- "prose"
(no structural rail exists yet, just a doc) is a legitimate, correct value.
Rounding a "prose" entry up to "L1" because it feels better defeats the
entire point of an honest registry.
.venv/Scripts/python.exe -m pytest -q
CI (.github/workflows/ci.yml) runs this suite on every push/PR and fails
the build if the Tests badge above drifts from what the suite actually
reports -- see scripts/check_readme_counts.py.
106 tests, all hermetic (every ledger/config path goes through tmp_path +
an autouse env-isolation fixture in tests/conftest.py; nothing touches a
real ./rails_data/). No network, no live-smoke gate needed -- this
server has no external API to fake.
tests/test_registry.py (24) -- the ported + generalized registry logic:
default-deny property tests, immutability, hash determinism, sign-off
ledger fold/append/load, structural no-shell-out proof.tests/test_spend_ledger.py (14) -- ported near-verbatim from the
internal suite: append/load roundtrips, budget window math, naive-
datetime honest-degrade, structural no-effector proof.tests/test_config.py (18) -- new: env-var resolution, YAML/JSON
loading in both entry shapes, honest-empty-when-unconfigured, loud
failure on an explicit missing path.tests/test_routes.py (14) -- the MCP tool surface's business logic,
exercised directly.tests/test_server.py (17) -- tool registration, passthrough
correctness, and the CLI-only sign-off structural + behavioral proof.tests/test_cli.py (8) -- the serve/sign subcommands, including
that sign is genuinely append-only and prints a human-readable
confirmation.tests/test_check_readme_counts.py (11) -- this CI gate's own TDD
suite: parse-claimed, parse-actual, compare, and main() end-to-end
against match/drift/missing fixtures.python -m venv .venv
.venv/Scripts/python.exe -m pip install -e ".[test]"
Registered in ~/.claude.json under mcpServers.rails-mcp as a stdio
server invoking run_server.py by absolute path (no cwd needed -- the
entrypoint adds its own directory to sys.path), OR via the rails-mcp
console script once installed from PyPI.
.venv/Scripts/python.exe scripts/list_tools.py
Prints the six registered tool names with no transport started.
The closest prior art is not a hosted dead-man's-switch product -- that's a different problem ("is the operator still alive and watching"). The closer comparisons, once actually verified:
spend_ledger.py specifically
(budgets, approvals, audit trail).rails-mcp's narrower bet: a small, inspectable, self-hosted
registry+ledger+audit-trail schema with one hard invariant (default-deny
classification can never be configured away) and one hard boundary
(sign-off is CLI-only, never MCP-reachable) -- not a full policy-engine
product.
settings.json deny rules, no
graduation gates. You wire classify_action/is_action_registered into
your own interceptor.coverage_audit.py) and is not part of this package.evaluate_budget tells you the
number; nothing here intercepts a call before it happens.record_spend_intent appends forever --
there's no rotation, size cap, or archival built in. A known limitation,
not yet a problem at v1 scale.Maintained by Jaimen Bell. For production MCP integrations, agent-governance rails, or agent-reliability work, see jaimenbell.dev or sponsor ongoing maintenance via GitHub Sponsors.
mcp-name: io.github.jaimenbell/rails-mcp
Be the first to review this server!
by Modelcontextprotocol · Developer Tools
Web content fetching and conversion for efficient LLM usage
by Toleno · Developer Tools
Toleno Network MCP Server — Manage your Toleno mining account with Claude AI using natural language.
by mcp-marketplace · Developer Tools
Create, build, and publish Python MCP servers to PyPI — conversationally.