Combo Engine MCP Server
by 1artik1
README.md
# Combo Engine MCP Server
Расчёт выгодных комбо по сетям доставки Воронежа. Это MCP-сервер + библиотека
парсеров: подбирает набор еды под бюджет, сравнивает сети по выгодности,
следит за ценами и меню.
Все зависимости и браузер Playwright изолированы внутри `.venv\` — системное
окружение не затрагивается.
Смысл и сценарии каждой команды — в [COMMANDS.md](COMMANDS.md).
## Сети
7 служб доставки Воронежа (kind=combo): **Ла Пицца**, **Пицца Куба**,
**Ниндзя Фуд**, **Сушитайм**, **Суши Даром**, **Антисуши**, **Додо Пицца**.
Дополнительно подключены продуктовые магазины (kind=store): **Магнит**
(включён — поиск товаров/категории), **Пятёрочка** (выключена, анти-бот).
## Как это работает
- Сервер получает меню сетей (парсинг сайтов/API, с кэшированием и защитой
от сбоев — «stale-if-error»).
- По запросу строит **комбо** под бюджет: по умолчанию 3 варианта —
Оптимум (вес + вкус, 1 напиток) → Без повторов → Макс. вес.
- Поддерживает категории, промо-скидки, сравнение сетей, мониторинг цен и меню.
- Комбо/меню/сравнение работают только по сетям доставки; магазины — отдельные
store-тулы (поиск товаров и категории через нативный API).
## Структура
```
├── combo_mcp\ # пакет MCP-сервера
│ ├── server.py # точка входа (stdio)
│ ├── config.py # конфиг сетей (config\chains_config.json)
│ ├── cache.py # дисковый кэш + stale-if-error
│ ├── http_client.py # requests-обёртка (UA, ретраи, куки)
│ ├── playwright_client.py # ленивый браузер (фолбэк dodo)
│ ├── weights.py # справочник расчётных весов (config\estimated_weights.json)
│ ├── engines\ # dp.py (комбо), taste.py (вкусность), drinks.py (напитки)
│ ├── chains\ # 1 файл = 1 сеть (@chain("id"))
│ └── tools\ # 1 файл = 1 инструмент MCP
├── scripts\ # selftest.py, autotest.py, smoke_test.py, gen_expected.py
├── tests\expected.json # эталоны для autotest
├── config\chains_config.json
├── config\estimated_weights.json # веса позиций без веса на сайте (с источником)
├── cache\ # создаётся автоматически
├── COMMANDS.md # для чего нужна каждая команда (смысл и сценарии)
└── archive\ # старые файлы + референсы разведки (recon\)
```
## Запуск
```powershell
# MCP-сервер (регистрируется opencode через opencode.json)
.\.venv\Scripts\python.exe combo_mcp\server.py
# Selftest: все сети + эталоны Ла Пиццы (3000₽ → 4400/2950, 4850/3000, 5100/2800)
.\.venv\Scripts\python.exe scripts\selftest.py
# Автотесты: эталоны комбо + инварианты + контрольные блюда + health_check
.\.venv\Scripts\python.exe scripts\autotest.py
# Smoke-тест реального MCP-протокола (15 инструментов)
.\.venv\Scripts\python.exe scripts\smoke_test.py
# Очистка кэша
.\.venv\Scripts\python.exe scripts\clear_cache.py
```
## Инструменты MCP (15)
### Подбор и сравнение
- **`best_combo(chain_id, budget, variations=3, refresh=, categories=, promos=, sort_by=)`**
варианты комбо под бюджет.
- по умолчанию 3 вариации в порядке: **Оптимум** (максимум веса еды в бюджете
+ вкус, напиток оптимизируется внутри подбора) → **Без повторов** (макс. вес,
каждый продукт по 1 шт) → **Макс. вес** (чистый максимум веса);
при `variations>3` — ещё доп. стратегии
- 1 напиток в каждой вариации (если есть напитки с весом, влезающие в бюджет)
- `chain_id` — одна сеть (обычный режим), пустая строка («все сети») или список
через запятую («dodo, ninja_food») — сквозной топ-N: кандидаты сетей
сортируются метрикой и отбирается топ `variations` (`mode=all/multi`)
- `sort_by=` — `price_per_100g` (по умолчанию), `weight`, `price`
(только в cross-chain режиме)
- `categories=` — фильтр по категориям: `pizza/rolls/sushi/sets/combo/noodles/
snacks/desserts/drinks/sauces/other` + русские слова («пицца», «напитки»…).
`combo` — наборы/комбо-предложения, `sets` — чистые ролл-сеты; напитки — только
если группа `drinks` в списке
- `promos=` — применить промо-скидки: `order` / `pickup` / `all`
- `refresh=true` — реальный прогон парсера, иначе ответ по кэшу
- **`compare(budget, categories=)`** — лучшее комбо каждой сети на бюджет,
отсортировано по выгодности (₽/100г).
### Меню и контент
- **`parse_menu(chain_id, category=, min_weight=, sort_by=, limit=, refresh=)`**
меню сети с фильтрами и сортировкой (limit не более 500).
### Контроль цен и меню
- **`check_price(chain_id, item_name, expected_price=)`** — свежая проверка цены
позиции (реальный парсинг).
- **`diff_menu(chain_id)`** — изменения меню с прошлой загрузки.
- **`chain_info(chain_id, refresh=)`** — доставка, акции, лояльность сети.
### Состояние и качество данных
- **`list_chains(refresh=)`** — сети: id, название, город, `available`, описание.
- **`status()`** — конфиг, возраст кэша, ошибки.
- **`verify_chain(chain_id)`** — качество данных сети: веса, дубликаты, аномалии.
- **`check_config()`** — валидация `chains_config.json`.
- **`health_check(refresh=False)`** — HTTP + парсинг + кол-во позиций по всем сетям
(по кэшу или live).
### Служебные
- **`help(action=, command=)`** — справочник команд: список с пагинацией
(`/help next`, `/help back`), детали одной команды (`/help best_combo`).
- **`favorites(action=, chain_id=, label=, items=, query=)`** — избранное:
сохранить (`add`), показать (`list`), удалить (`remove`), очистить (`clear`).
Хранение: `cache/favorites.json`.
### Магазины (kind=store)
- **`store_search(query, store=, min_price=, max_price=, in_stock=, sort=, limit=)`** —
поиск товаров по магазинам (живой нативный API, не меню-кэш).
- **`store_categories(store=)`** — серверное дерево категорий магазинов.
Только `kind=store`; рестораны — ошибка. Подробности — в разделе «Магазины».
### Параметры и защита от дурака
Числовые параметры передаются **строками** и валидируются. Капы: `budget` ≤ 100000,
`variations` ≤ 50, `limit` ≤ 500 — иначе явная ошибка `{"error": ...}`.
Нераспознанные `categories`/`sort_by`/`chain_id` — ошибка с перечнем доступных
значений, а не молча пустой ответ. Мусор в числовых параметрах (`abc`, `0`,
отрицательные) не роняет сервер — возвращается корректная ошибка.
## Автотесты
`tests\expected.json` — фиксированные эталоны (комбо по сетям/бюджетам,
контрольные блюда). Без record-режима: `scripts\autotest.py` сверяет фактический
результат с эталоном, расхождение → FAIL + дифф. Эталон обновляется только
осознанным коммитом.
Сравнение комбо: **вариант 1 (оптимум) сверяется по точному составу**, а
**варианты 2+ — с допуском ±5% на вес и цену**: меню сетей регулярно меняются,
и состав «добивок» (соусы, маффины, имбирь, палочки) в вариантах 2+ легко
сдвигается без потери качества. Так автотест ловит реальные регрессии оптимума,
но не спамит ложными падениями из-за дрейфа меню в не-оптимальных вариантах.
## CI (GitHub Actions)
- `.github\workflows\ci.yml` — на каждый push: компиляция + быстрый smoke без сети
(сервер стартует, 15 инструментов зарегистрированы, JSON-ответы).
Запуск вручную: `scripts\ci_smoke.py`.
- `.github\workflows\nightly.yml` — ежедневно в 06:00 UTC: живой парсинг всех сетей
(`health_check refresh=true`) + полный автотест (30 блоков). Это мониторинг
парсеров: упавшая сеть или регрессия → красный статус + артефакт с отчётом
(`cache/`).
## Магазины (kind=store)
Магазины — отдельный мир: комбо/меню/сравнение (parse_menu/best_combo/compare)
для них не работают, их обслуживают два store-тула:
- **`store_search(query, store=, min_price=, max_price=, in_stock=, sort=, limit=)`** —
поиск товаров через нативный серверный поиск магазина (`parser.search`):
свежие цены и наличие (не меню-кэш). `store=""` — все включённые магазины,
или список через запятую. Матчинг — `combo_mcp/engines/textmatch.py`
(регистр/ё→е не важны, Левенштейн ≤1 для слов ≥5 симв., бонус за категорию).
Сортировка: `relevance` (default) | `price_asc` | `price_desc`. В ответе
`score`, `chain_id`, `stores_errors` (ошибка одного магазина не роняет ответ).
- **`store_categories(store=)`** — серверное дерево категорий магазина
(`parser.get_categories`), `store=""` — все включённые.
- **magnit** — включён. API `webgate.magnit.ru` (requests, без браузера):
категории `/webgate/v3/categories/store/992301`, товары/поиск
`POST /webgate/v2/goods/search` (цены — рубли, вес из weight/имени,
`weight_source=name`). TTL кэша меню — неделя (`menu_ttl_minutes: 10080`).
- **pyaterochka** — выключен (`enabled: false`). API `5d.5ka.ru` закрыт
анти-ботом (капча вне браузера); парсер написан на Playwright, но headless
стабильно получает капчу → явная ошибка «магазин отключён (enabled=false)».
- Для обоих магазинов работают `store_search`/`store_categories` (серверный
путь) и общие `status`/`health_check`/`check_config`. Рестораны
(`kind=combo`) в store-тулах — ошибка.
## Конфиг сетей
`config\chains_config.json` — per-chain: `url`, `enabled`, `ttl_minutes`, `headers`,
`cookies`. Смена URL/куки не требует правки кода.
## Добавление новой сети
1. Скопировать `combo_mcp\chains\_template.py` → `combo_mcp\chains\my_chain.py`
(или проще: `python scripts\new_chain.py my_chain "Моя Сеть" https://example.com`)
2. Реализовать метод `parse()` (возвращает список позиций:
`{name, weight_g, price_rub, is_from_price, description, category, product_url, in_stock, extra}`)
3. Заполнить `category_map` — маппинг категорий меню → группы комбо
(pizza/rolls/sushi/sets/noodles/snacks/desserts/drinks/sauces/other)
4. Указать `url` в `chains_config.json` (генератор делает это сам)
Больше ничего: парсеры регистрируются автоматически (pkgutil), метаданные сети
(id/name/city/url/description) берутся из класса парсера, категории — из его
`category_map`. По желанию: `parse_extra()` (доставка/акции), веса в
`config\estimated_weights.json`, переводы в `config\translations.json`.
## Добавление нового инструмента
Положить файл с функцией-хендлером в `combo_mcp\tools\` и зарегистрировать
декоратором (см. существующие инструменты) — `server.py` не трогается.
## Правила данных
- Вес позиции: с сайта/API (`weight_source` `site`), из названия размера
(`size_name`, pizza_kuba), из справочника `config\estimated_weights.json`
(`reference`, поле `source` — откуда взят вес) или отсутствует (`none`).
- Позиции без веса исключаются из расчёта комбо (мерч, палочки — намеренно).
- Недоступные сети честно помечаются в `list_chains`/`compare` с причиной.
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues