Skip to main content
Glama
adamabdo-xynora

mcp-capability-guard

mcp-capability-guard

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

Это небольшой, но полный MCP-сервер поверх вымышленной in-memory CRM (Larkspur Supply Co., семь придуманных контактов), который демонстрирует авторизацию записи на основе capability-токенов — паттерн, извлечённый из продакшн-CRM-агента, которого я запускаю на реальной книге контактов (12 000+ записей). Данные здесь вымышлены; поставляется именно механизм принуждения.

Дизайн, в пяти слоях

  1. Многоуровневая поверхность инструментов. Чтения (list_contacts, get_contact) бесплатны. Операции записи не существуют в виде отдельных инструментов — нет ни инструмента add_note, ни инструмента delete_contact. Каждая мутация проходит ровно через два вызова: propose_write, затем execute_write.

  2. Capability-токены (центральный элемент). propose_write выпускает одноразовый warrant с ограничением по TTL, привязанный ровно к одной конкретной мутации — токен встраивает мутацию в себя, а не указывает на неё, так что нет таблицы поиска, которую можно было бы отравить, и нет id, который можно было бы перенацелить. execute_write предъявляет токен вместе с мутацией, и guard проверяет равенство поле за полем. Предъявление warrant с другой мутацией не просто приводит к отказу — оно сжигает токен, унося с собой и легитимную запись, для которой он был выпущен.

  3. Подтверждение человеком для разрушительного уровня. change_stage, remove_tag и delete_contact дополнительно требуют явного «да» оператора через MCP form elicitation — в приглашении, которое одним предложением называет операцию, контакт и payload. Запрос подтверждения выполняется до обращения к guard, так что отказ никогда не расходует warrant — а клиент без канала elicitation получает отказ в разрушительных записях, а не их молчаливое исполнение. Fail closed в обе стороны.

  4. Нижняя граница, которую верхние слои не могут обойти. Контакты в стадии Closed-Lost-DNC (не беспокоить, юридическая блокировка) отклоняют любую запись внутри самого хранилища, которое ничего не знает ни о токенах, ни об MCP. Полностью одобренный поток — действительный warrant, совпадающая мутация, подтвердивший человек — всё равно упирается здесь в тупик. Именно это делает защиту эшелонированной, а не одним шлагбаумом с тремя табличками.

  5. Append-only журнал аудита, из которого не могут утечь warrants. Записывается каждый propose, confirm, execute и отказ. Token id попадают в журнал только в виде 8-символьных отпечатков — и это гарантия на этапе компиляции: поле отпечатка хранит брендированный TypeScript-тип, который может произвести только усекающая функция. Свободный текст также зачищается по значению, потому что собственные сообщения guard об отказе называют токен, которому отказано. (Это та же дисциплина redact-by-value, что и в моём webhook-guard — там для HTTP-учётных данных, здесь для живых warrants.) read_audit — deny-by-default: если сервер не собран с exposeAudit: true, инструмент вообще не регистрируется.

Related MCP server: tenant-scoped-crm

Смотрите, как это работает

npm install
npm run demo

Демо соединяет реальный сервер со скриптовым клиентом через in-memory MCP-транспорт и проходит восемь шагов: две записи, которые проходят (одна обратимая, одна разрушительная и подтверждённая), затем пять атак — replay, bait-and-switch, отклонённое подтверждение, перенос цели и полностью одобренная операция записи против замороженной записи — каждая встречает свой типизированный отказ, а хранилище остаётся побайтово неизменным. В конце читается журнал аудита и проверяется, что нигде в нём не появляется полный token id. Демо проверяет каждое ожидание инлайн и при любом промахе завершается с ненулевым кодом, так что оно работает заодно как смоук-тест и выполняется в CI при каждом пуше.

npm test              # 158 offline tests
npm run typecheck     # strict TypeScript, no emit

Всё офлайн: никаких API-ключей, ни сети, ни переменных окружения — настраивать нечего. Именно поэтому CI запускает весь набор тестов, включая демо, при каждом пуше и без секретов.

Доказательства — в тестовых наборах

Два набора тестов существуют специально для того, чтобы заявления выше оставались честными:

  • test/structural.test.ts читает исходный код как текст и фиксирует архитектуру: src/tools.ts — единственный модуль, импортирующий MCP SDK; хранилище ничего не знает о том, что выше него; модули guard и audit импортируют только то, что заявлено в их заголовках; выпуск токенов ограничен guard; строка tokenId ни разу не появляется в src/audit.ts. Если рефакторинг незаметно перенесёт код SDK в хранилище, этот набор упадёт раньше, чем проявится какое-либо поведение.

  • test/adversarial.test.ts разыгрывает враждебную модель против полностью собранного сервера: пропуск propose, replay, bait-and-switch с проверкой сжигания, перенацеливание warrant на другой контакт, атака с полным одобрением на замороженную запись, просроченный warrant через инъецированные часы, уклонение от подтверждения и проверка целостности аудита с token id, выбранными атакующим. Каждая атака должна встретить свой точный типизированный отказ, а после всех атак хранилище должно остаться неизменным.

Оставшиеся ~140 тестов покрывают хранилище, guard, audit и поверхность инструментов покомпонентно, включая инвариант порядка: подтверждение выполняется до того, как warrant может быть потрачен.

Версионирование и область применения

Собрано на @modelcontextprotocol/sdk 1.30.0, который нацелен на ревизию MCP 2025-11-25. Ревизия 2026-07-28 делает выпущенные сервером хэндлы, передаваемые как обычные аргументы инструментов, каноническим механизмом состояния между вызовами (SEP-2567) — capability-токен в этом репозитории — ровно этот паттерн, использованный как примитив авторизации, так что дизайн без изменений переносится в протокол без состояния.

Две рантайм-зависимости: сам SDK и zod — язык схем, являющийся peer-dependency самого SDK; схемы входных данных инструментов по дизайну SDK являются zod-схемами, так что это не столько добавленная зависимость, сколько вторая половина SDK. Больше в дерево зависимостей ничего не попадает. Модули хранилища, guard и audit — чистый TypeScript с нулём импортов, кроме node:crypto и типов друг друга, — и именно это позволяет 158 тестам работать офлайн меньше чем за полсекунды.

Чем это не является. Этот репозиторий не реализует OAuth и не реализует роль resource-server из спецификации авторизации MCP. Они решают другую задачу: доказательство того, кем является клиент, на границе транспорта. Capability-токены определяют то, что аутентифицированная сессия может делать, по одной записи за раз — эти два механизма дополняют, а не конкурируют друг с другом, и именно их смешение приводит к тому, что у серверов оказывается один bearer-токен, авторизующий всё. Транспортная идентичность здесь намеренно вынесена за скобки, чтобы паттерн авторизации оставался читаемым.

Ограничения, без прикрас

  • CRM вымышленная и живёт в памяти. Персистентность, конкурентность и многопользовательские сессии — реальные проблемы, которых у этой демки нет.

  • Токены живут в памяти сервера; перезапуск забывает их. В продакшене тот же паттерн работает поверх персистентного хранилища с той же семантикой одноразового использования.

  • Подтверждение через elicitation настолько же надёжно, насколько хорошо клиент его отображает. Клиент, показывающий пользователю голое «Allow?» вместо предложения сервера, ослабляет гарантию — а это аргумент за то, чтобы класть полное предложение в запрос, как делает этот сервер, а не за то, чтобы пропускать запрос.

  • Временные метки заметок в хранилище используют настенные часы, поэтому одна строка вывода демо меняется от запуска к запуску. Guard и журнал аудита используют инъецированные часы и детерминированы.

Адаптация

Паттерн переносится на любой MCP-сервер, чьи операции записи имеют последствия: замените хранилище своей системой, сохраните разделение propose/execute, определите собственные уровни и держите правило нижней границы в слое данных, а не в слое инструментов. Модули guard и audit не импортируют ничего из MCP и могут быть вынесены целиком.

Лицензия MIT.

Related MCP Connectors

Related MCP Servers

  • A
    license
    A
    quality
    A
    maintenance
    Security-enforcing MCP proxy that sits between an AI agent and any number of downstream MCP servers, intercepting every tool call through a capability-token policy gateway that can allow, deny, or escalate to human approval before the call reaches any real tool. It also exposes built-in operator tools for approval workflows, audit trail queries, token management, voice/HUD output, and hierarchical
    21
    14
    Apache 2.0
  • A
    license
    Not graded
    quality
    D
    maintenance
    A reference MCP server demonstrating safe agent access to multi-tenant CRM data with tenant isolation enforced in the data layer, role-based permissions, and human confirmation on writes.
    MIT
  • F
    license
    Not graded
    quality
    C
    maintenance
    A secure MCP server for CRM operations (contacts and deals) with Auth0 OIDC authentication, role-based access control (sales-rep read-only vs sales-manager full access), and on-behalf-of token exchange.
    -
  • A
    license
    B
    quality
    A
    maintenance
    A secure MCP server enabling tool calls (kb_search, read_doc, publish_report) through a zero-trust CapabilityBroker with OWASP LLM Top-10 guardrails and human-in-the-loop approval.
    3
    Apache 2.0