Skip to main content
Glama
hishamalward

mcpclerk

by hishamalward

mcpclerk

ci python license

一个用于 MCP 服务器的治理代理:它位于任何 MCP 服务器之前,强制执行按工具划分的允许列表,将写类工具挂起等待人工批准,应用按工具划分的配额,对看起来像秘密的参数进行脱敏,并记录每次调用的哈希链审计日志。

AI 代理在 MCP 服务器上可以调用它们暴露的任何工具,想调用多少次就调用多少次,使用任何参数,而且没有任何东西以任何人都能审计的形式记录它做了什么。在企业环境中,问题不是“代理能否完成工作”,而是它被允许做什么,谁批准了危险的部分,以及它实际做了什么?

mcpclerk 用代码回答这三个问题。它本身就是一个 MCP 服务器:代理连接到它,它连接到真实的服务器,并将它们的工具重新暴露为 upstream.tool。每次调用都经过一个管道:允许列表、配额、脱敏、批准、转发、记录。未列出的工具会被拒绝。写类工具会等待人类回答 y。拒绝以可读的错误形式返回。日志是仅追加的 JSON Lines,每个条目与前一个条目进行哈希链接,因此任何地方的编辑都会破坏链条。

演示包装了官方的文件系统服务器:读取通过,写入被挂起并批准,移动被拒绝,一分钟内的第四次搜索因配额被拒绝,日志验证通过。49 个测试针对假上游验证了每个控制,包括上游始终收到未脱敏的参数。

demo

安装

pip install mcpclerk          # Python 3.10+ (the MCP SDK requires it); pulls in mcp and pyyaml
mcpclerk --version

从源码安装:git clone https://github.com/hishamalward/mcpclerk && cd mcpclerk && pip install -e ".[dev]" && pytest。

Related MCP server: mcp-policy-gateway

五分钟

  1. 编写一个策略。这是演示中的那个(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 op
  2. 查看上游提供了什么,以及你的策略如何处理它。服务器自身的注释会显示在你的决策旁边,这样你就能注意到你允许了一个破坏性工具:

    $ 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        -/3
  3. 在你的代理查找 MCP 服务器的地方注册代理。对于 Claude Code,examples/.mcp.json:

    { "mcpServers": { "fs-governed": {
        "command": "mcpclerk",
        "args": ["serve", "--policy", "examples/policy.filesystem.yaml"] } } }
  4. 在第二个终端中,等待批准:mcpclerk approve。当代理调用 fs.write_file 时,你会看到调用(秘密已被遮蔽),并回答 y 或 n。

  5. 之后:mcpclerk verify audit/mcpclerk.jsonl 和 mcpclerk report audit/mcpclerk.jsonl。

五个控制

控制

它做什么

它防止什么

它不能防止什么

由什么证明

允许列表

每个工具使用 allow / deny / approve,先精确名称,然后是最长通配符,然后是 defaults.unlisted(拒绝)。被拒绝和未列出的工具甚至不会列给代理。

代理使用未经任何人审查的工具。

策略本身中的错误决策。mcpclerk tools 显示上游的只读/破坏性提示,放在你的决策旁边,使这更难发生。

test_policy.py、test_pipeline.py::test_denied_hidden_tool_called_by_name_is_refused

批准

approve 类调用会被挂起。请求被写入 approvals/<id>.json,参数已脱敏;人类用 mcpclerk approve 回答(或通过编辑文件,或如果代理有终端提示)。超时即拒绝。

无监督的写入。

不阅读就批准的人类。--approve-session 就是为这样的人准备的,并记录在每个受影响的条目上。

test_approval.py、test_pipeline.py::test_approve_via_file_then_forward、test_approval_refused_and_timed_out

配额

每个工具使用 per_run 和 per_minute(滑动窗口)。超配额会被拒绝,并显示限制和窗口释放前的秒数。被拒绝的调用不消耗配额;被人类批准后拒绝的调用会消耗。

失控的循环;一个廉价工具因数量而变得昂贵。

将循环分布到多个工具,或跨代理重启(per_run 随进程重置)。

test_quota.py、test_pipeline.py::test_quota_exhaustion

脱敏

键规则(api_key、token、password、authorization、...)替换整个值;值规则(bearer 头、sk-/AKIA/ghp_/xox 令牌、JWT、PEM 块、URL 用户信息、password=...)替换匹配项。应用于记录和显示给人类的内容。上游收到原始参数。

秘密落入日志或批准者的屏幕。

形状不在列表上的秘密。通过 redaction.extend / extend_keys 扩展你自己的形状。

test_redact.py、test_pipeline.py::test_upstream_receives_unredacted_args

审计日志

每次调用一个 JSON Lines 条目,包含时间戳、上游、工具、脱敏参数、决策、批准者、结果、延迟,以及 hash = sha256(prev_hash + canonical(entry))。verify 重新计算链;report 总结它。

事后静默编辑、删除或重新排序条目;截断已完成的运行(run-end 携带计数)。

从创世块重写整个链的攻击者(这是链,不是签名;见下文)。截断中途被杀死的运行。

test_audit.py(编辑、删除、重新排序、截断)

结果不会被记录,只记录它们的大小和内容类型。日志是决策的审计,不是数据的副本;存储结果会使其成为秘密泄漏的第二个地方。

调用如何移动

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]

每条路径,包括每次拒绝,都以日志条目结束。拒绝以正常工具结果返回给代理,带有 is_error: true 和一行原因:mcpclerk: refused-quota fs.search_files: 3/min exhausted; retry after 60s。

批准,详细说明

代理通常由代理的 MCP 客户端启动,MCP SDK 在新会话中启动 stdio 服务器,因此代理通常没有自己的终端。这就是为什么机制是文件队列,终端提示是它的客户端:

  • approvals/<id>.json 为每个挂起的调用写入,包含脱敏参数、requested_at、expires_at 和 "approved": null。

  • mcpclerk approve(在同一台机器的任何终端中)显示待处理的请求并写入你的答案。--once 回答一个并退出;没有它,它会持续监视。

  • 手动编辑文件为 "approved": true 也可以,这是无头作业或脚本所做的。

  • 如果代理碰巧有控制终端(你手动启动它),它也会在那里提示。两条路径竞争;第一个答案获胜。

  • 在 approval_timeout_s 内没有答案就是拒绝,记录为 refused-timeout。对写入的沉默意味着不。

  • serve --approve-session 自动批准该进程的每个 approve 类调用。它在启动时打印警告,run-start 条目记录它,每个受影响的条目说 approved_by: session-flag,report 会大声喊出来。它不能在策略文件中设置;这是启动进程的人每次调用的行为。

审计日志

{"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…"}
  • decision 是 allowed、approved、refused-denied、refused-unknown、refused-quota、refused-timeout、refused-by-human 之一。

  • latency_ms 仅指上游时间;人类的思考时间是 held_ms,因此 report 中的 p95 延迟指的是工具,而不是人。

  • 事件条目(run-start 带有策略的 SHA-256 和标志,discover 带有暴露/隐藏计数,run-end 带有条目计数)共享同一条链。

  • verify 以 0 退出并显示 OK n entries, chain intact,或以 1 退出并显示 FAIL at line N: <what>。试试:sed -i '' 's/allowed/approved/' examples/audit.demo.jsonl && mcpclerk verify examples/audit.demo.jsonl。

examples/audit.demo.jsonl 中的示例日志是演示运行的真实输出。按构造发布是安全的:脱敏测试证明了这一点,演示将假 API 密钥写入文件,正是为了让日志在原本会出现的地方显示 [REDACTED:kv-secret]。

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]

退出代码:0 正常,1 验证失败或策略无效,2 用法错误。策略在启动时验证,任何问题(未知键、错误决策、未设置的 ${ENV_VAR}、没有 command 的 stdio 上游)都会在代理服务任何内容之前阻止它。

策略参考

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 }

先前的工作,以及这取而代之的是什么

MCP 的网关存在并且做得更多:Lasso Security 的 mcp-gateway、IBM 的 mcp-context-forge 和 Docker 的 MCP Gateway 带来了注册表、多租户认证、插件管道和可观测性。mcpclerk 不声称新颖。它声称小巧和可验证:一个单一用途、可读、本地的代理,其全部表面就是上述五个控制和一个你可以检查的日志。它大约有 1,000 行 Python,你可以在一个下午读完,除了 MCP SDK 之外只有一个依赖(一个 YAML 解析器)。

它不做什么(尚未)

  • 身份和按用户策略。假定只有一个操作员;日志记录的是有人类批准,而不是哪个人类。

  • Web UI 或远程审批渠道(Slack、email)。mcpclerk approve 是本地终端。

  • 跨上游的策略继承或模板化。

  • 资源和提示。v0.1 仅代理工具;resources/list 和 prompts/list 为空。

  • 需要请求头的 HTTP 上游。此版本中 SDK 的 HTTP 传输不接受任何请求头;设置 headers 的策略会大声失败,而不是静默地不发送任何内容。

  • Windows:文件队列和 mcpclerk approve 可用;进程内终端提示不可用(没有 /dev/tty)。CI 尽力运行 Windows。

威胁模型,坦诚地说

攻击者如果占据代理的位置,首先会尝试调用一个不在列表中的工具名称。这会被拒绝并记录(refused-unknown 或 refused-denied)。这并不能阻止:一个被允许的工具被用于有害目的(策略是你的判断,mcpclerk 强制执行),一个橡皮图章式的审批者,以及任何对日志文件有写权限的人从第一条记录开始重写整个链。该链防御的是静默编辑,这是现实中的威胁;签名或外部锚点(将每日头部哈希发布到你无法控制的地方)将是下一步,但不在 v0.1 中。

开发

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 GIF

测试在两侧使用 MCP SDK 的内存传输:Client(proxy) → proxy → Client(fake_upstream)。假上游(tests/fake_upstream.py)有一个 secret_sink 工具,它返回它收到的确切内容,这就是测试套件如何证明上游看到未脱敏的参数而日志却没有。

相关项目:toilscan(同样的写安全直觉应用于开发者工具)、agent-slots(并行代理的运行时隔离)以及 agentkeel(进程侧:代理编写代码的门禁和爆炸半径;进行中)。

对于下一个接手的人:docs/learning/how-it-works.html 是导览(按调用顺序的代码、控制、面试答案);docs/spec.md 是契约。

许可证

MIT.

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    B
    maintenance
    A 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
  • A
    license
    Not graded
    quality
    A
    maintenance
    An 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
  • A
    license
    A
    quality
    C
    maintenance
    Provides a security governance layer for AI agents to safely access upstream MCP servers, enforcing tool-level RBAC, parameter constraints, authentication via static tokens or OIDC, and tamper-evident audit logging.
    2
    1
    Apache 2.0
  • F
    license
    Not graded
    quality
    B
    maintenance
    Enforces default-deny policies, budgets, and tamper-evident audit logging for MCP tool calls before they reach upstream servers.
    -