MCP - что это, нужен ли он, и какие есть уязвимости

MCP - что это, нужен ли он, и какие есть уязвимости

Evgenii Nikitin

Предположим, я хочу общаться с нашей документацией в Notion прям из Cursor. Что делать?

В нашей ИИ-вики мы для каждого нужного источника написали свой коннектор, который присоединяется к сервису по API или иным доступным способом, а затем уже передаёт полученные данные в чанкер вместе с мета-информацией об источнике. По сути, MCP (Model Context Protocol) - это унифицированный протокол для таких коннекторов. Они уже написаны для самых разных источников, например, на Smithery есть очень большой каталог. Для популярных источников может существовать несколько десятков разных имплементаций MCP-серверов.

Вот как это работает в Курсоре:

Удобно! Для этого мне пришлось лишь поднять локально один из MCP-серверов (есть и удалённые серверы), написанных для Notion, немножко поправить код, чтоб он использовал мой прокси (API ноушна не работает в России), и подставить токен авторизации. После этого LLM может решить использовать один из инструментов, которые предоставляет этот сервер. А может и не решить, как и с обычными инструментами, всё зависит от запроса, модели и описания инструментов.

Вообще концепт MCP довольно подробно описан в этом посте в рассылке Pragmatic Engineer (он за пейволлом, я прикреплю PDF-версию в комментах к посту в ТГ) или в недавнем посте на Хабре. Но давайте всё равно кратко пробежимся по основам.

LSP - предтеча MCP

Идея MCP отдалённо похожа на широко распространённый LSP (Language Server Protocol), который используется в каждой современной IDE, и позволяет любому IDE обмениваться сообщениями с сервером, написанным под каждый язык. Чем полнее LSP для конкретного языка, тем больше фич может накрутить IDE - проверку ошибок в реальном времени, показ документации на лету, переход к определению, хайлайт синтаксиса и так далее.

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

  • В виде кода - например, выполняется запрос к стороннему API и постпроцессинг результатов.
  • На человеческом языке - чтобы LLM могла понимать, когда использовать тот или иной инструмент.

Проще всего изучить, что такое MCP-сервер, на примере - скажем, имплементации для Докера. Мы видим

  • Список промптов - в данном случае один, который собирает текущее состояние проекта (контейнеры, тома, сети и так далее) и подставляет в шаблон промпта, который просит создать план выполнения команд. Эти динамические промпты никак не используются самим сервером, но приложение (тот же Курсор) может каким-нибудь образом (например, через стандартный слэш) предоставить их пользователю.
  • Список ресурсов - какая-то информация, которую может запрашивать клиентское приложение, а не LLM напрямую. Скажем, приложение знает, что ему нужен конкретный файл, который затем будет добавлен в контекст LLM. В случае докера есть два ресурса - чтобы получить логи контейнера или статистику потребления контейнера.
  • Список инструментов - самая важная часть, в случае докера это pull, build, run и так далее. Функция list_tools предоставляет описание каждого инструмента на английском языке.

Ещё один хороший способ для изучения - попробовать написать свой MCP-сервер.

Если для вашего источника есть MCP-сервер, это позволяет не писать собственные коннекторы для каждого приложения и из коробки предоставляет полезную функциональность, включая аутентификацию, ретраи и так далее. За 15 минут я смог подключить в Cursor MCP-серверы для Notion, PostgreSQL и Google Docs. Полноценную RAG-систему это не заменит, но позволит, не выходя из IDE, обращаться к БД или к определённой документации.

На днях Google представили свой протокол Agent2Agent, который в теории должен дополнять MCP. MCP предоставляет единый стандарт для использования инструментов, а A2A позволяет агентам общаться с пользователем и друг с другом. Звучит абстрактно, но можно посмотреть примеры и почитать документацию. Уже можно признать, что MCP стал распространённым стандартом в индустрии, посмотрим, что будет с A2A.

Зачем всё это нужно, если есть API?

Хороший вопрос, на который многие отвечают, что низачем. Давайте разбираться в аргументах за и против.

Против:

  • У всех популярных инструментов есть API, LLM и так могут читать их документацию через какой-нибудь Сваггер
  • Большинство MCP-серверов - это просто зеркальная копия API, и для сложных API такой подход не принесёт выгоды. LLM будет часто иметь сложности при выборе уже среди 5-10 инструментов, которые по факту будут являться эндпойнтами с кучей параметров и сложным аутпутом. А в какой-то момент описание инструментов просто может перестать помещаться в контекст. Альтернативный подход - создавать высокоуровневые инструменты под конкретные задачи на основе существующих API, но так мало кто делает
  • Промпты и ресурсы никто кроме Anthropic не имплементирует, всё вертится вокруг инструментов (читай, эндпойнтов)

За:

  • Стандартизация вызовов API - не надо писать свои надстройки и коннекторы или полагаться на то, что LLM корректно найдёт и прочитает документацию. MCP-серверы в теории оптимизированы для использования LLM
  • Динамическое обновление списка инструментов, особенно если MCP-сервер поддерживается самим сервисом, который предоставляет API
  • Потенциал для развития более крутых фичей
  • Есть MCP-серверы для инструментов, у которых нет нормальных API - например, для Пейнта

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

Проблемы безопасности

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

  • Самый простой вариант - описание инструмента может не соответствовать его наполнению. Например, LLM хочет вызвать апишку сервиса, а инструмент заодно отправляет наши приватные ключики или имейлы на сторонний адрес.
  • Подмена инструмента - изначально пользователь одобряет один инструмент, а после этого он подменяется другим.
  • Лики данных между MCP-серверами. В описании инструмента может быть спрятано, что другой инструмент (например, для взаимодействия с почтой) должен вызываться с другими аргументами - к примеру, отправлять письмо на заданный в инструменте почтовый адрес.

Что делать разработчикам и пользователям, чтобы защититься от таких атак?

При использовании MCP и вообще агентских LLM-системы рекомендуется соблюдать ряд требований безопасности.

Для разработчиков MCP-серверов:

  • Не использовать в коде опасные команды типа os.system, этим могут воспользоваться разработчики других вредоносных MCP-серверов.
  • Валидировать и по возможности очищать по правилам все инпуты.

Для разработчиков агентских систем с использованием MCP:

  • Обязательно запрашивать разрешение на вызов инструмента у пользователя и полностью отображать все мета-данные инструмента - описание, аргументы. К сожалению, пользователь часто не захочет всё это читать и просто нажмёт Approve.
  • Не обновлять автоматически MCP-сервера и описания инструментов - изначально безопасный MCP-сервер может быть изменён в источнике.

Для пользователей:

  • Использовать только проверенные MCP-серверы или проверять код новых MCP-серверов.
  • Внимательно читать, какие инструменты вызываются.

Увы, эти советы слишком сильно полагаются на человеческий фактор. Одна из фундаментальных причин существования prompt injection - LLM не умеют разделять системную инструкцию и остальную часть инпута. Сколько бы мы ни умоляли LLM не удалять наши имейлы и не отправлять наши данные злоумышленникам, это не гарантирует, что не найдётся инструкции, которая не обойдёт наш промпт.

Dual LLM Pattern и CaMeL

Саймон Виллисон ведёт классный блог про LLM, в том числе он часто пишет про вопросы безопасности и prompt injection. Два года назад он описал прикольную архитектуру с двумя LLM, которая позволяет защититься от большого количества prompt injection атак.

  • Привилегированная LLM получает на вход только проверенный или одобренный пользователем контент, имеет доступ ко всем инструментам (включая инструменты MCP-серверов) и отвечает за выполнение всех "опасных" операций - запись в базу данных, отправку писем и так далее.
  • Карантинная LLM не имеет доступа к инструментам и используется каждый раз, когда мы работаем с недоверенным инпутом - например, содержимым писем, веб-страниц, в общем не с пользовательским проверенным инпутом. Она преобразует эту информацию в структурированные данные, которые затем валидируются и по необходимости одобряются пользователем.
  • Ещё в этой схеме есть контроллер, который является не LLM, а просто обычной программой, которая делает запросы в модели, выполняет действия и передаёт ответы пользователю.

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

Недавнаяя работа CaMeL от DeepMind развивает идею Dual LLM. Основная LLM получает инпут пользователя и декомпозирует его в набор питоновских команд с жёстко заданной схемой входов и выходов.

К примеру, промпт "Найди дату дедлайна задачи X на странице планинга в Notion и запиши её в поле Deadline этой задачи" может быть декомпозирован на такие команды:

  • page = get_notion_page(database_id="planning_db_id")
  • deadline = query_quarantined_llm(f"На странице {page} найди дату дедлайна задачи {X} и верни её", output_schema=DateStr)
  • notion_update_page(page_id=page.id, properties={"Deadline": deadline})

Таким образом, мы используем только защищённые команды - получаем страницу из страницы Notion, достаём жёстко типизированный (строчка с датой) выход карантинной LLM и обновляем Notion-страницу.

Если page - это доверенный инпут (например, мы заранее одобрили страницу "Планинг" как безопасную), то полученный дедлайн тоже будет считаться доверенным и автоматически запишется в базу. В ином случае deadline будет считаться недоверенным, и будет запрошено пользовательское подтверждение.

Две LLM могут быть разными - например, карантинная LLM может быть слабее, если задачи, которая она выполняет (скажем, саммеризация), не требуют особо больших моделей.

Пример из статьи

Защита на уровне модели

Отдельный интересный класс методов пытается решить проблему на уровне модели.

Свежая статья ASIDE разделяет эмбеддинги для системного промпта и пользовательского инпута. Мы берём обычную LLM, копируем все эмбеддинги с ортогональным повторотом (чтобы они отличались от оригинальных), файнтюним несколько эпох на специальном датасете и затем используем разные эмбеддинги в зависимости от того, к какой части инпута относится токен. Таким образом достигается значительное снижение влияние вредоносных вставок в инпут на системную инструкцию, хотя полной гарантии это опять же не даёт.

Ещё можно пробовать оптимизировать модель таким образом, чтобы она предпочитала безопасные варианты ответа и не велась на фейковые инструкции промпт-инджектеров и джейлбрейкеров в духе "ignore previous instructions".

Знаменитый Pliny

Оценка уязвимости

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

Бенчмарк BIPIA сфокусирован на косвенных атаках, которые пытаются повлиять на модель через внешние источники - например, веб-страницы или инъекции в документацию. Авторы даже предлагают несколько несложных методов, которые помогают модели разделить системный промпт и инпут, таким образом снижая влияние вредоносных вставок. К примеру, при дообучении LLM мы можем явно разделять инструкцию и пользовательский инпут специальными токенами. А в статье Spotlighting от Microsoft показано, как можно добиться хорошего эффекта вообще без дообучения, просто модифицируя промпт.

А ещё?

Я описал далеко не все методы борьбы с prompt injection, довольно подробный список можно посмотреть здесь. Пожалуй, с точки зрения пользователя самый безопасное - это жёстко ограничивать права доступа (например, использовать рид-онли токены и пользователей), использовать только проверенные MCP-серверы и читать, какие именно команды вы подтверждаете.

Report Page