mcpclerk
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., "@mcpclerkapprove the pending write_file call"
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.
mcpclerk
A governance proxy for MCP servers: sits in front of any MCP server, enforces a per-tool allowlist, holds write-class tools for human approval, applies per-tool quotas, redacts secret-looking arguments, and writes a hash-chained audit log of every call.
An AI agent on MCP servers can call any tool they expose, as often as it likes, with any arguments, and nothing records what it did in a form anyone can audit. In an enterprise the question is not "can the agent do the job" but what is it allowed to do, who approved the dangerous parts, and what did it actually do?
mcpclerk answers those three with code. It is itself an MCP server: the agent connects to it, it connects to the real servers and re-exposes their tools as upstream.tool. Every call goes through one pipeline: allowlist, quota, redaction, approval, forward, log. An unlisted tool is denied. A write-class tool waits for a human to answer y. Refusals come back as readable errors. The log is append-only JSON Lines, each entry hashed with the previous one, so an edit anywhere breaks the chain.
The demo wraps the official filesystem server: a read passes, a write is held and approved, a move is refused, the fourth search in a minute is refused on quota, and the log verifies. 49 tests prove each control against a fake upstream, including that the upstream always receives the unredacted arguments.

Install
pip install mcpclerk # Python 3.10+ (the MCP SDK requires it); pulls in mcp and pyyaml
mcpclerk --versionFrom source: git clone https://github.com/hishamalward/mcpclerk && cd mcpclerk && pip install -e ".[dev]" && pytest.
Related MCP server: Agentrim MCP
Five minutes
Write a policy. This is the one from the demo (
examples/policy.filesystem.yaml):version: 1 defaults: unlisted: deny # a tool not named here is an unreviewed tool approval_timeout_s: 120 # a call nobody answers in time is refused, and logged as such upstreams: fs: transport: stdio command: npx args: ["-y", "@modelcontextprotocol/server-filesystem", "/tmp/mcpclerk-demo-sandbox"] tools: "read_*": allow list_directory: allow search_files: { decision: allow, quota: { per_minute: 3 } } write_file: approve edit_file: approve create_directory: approve move_file: deny # the filesystem server has no delete; move is its destructive opSee what the upstream offers and what your policy does with it. The server's own annotations are shown next to your decision, which is how you notice you allowed a destructive tool:
$ mcpclerk tools --policy examples/policy.filesystem.yaml tool decision rule read_only destructive quota fs.read_file allow glob:read_* True None -/- fs.write_file approve exact False True -/- fs.move_file deny exact False True -/- fs.search_files allow exact True None -/3Register the proxy where your agent looks for MCP servers. For Claude Code,
examples/.mcp.json:{ "mcpServers": { "fs-governed": { "command": "mcpclerk", "args": ["serve", "--policy", "examples/policy.filesystem.yaml"] } } }In a second terminal, wait for approvals:
mcpclerk approve. When the agent callsfs.write_file, you see the call with secrets already masked, and answeryorn.Afterwards:
mcpclerk verify audit/mcpclerk.jsonlandmcpclerk report audit/mcpclerk.jsonl.
The five controls
Control | What it does | What it prevents | What it cannot prevent | Proven by |
Allowlist |
| The agent using a tool nobody reviewed. | A bad decision in the policy itself. |
|
Approval |
| An unsupervised write. | A human who approves without reading. |
|
Quotas |
| Runaway loops; a cheap tool becoming expensive by volume. | Distributing a loop across many tools, or across proxy restarts ( |
|
Redaction | Key rules ( | Secrets landing in the log or on an approver's screen. | A secret shaped like nothing on the list. Extend |
|
Audit log | One JSON Lines entry per call with timestamp, upstream, tool, redacted args, decision, who approved, outcome, latency, and | Quiet editing, deletion or reordering of entries after the fact; truncation of a completed run ( | An attacker who rewrites the entire chain from genesis (this is a chain, not a signature; see below). Truncation of a run that was killed mid-way. |
|
Results are not logged, only their size and content types. The log is an audit of decisions, not a copy of the data; storing results would make it a second place for secrets to leak.
How a call moves
agent ──tools/call fs.write_file──▶ mcpclerk ──▶ [namespace] ──▶ [allowlist] ──▶ [quota] ──▶ [redact for log]
│ │ │
refused-unknown refused-denied refused-quota
│
┌── decision = approve ──▶ [hold: approvals/<id>.json] ──▶ y ─┐
│ │ n / timeout │
│ refused-by-human / refused-timeout │
└── decision = allow ────────────────────────────────────────┤
▼
[forward with ORIGINAL args] ──▶ upstream ──▶ result
│
[append log entry, hash-chained]Every path, including every refusal, ends in a log entry. Refusals return to the agent as a normal tool result with is_error: true and a one-line reason: mcpclerk: refused-quota fs.search_files: 3/min exhausted; retry after 60s.
Approval, in detail
The proxy is usually started by the agent's MCP client, and the MCP SDK starts stdio servers in a new session, so the proxy normally has no terminal of its own. That is why the mechanism is a file queue and the terminal prompt is a client of it:
approvals/<id>.jsonis written for every held call, with the redacted arguments,requested_at,expires_at, and"approved": null.mcpclerk approve(in any terminal, on the same machine) shows pending requests and writes your answer.--onceanswers one and exits; without it, it keeps watching.Editing the file by hand to
"approved": trueworks too, which is what a headless job or a script does.If the proxy does happen to have a controlling terminal (you started it by hand), it also prompts there. Both paths race; the first answer wins.
No answer within
approval_timeout_sis a refusal, logged asrefused-timeout. Silence on a write means no.serve --approve-sessionauto-approves every approve-class call for that process. It prints a warning at start, therun-startentry records it, every affected entry saysapproved_by: session-flag, andreportshouts about it. It cannot be set in the policy file; it is a per-invocation act by whoever starts the process.
The audit log
{"kind":"call","ts":"2026-08-24T01:14:40.822Z","run_id":"20260824T011440Z-3e1c","id":"20260824T011440Z-0002",
"name":"fs.write_file","upstream":"fs","tool":"write_file","rule":"exact",
"args":{"content":"# notes\n[REDACTED:kv-secret]\n","path":"/tmp/mcpclerk-demo-sandbox/notes.md"},
"decision":"approved","approved_by":"file","held_ms":253.7,"outcome":"ok","is_error":false,
"latency_ms":7.7,"content_bytes":57,"content_types":["text"],
"seq":4,"prev_hash":"5c0e…","hash":"b41a…"}decisionis one ofallowed,approved,refused-denied,refused-unknown,refused-quota,refused-timeout,refused-by-human.latency_msis upstream time only; the human's thinking time isheld_ms, so p95 latency inreportmeans the tool, not the person.Event entries (
run-startwith the policy's SHA-256 and the flags,discoverwith exposed/hidden counts,run-endwith the entry count) share the same chain.verifyexits 0 withOK n entries, chain intactor 1 withFAIL at line N: <what>. Try it:sed -i '' 's/allowed/approved/' examples/audit.demo.jsonl && mcpclerk verify examples/audit.demo.jsonl.
The example log in examples/audit.demo.jsonl is the real output of the demo run. It is safe to publish by construction: the redaction tests are what prove it, and the demo writes a fake API key into a file precisely so the log can show [REDACTED:kv-secret] where it would have been.
CLI
mcpclerk serve --policy policy.yaml [--log audit/mcpclerk.jsonl] [--approvals approvals] [--approve-session] [--no-tty]
mcpclerk approve [--approvals approvals] [--once] [--wait 60]
mcpclerk tools --policy policy.yaml [--json]
mcpclerk verify audit/mcpclerk.jsonl
mcpclerk report audit/mcpclerk.jsonl [--json]Exit codes: 0 ok, 1 verify failed or policy invalid, 2 usage. The policy is validated at startup and any problem (unknown key, bad decision, unset ${ENV_VAR}, a stdio upstream without command) stops the proxy before it serves anything.
Policy reference
version: 1
namespace_separator: "." # "__" for clients that reject dots in tool names
defaults:
unlisted: deny # allow | deny | approve
approval_timeout_s: 120
quota: { per_run: null, per_minute: null }
redaction:
extend: ['(?i)my[-_ ]?internal[-_ ]?token\s*[:=]\s*\S+'] # value regexes, added to the built-ins
extend_keys: [client_secret] # key names, added to the built-ins
replace_builtin: false # true: only your patterns (warned about)
upstreams:
<name>: # [a-z0-9_-]+ ; becomes the prefix in <name>.<tool>
transport: stdio | http
command: ... args: [...] env: { KEY: "${FROM_PROXY_ENV}" } cwd: ... # stdio
url: https://... # http
tools:
<tool or glob>: allow | deny | approve
<tool>: { decision: approve, quota: { per_run: 10, per_minute: 3 }, approval_timeout_s: 60 }Prior art, and what this is instead
Gateways for MCP exist and do more than this: Lasso Security's mcp-gateway, IBM's mcp-context-forge, and Docker's MCP Gateway bring registries, multi-tenant auth, plugin pipelines and observability. mcpclerk claims no novelty. It claims smallness and verifiability: a single-purpose, readable, local proxy whose whole surface is the five controls above and a log you can check. It is about 1,000 lines of Python you can read in an afternoon, with one dependency beyond the MCP SDK (a YAML parser).
What it does not do (yet)
Identity and per-user policies. One operator is assumed; the log records that a human approved, not which human.
A web UI, or remote approval channels (Slack, email).
mcpclerk approveis a local terminal.Policy inheritance or templating across upstreams.
Resources and prompts. v0.1 proxies tools only;
resources/listandprompts/listare empty.HTTP upstreams that need request headers. The SDK's HTTP transport takes none in this version; a policy that sets
headersfails loudly rather than silently sending nothing.Windows: the file queue and
mcpclerk approvework; the in-process terminal prompt does not (no/dev/tty). CI runs Windows as best-effort.
Threat model, honestly
What an attacker with the agent's seat would try first is to call a tool by name that is hidden from the list. That is refused and logged (refused-unknown or refused-denied). What this does not stop: a tool that is allowed being used for something harmful (the policy is your judgement, mcpclerk enforces it), an approver who rubber-stamps, and anyone with write access to the log file rewriting the whole chain from the first entry. The chain defends against quiet edits, which is the realistic threat; signatures or an external anchor (publishing the daily head hash somewhere you do not control) would be the next step, and are not in v0.1.
Development
pip install -e ".[dev]"
pytest -q # 49 tests, all in-process, no network, no subprocesses
python examples/demo_driver.py --approve-via-file # the demo against the real filesystem server (needs npx)
vhs examples/demo.tape # re-record the GIFTests use the MCP SDK's in-memory transport on both sides: Client(proxy) → proxy → Client(fake_upstream). The fake upstream (tests/fake_upstream.py) has a secret_sink tool that returns exactly what it received, which is how the suite proves the upstream sees unredacted arguments while the log does not.
Related: toilscan (the same write-safety instinct applied to a developer tool), agent-slots (runtime isolation for parallel agents), and agentkeel (the process side: gates and blast radius for agent-written code; in progress).
For whoever owns this next: docs/learning/how-it-works.html is the tour (the code in call order, the controls, the interview answers); docs/spec.md is the contract.
License
MIT.
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
- AlicenseBqualityCmaintenanceSecurity gateway that wraps any MCP server with per-tool policies, approval gates, and optional Ed25519-signed decision receipts. Shadow mode logs every tool call without blocking; enforce mode applies block, rate-limit, and minimum-tier rules. Receipts are independently verifiable offline with no accounts needed.54699MIT
- AlicenseNot gradedqualityBmaintenanceA least-privilege enforcement proxy for MCP servers. It sits between MCP clients and upstream servers, enforcing tool policies, hiding denied tools, requiring human approval for risky actions, and providing a structured audit trail.MIT
- AlicenseAqualityAmaintenanceAn MCP proxy that enforces policy on every tool call, blocking or flagging actions before they reach downstream MCP servers.1249MIT
- 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
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.
A paid remote MCP for CLI tool MCP, built to return verdicts, receipts, usage logs, and audit-ready
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/hishamalward/mcpclerk'
If you have feedback or need assistance with the MCP directory API, please join our Discord server