Реальный баг в Python MCP сервере: тесты проходили, а продакшен падал. Узнайте, как демо-скрипт за минуту нашёл проблему, и как её исправить.
Вы когда-нибудь запускали демо перед презентацией и получали HTTP 400 вместо работающего приложения? Я — да. В этой статье я расскажу, как мой демо-скрипт нашёл баг в продакшене за минуту, хотя 10 юнит-тестов были зелёными, а линтеры — чистыми. Вы узнаете, как тесты могут врать, а демо-скрипт — спасать.
Проект — небольшой MCP сервер на Python, который оборачивает модель Google gemini-3.1-flash-lite-image. Сервер предоставляет четыре инструмента для генерации и редактирования изображений. Любой агент, говорящий на MCP (Claude Code, Google ADK агент, Rust CLI), может их вызывать. Полный код — около 300 строк.
По всем обычным метрикам проект был здоров:
Затем я написал демо-скрипт. Он нашёл баг в продакшене меньше чем за минуту работы.
Демо-скрипт 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, а свою устаревшую копию.
Тесты мокали 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 дальше. Но передача невалидного значения — всё равно баг, просто невидимый из-за мока.
Два условия совпали, чтобы баг попал в продакшен:
SUPPORTED_THINKING_LEVELS — это копия факта, которым владеет только API. Копии устаревают.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). Ошибки, которые называют исправление, — половина отладки.
DEMO_FAST=1 ./demo.sh.Напишите демо-скрипт для вашего сервиса прямо сейчас. Пусть он вызывает реальное API с реальными данными. Запустите его. Если он упадёт — вы только что нашли баг, который не нашли тесты. Если нет — у вас есть уверенность, что ваш код работает не только в изоляции.
Хочешь закрепить знания на практике?
Решай задачи на Algolit — интерактивная платформа для обучения
Начать бесплатно →