Skip to main content
Glama
README.md
# dodo-mcp

MCP-сервер, который позволяет AI-агенту заказывать пиццу из чата — в Claude,
ChatGPT или любом клиенте с поддержкой MCP.

> **Дисклеймер.** Исследовательский концепт. Проект **не аффилирован** с Dodo Brands,
> Додо Пиццей и Drinkit, не одобрен и не поддерживается ими. Публичного клиентского
> ordering API у Додо нет — модель данных реконструирована наблюдением за собственным
> веб-заказом в браузере. Сервер работает **только с вашим аккаунтом на вашей машине**:
> никакие данные никуда не отправляются, чужие сессии нигде не хранятся.
> Используя проект, вы действуете на свой страх и риск и сами отвечаете за
> соблюдение условий использования сервисов Додо.

## Зачем это

Не «заказать через чат» — это и на сайте две минуты. Ценность в том, что агент
понимает **«как обычно, но большую»** и снимает боль многошаговой кастомизации:

```
▸ Закажи пепперони на тонком, добавь моцареллу
  Пепперони · 30 см, тонкое · + моцарелла — 4 600 ₸ (1214 ккал)

▸ Ту же, но большую, и без ананасов
  Пепперони · 35 см, тонкое · + моцарелла — 5 700 ₸
  ⚠️ нельзя убрать «ананасы»: их нет в составе. Убрать можно: пепперони из цыпленка

▸ Нас четверо, уложись в 12000
  Пепперони фреш 35 см + Ветчина и грибы 35 см — 8 740 ₸, ~950 ккал на человека
```

## Установка

```bash
git clone <repo> dodo-mcp && cd dodo-mcp
npm install
npm run build
npm run doctor      # проверит Node, Chrome, сборку и подскажет, чего не хватает
```

Нужны **Node.js 20+** и **Chrome**. Для доступа по сети — ещё `cloudflared`.

Посмотреть, как это работает, можно сразу и без всякой настройки:

```bash
npm run demo        # проигрывает сценарий на настоящем меню, заказов не создаёт
```

## Два режима

| Режим | Что делает |
|---|---|
| `mock` | Настоящее меню из репозитория, корзина в памяти, заказ имитируется. Безопасно для демо |
| `live` | Реальные вызовы Додо через ваш Chrome. Заказ настоящий |

Для `live` нужно один раз войти в аккаунт:

```bash
node scripts/live-check.mjs
```

Откроется отдельное окно Chrome с профилем `~/.dodo-mcp/chrome-profile` — введите
телефон и код из SMS. Автоматизировать вход нельзя: там Yandex SmartCaptcha, и
обходить её мы не собираемся. Дальше сессия живёт в профиле.

## Подключение

### Claude Code

В репозитории есть `.mcp.json` — просто запустите `claude` из этой папки.

### Claude Desktop

В `~/Library/Application Support/Claude/claude_desktop_config.json`:

```json
{
  "mcpServers": {
    "dodo": {
      "command": "/opt/homebrew/bin/node",
      "args": ["/полный/путь/к/dodo-mcp/dist/index.js"],
      "env": { "DODO_MCP_MODE": "live", "DODO_MCP_LOCALITY": "almaty",
               "DODO_MCP_ORIGIN": "https://dodopizza.kz" }
    }
  }
}
```

Путь к `node` — абсолютный: приложение запускает процесс без shell-окружения.

### ChatGPT и другие клиенты по сети

```bash
npm run serve:remote
```

Скрипт поднимет туннель и напечатает адрес вида
`https://xxx.trycloudflare.com/mcp`. В ChatGPT: **Настройки → Плагины →
Режим разработчика**, затем создать коннектор с этим адресом.

**Ноутбук должен быть включён**: Chrome с вашей сессией живёт локально, туннель
ведёт на него. Это не облачный сервис.

## Безопасность

Сервер даёт доступ к вашему аккаунту Додо — то есть к возможности потратить ваши
деньги. Поэтому защита многослойная.

**Сервер слушает только localhost.** Из локальной сети к нему не подключиться,
наружу ведёт исключительно туннель.

**OAuth 2.1 с PKCE** — способ по умолчанию (`DODO_MCP_AUTH=oauth`), проверен
с живым коннектором ChatGPT. Клиент регистрируется сам (RFC 7591), а доступ
подтверждаете вы: при запросе авторизации в терминале сервера печатается
шестизначный код, и без него токен не выдаётся.
У доступа есть срок жизни (час, с обновлением), его можно отозвать:

```bash
curl -X POST http://127.0.0.1:8787/oauth/revoke
```

**Статический токен** (`DODO_MCP_AUTH=token`) — запасной вариант для клиентов,
которые не умеют OAuth. Секрет кладётся прямо в адрес `/mcp/<токен>`. Работает,
но такой ключ вечен и утекает вместе со скриншотом — используйте, только если
иначе никак.

**Ограничители ущерба** — на случай, если доступ всё же утёк:

| Переменная | По умолчанию | Что делает |
|---|---|---|
| `DODO_MCP_MAX_ORDER` | 20 000 ₸ | потолок суммы одного заказа |
| `DODO_MCP_MAX_ORDERS_PER_DAY` | 5 | сколько заказов в сутки |

**Подтверждение человеком.** Оформление, отмена и отзыв требуют явного
согласия: агент обязан показать итог и дождаться «да».

**Логи** пишут каждое обращение с IP, включая отказы:

```
[dodo-mcp] ⛔ 401 POST /mcp от 203.0.113.77
[dodo-mcp] tools/call place_order → 200 (2426 мс) OAuth: ChatGPT от 74.161.200.110
```

## Инструменты

| Инструмент | Назначение |
|---|---|
| `suggest_addresses` | Варианты адреса по неточному запросу |
| `find_stores` | Пиццерия по адресу: часы, время доставки, самовывоз |
| `browse_menu` | Меню с осями кастомизации и фильтрами: калории, цена, без мяса, не острое |
| `plan_order` | Подбор набора: «на компанию из N», «в бюджет», «до N ккал на человека» |
| `compose_order` | Пожелания → валидная конфигурация: подбор SKU, проверка допустимости, цена |
| `estimate` | Итог: сумма, доставка, время, бонусы, калорийность |
| `place_order` | Оформление с оплатой при получении. Требует подтверждения |
| `order_status` | Активные заказы: стадия, время, курьер, оплата и сдача |
| `cancel_order` | Отмена, пока она доступна |
| `rate_order` | Оценка 1–5 и отзыв |
| `my_usual` | Прошлые заказы и «как обычно» |
| `clear_cart` | Очистить корзину |
| `auth_status` | Выполнен ли вход в аккаунт и где сейчас заказываем |
| `start_here` | Сценарии работы и текущее состояние — с этого начинает агент |
| `list_countries` | Страны Додо с числом пиццерий |
| `list_cities` | Города страны, где есть доставка |
| `set_location` | Переключить страну и город |

Инструменты спроектированы под **намерения агента**, а не как обёртки над
эндпоинтами: `compose_order` за один вызов делает то, на что в реальном API
уходит цепочка из четырёх шагов, и объясняет отказы вместо молчания.

## Города и страны

Сервер работает во всех странах, где есть Додо Пицца, — **27 стран, 1519
пиццерий**. Переключение делается из чата, перезапуск не нужен:

```
— Я на неделю в Варшаве, что там в меню?
→ set_location(city: "Warszawa", country: "pl")
  Теперь заказываем: Warszawa, Польша. Меню: 164 позиции, валюта zł, язык pl-PL.
```

Что при этом происходит само:

- **домен** — почти везде `dodopizza.<код страны>`, но четыре исключения
  (`dodopizza.com.tr`, `dodopizza.com.cy`, `dodopizza.co.id`, `dodo.mn`)
  зашиты в справочнике;
- **язык меню** — берётся со страницы самой страны (`link rel=alternate`):
  русский, если страна его поддерживает, иначе родной. Выдумывать коды культур
  нельзя — на неизвестной культуре меню отвечает 400;
- **валюта** — из отформатированных сумм живой корзины, а не из таблицы;
- **город** — из списка доставки этой страны. Если запрос подходит под
  несколько городов («моск» → Москва и Московский), сервер не выбирает
  молча, а спрашивает.

Стартовая локация задаётся в `.env` (`DODO_MCP_ORIGIN`, `DODO_MCP_LOCALITY`).
Меню в `fixtures/` снято для Алматы и используется только в `mock`.

Оговорка: фильтры «без мяса» и «не острое» опознают состав по словарю
ингредиентов. Он покрывает русский и основные латинские языки Додо, но не все
27 стран — вне русскоязычных стран сервер сам предупреждает, что проверять
состав надо глазами.

## Как это устроено

Подробная карта реального API — в [`docs/recon/endpoints.md`](docs/recon/endpoints.md).
Несколько находок, без которых ничего бы не работало:

**Тесто — не свойство пиццы, а отдельный товар.** «Пепперони 30 см тонкое» и
«Пепперони 30 см традиционная» — два разных SKU. Топинги — третья ось, у каждого
свой идентификатор и цена, зависящая от размера.

**Калории даны на 100 г, а не на порцию.** Пепперони 30 см — не 269 ккал,
а 269 × 580/100 = 1561 ккал. Сервер пересчитывает на порцию, иначе фильтр
«до 1000 ккал» пропускал бы целую большую пиццу.

**Корзина не работает без выбранного адреса**, а строка корзины адресуется
не идентификатором, а полной конфигурацией — у неё есть `cartLineId`, но
`DELETE` по нему отвечает 400.

**Позиция может вернуться с кодом 200 и не попасть в корзину** — так выглядит
стоп-лист. Поэтому сервер сверяет состав и предупреждает, а не рапортует об успехе.

**Сумма сдачи передаётся через комментарий к заказу** — отдельного поля у неё нет.

**Запросы идут изнутри страницы** dodopizza.kz, а не отдельным HTTP-клиентом:
сайт прогоняет antifraud-фингерпринт на каждом действии, и вызовы мимо браузера
этих сигналов не несут. Мы ничего не подделываем — это тот же контекст, что и
при ручном заказе.

## Ограничения

- **Только оплата при получении** — наличными или картой курьеру. Онлайн-оплата
  не поддерживается намеренно: там форма карты на стороне шлюза и 3DS от банка.
- **Один аккаунт на установку.** Сервер работает с вашей сессией; мультитенантности
  нет и не планируется — хранить чужие сессии Додо мы не будем.
- **Приватный API может измениться в любой день.** Это не публичный контракт,
  и никаких гарантий у него нет.
- **Чата с поддержкой нет**: на сайте сторонний Intercom, API у него своё.

## Разработка

```bash
npm run dev            # пересборка на лету
npm run demo           # демо-сценарий
node scripts/smoke.mjs      # прогон инструментов через настоящий MCP-клиент
node scripts/scenarios.mjs  # сценарии: компания, калории, диета
node scripts/oauth-test.mjs # полный цикл OAuth
```

Структура:

```
docs/recon/endpoints.md   карта реального ordering API
fixtures/                 меню Алматы для mock-режима
src/menu/                 загрузка меню, индекс, оси кастомизации
src/domain/               сборка и валидация заказа, подбор, память
src/backend/              mock / live, управление браузером
src/oauth.ts              OAuth 2.1 для remote-доступа
src/index.ts              stdio-транспорт
src/http.ts               HTTP-транспорт
```

## Лицензия

MIT