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

Аннотации инструментов MCP полезны как документация. Но из-за них небезопасная система подтверждений может выглядеть аккуратно и надежно. Если клиент воспринимает readOnlyHint, destructiveHint или idempotentHint как разрешение, автор сервера фактически сам написал политику подтверждений пользователя, не доказав, что реализация этого заслуживает.
Это неверный порядок действий. Аннотация может помочь объяснить запрос, упорядочить список инструментов или предложить разумный вариант по умолчанию человеку, который проверяет действие. Подтверждение должно зависеть от запроса, который выполнит сервер, учетных данных, которые он использует, цели, к которой он обратится, и побочных эффектов, которые он может вызвать. Я слишком часто видел интеграции, которые обращались к конечной точке с именем get, возвращали вежливый JSON-объект и при этом создавали работу где-то еще.
Это особенно важно для агентов: они повторяют вызовы, объединяют инструменты в цепочки и действуют с такой скоростью, что небольшая ошибка классификации становится дорогостоящей. Инструмент, безопасный при однократном вызове, может стать небезопасным в цикле. Инструмент, доступный только для чтения в одном API, может превратиться в механизм экспорта в другом. Инструмент, который кажется идемпотентным, может создать дублирующую работу, если тайм-аут скрыл первый успешный вызов.
Аннотации описывают поведение, но не дают полномочий
Спецификация инструментов Model Context Protocol описывает аннотации как подсказки о поведении инструмента. Такая формулировка выбрана намеренно: клиент может использовать их для улучшения интерфейса, но не может безопасно превращать непроверенное заявление в решение безопасности.
Три рассматриваемых поля описывают разные утверждения:
readOnlyHint: trueутверждает, что инструмент не изменяет свою среду.destructiveHint: trueутверждает, что инструмент может выполнять разрушительные обновления.idempotentHint: trueутверждает, что повторные вызовы с теми же аргументами не оказывают дополнительного влияния на среду.
Эти утверждения не охватывают весь риск вызова. Инструмент может прочитать всю базу данных клиентов, отправить результат агенту и честно пометить себя как доступный только для чтения. Другой инструмент может записывать лишь время доступа, что звучит безобидно, пока не выясняется, что эта отметка меняет срок хранения данных, тарификацию или запись об инциденте. Идемпотентность ничего не говорит о том, был ли приемлем первый эффект.
В спецификации для этих полей также заданы консервативные значения по умолчанию. readOnlyHint по умолчанию равен false. idempotentHint по умолчанию равен false. destructiveHint по умолчанию равен true и имеет полезный смысл только для инструмента, который не является доступным только для чтения. Не заменяйте эти значения самодельным правилом вроде «отсутствующие метаданные достаточно безопасны». Отсутствие метаданных часто означает, что автор сервера не продумал классификацию.
Есть и другая неприятная деталь: безобидный сервер может ошибаться. Разработчик добавляет readOnlyHint: true, потому что обработчик выполняет SELECT, а затем библиотечный слой обновляет токен, записывает элемент в кэш или вызывает хук запроса. Аннотация остается верной еще долго после изменения поведения. Никто не собирался вводить клиента в заблуждение, но клиент все равно принял плохое решение, если автоматически подтвердил действие.
Чтение не означает безобидный результат
Операция только для чтения может раскрыть данные, израсходовать дефицитный ресурс или активировать поведение в удаленном сервисе. Считать, что «ничего не записывает» означает «не требует подтверждения», значит смешивать разные категории риска.
Представьте инструмент с именем get_build_log, который принимает идентификатор задания. Сервер читает данные из системы сборки и возвращает результат. Он вполне может объявить readOnlyHint: true. Но журнал может содержать исходный код, сведения об окружении, подписанные URL для скачивания или учетные данные, которые другая система случайно вывела в лог. Передача этого ответа автономному агенту меняет круг лиц, способных использовать информацию, даже если база данных системы сборки осталась нетронутой.
Та же проблема возникает в административных API. get_user может вернуть коды восстановления. list_invoices может раскрыть банковские реквизиты. search_documents превращается в массовое извлечение данных, если агент увеличит размер страницы или пройдет по всем префиксам. Побочный эффект здесь состоит в раскрытии информации, а в аннотации нет поля для чувствительности раскрываемых данных.
Операции чтения также могут менять удаленный сервис. Некоторые API обновляют last_accessed_at, расходуют одноразовый токен скачивания, регистрируют предварительный просмотр или выполняют тарифицируемый запрос. Промах кэша может разогреть дорогой зависимый сервис. Это не делает каждое чтение опасным, но лишает оснований безусловное правило, по которому для чтения подтверждение не нужно.
Классифицируйте вызов по двум отдельным осям: изменяет ли он систему и что он может раскрыть или вызвать за ее пределами? Проверка состояния с низким риском и массовый экспорт могут не менять данные. Но подход к их подтверждению должен быть разным.
В практической записи для проверки нужно простыми словами обозначать границу данных. «Читает статус развертывания проекта A» можно проверить. «Вызывает get_status» нельзя. Второй вариант скрывает цель, область действия и учетную запись, а также тот факт, что похожий метод на другом сервере может означать совсем другое.
Проверяйте обработчик на одноразовом тестовом объекте
Безопасность аннотации нельзя установить по названию инструмента или схеме входных данных. Запустите сервер там, где можно наблюдать его запрос, ответ и состояние до и после вызова.
Начните с тестовой учетной записи, содержащей записи, которые допустимо потерять. Выдайте ей отдельные API-учетные данные, а вебхуки уведомлений направьте на конечную точку для перехвата. Записывайте исходящие запросы сервера, состояние базы данных, если вы его контролируете, события аудита, письма, задания в очереди, счетчики тарификации и использования. Тело ответа служит доказательством, но это не вся картина.
Для каждого инструмента, который может влиять на подтверждения, используйте небольшую матрицу тестов:
- Вызовите его один раз с обычными корректными данными и сохраните полное состояние до и после.
- Вызовите его снова с побайтно идентичными данными и сравните все наблюдаемые эффекты.
- Вызовите его с отсутствующим ресурсом, уже завершенной операцией и некорректным полем.
- Прервите клиент после получения запроса сервером, затем повторите тот же вызов.
- Запустите два одинаковых вызова одновременно, если агенты могут выполнять их параллельно.
Сценарий с тайм-аутом выявляет частую ошибку. Допустим, create_ticket отправляет запрос на создание тикета, а затем соединение разрывается до ответа сервера. Агент видит ошибку и повторяет запрос. Если в системе тикетов нет токена идемпотентности, инструмент создает два тикета. Пометка обработчика как идемпотентного лишь потому, что его код принимает одинаковые данные дважды, не меняет результат в удаленной системе.
Фиксируйте результат в форме, которая заставляет проверяющих изучать эффекты, а не доверять зеленому статусу:
case: retry after response timeout
request: {"title":"rotate staging certificate","request_id":"test-104"}
first call: transport timeout after request received
second call: 201 {"ticket":"842"}
remote records: ["841", "842"]
result: not idempotent without a remote idempotency mechanism
request_id в этом примере помогает только в том случае, если удаленный API сохраняет его и обеспечивает его соблюдение. Идентификатор, созданный клиентом и игнорируемый сервером, остается лишь украшением. Проверьте это, повторив точно такой же идентификатор и убедившись, что удаленная система возвращает исходную операцию, а не создает новую.
Храните тесты вместе с сервером. Расхождение аннотации с поведением обычно появляется после изменения кода, обновления зависимости или добавления конечной точки. Проходящий тест, который сравнивает заявленную подсказку с наблюдаемым поведением, полезнее комментария рядом с определением инструмента.
Утверждения о режиме только для чтения рушатся на границах
Самая простая причина ложного readOnlyHint состоит в том, что проверяют только основной запрос к базе данных. Реальная граница включает каждый сервис, к которому обращается обработчик, и каждое действие, вызванное его ответом.
Возьмем инструмент сервера, который получает документ. Его основной запрос выглядит как GET /documents/42, но обработчик может сначала обменяться токеном обновления, выдать временный URL для скачивания, обновить локальный кэш и записать событие доступа. Каждая операция может завершиться по-своему. Для каждой могут действовать собственные учетные данные и требования к аудиту.
Не принимайте аргумент, что слишком маленькая запись не считается. Небольшие записи создают собственные сценарии сбоев. Отметка о последнем просмотре может повлиять на хранение данных. Кэш может сохранить содержимое после окончания разрешенного доступа. Событие доступа может уведомить владельца. Счетчик использования может перевести учетную запись на платный тариф. Спросите, меняет ли запись факт, который заметит другой человек, процесс или счет. Если да, задокументируйте это.
Поведение, вызванное ответом, заслуживает такого же внимания. Инструмент, который возвращает подписанную ссылку, может привести к тому, что агент позже перейдет по ней. Инструмент, который возвращает исполняемую команду, может побудить агента выполнить ее в другом канале. В узком смысле первый инструмент остается доступным только для чтения, но экран подтверждения с надписью «безопасное чтение» создает у человека ложное представление о следующем действии, которое агент может выполнить.
Хороший сервер разделяет операции, если их риски различаются. get_document_metadata может остаться узким инструментом для проверки. create_download_link должен быть отдельным инструментом, потому что он создает возможность предъявителю, даже если байты исходного документа не меняются. Такое разделение помогает агентам выбирать правильное действие и дает проверяющим формулировку, которую действительно можно утвердить.
Разрушительность определяется обратимостью, а не списком глаголов
destructiveHint должен отражать способность вызова вызывать вредные обновления, которые трудно отменить, а не наличие слова delete в названии инструмента. Команды часто ошибаются в обе стороны.
Некоторые очевидные глаголы обратимы в одной системе и необратимы в другой. archive может лишь скрывать запись, а может запускать таймер окончательного удаления. disable_user может сохранить все права и файлы, а может отозвать доступ так, что автоматизированный процесс окажется заблокирован. replace_config может обновить черновик, а может немедленно запустить развертывание в рабочей среде. Обработчику нужны сведения о конкретной цели, которых не выразить одним логическим значением.
Некоторые инструменты с безобидными названиями явно разрушительны. sync_members может удалить учетные записи, отсутствующие в переданном списке. apply_labels может перезаписать тщательно поддерживаемую таксономию. reconcile может исправить внешний реестр журнальными проводками, которые нельзя создавать без тщательной проверки. Автор сервера, который помечает такие инструменты как неразрушительные только потому, что API технически позволяет отменить изменения, скрывает операционную цену исправления.
Рассматривайте обратимость как последовательность действий, а не как флажок. Спросите, кто может отменить результат, какие доказательства ему понадобятся, как долго отмена будет доступна и не использует ли другой процесс изменение до того, как его можно будет отменить. Если человеку придется восстанавливать намерение по журналам после массового обновления, действие заслуживает разрушительной классификации, даже если API предоставляет обратный метод.
Популярная плохая рекомендация оставляет подтверждение только для явного удаления. Она популярна, потому что позволяет агентам двигаться дальше и делает демонстрацию плавной. В рабочей среде она не выдерживает проверки: разрушительные изменения обычно выполняются через замену, отзыв, отправку или сверку. Подтверждайте значимое изменение состояния, а не слова, которыми его описали.
Для действия, затрагивающего коллекцию, требуйте, чтобы в записи проверки были указаны правило отбора и количество объектов. «Синхронизировать пользователей» слишком расплывчато. «Удалить 14 неактивных подрядчиков, выбранных по переданным идентификаторам» позволяет оценить область действия. Если сервер не может сообщить эту область до начала операции, клиенту не хватает информации для серьезного запроса подтверждения.
Идемпотентность должна сохраняться при повторах и параллельной работе
idempotentHint содержит узкое утверждение: одинаковые аргументы не должны создавать дополнительный эффект после первого вызова. Это не означает, что вызов безопасен, дешев, обратим или подходит для бесконечных повторов агентом.
Обновление статуса может быть идемпотентным, если повторная установка state=closed оставляет ту же запись закрытой. Но обработчик перестает быть идемпотентным, если каждый раз отправляет письмо, добавляет комментарий аудита или увеличивает счетчик версии. Часто проверяют строку в базе данных и не замечают вторичных эффектов, которые пользователи видят раньше всего.
Равенство входных данных тоже нужно определить точно. Порядок полей JSON не должен иметь значения. Сервер, который по-разному обрабатывает отсутствие note и note: "", может получить запрос, который агент считает одинаковым, но выполнить два разных обновления. Временные значения, автоматически созданные значения по умолчанию и относительные выражения вроде tomorrow ослабляют такое утверждение, поскольку меняют фактическую команду, хотя видимые аргументы выглядят неизменными.
Именно при параллельной работе небрежная идемпотентность рушится. Два рабочих процесса могут одновременно проверить, что объекта нет, а затем оба создать его. Уникальное ограничение, транзакционный upsert или механизм идемпотентности удаленного сервиса могут предотвратить это. Кэш в памяти одного процесса MCP-сервера не защитит развертывание, работающее в нескольких процессах.
Используйте запись идемпотентности только после определения ее области действия. Сохраняйте переданный вызывающей стороной токен вместе с аутентифицированной учетной записью, нормализованным телом запроса, результатом и сроком действия, подходящим для операции. Отклоняйте повторное использование токена с другими нормализованными данными. Иначе агент может случайно прикрепить старый токен к новому запросу и получить результат другого действия.
Не запускайте автоматический повтор только потому, что подсказка имеет значение true. Повторяйте запрос лишь при сбоях, для которых вы знаете, получил ли сервер вызов. Если это неизвестно, решением служит механизм идемпотентности в самой точке изменения. Подсказка на стороне клиента такой механизм не заменяет.
Стройте подтверждения по фактически выполняемому действию
Система подтверждений должна отвечать на следующие вопросы: какой процесс отправляет запрос, какие учетные данные будут использованы, какая внешняя цель получит запрос, какие состояние или данные входят в область действия и что произойдет при успешном выполнении. Аннотации инструментов могут сделать это объяснение короче. Но они не могут предоставить сведения, которые сервер не раскрыл.
Разделяйте подтверждение сессии и подтверждение каждого вызова. Подтверждение сессии подходит известному процессу агента, выполняющему ограниченный запуск с обычными возможностями. Подтверждение каждого вызова подходит для учетных данных, которые могут переводить деньги, менять доступ к рабочей среде, отправлять сообщения, раскрывать чувствительные записи или создавать необратимое обязательство перед внешней системой. Решение должно зависеть от учетных данных и контекста действия, а не от оптимистичного readOnlyHint.
Полезный запрос на подтверждение называет конкретную операцию: «Агент, подписанный этим источником полномочий, использует учетные данные для развертывания, чтобы перезапустить сервис X в учетной записи Y». Слабый запрос звучит так: «Разрешить инструмент deploy?» В первом случае проверяющему есть что оценивать. Во втором его просят довериться детали реализации.
Sallyport следует этому разделению: хранит API- и SSH-учетные данные в зашифрованном хранилище, сам выполняет HTTP- или SSH-действие и возвращает агенту результат, а не учетные данные. Авторизация сессии указывает запрашивающий процесс, а настройка для каждой учетной записи может требовать решения при каждом использовании. Это более подходящее место для человеческого контроля, чем логическое значение, предоставленное сервером.
Даже при наличии шлюза перед учетными данными храните запись активности, в которой указаны конечная цель и результат запроса. Подтверждение отвечает на вопрос, можно ли выполнить действие. Запись аудита отвечает на вопрос, что произошло. Не объединяйте эти вопросы в одно расплывчатое событие под названием «инструмент использован».
Включите проверки аннотаций в обслуживание сервера
Аннотации инструментов MCP стоит использовать для честного описания поведения, подтвержденного тестами. Автор сервера должен задавать их консервативно, документировать пограничные случаи и менять их при изменении поведения. Автор клиента должен использовать их как один из источников для проектирования интерфейса, но никогда как единственный источник решения о доступе.
Добавьте тесты, которые намеренно опровергают заявленное свойство. Для объявления режима только для чтения тест должен завершаться ошибкой, если тестовый стенд обнаруживает запись, исходящее уведомление, обновление учетных данных или возможность, созданную для последующего получения. Для разрушительного объявления проверяйте путь сбоя и путь отмены, включая ситуацию, когда зависимое задание уже использовало изменение. Для идемпотентности запускайте тот же нормализованный запрос после имитации тайм-аута и при одновременных вызовах.
Не скрывайте несоответствие, меняя тестовую цель до тех пор, пока тест не пройдет. Либо сузьте инструмент, чтобы подсказка стала верной, либо измените аннотацию, либо раскройте эффект в деталях подтверждения. Каждый вариант сообщает следующему сопровождающему, что на самом деле делает код.
Запускайте sp audit verify во время разбора инцидента, если Sallyport используется как шлюз действий. Команда автономно проверяет зашифрованную цепочку хешей, поэтому можно убедиться в целостности записанной истории действий, не открывая хранилище. Это не доказывает честность аннотации сервера, но дает защищенную от незаметного изменения запись вызовов, последовавших за ней.
Практический стандарт прост: аннотация должна выдерживать проверку, которая намеренно оценивает эффект, важный для пользователя. Если она не выдерживает такую проверку, оставьте ее консервативной и сохраняйте подтверждение там, где выполняется действие.
Вопросы и ответы
Можно ли безопасно автоматически подтверждать инструмент MCP с readOnlyHint?
Считайте это утверждением, которому нужны доказательства, а не разрешением. Изучите реализацию сервера, затем запустите инструмент на одноразовом тестовом объекте и проверьте все побочные эффекты, которые он может вызвать.
Что на самом деле гарантирует idempotentHint?
Это означает, что автор считает: повторные вызовы с теми же аргументами не вызывают дополнительных изменений среды. Но вам все равно нужно проверить внешние системы, поведение при повторах, временные метки, уведомления и нормализацию аргументов.
Являются ли аннотации инструментов MCP границей безопасности?
Нет. Спецификация Model Context Protocol описывает эти поля как подсказки о поведении, а не как замену решению клиента в области безопасности. Злонамеренный, устаревший или просто ошибившийся сервер может публиковать вводящие в заблуждение метаданные.
Какие инструменты MCP все равно должны требовать подтверждения?
Сохраняйте подтверждение, если вызов может изменить состояние бизнеса, раскрыть чувствительные данные, запустить дорогую задачу или обратиться к системе за пределами контролируемого тестового объекта. Безобидное название и оптимистичная аннотация не устраняют эти риски.
Как проверить, что инструмент идемпотентен?
Используйте одноразовую учетную запись или локальный тестовый стенд, зафиксируйте исходное состояние, дважды вызовите инструмент с одинаковыми аргументами и сравните итоговое состояние и внешние признаки. Повторите тест с некорректными данными и прерванным запросом, поскольку именно пути повторной отправки часто выявляют ущерб.
Может ли разрушительный инструмент быть безопасным, если ничего не изменилось?
Инструмент удаления может быть неразрушающим для конкретной записи, потому что записи уже не существует, и при этом в целом оставаться разрушительным. При проектировании подтверждений классифицируйте возможность и контекст цели, а не только результат одного вызова.
Что происходит, если аннотации MCP отсутствуют?
Считайте отсутствие аннотации консервативным вариантом и изучите семантику полей, прежде чем писать автоматизацию. В текущих аннотациях инструментов MCP пропущенный destructiveHint по умолчанию равен true, а пропущенные readOnlyHint и idempotentHint равны false.
Как у API-вызова только для чтения все равно могут быть побочные эффекты?
Инструмент может вернуть правдоподобный ответ, создать запись аудита, обновить поле времени последнего доступа, вызвать вебхук или списать средства со счета, оставив очевидный ресурс неизменным. Проверяйте зависимые записи, сетевые вызовы и системные журналы, а не только ответ инструмента.
Как проектировать подтверждения для автономных агентов программирования?
Подтверждайте запуск агента только после определения полномочий на подпись кода и предполагаемой области действий, а подтверждение каждого вызова оставляйте для учетных данных или действий, где каждое использование требует решения человека. Метаданные могут помочь сформулировать текст запроса подтверждения, но не должны определять его результат.
Как Sallyport помогает контролировать действия MCP?
Sallyport хранит учетные данные за пределами агента и позволяет человеку подтвердить сессию или потребовать подтверждение каждого использования выбранных учетных данных. Это поддерживает схему, основанную на фактически выполненном действии, а не на аннотации, которую предоставил сервер.