MCP - это открытый протокол от Anthropic, который стандартизирует способ предоставления контекста LLM (большим языковым моделям). Подобно тому, как USB-порты обеспечивают стандартизированный способ подключения к различным периферийным устройствам, MCP обеспечивает стандартизированный способ подключения ИИ-моделей к различным источникам данных и инструментам.
В последние месяцы протокол MCP (Model Context Protocol) резко стал набирать популярность. Идея проста: давайте стандартизируем API для общения LLM/Агент, чтобы они могли взаимодействовать с внешним миром, и наоборот, Aгент/LLM чтобы предоставлять им контекстную информацию.
Конкуренты не стоят на месте, и недавно IBM представила собственный "ортогональный стандарт" к MCP под названием Agent Communication Protocol (ACP), за которым вскоре последовало объявление Google о запуске Agent2Agent (A2A).
Однако, меня поражает очевидное отсутствие зрелых инженерных практик. Все крупные игроки тратят миллиарды долларов на обучение и настройку своих моделей, а затем, судя по всему, поручают написание документации стажерам, предоставляют низкокачественные SDK и очень мало руководств и примеров.
Эта тенденция, похоже, продолжилась и с MCP, что привело к некоторым очень странным архитектурным решениям, плохой документации и еще худшей спецификации самих протоколов. Я пришел к выводу, что всю предложенную настройку для HTTP-транспорта (SSE+HTTP и Streamable HTTP) следует отбросить и заменить чем-то, имитирующим stdio - вебсокетами.
Предыстория
Около трех недель назад я решил присоединиться к движухе, чтобы попробовать эту технологию и понять, как её можно применить в нашей команде. Я из тех людей, которым важно разобраться, как что-то работает под капотом, прежде чем использовать абстракции. А здесь у нас новый протокол, работающий поверх разных транспортов - интересно!
Судя по всему, MCP является одной из главных причин, по которым CEO Anthropic считает, что уже через год большую часть кода будет писать ИИ. Ставка на инструменты для разработки стала основным принципом стандартизации - по крайней мере, так ощущается при работе с этим протоколом.
Транспорт
Судя по всему MCP следует парадигме «local first» (что иронично). Если взглянуть на транспортный протокол, становится понятно, откуда ноги растут: их цель - создавать инструменты на основе ИИ для разработки прямо на вашем ноутбуке. Скорее всего, они ориентируются на локальные IDE (Cursor или Windsurf) и думают о том, как интегрировать ИИ с локальной файловой системой, базами данных, редакторами, языковыми серверами и пр.
По сути, есть два основных транспортных протокола:
stdio (стандартный ввод/вывод);
что-то поверх HTTP (вероятно, чтобы проще поддерживать).
Stdio
По сути означает запуск локального сервера MCP, подключение каналов stdout и stdin от сервера к клиенту, и использование stderr для логов. Двунаправленная коммуникация в какой-то мере нарушает логику пайплайнов unix (обычно для этого используют сокеты), однако, решение получается простое и работает из коробки на всех ОС.
HTTP+SSE / Streamable HTTP
Следующий вариант - HTTP. Существуют две версии и обе наступают на одинаковые грабли: HTTP+SSE (Server-Sent Events), и попытка заменить его на "Streamable HTTP", что-то вроде REST поверх SSE. Получили в итоге сплошную мешанину и кучу особых случаев сверху.
Обсуждение причин и проблем при внедрении "Streamable HTTP" можете найти в этом PR. Правда почти весь тред состоит из жиденьких оправданий почему Websocket не стоит использовать, но пара человек со мной согласны.
Погружение в безумие
Я решил реализовать MCP-сервер на Go. Официального Go SDK нет, и я хотел разобраться в протоколе. Оказалось, это повлияет на мое ментальное здоровье...
Предупреждающие знаки...
Если посмотреть на https://modelcontextprotocol.io, документация там плохо написана (как будто у всех поставщиков LLM идет соревнование по написанию запутанной документации). Спецификация поверхностно затрагивает или вовсе игнорирует важные аспекты протокола и не содержит примеров диалога (чата). Кажется, что сайт не предназначен для описания стандарта, а вместо этого подсовывает вам руководства по готовым SDK.
Все примеры серверов реализованы на Python или JavaScript, с расчетом на то, что вы скачаете и запустите их локально, используя стандартный ввод/вывод (stdio). Python и JavaScript, пожалуй, одни из худших языков для чего-то, что должно работать на компьютере другого пользователя. Авторы, кажется, осознают это, поскольку все примеры завернуты в Docker-контейнеры.
Действительно, когда вы в последний раз запускали команду
pip install
и не провалились в dependency hell?
Не буду ли я предвзятым, считая, что люди в сфере ИИ в основном знают только Python, и подход «ну, у меня на компьютере работает» всё ещё считают приемлемым? Это должно быть очевидно любому, кто хоть раз пытался запустить что-либо из Hugging Face.
Если вы хотите запускать MCP локально, разве вы не предпочли бы компилируемый язык - Rust, Go или варианты на основе виртуальных машин, такие как Java или C#?
Проблема
Когда я приступил к реализации протокола, я сразу почувствовал, что мне придется заниматься его реверс-инжинирингом. Важные аспекты, касающиеся части SSE, отсутствовали в документации, и, кажется, никто еще не реализовал "Streamable HTTP", даже их собственные либы, такие как npx @modelcontextprotocol/inspector
(но спустя пару недель какая-то поддержка все же появилась).
Как только вы разберётесь в архитектуре, вы быстро поймёте, что реализация сервера или клиента MCP может потребовать огромных усилий. Проблема в том, что реализация SSE/Streamable HTTP пытается вести себя как сокеты, эмулируя stdio, не являясь при этом таковым. А ещё MCP пытается Делать Всё и Сразу.
Режим SSE
В режиме HTTP+SSE, чтобы достичь двунаправленного взаимодействия (дуплекса), клиент устанавливает SSE-сессию (например, по адресу GET /sse
) для получения данных для чтения. Первое полученное сообщение предоставляет URL-адрес, по которому могут быть отправлены данные для записи. Затем клиент использует указанную конечную точку для отправки данных, например, отправляя запрос POST /endpoint?session-id=1234
. Сервер возвращает ответ 202 Accepted
без тела, а ответ на этот запрос следует читать из предварительно установленного открытого SSE-соединения по адресу /sse
.
Режим Streamable HTTP
В режиме "Streamable HTTP" они поняли, что вместо предоставления нового эндпоинта в первом запросе можно использовать HTTP-заголовок для идентификатора сессии и REST-семантику для эндпоинта. Например, GET или POST /mcp могут открыть сессию SSE и вернуть HTTP-заголовок mcp-session-id=1234
. Для отправки данных клиент выполняет запросы POST /mcp
и добавляет HTTP-заголовок mcp-session-id=1234
. Ответ может:
Открыть новый поток SSE и отправить ответ
Вернуть код 200 с ответом в теле
Вернуть код 202, указывая, что ответ будет записан в один из существующих потоков SSE
Чтобы завершить сессию, клиент может (но не обязан) отправить DELETE /mcp
с заголовком mcp-session-id=1234
. Сервер должен поддерживать состояние, не имея четкого способа узнать, когда клиент покинул сессию, если только клиент не завершит ее должным образом.
Проблемы режима SSE
Это настолько проблемная архитектура, что я даже не знаю, с чего начать. Хотя некоторые ключевые особенности режима SSE не задокументированы, всё становится довольно понятно после реверс-инжиниринга. В этом варианте создаётся огромная и ненужная нагрузка на сервер, который должен "склеивать" соединения между вызовами. Для выполнения каких-либо реальных задач вам практически обязательно придётся использовать очередь сообщений, чтобы отвечать на запросы. Например, если сервер реплицирован, это приведёт к тому, что поток SSE-событий может поступать к клиенту с первого сервера, в то время как запросы отправляются на второй.
Проблемы Streamable HTTP
Подход Streamable HTTP выводит проблемы на новый уровень, добавляя множество вопросов относительно безопасности и запутанного потока управления. Сохраняя все недостатки режима SSE, Streamable HTTP, похоже, представляет собой ещё более запутанную надстройку над SSE.
Что касается реализации, я только начал разбираться, но, насколько я понял из документации...
Новая сессия может быть создана тремя путями:
Пустой GET запрос
Пустой POST запрос
POST запрос с RPC-вызовом
SSE создаемся четырьмя разными путями:
GET запрос на инициализацию
GET запрос на подключение к существующей сессии
POST запрос на инициализацию
POST запрос который ожидает ответы через SSE
Есть три варианта получить ответы:
Простой синхронный HTTP response на POST запрос с RPC-вызовом
Событие SSE как асинхронный ответ на RPC-вызов
Событие в любой из открытых SSE-сессий
Прочие проблемы
Множество способов инициации сессий, открытия соединений SSE и обработки запросов вносит значительную сложность. Эта сложность имеет несколько последствий:
Увеличение сложности: Множественные способы выполнения одних и тех же действий (создание сессии, открытие SSE, отправка ответа) увеличивают когнитивную нагрузку на разработчиков. Код становится сложнее понимать, отлаживать и поддерживать.
Потенциальная несогласованность: Из-за различных способов достижения одного и того же результата возрастает риск несогласованных реализаций на разных серверах и клиентах. Это может привести к проблемам совместимости и неожиданному поведению, когда клиенты и серверы реализуют только те части, которые считают нужными.
Проблемы с масштабируемостью: Хотя Streamable HTTP стремится повысить эффективность, сложность приведёт к появлению узких мест в масштабируемости, которые придётся преодолевать. Серверы могут столкнуться с трудностями при управлении разнообразными состояниями соединений и механизмами ответов на большом количестве машин.
Проблемы безопасности
"Гибкость" Streamable HTTP вводит несколько проблем безопасности, и вот лишь некоторые из них:
Уязвимости в управлении состоянием: Управление состоянием сессии через различные типы соединений (HTTP и SSE) может привести к таким уязвимостям, как захват сессии, атаки повторного воспроизведения или DoS-атаки путем создания состояния на сервере, которое необходимо держать в ожидании возобновления сессии.
Увеличение поверхности атаки: Множественные точки входа для создания сессий и SSE-соединений расширяют поверхность атаки. Каждая точка входа представляет собой потенциальную уязвимость, которую злоумышленник может использовать.
Путаница и обфускация: Разнообразие способов инициирования сессий и доставки ответов может использоваться для маскировки вредоносной активности.
Авторизация
Последняя версия протокола содержит некоторые очень категоричные требования о том, как должна быть реализована авторизация.
https://modelcontextprotocol.io/specification/2025-03-26/basic/authorization
Реализации, использующие HTTP-транспорт, ДОЛЖНЫ соответствовать этой спецификации.
Реализации, использующие STDIO-транспорт, НЕ ДОЛЖНЫ следовать этой спецификации и вместо этого получать учетные данные из окружения.
Я читаю это так: для stdio можно делать всё, что угодно. А для HTTP нужно обязательно подпрыгивать с OAuth2. Почему для stdio достаточно API-токена - загадка.
А как надо?
Как бы это грубо не звучало, но кажется индустрия сейчас обделалась прям в штаны. Сейчас они думают, что совершили великий прорыв, но спустя какое-то время все эти костыли придется разгребать.
Есть JSON RPC, и Stdio явно предпочтителен в качестве транспортного протокола. Поэтому мы должны постараться сделать HTTP-транспорт максимально похожим на Stdio и отклоняться от него только если уж совсем, совсем необходимо.
В Stdio у нас есть переменные окружения, в HTTP есть заголовки. В Stdio реализовано поведение, похожее на сокет, с входными и выходными потоками, в HTTP есть WebSockets для этого.
Собственно, вот и всё. Мы должны иметь возможность делать то же самое в WebSocket, что и в Stdio. WebSocket - это подходящий выбор для транспорта поверх HTTP. Мы можем избавиться от сложного межсерверного управления состоянием сессий. Мы можем избавиться от множества краевых случаев и так далее.
Конечно, некоторые вещи, например авторизация, могут быть немного сложнее в некоторых случаях (и проще в других); некоторые фаерволы могут блокировать WebSocket; может быть дополнительная нагрузка для коротких сессий; может быть сложнее возобновить прерванную сессию.
Но они сами сказали:
Клиенты и серверы МОГУТ реализовать дополнительные пользовательские механизмы транспорта для удовлетворения своих специфических потребностей. Протокол не зависит от транспорта и может быть реализован поверх любого коммуникационного канала, который поддерживает двунаправленный обмен сообщениями.
https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#custom-transports
Альтернативы
Как обсуждалось выше, похоже, появляется ещё больше протоколов. MCP по сути — это «протокол для предоставления API LLM» (чтобы создать агента). Более свежие протоколы от IBM и Google (ACP и A2A) — это «протоколы для предоставления агентов LLM» (чтобы создать агента для агентов).
Изучив спецификации A2A, кажется, что в них нет особой необходимости. Хотя они утверждают, что являются ортогональными, большинство функций A2A можно реализовать с помощью MCP в его текущем виде или с небольшими дополнениями.
Даже IBM, похоже, признаёт, что их протокол на самом деле не обязателен:
"Агенты могут рассматриваться как ресурсы MCP и дополнительно вызываться в качестве инструментов MCP. Такой взгляд на агентов ACP позволяет клиентам MCP обнаруживать и запускать агентов ACP..." - IBM
Мое первоначальное чувство было, что протокол ACP в основном выглядит как попытка IBM продвинуть свой "инструмент для создания агентов" BeeAI.
Оба протокола A** лишь обеспечивают более разумный транспортный уровень и способ обнаружения агентов.