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** лишь обеспечивают более разумный транспортный уровень и способ обнаружения агентов.
