ГлавнаяБлогКак демо-скрипт нашёл баг в продакшене: история про MCP сервер
Python

Как демо-скрипт нашёл баг в продакшене: история про MCP сервер

Реальный баг в Python MCP сервере: тесты проходили, а продакшен падал. Узнайте, как демо-скрипт за минуту нашёл проблему, и как её исправить.

Al
Редакция Algolitalgolit.ru
10 мин чтения16 июля 2026 г.

Зачем читать эту статью?

Вы когда-нибудь запускали демо перед презентацией и получали HTTP 400 вместо работающего приложения? Я — да. В этой статье я расскажу, как мой демо-скрипт нашёл баг в продакшене за минуту, хотя 10 юнит-тестов были зелёными, а линтеры — чистыми. Вы узнаете, как тесты могут врать, а демо-скрипт — спасать.

Контекст: MCP сервер для генерации изображений

Проект — небольшой MCP сервер на Python, который оборачивает модель Google gemini-3.1-flash-lite-image. Сервер предоставляет четыре инструмента для генерации и редактирования изображений. Любой агент, говорящий на MCP (Claude Code, Google ADK агент, Rust CLI), может их вызывать. Полный код — около 300 строк.

По всем обычным метрикам проект был здоров:

  • 10/10 юнит-тестов проходят
  • ruff и mypy чисты
  • Успешно используется AI-агентами несколько дней
  • Опубликован как Docker-образ

Затем я написал демо-скрипт. Он нашёл баг в продакшене меньше чем за минуту работы.

Баг: невалидный thinking_level

Демо-скрипт demo.sh делает следующее: обнаруживает инструменты, генерирует изображение, затем редактирует его. Для экономии на втором шаге я указал минимальный уровень качества:

cargo run --quiet -- generate "a tiny robot chef cooking ramen" 16:9 minimal

Результат:

🔴 Image generation failed: Error code: 400 - {'error': {'message':
"'minimal' is not a supported thinking level for this model.
Allowed values are: low, high.", 'code': 'invalid_request'}}

Странно. Валидация сервера пропустила minimal. Вот код:

# server.py — как было в релизе
SUPPORTED_THINKING_LEVELS = {"minimal", "low", "medium", "high"}

@mcp.tool()
def generate_image(
    prompt: str,
    aspect_ratio: str = "1:1",
    thinking_level: str = "medium"
) -> str:
    ...

Допустимых значений четыре, но API принимает только low и high. А значение по умолчанию — medium, которое тоже невалидно. Каждый вызов, не переопределяющий thinking_level, гарантированно падал с HTTP 400.

Валидация проверяла не контракт API, а свою устаревшую копию.

Почему 10 зелёных тестов не заметили бага?

Тесты мокали Gemini клиент:

@patch("server._get_client")
def test_generate_image_success(self, mock_get_client):
    mock_client.interactions.create.return_value = mock_interaction
    result = generate_image(prompt="test", thinking_level="medium")
    self.assertIn("🟢 Image successfully saved!", result)

Мок возвращает успех для любого входа — даже для того, что реальное API отвергает. Тесты корректно проверяли, что сервер передаёт medium дальше. Но передача невалидного значения — всё равно баг, просто невидимый из-за мока.

Два условия совпали, чтобы баг попал в продакшен:

  1. Локальный список разрешённых значений дублировал контракт API. SUPPORTED_THINKING_LEVELS — это копия факта, которым владеет только API. Копии устаревают.
  2. Все предыдущие вызовы переопределяли значение по умолчанию. Агенты запрашивали high для качества, поэтому сломанный дефолт и два фантомных значения никогда не проверялись.

Исправление: две строки кода и регрессионный тест

-SUPPORTED_THINKING_LEVELS = {"minimal", "low", "medium", "high"}
+SUPPORTED_THINKING_LEVELS = {"low", "high"}

-    prompt: str, aspect_ratio: str = "1:1", thinking_level: str = "medium"
+    prompt: str, aspect_ratio: str = "1:1", thinking_level: str = "low"
# Новый регрессионный тест: API принимает только low/high для этой модели
# medium теперь должно отвергаться локально с читаемой ошибкой
result = generate_image(prompt="test", thinking_level="medium")
self.assertIn("Unsupported thinking level 'medium'", result)

Затем я обновил три сигнатуры инструментов, докстринги, get_help, все документы с неверными значениями. Пересобрал и запушил Docker-образ, который до этого распространял баг.

До и после

ДоПосле
Вызов с параметрами по умолчаниюHTTP 400 каждый раз🟢 изображение сохранено
thinking_level="minimal"/"medium"Одобрено локально, отвергнуто удалённоОтвергнуто локально с указанием разрешённых значений
Тесты10/10 зелёных (баг невидим)11 проверок, включая регрессионный тест контракта
Опубликованный образСодержал сломанный дефолтПересобран, запушен, проверен вживую

От первого отказа до фикса на Docker Hub прошло около десяти минут — потому что сообщение об ошибке содержало подсказку (Allowed values are: low, high). Ошибки, которые называют исправление, — половина отладки.

Что я выучил

  • Мокированные тесты проверяют ваш код, но не контракт. Они не могут проверить, что API принимает.
  • Держите один дешёвый живой smoke-тест в цикле. Мой теперь живёт в демо-скрипте: DEMO_FAST=1 ./demo.sh.
  • Тестируйте значения по умолчанию отдельно. Дефолты — это то, что никто не передаёт явно, поэтому их никто не тестирует. Там контракт дольше всего расходится.
  • Локальный список значений, которыми владеет удалённая система — бомба замедленного действия. Если вы предварительно валидируете для лучших ошибок агентам (это стоит того!), закрепите регрессионный тест на значения, которые вы наблюдали как отклонённые API.
  • Демо-скрипт — самый дешёвый end-to-end тест. Реальные креды, реальное API, реальный happy path — именно то, что моки не могут покрыть. Мой окупился на первом же запуске, до того как его увидела аудитория.

Практический вывод

Напишите демо-скрипт для вашего сервиса прямо сейчас. Пусть он вызывает реальное API с реальными данными. Запустите его. Если он упадёт — вы только что нашли баг, который не нашли тесты. Если нет — у вас есть уверенность, что ваш код работает не только в изоляции.

#MCP#отладка#тестирование#Python#баги
Al
Редакция Algolit

Пишем про алгоритмы, подготовку к собеседованиям и карьеру в IT — так, чтобы было понятно и полезно.

Хочешь закрепить знания на практике?

Решай задачи на Algolit — интерактивная платформа для обучения

Начать бесплатно →