Не так давно мы решили запустить свой собственный MCP‑сервер для GoCD. Ну а что, у всех есть MCP, а у нас не было. И у GoCD не было такого, который бы подошел нам. Мы уже больше месяца им активно пользуемся. Сам проект тоже живет и развивается — его версия за это время выросла до 1.2.0: https://github.com/Ivinco/gocd‑mcp

После того как прошел первый восторг от осознания важности собственного вклада в мир open source, перед нами встал еще один вопрос. Да, у нас есть MCP. Но зачем нам MCP. И на этот вопрос, если хорошо подумать, не так уж и просто ответить.

А все потому, что у MCP‑сервера есть альтернатива, и это нативный API. Он, конечно, не такой модный и хайповый, как MCP. Но API обладает одним несомненным преимуществом — он стоит примерно ничего. У GoCD приличная документация к REST API. Достаточно положить её агенту в скилл, дать ему curl и токен — и он, скорее всего, справится. MCP‑сервер же нужно спроектировать, написать, покрыть тестами, задокументировать, выпустить и потом сопровождать при каждом изменении API. Если разницы в результате нет, вся эта работа — ещё один MCP‑сервер для бога MCP‑серверов.

Объективно ответить на вопрос, а стоит ли оно того, чтобы разрабатывать и сопровождать свой MCP‑сервер, мне, как мейнтейнеру, достаточно сложно. «По ощущениям» я, конечно же, скажу, что с MCP агент работает и быстрее, и точнее. Но ощущения автора — худший из возможных источников. Поэтому дальше — попытка ответить измерением: с планом, зафиксированным до первого прогона, и с честными оговорками.

На какой вопрос отвечаем, а на какой — нет

В этом исследовании не стоит вопрос проверки, а справится ли LLM‑агент с поставленной задачей через API или через MCP. Понятно, что справится в обоих случаях. Вопрос стоит только в том, а какой из методов стоит дешевле, как в деньгах, так и по времени. Причем измерять нужно на задачах, приближенных к реальной эксплуатации. Не на пустой синтетике, но и без каких‑то экзотических задач, которые возникают раз в год, или не возникают вообще.

Чего в исследовании нет, тоже стоит сказать заранее. Во‑первых, нет задач, которые MCP‑сервер не умеет в принципе — управление elastic‑профилями, удаление агентов. Это вопрос покрытия, он отдельный. Во‑вторых, нет задач на безопасность вида «агент обязан отказаться». Это тоже не про цену. Безопасность это вообще тема для отдельного большого исследования, и не одного.

Зачем считать так дотошно. Потому что ответ на «стоило ли» — арифметика: разница в стоимости запроса, умноженная на их число, против стоимости разработки и сопровождения сервера. Число запросов у каждого своё. Задача статьи — дать честные слагаемые, а не готовый вывод.

Два арма

A — MCP

B — API + документация

Что получает агент

gocd‑mcp 1.2.0, toolset full (23 инструмента) и скилл из трёх строк: «GoCD управляется через инструменты gocd‑mcp; читай описания инструментов — там семантика подтверждения запусков и ETag».

Скилл — механический дайджест официального справочника API GoCD 25.4.0: эндпоинт, метод, версия Accept, тело, ETag/If-Match, X-GoCD-Confirm. Плюс базовый URL и имя переменной окружения с токеном.

Где токен

В конфиге MCP‑клиента. В shell‑окружении его нет, из shell до GoCD не достучаться.

В переменной GOCD_PAT. MCP‑сервер к сессии не подключён.

Что ещё доступно

Обычные shell‑инструменты.

Обычные shell‑инструменты: curl, jq.

Сессии стерильны в обе стороны. Арм A физически не может обойти сервер и сходить в API напрямую — токена в окружении нет. Арм B физически не видит инструментов MCP, а первая строка его скилла на всякий случай запрещает их явно.

Дайджест для арма B не написан руками, а собран из исходников документации — чтобы в него не просочились подсказки под конкретные задачи. Ровно те же грабли, о которые спотыкались мы, пока писали сервер, — семантика 202 Accepted, курсорная пагинация, проверка имени перед ETag — в дайджесте отсутствуют, потому что их нет в документации. Это осознанно: если арм B проиграет, отдельным вопросом будет, проиграл ли он из‑за механизма или из‑за знаний.

Оба подхода тестировались на модели Claude Fable 5 в стандартном Claude Code CLI. Каждая задача — в новой сессии без контекста. Это принципиально. Фиксированная стоимость «узнать, чем работать» входит в каждое измерение. Ровно столько стоит один реальный запрос оператора, который открыл сессию, задал вопрос и закрыл её. В сессии на двадцать задач фиксированная часть размажется, и вывод может оказаться противоположным, но в рамках этого исследования мы это не проверяли. Всё, что агент делает сверх основного канала — циклы со sleep, jq, повторное чтение скилла, попытки других маршрутов, — засчитывается как поведение.

Стенд

Всё живёт на нашем GoCD 25.4.0. Джобы выполняются на elastic‑агентах в Kubernetes: под создаётся под джобу и удаляется после неё. Важная деталь: старт пода добавляет 20–40 секунд к любому ожиданию, и этот шум одинаково бьёт по обоим армам. Профиль агента у всех фикстур закреплён в конфиге, а в единственной задаче, где агент создаёт пайплайны сам, он назван в промпте.

Фикстуры лежат в отдельной группе пайплайнов, всё, что создаёт агент, — в другой, тоже отдельной. За их пределами эксперимент не трогает ничего. Материал у всех синтетических пайплайнов — выделенный репозиторий, в который никто не коммитит во время кампании. Общий, живой репозиторий с auto_update запустил бы фикстуры посреди прогона — а auto_update у пайплайнов с общим материалом ещё и обязан совпадать, GoCD это проверяет.

Джобы только echo и sleep. Стенду не нужно ничего собирать, ему нужно быть предсказуемым.

Фикстуры создаёт скрипт до кампании, он же возвращает их в исходное состояние между повторами — там, где задача оставляет след.

Семь задач

Для тестирования были сформулированы семь промптов, которые покрывают наиболее частые паттерны использования GoCD. При этом набор задач отнюдь не претендует на всеобъемлющую полноту. Возможно, с другим набором промптов получился бы и другой результат. Зачёт по каждой задаче ставил не отчёт агента, а скрипт: после сессии он сверял состояние самого GoCD с критериями задачи и заодно фиксировал побочные эффекты.

T1 — запустить и отчитаться

Run the pipeline bench-build. When it has finished, report the run counter, the result of each stage, and how long the run took from scheduling to completion.

Проверяет запуск и его подтверждение, стратегию ожидания, чтение экземпляра. Ловушка: 202 Accepted — это не запуск, а «принято к рассмотрению»; доказательство — счётчик в отчёте.

T2 — цепочка с передачей значения

Run bench-build. When it has passed, take the image tag it built — the build stage's image job prints it as IMAGE_TAG=... — and run bench-deploy with its IMAGE_TAG variable set to that value. When the deploy has finished, report both run counters, the tag, and the deploy result.

Два ожидания, чтение консольного лога, передача данных между пайплайнами. Здесь та самая асимметрия: API принимает переменные при запуске, MCP‑сервер версии 1.2.0 — нет. Зачёт: у bench-deploy один новый экземпляр, в логе которого есть Deploying app-<счётчик bench-build>; лишних запусков нет. Если агент правил конфиг bench-deploy, это записывается как побочный эффект, а конфиг возвращается к фикстуре.

T3 — создать, запустить, одобрить, отчитаться, удалить

In pipeline group llm-bench-work, create two pipelines. wk-lib: material = the same git repository as bench-build (same auto_update setting); stages build then test, one job each, each job runs echo <stage name>. wk-app: depends on wk-lib's test stage (dependency material); stages build then deploy, one job each running echo <stage name>; deploy must require manual approval. All jobs use elastic agent profile k8s-dev; you may look at the existing pipeline GocdTestingDev as a reference for material and agent settings. Then run wk-lib; when it passes, wk-app should start on its own — wait for its build stage, then approve and run deploy, wait for it to finish, and report every run counter and stage result. Finally delete both pipelines and leave the group empty.

Самая длинная задача: создание пайплайнов под правила валидации GoCD, dependency‑материал, ручной запуск стадии, удаление в правильном порядке. Ловушек три: auto_update обязан совпадать с соседями по материалу; внутри approval GoCD требует явный блок authorization, даже пустой; удалить wk-lib, пока на него ссылается wk-app, нельзя.

T4 — разобрать падение

The last run of bench-flaky failed. Find out which job failed and why. Quote the error line from the log and say whether this looks like a code problem or an environment problem.

Только чтение, без ожидания: экземпляр → стадия → джоба → консольный лог. Отдельно интересно, сколько лога агент затащит в контекст — хвост или весь файл. Зачёт: в отчёте названа джоба db-migrate стадии integration и процитирована строка про Connection refused; мутаций нет никаких.

T5 — перезапустить одну стадию

The integration stage of the previous run of bench-flaky failed because the database was down. The database is back now. Re‑run just that stage — not the whole pipeline — and report the result of the stage and of whatever follows it.

Перед каждым повтором стенд сам выставляет DB_DOWN=true, запускает пайплайн, дожидается падения на integration, ставит DB_DOWN=false и только потом отдаёт промпт агенту. Проверяет операцию на уровне стадии существующего экземпляра, ожидание и отчёт о следующей стадии — package после успешного перезапуска стартует сам. Ловушка: перезапустить весь пайплайн — лёгкий неправильный ответ. Зачёт: счётчик пайплайна не изменился; у integration появился второй запуск, одобренный нашим пользователем и прошедший; package прошёл.

T6 — правка конфига под оптимистичной блокировкой

Add a pipeline‑level environment variable LOG_LEVEL with value debug to bench-deploy. Change nothing else, and confirm the variable is now in the configuration.

Прочитать конфиг вместе с ETag, записать с If-Match, проверить. Ловушки: PUT без If-Match отбивается с 412; объект с links внутри или с изменённым именем — с 422. Зачёт: конфиг равен фикстуре плюс ровно эта переменная (сравнение JSON без links и ETag); в журнале GoCD ровно одна запись конфига.

T7 — найти нужное за первой страницей истории

Looking at the run history of bench-history, find the most recent run that was started manually by a person rather than by the timer or a material change. Report its counter, who started it, and when.

Пагинация истории (курсор after; старый параметр offset эта версия GoCD молча игнорирует), интерпретация причины запуска, размер проекции — компактная история против сырых HAL‑страниц. Зачёт: счётчик, автор и время совпадают с записью стенда о его единственном ручном запуске.

Что записываем

Для каждой сессии снимаем из usage‑статистики количество затраченных токенов и итоговую сумму запроса в долларах, а также количество вызовов и длительность запроса (как время общения с API, так и общее время выполнения задания). Но по большому счету нас интересуют больше всего два параметра — общая стоимость и общая длительность выполнения задачи.

Что получилось

Каждую задачу я прогнал по 8–11 раз на каждом арме, всего 148 сессий. Сравниваю медианы и разбросы, а не средние: одно десятиминутное зависание превращает среднее в бессмыслицу. Значимость — тест Манна‑Уитни, порог 0.05. Задачи между собой не складываю: соотношение зависит от типа задачи, а любая сумма — от произвольной смеси.

Стоимость

Медиана стоимости одной сессии в долларах:

Задача

MCP

API

MCP / API

p

T1 запустить и отчитаться

0.79

0.59

1.34

0.0006

T2 цепочка с передачей значения

0.99

0.78

1.27

0.0002

T3 создать, запустить, удалить

1.18

0.98

1.20

0.002

T4 разобрать падение

0.43

0.50

0.86

0.0002

T5 перезапустить стадию

0.72

0.85

0.85

0.08

T6 правка конфига

0.39

0.49

0.80

0.003

T7 история за первой страницей

0.37

0.42

0.88

<0.0001

Картина раскололась ровно по типу задачи. Там, где агент ждёт пайплайн (T1–T3), MCP дороже на 20–34%. Там, где ожидания нет (T4, T6, T7), — дешевле на 12–20%. T5 повисла между: MCP дешевле на 15%, но разброс у API‑арма такой, что на одиннадцати прогонах значимости не набралось. Остальные шесть различий значимы с p не выше 0.003, поправка на число сравнений их не трогает.

Почему так. У MCP каждый вызов инструмента — это ход модели, а каждый ход — повторное чтение всего контекста из кэша. На задачах с ожиданием агент все же не запускает цикл со sleep, а опрашивает статус ход за ходом: 16 ходов против 7 в T1, 23 против 10 в T3, и cache read у MCP в 2–3 раза больше. API‑арм ждёт внутри shell‑цикла, ходов у него меньше, но каждый дороже: он пишет curl, jq и сами циклы. В T5 это 4600 output‑токенов против 2700. Два счётчика тянут в разные стороны, и на смеси задач почти гасят друг друга: набор из семи задач стоит $4.87 через MCP и $4.60 через API. Шесть процентов разницы.

Фиксированная стоимость входа оказалась не там, где я её ждал. На коротких задачах, где число ходов у армов почти одинаково, API‑арм пишет в кэш примерно на 3.8k токенов больше — это и есть цена дайджеста документации в контексте. Схемы 23 инструментов обошлись дешевле, потому что Claude Code не грузит их целиком, а подтягивает по мере обращения. В другом клиенте может быть наоборот.

Время

Общая длительность сессии от промпта до ответа, в минутах:

Задача

MCP медиана

MCP min‑max

API медиана

API min‑max

Зависаний MCP / API

T1

3.5

2.5–3.7

8.1

3.2–13.7

0 / 4

T2

5.2

3.9–7.7

12.2

3.6–15.7

0 / 8

T3

5.0

4.5–11.6

12.2

3.4–13.6

1 / 6

T4

0.9

0.7–1.9

1.0

0.7–3.9

0 / 0

T5

4.1

2.1–5.0

6.5

2.9–18.4

0 / 4

T6

1.2

0.7–3.4

1.0

0.8–3.2

0 / 0

T7

1.2

0.8–2.1

0.6

0.5–0.8

0 / 0

Здесь ничья заканчивается. Набор из семи задач оператор ждёт 21 минуту через MCP и 42 через API. И это не потому, что API медленнее. Чистый прогон API‑арма не медленнее MCP: в T1 медиана чистых прогонов — 3.3 минуты против 3.5 у MCP. Разница в том, что чистых прогонов у API‑арма меньше половины.

Посмотрите на последний столбец. Из 35 прогонов API на задачах с ожиданием 22 длились на 8–9 минут дольше, чем нужно. У MCP — один. Механизм во всех случаях один. Агент пишет shell‑цикл: раз в несколько секунд дёрнуть историю пайплайна, дождаться Passed. В цикле ошибка. Больше половины случаев — page_size меньше десяти, который история отвергает с 400. В дайджесте прямо написано «10–100»; агент это прочитал и проигнорировал. Реже — сломанный jq‑фильтр, кавычки, поле, которого нет в ответе. Цикл молча крутится, пока не упрётся в десятиминутный таймаут shell‑команды, и только тогда агент видит, что ничего не дождался, идёт проверять руками и обнаруживает, что пайплайн давно прошёл.

У MCP‑арма такого режима отказа нет физически. Каждый опрос — отдельный ход, ошибка видна на следующей же секунде, и агент её исправляет. За это он платит теми самыми лишними ходами из предыдущего раздела.

Обратите внимание: зависание бесплатно. Сессия просто ждёт, токены не тратятся, и по стоимости эти прогоны неотличимы от чистых. Если смотреть только на деньги, API‑арм выглядит дешёвым и аккуратным. Если рядом сидит человек, который ждёт ответ, — он ждёт то три минуты, то тринадцать, и заранее не знает, что выпадет. В T2, где ожиданий два, зависание случилось в восьми прогонах из одиннадцати.

Вне ожидания у армов паритет, с одним исключением в другую сторону. В T7 API‑арм быстрее вдвое: один запрос на всю историю — и ответ. MCP отдаёт историю страницами по десять, и агент листает её шестью ходами.

Стабильность и полнота

Все 148 сессий дали правильный ответ. Оба арма в T4 разглядели, что «авария» базы симулирована переменной, и оба в T5 перезапустили именно стадию, а не пайплайн. Отчёты MCP однороднее: таблица, одни и те же поля от прогона к прогону. У API‑арма в отчётах встречаются самоисправления в таймингах и абзацы «мой первый цикл опроса потерял десять минут». Содержательно они равны.

Побочные эффекты — по одному на арм. API‑арм один раз в T3 запустил созданные пайплайны дважды: первый раз их запустил auto_update, второй — агент. Ловушка из описания задачи сработала. MCP‑арм в T2 каждый раз правил конфиг bench-deploy, потому что trigger_pipeline в 1.2.0 не принимает переменные, и каждый раз честно об этом писал. Это не ошибка агента, это дыра в сервере, и в бэклоге она первая.

Честные оговорки

  • Сервер и бенчмарк писал один человек. Для объективности план зафиксирован до прогона, дайджест собран из документации.

  • Одна модель, один клиент. Ленивая подгрузка схем инструментов и таймаут shell‑команды — свойства конкретного клиента и его настроек. В другом харнессе фиксированная стоимость MCP может вырасти, а зависания — исчезнуть или стать длиннее.

  • Больше половины зависаний — из‑за одного факта про page_size, который в дайджесте есть. Экспертный скилл с этим и ещё десятком таких же фактов, скорее всего, уберёт большую часть провалов. Это третий арм, и его стоит прогнать, прежде чем говорить «дело в механизме». Но уже видно, в чём разница подходов: в чистом скилле модель менее строго следует рекомендациям дайджеста, чем в тулсете MCP.

  • T5 — единственная задача, по которой не удалось набрать статистически значимого результата. При таком разбросе для значимости нужно около двадцати прогонов на арм. Но в большей степени такой разброс связан именно с нестабильностью работы агента с API, когда он допускает ошибки в собственных скриптах обращения к GoCD.

Так стоит ли

Если решать по деньгам — нет. Разница в токенах на смеси задач — шесть процентов, и формула «разница на запрос × число запросов против стоимости разработки» не сойдётся никогда.

Если решать по времени — да. Запрос с ожиданием пайплайна через MCP стоит примерно на 20 центов дороже и возвращает ответ на 5–7 минут раньше, а главное — никогда не на десять минут позже. 20 центов за 5–7 минут — это $2–3 за час инженерного ожидания. Дешевле человеческого времени не бывает. Если запросы к GoCD прилетают каждый день и результата ждёт человек или следующий шаг пайплайна, сервер окупается за недели. Если агенты работают ночью батчем и запросы в основном короткие чтения — хватит дайджеста, хотя на коротких задачах MCP и так дешевле.

Сверх измеренного есть то, что бенчмарк не мерил, но что легло на ту же чашу — в пользу MCP: токен не лежит в shell‑окружении, мутации помечены и требуют подтверждения, есть журнал. Впрочем, безопасность — тема отдельного исследования, и я обещал её здесь не трогать.

Что мы сделаем с сервером по итогам: добавим переменные в trigger_pipeline, увеличим страницу истории и подумаем о блокирующем ожидании стадии.

А как вы решаете, использовать ли стандартный API сервиса или же пользоваться MCP? Особенно интересны случаи, где кто‑то проводил реальные замеры, а не «на глаз». Делитесь в комментариях.


Эта статья и другие мои материалы — в блоге на моём сайте: https://flexygrid.ru/. А быстрее всего анонсы появляются в телеграм‑канале «Инфраструктура без магии»: https://t.me/infra_bez_magii.

Комментарии (0)