Skip to main content
Glama
hishamalward

mcpclerk

by hishamalward

mcpclerk

ci python license

Управляющий прокси для MCP-серверов: встаёт перед любым MCP-сервером, применяет разрешённый список инструментов, удерживает инструменты класса записи для одобрения человеком, применяет квоты на каждый инструмент, редактирует аргументы, похожие на секреты, и пишет хэш-цепочный журнал аудита каждого вызова.

ИИ-агент на MCP-серверах может вызывать любой инструмент, который они предоставляют, сколько угодно раз, с любыми аргументами, и ничто не фиксирует, что он делал, в форме, которую можно проверить. В корпоративной среде вопрос не в том, «может ли агент выполнить работу», а в том, что ему разрешено делать, кто одобрил опасные части и что он фактически сделал?

mcpclerk отвечает на эти три вопроса кодом. Он сам является MCP-сервером: агент подключается к нему, он подключается к реальным серверам и повторно предоставляет их инструменты как upstream.tool. Каждый вызов проходит через один конвейер: разрешённый список, квота, редактирование, одобрение, пересылка, журнал. Неперечисленный инструмент отклоняется. Инструмент класса записи ждёт, пока человек ответит y. Отказы возвращаются как читаемые ошибки. Журнал — это append-only JSON Lines, каждая запись хэшируется с предыдущей, так что любое изменение где-либо разрывает цепочку.

Демонстрация оборачивает официальный файловый сервер: чтение проходит, запись удерживается и одобряется, перемещение отклоняется, четвёртый поиск в минуту отклоняется по квоте, и журнал проверяется. 49 тестов доказывают каждый контроль против фейкового upstream, включая то, что upstream всегда получает нередактированные аргументы.

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. Посмотрите, что предлагает upstream и что ваша политика с ним делает. Собственные аннотации сервера показаны рядом с вашим решением, что позволяет заметить, что вы разрешили разрушительный инструмент:

    $ 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 для каждого инструмента: сначала точное имя, затем самый длинный glob, затем defaults.unlisted (deny). Отклонённые и неперечисленные инструменты даже не показываются агенту.

Использование агентом инструмента, который никто не проверял.

Плохое решение в самой политике. mcpclerk tools показывает подсказки upstream о read-only / разрушительных инструментах рядом с вашим решением, чтобы это было сложнее.

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-блоки, userinfo в URL, password=...) заменяют совпадение. Применяется к тому, что записывается в журнал и показывается человеку. Upstream получает исходные аргументы.

Попадание секретов в журнал или на экран утверждающего.

Секрет, не похожий ни на что из списка. Расширьте redaction.extend / extend_keys для своих форм.

test_redact.py, test_pipeline.py::test_upstream_receives_unredacted_args

Журнал аудита

Одна запись JSON Lines на вызов с временной меткой, upstream, инструментом, редактированными аргументами, решением, кто одобрил, результатом, задержкой и hash = sha256(prev_hash + canonical(entry)). verify пересчитывает цепочку; report суммирует её.

Тихое редактирование, удаление или переупорядочивание записей после факта; усечение завершённого запуска (run-end содержит счётчик).

Атакующий, который переписывает всю цепочку с начала (это цепочка, а не подпись; см. ниже). Усечение запуска, убитого на полпути.

test_audit.py (edit, delete, reorder, truncate)

Результаты не записываются, только их размер и типы содержимого. Журнал — это аудит решений, а не копия данных; хранение результатов сделало бы его вторым местом утечки секретов.

Как движется вызов

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 тоже работает — так делают headless-задачи или скрипты.

  • Если у прокси всё же есть управляющий терминал (вы запустили его вручную), он также запрашивает там. Оба пути соревнуются; первый ответ побеждает.

  • Отсутствие ответа в течение 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 — только время upstream; время размышлений человека — это held_ms, так что p95 задержки в report означает инструмент, а не человека.

  • Событийные записи (run-start с SHA-256 политики и флагами, discover со счётчиками exposed/hidden, 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 — verify не прошёл или политика недействительна, 2 — использование. Политика проверяется при запуске, и любая проблема (неизвестный ключ, плохое решение, неустановленный ${ENV_VAR}, stdio-upstream без command) останавливает прокси до того, как он начнёт обслуживать что-либо.

Справочник по политике

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's mcp-gateway, IBM's mcp-context-forge и Docker's MCP Gateway приносят реестры, мультитенантную аутентификацию, плагинные конвейеры и наблюдаемость. mcpclerk не претендует на новизну. Он претендует на малость и проверяемость: одноцелевой, читаемый, локальный прокси, вся поверхность которого — пять контролей выше и журнал, который можно проверить. Это около 1000 строк Python, которые можно прочитать за вечер, с одной зависимостью помимо MCP SDK (парсер YAML).

Что он (пока) не делает

  • Идентичность и политики для каждого пользователя. Предполагается один оператор; журнал фиксирует что человек одобрил, а не какой человек.

  • Веб-интерфейс или удалённые каналы одобрения (Slack, email). mcpclerk approve — это локальный терминал.

  • Наследование политик или шаблонизация для вышестоящих систем.

  • Ресурсы и промпты. v0.1 проксирует только инструменты; resources/list и prompts/list пусты.

  • HTTP-апстримы, которым нужны заголовки запросов. HTTP-транспорт SDK в этой версии не принимает их; политика, устанавливающая headers, громко завершается с ошибкой, а не молча ничего не отправляет.

  • Windows: файловая очередь и mcpclerk approve работают; внутрипроцессный терминальный промпт — нет (нет /dev/tty). CI запускает Windows по принципу best-effort.

Модель угроз, честно

Что атакующий, находящийся на месте агента, попробует в первую очередь — вызвать инструмент по имени, скрытый из списка. Это отклоняется и записывается в журнал (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.
    -