dns-customer-mcp
# dns-customer-mcp
MCP-сервер для [DNS Shop](https://www.dns-shop.ru) (сторона покупателя): поиск, карточки товаров, отзывы, характеристики, каталоги, наличие по городу и список магазинов через [Model Context Protocol](https://modelcontextprotocol.io).
Репозиторий: `dns-customer-mcp`. Имя MCP-сервера и Docker-образа совпадает.
Работает в Cursor, Claude Desktop, VS Code и других MCP-клиентах.
## Зачем
У DNS нет публичного consumer API. Сайт защищён **Qrator** — анонимный `curl`/`requests` с датацентрового IP получает HTTP 401.
Этот сервер держит сессию anti-detect браузера [Camoufox](https://github.com/daijro/camoufox), один раз проходит JS-челлендж и дальше читает страницы через in-page `fetch` (часто с `X-Requested-With: XMLHttpRequest` → `{ html }`) или DOM-парсинг. Цены и наличие зависят от **города** сессии.
Архитектура: Node MCP (stdio) → Python Camoufox.
## Инструменты
| Tool | Назначение |
|---|---|
| `dns_search` | Поиск товаров |
| `dns_search_multi` | Пакетный поиск (1–10 запросов) |
| `dns_product_details` | Карточка (+ location) |
| `dns_products_batch` | Пакет 1–40 карточек |
| `dns_product_reviews` | Отзывы (`/product/opinion/`) |
| `dns_product_specs` | Характеристики |
| `dns_product_variants` | Конфигурации на карточке |
| `dns_product_availability` | Наличие по городу/магазинам |
| `dns_list_stores` | Магазины города |
| `dns_related_products` | Аналоги / аксессуары |
| `dns_compare_products` | Сравнение 2–8 товаров |
| `dns_category_browse` | Товары в категории |
| `dns_list_categories` | Живое дерево категорий |
| `dns_brand_catalog` | Витрина / поиск по бренду |
| `dns_search_filters` | Фильтры поиска |
| `dns_get_location` | Текущий город сессии |
| `dns_search_cities` | Поиск городов |
| `dns_set_city` | Сменить город |
Seller/offers маркетплейса нет — DNS один продавец. Q&A не реализован.
### Город
По умолчанию анонимная сессия ≈ **Москва** (cookie `current_path` / `city_path`).
```
dns_get_location
dns_search_cities({ query: "Каз" })
dns_set_city({ city: "Казань" })
dns_product_availability({ product: id })
```
### Идентификаторы
- **Товар:** 16-hex id (`b7a1667f9b19ed20`) или URL `https://www.dns-shop.ru/product/{id}/`
- **Категория:** slug из `dns_list_categories` — не выдумывать путь
## Быстрый старт
```bash
git clone <repo> dns-customer-mcp
cd dns-customer-mcp
docker build -t dns-customer-mcp:latest .
```
### MCP-конфиг (Docker)
Скопируйте [docs/mcp.example.json](docs/mcp.example.json) в конфиг клиента или используйте [.cursor/mcp.json](.cursor/mcp.json):
```json
{
"mcpServers": {
"dns-customer": {
"command": "docker",
"args": [
"run", "-i", "--rm", "--init",
"--stop-timeout=10",
"--shm-size=1g",
"--label", "dns-customer-mcp=1",
"dns-customer-mcp:latest"
]
}
}
}
```
`--shm-size=1g` обязателен для браузера. Контейнер живёт пока открыт MCP-сеанс (`--rm` убирает его после выхода).
### Локальный запуск (без Docker)
```bash
npm install
pip install -r python/requirements.txt
python -m camoufox fetch
npm start
```
Windows: задайте `DNS_PYTHON=python` при необходимости.
## Переменные окружения
| Var | Default | Purpose |
|---|---|---|
| `DNS_HEADLESS` | `true` | Camoufox headless |
| `DNS_GEOIP` | `false` | Camoufox geoip |
| `DNS_CHALLENGE_WAIT_MS` | `25000` | Ожидание Qrator |
| `DNS_NAV_TIMEOUT_MS` | `90000` | Таймаут навигации |
| `DNS_PACE_MS` | `600` | Пауза между запросами |
| `DNS_IDLE_TIMEOUT_MS` | `1800000` | Idle браузера (Python; выровнен с Node) |
| `DNS_EXIT_IDLE_MS` | `1800000` | Idle Node → exit (Docker `--rm`) |
| `DNS_PYTHON` | `python3` | Интерпретатор bridge |
| `DNS_WARMUP` | on | Background warmup (`0` = off) |
| `DNS_ORIGIN` | `https://www.dns-shop.ru` | Базовый URL сайта |
| `DNS_RESTAPI` | `https://restapi.dns-shop.ru` | REST API отзывов |
| `DNS_ALLOWED_HOSTS` | `www.dns-shop.ru,...` | Allowlist для fetch_url (SSRF) |
| `DNS_TOOL_TIMEOUT_MS` | `55000` | Таймаут MCP tool по умолчанию |
| `DNS_TOOL_CONCURRENCY` | `1` | Параллельные MCP handlers (очередь вне timeout) |
| `DNS_TOOL_SESSION_RETRIES` | `1` | Auto-retry SESSION_DROP + warmup |
| `DNS_RATE_LIMIT_PER_MIN` | `20` | Token bucket на tool calls (`0` = off) |
| `DNS_BRIDGE_RETRY_MAX` | `3` | Перезапуски Python bridge |
| `DNS_SEARCH_TIMEOUT_MS` | `180000` | Таймаут dns_search |
| `DNS_META_TTL_MS` | `300000` | TTL кэша product meta |
| `HTTP_PROXY` / `HTTPS_PROXY` | — | Прокси (RU IP при бане Qrator) |
## Разработка
```bash
npm test # node:test — парсеры + config allowlist
npm run test:parse # alias для npm test
npm run smoke:warmup # Camoufox + Qrator
npm run discover # снять XHR/DOM после warmup
npm run smoke # search → details → specs → reviews
npm run smoke:location # города (+ --set для смены)
```
Маршруты зафиксированы в [`src/dns/`](src/dns/) по live discovery (Aug 2026):
- Cookie `current_path`: `{ city: UUID, cityName }` (Москва = `30b7c1f3-03fb-11dc-95ee-00151716f9f5`)
- `/catalog/` + XHR → JSON `{ html, data, … }`
- `/search/` — in-page fetch (XHR часто 302); **цены** через `POST /ajax-state/product-buy/`
- Карточка товара — Vue SPA (hydrate wait) + meta/og fallback
- `REST_API_URL` из Pinia: `https://restapi.dns-shop.ru` (снаружи за Qrator)
- `/product/characteristics/{id}/` может 404 — specs с карточки после hydrate
### Парсер
- Плитка: точный класс `.catalog-product`
- Цена в SSR-HTML пустая — догрузка через AjaxState; в DOM — `.product-buy__price` (не рассрочка «от N ₽/мес»)
- При срыве селекторов — `parser_drift: true`, не `price: 0`
## Разработка
### Тесты и CI
```bash
npm test # Node unit tests (parse, errors, bridge protocol, config parity)
npm run test:python # pytest (config, locations, shared config parity)
npm run test:all # оба набора
npm run lint # ESLint (JS)
npm run lint:python # Ruff (Python) — pip install -r python/requirements-dev.txt
npm run build:config # проверка parity shared/config.json ↔ runtime
npm run build:selectors # генерация python/dns_selectors.py из src/selectors.js
```
GitHub Actions (`.github/workflows/test.yml`): `npm test`, `pytest`, `docker build` на каждый push/PR.
Smoke-тесты (живой Camoufox + сеть):
```bash
npm run smoke
npm run smoke:warmup
npm run smoke:location
npm run discover
```
### Discovery-скрипты
Одноразовые скрипты для reverse-engineering HTML/API DNS лежат в [`tools/discovery/`](tools/discovery/) (не входят в CI). Основные smoke/discover — в [`scripts/`](scripts/).
Протокол Node↔Python: [`docs/bridge-protocol.md`](docs/bridge-protocol.md).
### Переменные окружения (доп.)
| Переменная | По умолчанию | Назначение |
|---|---|---|
| `DNS_RATE_LIMIT_PER_MIN` | `20` | Token bucket на MCP tool calls (`0` = off) |
| `DNS_TOOL_CONCURRENCY` | `1` | Сериализация MCP handlers (таймаут не тикает в очереди) |
| `DNS_WARMUP` | on | Фоновый прогрев Camoufox при старте (`0` = off) |
## Ограничения
- Только публичный каталог (не корзина / профиль / заказы)
- Без CDP к локальному Chrome и без Parse/Apify
- При бане Qrator — RU residential proxy через `HTTPS_PROXY`
## Лицензия
MIT
TDQS
Scored across 18 tools
Most tools map cleanly to separate resources or actions—search, product details, specs, reviews, variants, availability, stores, categories, brands, and city state—and the descriptions call out intended use cases. A few pairs require careful reading, especially product_details vs products_batch and product_availability vs list_stores, but the boundaries are drawable.
All names share the dns_ prefix and snake_case, but the internal convention is mixed: verb-first names like list_stores and get_location coexist with noun-first names like product_details and category_browse, plus adjective-noun forms like related_products. There is also singular/plural inconsistency between product_details and products_batch, making the scheme readable but not uniform.
18 tools sits within the 16–25 range that starts to feel heavy for a single server, even though the domain is fairly broad. The count is somewhat justified by purpose-built batching tools like search_multi, products_batch, and compare_products, but several product-data tools could potentially be consolidated.
The read-only shopping surface is nearly comprehensive: city selection, search, category and brand browsing, product cards, specs, reviews, variants, availability, stores, related products, and comparison are all covered. The main gap is that dns_search_filters exposes filters but no tool clearly documents how to apply those filters back into a search or category query.