Демо-скрипт против «памяти контракта»: как MCP-сервер ловит дрейф API

Зелёные мок-тесты и агенты в проде могут жить в разных вселенных: соло-разработчику, который крутит свой MCP-сервер и проверяет инструменты через narrated demo, важнее один честный live-запуск, чем десять изолированных unit-тестов. В мини-разборе автор MCP image server показывает, как demo.sh на первом же запуске вскрыл production-баг — дрейф параметра thinking_level, который моки не заметили.
MCP-сервер, модель и «дроссель» качества
Сервер по протоколу MCP оборачивает модель Google gemini-3.1-flash-lite-image: генерация и stateful-редактирование картинок, четыре инструмента для любого MCP-клиента. До вызова внешнего API аргументы валидируются локально; параметр thinking_level задуман как переключатель между latency и качеством.
В коде изначально допускались четыре уровня — minimal, low, medium, high — с дефолтом medium. Живой API для этой модели принимает только low и high. Хуже сбоя в демо оказалась другая ловушка: любой live-вызов без явного thinking_level уходил с дефолтом medium и получал гарантированный HTTP 400, пока локальная валидация спокойно одобряла «память» о контракте, которым владеет уже не репозиторий, а удалённый сервис.
Первый запуск narrated demo
Сценарий demo.sh — list tools, generate, stateful edit. Чтобы удешевить показ, автор в CLI запросил минимальный уровень:
cargo run --quiet -- generate "a tiny robot chef cooking ramen" 16:9 minimal
На шаге 2 первого запуска пришёл 400: значение minimal не поддерживается, разрешены low и high. Ошибки инструментов в этом сервере не бросают исключения наружу — возвращают читаемые строки с маркерами вроде 🔴, так что и человеку в терминале, и агенту видно текст вроде «Allowed values are: low, high» и можно перезапросить с корректным флагом.
Сервер до этого демонстрировали через агентов (в посте — Claude Code, Google ADK agent) и отдельный Rust CLI; в live чаще передавали high, поэтому сломанный дефолт и «фантомные» значения в allowlist маскировались.
Почему десять unit-тестов молчали
Весь suite — десять тестов, все с моком клиента (@patch("server._get_client")). Пример: test_generate_image_success гоняет thinking_level="medium" и проверяет, что сервер корректно форвардит аргумент в мок interactions.create — не то, что API вообще принимает medium.
Мок отрезает сеть и не способен поймать смену удалённого контракта. Два сочетавшихся фактора: allowlist как локальная копия remote-owned факта и то, что агенты в бою не трогали дефолт. Именно связка «MCP + агентский вызов инструментов + отдельный demo-скрипт» дала сигнал, который зелёная матрица моков не дала.
Root cause, фикс и привычки после инцидента
Автор формулирует корень так: allowlist дублировал устаревший контракт, а прежние live-вызовы с high скрывали последствия. Исправление — сузить allowlist до {low, high}, сменить дефолт на low, обновить сигнатуры инструментов, docstrings, README и сопутствующие материалы; добавить регрессию, где generate_image(..., thinking_level="medium") должен вернуть строку с Unsupported thinking level 'medium', а не успех.
После правок — live-проверка с чистыми дефолтами (успешное сохранение изображения) и пересборка с публикацией Docker-образа, потому что в опубликованном image ещё жил баг. По оценке автора, цепочка от падения demo до push заняла порядка десяти минут.
Выводы, которые переносятся в соло-workflow вокруг MCP: хотя бы один touch реального API, а не только моки; не хранить remote enum в голове репозитория; явно тестировать defaults; держать дешёвый E2E в виде demo-скрипта и режим вроде DEMO_FAST=1 при изменениях API-facing кода; smoke через /verify-stack — одно маленькое изображение с дефолтными параметрами. Ошибки, которые агент может прочитать и исправить запросом, здесь оказались не косметикой, а частью отладки.
Источники
- Пост-mortem на Dev.to (автор xbill, 15 июля 2026): Dev.to — дата доступа при обогащении: 2026-07-16T21:02:00Z.