mcp-permission-server
Click on "Install Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@mcp-permission-serverdeny fs.read on /data/reports-private and log the denial"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
mcp-permission-server
A permission layer in front of MCP tools, and a log that can prove what it decided.
Every audit trail records what happened. Almost none record enough to recompute why it was allowed, and most record only what succeeded — so when somebody asks "how did the agent reach that file", the log cannot answer. It can only repeat the assertion that a decision was made.
NAIVE GUARDED
calls in the session 17 17
calls that must be denied 8 8
EXECUTED WITHOUT A GRANT 7 0
LEGITIMATE CALLS REFUSED 0 0 ← no over-blocking
DECISIONS THAT CANNOT BE 16 0 ← the number nobody reports
RECONSTRUCTED FROM THE LOG
denials that left no trace 1 0
secrets sitting in the log 2 0
entry removable unnoticed 1 0NAIVE is the permission model an MCP server ships with when permissions were
not the point: a list of tool names that are allowed, and a log of what
succeeded. Every call in this session except one passes it.
python3 demo.py
python3 gate.py --log audit.json # run the session
python3 gate.py --verify audit.json # re-decide every entry from the log alonechain: OK - the chain is intact
replay: 17/17 entries recompute to the verdict they record
secrets in the log: noneExit code: 0 all allowed, 1 something denied, 2 the log does not verify.
A grant is four things at once
Which is four separate ways for a live grant to still not cover a call — and the reason "does this caller have permission for this tool" is the wrong question to ask.
{"id": "G1", "tool": "fs.read", "scope": "/data/reports",
"purpose": "q3-report", "expires_at": 100, "max_uses": 10}the call | what is wrong with it | what else would have allowed it |
| a different directory that starts with the same characters | tool ✓ purpose ✓ expiry ✓ |
| the same directory, reached through the granted one | tool ✓ purpose ✓ expiry ✓ |
| the grant ran out forty ticks ago. Nothing about the call is wrong | tool ✓ scope ✓ purpose ✓ |
the third | a grant is a budget as well as a permission | tool ✓ scope ✓ purpose ✓ |
| identical to the first call in the session except for why it is being made | tool ✓ scope ✓ expiry ✓ |
| the template pulls in | tool ✓ scope ✓ purpose ✓ expiry ✓ |
| holding a grant for one key is not holding the keyring | tool ✓ purpose ✓ expiry ✓ |
There is a test that, for each of these, turns off only the check it names and requires the call to get through. A denial that several checks catch is a denial that proves nothing about any of them.
Related MCP server: MCP Airlock
The confused deputy
render_template is granted. /templates/leak.md is inside the granted scope.
The template contains {{include:/etc/service-token}}.
The server holds rights the caller does not, and is being asked to spend them on a resource the caller never named. A permission layer that checks the resource in the arguments is checking the wrong thing for exactly the tools where it matters.
So the check looks at what a call reaches, before it reaches it — and then
still says yes to /templates/report.md, which reaches
/data/reports/q3.csv, which this caller does hold a grant for. Half of a
permission check is the yes. A rule that blocks every template because
templates can include things is not a rule.
Path containment, which is where this is usually got wrong
Both mistakes are one line long.
path.startswith(scope) # /data/reports-private is "inside" /data/reports
".." in path # and this breaks ordinary callers to catch nothingThe second one deserves its own case. /data/reports/../reports/q3.csv
resolves back inside the granted scope, and it is in the corpus as a call
that must be allowed. Banning the characters is the cheap fix; resolving
the path catches everything banning it would, and nothing it would not.
Resolution here is lexical and never touches a filesystem — there is a test
that greps for realpath, os.stat and open(. The decision runs before the
call, on a path the caller supplied, and it must not be answerable differently
depending on what happens to exist.
The log is the product
Two properties make it evidence rather than a diary.
It records denials. A log of what succeeded cannot answer "what did the agent try", which is the question anybody actually asks afterwards. Here, eight of the seventeen entries are refusals, each with the check that made it and the reason in plain English.
It can be replayed. Every entry carries the grant it was decided under and the policy version, so the decision can be recomputed from the log alone and compared to the verdict it records:
python3 gate.py --verify audit.json
replay: 17/17 entries recompute to the verdict they recordWith the replay field off, that number is 0/17 — the entries still say
what was decided and give nobody any way to check it.
And the entries are hash-chained, so a removed or edited one is visible:
entry 6 does not follow the one before it -- something was removed or editedSecrets never enter it. An audit trail holding the secret is a second copy
of the secret, in a file more people can read. secrets.get is allowed in this
session and its value appears nowhere in the log; the naive log contains two.
The nine checks, and the proof that each earns its place
check what turning it off lets through
grant 1 more unauthorised, 1 more unreconstructable, 1 more egress
scope 3 more unauthorised, 3 more unreconstructable
expiry 2 more unauthorised, 2 more unreconstructable
purpose 1 more unauthorised, 1 more unreconstructable
deputy 1 more unauthorised, 1 more unreconstructable
redact 1 more secret in the log
deny_log 8 more denials that left no trace
replay 17 more decisions that cannot be reconstructed
chain 1 more entry removable unnoticedpython3 ablate.pyOne correction worth recording: the first version of the scope ablation did
not turn the check off — it substituted the string-prefix comparison, which is
a different check rather than the absence of one, and it quietly kept
denying a call that existed to prove the check was needed. Turning a check
off has to mean turning it off, or the ablation is measuring a swap.
The same session also exposed a grant sized so that its last call was denied for being one use too many rather than for naming a key it did not cover. It was testing the wrong check and passing. The fix was in the fixture, and the test that found it is the one that requires each denial to isolate its own check.
Known limits, stated rather than hidden
This is the decision layer, not a transport. It decides, executes against an in-process tool table, and logs. Wiring it to a real MCP transport is ordinary work; what is here is the part that is usually missing, and it runs with no network and no key.
Grants are written down, not elicited. A real deployment asks a person and stores the answer. What that answer has to contain — tool, scope, purpose, expiry, use count — is the part this argues about.
purposeis declared by the caller and taken at face value. A caller that lies about its purpose defeats the check. It still buys something real: the declared purpose is in the log, so the lie is on the record and the grant's scope did not widen to accommodate it.The deputy check knows one tool's reach.
render_templateparses its includes. A tool whose reach the server cannot compute cannot be checked this way, and the honest answer for such a tool is a narrower grant rather than a wider check.The chain proves ordering and integrity, not custody. Anyone who can rewrite the whole file can rewrite the whole chain. It makes a selective edit visible, which is the realistic threat; an append-only store or an external anchor is what makes the rest visible, and that is deployment work.
Layout
fixtures/spec.py the session, the grants, the world, and the answer key
build_fixtures.py renders session.json, policy.json, world.json, truth.json
mcpgate/scope.py is this path inside that scope
mcpgate/policy.py a grant is four things at once
mcpgate/tools.py the tool table, and what a call actually reaches
mcpgate/audit.py the hash-chained, redacted, replayable log
mcpgate/server.py naive() and guarded(), and replay()
gate.py the CLI: run a session, or verify a log
score.py every number above
ablate.py each check off, one at a time
tests/ 38 testsRun it
python3 demo.py # both servers + ablation
python3 gate.py --log audit.json # the product
python3 gate.py --verify audit.json # the proof
python3 -m pytest tests -q # 38 testsDocker, with the network switched off:
docker compose run --rm gateTests
One asserts that the allowlist still lets almost everything through. One replays every logged decision and requires the same verdict. One requires each denial to survive only because of the check it names, so the ablation numbers mean what they say. One greps the path resolver for filesystem calls.
This server cannot be installed
Maintenance
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
AlicenseNot gradedqualityCmaintenancePolicy enforcement gateway for MCP tool calls, evaluating every tool invocation against declarative YAML policies (allow/deny/escalate-to-human), generating cryptographic hash-chained audit receipts, and including built-in content safety scanning.2MIT- AlicenseCqualityDmaintenanceEnables secure, zero-trust access to MCP tools through short-lived, signed capability leases that bind tool execution to specific sessions, intents, and constraints. Prevents prompt injection attacks and privilege escalation with dynamic risk scoring, policy enforcement, and tamper-evident audit logging.41MIT
- AlicenseNot gradedqualityAmaintenanceAn authorizing reverse proxy for MCP servers that enforces per-call policy rules on tool arguments with audit logging, dry-run, and rate limiting.Apache 2.0
- AlicenseNot gradedqualityBmaintenanceEnforces identity-based access control and audit logging for MCP servers, letting you grant fine-grained tool permissions to users and systems while failing secure by default.MIT
Related MCP Connectors
Remote MCP for A2A caller identity, scope policy, verdict receipts, and audit history.
Runtime permission, approval, and audit layer for AI agent tool execution.
Remote MCP for Copilot CLI switch gate MCP, structured receipts, audit logs, and reviewer-ready evid
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
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/jigonyoo/mcp-permission-server'
If you have feedback or need assistance with the MCP directory API, please join our Discord server