# Проверка пользовательского MCP-инструмента: практический чек-лист безопасности

К пользовательским инструментам MCP стоит относиться так же, как к небольшой рабочей интеграции с доступом к вашему компьютеру, сети и учетным данным. То, что агент вызывает инструмент через MCP, не уменьшает его полномочия. Напротив, небрежные полномочия часто становится проще многократно применять.

Я не раз видел один и тот же сбой: разработчик читает описание инструмента, замечает полезное имя вроде `deploy_preview` или `search_docs` и дает ему доступ, потому что инструмент выглядит локальным. Позже реализация превращает строку, переданную моделью, в URL, аргумент shell-команды или путь для рекурсивного чтения файлов. Безопасной границей был вовсе не полезный инструмент. Ею были реализация и путь к учетным данным.

Спецификация Model Context Protocol описывает инструменты как функции, которые сервер предоставляет клиенту для обнаружения и вызова. В ней также прямо обозначен неудобный факт: выполнение инструментов контролирует модель. Клиент может добавить человека в цикл подтверждения, но автор инструмента все равно должен исходить из того, что аргументы могут оказаться неожиданными, чрезмерными или направленными не туда. Проверьте инструмент до того, как агент получит возможность проявить изобретательность.

## Начните с карты полномочий, а не с README

Пользовательский инструмент MCP приемлем только тогда, когда вы можете нарисовать короткий и конкретный путь от запроса агента до создаваемого им эффекта. Сначала запишите, что инструмент может читать, куда отправлять данные, что менять и какая учетная запись или идентичность операционной системы делает это возможным.

Сделайте это до изучения деталей реализации. Так у вас появится собственный критерий оценки кода, а не стандарт, заданный приятным описанием. Инструмент с именем `get_build_status` может читать локальный файл конфигурации, вызывать размещенный API, записывать кэш и запускать вспомогательную команду. У каждого действия свой сценарий сбоя.

Используйте небольшую карту полномочий:

| Часть | Что записать | Почему это важно |
|---|---|---|
| Вход агента | Точные поля инструмента и максимальный размер | Показывает, на что может влиять модель |
| Локальное чтение | Пути, переменные окружения, файлы конфигурации | Выявляет случайный сбор данных |
| Локальная запись | Кэш, рабочая папка, состояние git, временные файлы | Помогает найти постоянные побочные эффекты |
| Сеть | Имена хостов, порты, методы, перенаправления | Определяет риск утечки и запросов |
| Процессы | Путь к исполняемому файлу, аргументы, дочерние процессы | Помогает найти внедрение shell-команд и унаследованные полномочия |
| Учетные данные | Имя, область действия, хранение, ответственный за отзыв | Делает удаление доступа возможным |
| Результаты | Данные, возвращаемые агенту | Не дает секретам возвращаться через инструмент |

Не пишите в строке сети «интернет», а в строке учетных данных - «учетные данные разработчика». Такие формулировки означают, что проверка еще не началась. Укажите хост, семейство маршрутов API, учетную запись или токен, а также человека или систему, которые могут отозвать доступ.

Это упражнение также разделяет две вещи, которые команды часто смешивают: заявленное назначение инструмента и его реальные полномочия. `create_issue` звучит узко. Но функция, принимающая произвольный базовый URL, произвольные заголовки и произвольное тело запроса, обладает полномочиями универсального HTTP-клиента. Проверяйте именно ее, а не название.

## Схема входных данных должна уменьшать выбор

Схема входных данных MCP должна ограничивать агента операцией, которую вы действительно хотите разрешить. Она не должна быть декоративным описанием типов вокруг свободного канала передачи команд.

Спецификация MCP использует для схем входных данных инструментов JSON Schema. Это помогает клиентам показывать аргументы и реализациям проверять их, но JSON Schema не обеспечивает защиту, пока сервер не отклоняет недопустимые значения до начала работы. Считайте схему первым фильтром, а серверную проверку - вторым.

Вот схема, которую можно проверить при создании инструмента для получения статуса известной сборки:

```json
{
  "name": "get_build_status",
  "description": "Return the status for one build in the approved CI project.",
  "inputSchema": {
    "type": "object",
    "additionalProperties": false,
    "required": ["build_id"],
    "properties": {
      "build_id": {
        "type": "string",
        "pattern": "^[A-Z]{2,8}-[0-9]{1,10}$",
        "maxLength": 20
      },
      "include_logs": {
        "type": "boolean",
        "default": false
      }
    }
  }
}
```

Она задает для агента несколько ограничений. Агент не может выбрать хост, добавить заголовки или передать shell-команду. Поля, которых нет в схеме, также нельзя передать, поскольку `additionalProperties` имеет значение false. Идентификатор сборки ограничен по формату, поэтому его безопаснее использовать дальше и проще записывать в журналы.

Теперь сравните эту схему с проблемной:

```json
{
  "name": "request",
  "inputSchema": {
    "type": "object",
    "properties": {
      "url": {"type": "string"},
      "method": {"type": "string"},
      "headers": {"type": "object"},
      "body": {}
    }
  }
}
```

Это HTTP-клиент с удобным именем. Если у него есть bearer-токен, он может отправить этот токен или данные из промпта, управляемого агентом, на произвольный адрес. Такой дизайн часто оставляют, потому что на этапе прототипирования он экономит время. Для рабочей системы это все равно плохая граница, даже если имя инструмента звучит конкретно.

Проверяйте обработку входных данных значениями, которые меняют разбор, а не только выглядят некорректно. Попробуйте дублирующееся поле идентификатора, неожиданное поле, длинную строку, похожие символы Unicode, перевод строки и синтаксически корректное значение, указывающее за пределы нужной бизнес-области. Если вход превращается в путь, требуйте относительный идентификатор и разрешайте его относительно фиксированного каталога. Если он становится фильтром API, используйте структурированные параметры вместо конкатенации строк запроса.

Никогда не передавайте аргумент в shell только потому, что проверили схему. Используйте массив аргументов с фиксированным путем к исполняемому файлу. Так безопаснее:

```python
subprocess.run(
    ["/usr/local/bin/buildctl", "status", "--id", build_id],
    check=True,
    text=True,
    capture_output=True,
    env={"PATH": "/usr/bin:/bin"}
)
```

А это ошибка при проверке:

```python
subprocess.run(f"buildctl status --id {build_id}", shell=True)
```

Первому варианту все еще нужны проверка, обработка ошибок и надежный исполняемый файл. Но shell не будет заново интерпретировать пунктуацию, которой управляет модель.

## Для каждого исходящего запроса нужен фиксированный адрес

Пользовательский инструмент MCP должен работать с небольшим набором адресов, а код обязан отклонять все остальные до открытия соединения. Разрешенный список хостов в документации бесполезен, если код запроса принимает произвольный URL.

Проверяйте исходящий трафик на двух уровнях. Сначала изучите исходный код: HTTP-библиотеки, клиенты WebSocket, DNS-запросы, установщики пакетов, SDK телеметрии, библиотеки вебхуков и любые вспомогательные процессы, способные подключаться к другим адресам. Затем понаблюдайте за реальным запуском. Статическая проверка показывает предусмотренные пути. Наблюдение во время работы выявляет зависимость, которая отправляет данные разработчику, или изменившееся значение конфигурации, поменявшее адрес назначения.

Безопасный клиент собирает URL из фиксированных частей и кодирует только идентификатор:

```python
from urllib.parse import quote

BASE = "https://ci.example.internal/api/builds/"
url = BASE + quote(build_id, safe="")
response = client.get(url, timeout=10, follow_redirects=False)
```

Главный контроль здесь не `quote`, а фиксированный источник. Если клиент следует перенаправлениям, доверенный источник может отправить его на недоверенный хост. Отключите перенаправления, если только инструмент не проверяет каждый новый адрес по тому же разрешенному списку.

Это важно и для внутренних сервисов. Инструмент, принимающий `http://host/path`, можно заставить обратиться к локальным административным сервисам или конечным точкам метаданных, недоступным агенту напрямую. Блокировка публичных хостов не решает проблему. Нужен явный список разрешенных источников и правило, запрещающее буквальные IP-адреса и частные адреса, если инструменту они не нужны по замыслу.

Записывайте трафик в одноразовой тестовой среде. В macOS команда `lsof` быстро показывает текущие сетевые сокеты:

```sh
lsof -nP -iTCP -sTCP:ESTABLISHED -c python
```

Формат вывода показывает процесс, пользователя, файловый дескриптор и удаленную конечную точку:

```text
COMMAND   PID  USER   FD   TYPE             DEVICE SIZE/OFF NODE NAME
python   8421  alex   12u  IPv4 0x...             0t0  TCP 10.0.0.8:51244->203.0.113.20:443 (ESTABLISHED)
```

Замените `python` фактическим именем процесса и повторите проверку, пока выполняете одну операцию инструмента. Эта команда не заменяет полный аудит трафика. Короткие соединения могут исчезнуть до проверки. Но она хорошо показывает неожиданное долгое соединение или неизвестный вам вспомогательный процесс.

С такой же тщательностью проверяйте тела запросов. Инструмент может правильно обращаться к одному разрешенному API, одновременно отправляя в параметре запроса весь diff репозитория, переменные окружения или расшифровку сессии агента. Ограничьте исходящие поля в коде. Формируйте тело из именованных значений, нужных операции, вместо сериализации целого объекта, полученного от агента.

## Идентичность процесса входит в состав разрешения

Нельзя ответственно подтверждать использование инструмента, если неизвестно, какой исполняемый файл его запросил. Одного имени процесса недостаточно: любой процесс может выбрать знакомое имя.

Запишите полную команду запуска, путь к исполняемому файлу, версию, рабочий каталог, родительский процесс и учетную запись пользователя. Если применяется подпись кода macOS, проверьте и полномочия подписи. Команда `codesign` показывает идентификационные данные, которые видит macOS:

```sh
codesign -dv --verbose=4 /absolute/path/to/mcp-server 2>&1 | grep -E 'Identifier|TeamIdentifier|Authority'
```

Обычно вывод содержит такие поля:

```text
Identifier=com.example.mcpserver
Authority=Developer ID Application: Example Developer
TeamIdentifier=ABCDE12345
```

Эти поля служат свидетельством, но сами по себе не принимают решение о разрешении. Подписанный бинарный файл все равно может оказаться не тем, который нужен для этой задачи, а неподписанный внутренний скрипт не обязательно вредоносен. Проверка должна установить, совпадают ли путь, владелец, исходный код и идентичность с ожидаемым инструментом.

Затем изучите дерево процессов во время вызова:

```sh
ps -axo pid,ppid,user,command | grep -E 'mcp-server|sp-ssh|node|python'
```

Ответ должен быть скучным: клиент агента запускает MCP-сервер, а сервер создает только ожидаемые вспомогательные процессы. Насторожитесь, если сервер запускает shell, менеджер пакетов, интерпретатор из изменяемого каталога проекта или фоновый процесс, переживающий сессию.

Частая проблема выглядит безобидно в конфигурационном файле:

```json
{
  "command": "npx",
  "args": ["-y", "some-mcp-package"]
}
```

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

Идентичность процесса включает и унаследованное окружение. Сервер, запущенный из shell разработчика, может получить облачные токены, токены системы контроля версий, настройки прокси и широкий `PATH`. В тестовых запусках выведите обезличенный список окружения или запускайте процесс с минимальным окружением. Не записывайте значения секретов. Записывайте имена переменных, влияющих на поведение, и убедитесь, что инструмент не использует случайные учетные данные, которые ему никогда не предназначались.

## Журналы должны восстанавливать действия, не повторяя секреты

Полезная запись аудита отвечает на вопросы: кто запустил инструмент, в каком процессе, с какими обезличенными аргументами, для какого адреса и с каким результатом. Строка `tool call succeeded` не поможет ответить ни на один вопрос, который возникнет, когда агент отправит данные не туда.

Отделяйте рабочие журналы от отладочного вывода, содержащего секреты. Операторам нужны подробности для расследования. Им не нужны bearer-токены, заголовки авторизации, приватные ключи, исходные расшифровки сессий агента или полные тела ответов в текстовом файле.

Для каждого вызова записывайте примерно такие поля:

```json
{
  "time": "2025-03-08T14:22:11Z",
  "session_id": "run_7c2f",
  "process": "/opt/tools/build-mcp",
  "tool": "get_build_status",
  "argument_summary": {"build_id": "CI-4812", "include_logs": false},
  "destination": "ci.example.internal",
  "decision": "approved",
  "result": "success",
  "request_id": "c4e8..."
}
```

В примере намеренно используется сводка аргументов. Она должна сохранять идентификаторы и ограниченные поля, полезные для расследования, а чувствительные значения скрывать или хешировать. Если операция действительно отправляет документ, записывайте его размер в байтах и дайджест содержимого, если это помогает сопоставлению. Не помещайте сам документ в журнал только ради удобства отладки.

Модель журналирования OpenTelemetry полезна и без внедрения OpenTelemetry. Она разделяет тело события и атрибуты и делает акцент на структурированных полях для фильтрации и сопоставления. Примените ту же идею локально: сделайте адрес назначения, операцию, результат и идентичность процесса машиночитаемыми. Набор текстовых строк становится бесполезным, когда нужно выяснить, отправил ли агент один и тот же запрос десять раз.

Проверяйте и пути ошибок. Многие инструменты скрывают данные успешных запросов, но при ошибке API печатают весь объект запроса. Вызовите 401, создайте тайм-аут, передайте некорректный JSON и вызовите ошибку DNS. Прочитайте каждую выведенную строку. Именно в отладочном выводе чаще всего утекают учетные данные.

Sallyport хранит журнал Sessions для запусков агентов и журнал Activity для отдельных вызовов. Оба журнала строятся из защищенного от записи зашифрованного журнала аудита с цепочкой хешей. Такой подход полезен, когда нужен локальный источник записей помимо журналов самого автора инструмента, но сам по себе он не делает широкий инструмент безопасным. Инструменту все равно нужны узкие входные данные и известные адреса.

## Запросы на подтверждение не исправляют широкие полномочия

Подтверждение человеком служит полезным тормозом только тогда, когда понятен объем подтверждаемого действия. Карточка с сообщением, что неизвестный процесс хочет использовать учетные данные, уже сообщает важную информацию. Но она не объясняет, отправит ли универсальный инструмент запроса файл репозитория на хост, выбранный моделью через пять секунд.

Согласуйте единицу подтверждения с единицей полномочий. Токен только для чтения статуса и токен для развертывания в рабочей среде не должны скрываться за одним подтверждением, поскольку последствия у них разные. Инструмент, читающий запись о релизе, не должен незаметно получать возможность создавать ее только потому, что обе операции используют один API.

Спецификация Model Context Protocol рекомендует клиентам получать согласие пользователя до вызова инструментов. Это разумная рекомендация, но у согласия есть слабое место: люди подтверждают повторяющиеся плохо описанные запросы, пока запрос не перестает нести информацию. Не решайте усталость от подтверждений вечным разрешением целой категории действий. Исправьте границу инструмента, из-за которой запросы получаются расплывчатыми или чрезмерными.

Лестница решений Sallyport ставит заблокированное хранилище перед всеми действиями, по умолчанию запрашивает авторизацию для каждой сессии и может требовать решения при каждом использовании конкретных учетных данных. Используйте подтверждение каждого вызова для учетных данных, применение которых вы хотите проверять каждый раз, например для учетных данных развертывания или API с правом записи. Не используйте такой режим как оправдание для передачи этих учетных данных универсальному HTTP-инструменту.

У хорошего запроса на подтверждение есть честное продолжение: «Этот подписанный процесс, запущенный из указанного пути, может использовать эти учетные данные, чтобы получить статус этого сервиса в рамках этого запуска». Если вы не можете так сказать, отклоните запрос и вернитесь к карте полномочий.

## Проверьте пути отказа до того, как доверять успешной работе

Инструмент, который работает в идеальном сценарии, еще не прошел проверку безопасности. Нужно увидеть, что происходит при неверных входных данных, отсутствии учетных данных, неожиданном сетевом адресе и сбое вспомогательного процесса.

Запускайте инструмент в тестовой учетной записи или изолированном проекте с минимально достаточными полномочиями. Используйте тестовые данные, похожие на настоящие настолько, чтобы проверить сериализацию и ограничения размера, но никогда не передавайте рабочие секреты непроверенному инструменту ради эксперимента.

Проведите пять тестов:

1. Вызовите инструмент с одним корректным запросом и запишите дерево процессов, исходящий адрес и запись аудита.
2. Передайте необъявленное поле, значение максимальной длины, значение с переводами строк и похожий на корректный идентификатор, относящийся к другому проекту. Сервер должен отклонить каждый запрос до отправки сетевого запроса.
3. Попробуйте задать неразрешенный хост через все доступные входные данные и маршруты конфигурации. Если инструмент использует HTTP, проверьте и перенаправления. Инструмент должен отказать и записать отказ, не раскрывая чувствительные входные данные.
4. Удалите или отзовите учетные данные, пока сервер продолжает работать, и повторите корректный запрос. Убедитесь, что следующее действие завершится ошибкой, а не выполнится из скрытого кэша или унаследованного окружения.
5. Завершите родительский процесс агента и проверьте, не остались ли сервер или вспомогательные процессы. Фоновому процессу, сохраняющему доступ после завершения агента, нужны ясное обоснование и отдельная проверка.

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

Особого внимания заслуживает такой сбой. Допустим, инструмент поиска документов принимает `repository_path` и вызывает помощник через строку shell-команды. Обычные запросы работают. Позже агент получает инструкцию, встроенную в задачу, и ищет путь с shell-пунктуацией. Помощник выполняет вторую команду от имени разработчика, читает файл с учетными данными, а инструмент отправляет результат на свой в остальном разрешенный адрес поиска. Каждый компонент сделал то, чего ожидал его автор. Но композиция провалилась: схема разрешила путь, shell переинтерпретировал его, а исходящие данные приняли произвольный результат помощника. Тестируйте цепочки, а не отдельные функции.

## Для удаления доступа недостаточно убрать запись конфигурации

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

Планируйте удаление доступа одновременно с его выдачей. Владелец учетных данных должен знать, где их отозвать, инструмент по возможности должен использовать отдельные учетные данные, а оператору нужно понимать, какой процесс и какие локальные файлы удалить. Общие токены разработчика превращают простое удаление в инцидент, потому что невозможно определить, какое использование принадлежит инструменту.

Для инструмента, использующего HTTP, сначала отзовите или отключите удаленный токен. Затем остановите сервер MCP и все дочерние помощники, удалите локальную ссылку на учетные данные и конфигурацию запуска. В конце повторите тот же запрос и сохраните результат с отказом. Для SSH удалите нужный открытый ключ из удаленной учетной записи или репозитория, завершите локальные процессы-помощники и проверьте конфигурацию агента на наличие других идентификаторов.

Не путайте блокировку локального хранилища с удаленным отзывом. Блокировка запрещает дальнейшее использование через это хранилище, пока оно заблокировано. Она не может отозвать уже отправленные данные, аннулировать токен, сохраненный в другом месте, или остановить независимый процесс, ранее получивший учетные данные из окружения.

Сделайте удаление проверяемым еще до инцидента. Добавьте в короткую инструкцию адрес удаленного отзыва, ожидаемый ответ с отказом, команду остановки сервера, расположение конфигурации и поля журнала, подтверждающие неудачу попытки. Если владелец инструмента не может предоставить такую инструкцию, интеграция еще не завершена.

## Узкий инструмент заслуживает повторного доверия

Инструменты, успешно проходящие проверку, обычно скучны в лучшем смысле. Они принимают несколько типизированных полей, подключаются к одному известному сервису, при необходимости запускают известный исполняемый файл, возвращают агенту только нужный результат и оставляют запись, которую позже можно проверить.

Широкие инструменты кажутся гибкими, потому что переносят решения о дизайне на агента во время работы. Одновременно они превращают одно подтверждение в полномочия над адресами, данными и командами, которые никто не проверял. Оставляйте гибкость в коде, которым вы управляете, а агенту показывайте узкую операцию.

Перед подтверждением пользовательского инструмента попробуйте убрать одно входное поле, один адрес, одну область действия учетных данных или один дочерний процесс. Если никто не может объяснить, почему этот элемент должен остаться, удалите его. Проверка станет проще, как и последующее реагирование на инцидент.
