Если вы последний год не жили под камнем, то вы точно слышали заморскую аббревиатуру MCP (эм си пи). Это такой протокол, который позволяет языковым моделям взаимодействовать с внешними функциями и сервисами. Он работает по принципу Remote Procedure Call, то есть модель может вызвать функцию, которая исполняется на другом сервере или в другом приложении.
Короче, чтобы каждый не писал кастомные tools для взаимодействия с внешними ресурсами и сервисами, ребята из Anthropic решили стандартизировать всё это мракобесие и теперь вы можете без забот подключить к вашему кодинг агенту кучу MCP tools (о самом полезном MCP я писал 👉 тут) с помощью которых он может взаимодействовать с различными системами во время выполнения поставленной задачи.
Но если с использованием MCP всё понятно, то как нам написать свой кастомный MCP server? В интернете есть куча стартапов и SaaS (пример) которые сгенерируют вам готовый MCP сервер по вашей OpenAPI спецификации, но проблема в том, что это жёсткий антипаттерн, который, к сожалению, в индустрии встречается крайне часто.
Ну а чо, просто обернуть готовые REST эндпоинты в MCP tools и готово, теперь агент может взаимодействовать с нашим приложением, бюджет освоен, стейкхолдеры с довольным лицом и покрасневшими щёчками добавляют плашку «эй ай копайлот» к своему продукту.
Почему это плохо и так делать не надо?
Давайте представим ситуацию у нас есть проект где есть users и teams. Есть следующие эндпоинты:
- GET /user/:id – получить конкретного пользователя по айдишнику
- PUT /user/:id – изменить информацию о пользователе
- GET /teams – получить все команды
- GET /teams/:id – получить конкретную команду по айдишнику
Вы заворачиваете эти эндпоинты в MCP tools один в один. Дело сделано.
Теперь представьте, что юзер просит агента: «Добавь Сашу из команды маркетинга в команду разработки». Простой запрос, в UI вы бы просто выбрали человека из дропдауна и нажали кнопочку. Но агент с вашими REST-обёртками начинает танцевать балет из пяти актов:
1. Вызывает GET /users чтобы найти всех Саш
2. Парсит ответ в поисках нужного Саши
3. Вызывает GET /teams чтобы найти команду маркетинга (проверить что Саша там)
4. Вызывает GET /teams снова чтобы найти команду разработки
5. Наконец вызывает PUT /user/:id с UUID Саши
Пять вызовов вместо одного. И это я ещё упростил, в реальности там может быть проверка прав доступа, валидация, логирование и всякая другая хрень.
Но это ещё не всё, дальше веселее. Проблема номер два – UUID это кошмар для LLM. Языковые модели галлюцинируют UUID-ы как проклятые. Они могут решить что a1b2c3d4-e5f6-7890-abcd-ef1234567890 это валидный айдишник пользователя просто потому что он похож на UUID который вы передали в контексте.
И вот ваш агент пытается обновить пользователя который не существует, получает 404, начинает ретраиться, жрёт токены и в итоге сдаётся, а юзер сидит и думает «ну и хрень это вашэ эй-ай».
К тому же, UUID токенизируется крайне неэффективно. Один UUID это примерно 23 токена в зависимости от модели, а если у вас в ответе приходит массив из сотни пользователей с их UUIDs, то сами можете представить качество такого context engineering’а.
Что делать с UUID почитайте 👉 тут.
Итак, наш запрос: «Добавь Сашу из маркетинга в команду разработки»
Это конкретное действие с понятной бизнес-логикой. Но ваш REST API думает ресурсами: юзер, команда, их свойства.
Агент хочет выполнить действие
moveUserToTeam(user: "Саша из маркетинга", targetTeam: "разработка")
а получает набор CRUD операций которые надо самому оркестрировать. И вот агент начинает дирижировать этим оркестром: проверить, получить, обновить, проверить ещё раз… А если где-то в середине что-то упало? У REST API нет транзакционности. Через несколько запросов вы останетесь с Сашей, который наполовину в маркетинге и наполовину в разработке…
А как же правильно?
Ответ в комментах (место закончилось) 👇 @makebugger