iikocloud-mcp
by UserVanya
README.md
# iikocloud-mcp
MCP-сервер поверх [`Iikocloud-manager`](https://github.com/UserVanya/Iikocloud-manager)
(`IikoCloudApiClientManager`): интроспекцией менеджера сервер отдаёт **236 методов iikoCloud
в 22 доменах как MCP-тулы**, но запускается с вариативно задаваемым подмножеством, а не
целиком. Мультиарендный: учётные данные iikoCloud передаёт клиент **через канал
транспорта** (HTTP-заголовки под TLS или переменные окружения для stdio) — секреты никогда
не попадают в аргументы тулов, а значит и в контекст модели или логи.
Прямой аналог [`iikoserver-mcp`](https://github.com/UserVanya/Iikoserver-mcp): тот же принцип,
та же модель безопасности, отличия — только там, где различаются сами API (auth v2 вместо
логина/пароля, асинхронные команды с опросом, кэш справочников).
- **Транспорты:** `stdio` и streamable-HTTP.
- **Отбор тулов:** по домену, по типу операции (read/write), по именам/glob, из разных
источников (CLI / env / YAML).
- **Безопасность по умолчанию:** read-only; запись — явным опт-ином, под подтверждением.
## Зачем подмножество, а не все 236 тулов
В контекст модели при каждом подключении идут и JSON-схемы тулов, и их описания — считать надо
обе части (здесь и далее вес в символах: у схем это практически байты, у русских описаний в
UTF-8 байт вдвое больше).
| Подмножество | Тулов | Схемы | Описания | Итого в контекст |
|---|---|---|---|---|
| все | 236 | ~356 КБ | ~83 КБ | **~439 КБ** |
| только read | 106 | ~95 КБ | ~29 КБ | **~124 КБ** |
| домен `invoice_processing` | 93 | ~114 КБ | ~23 КБ | ~137 КБ |
Сервер сам вырезает из схем служебные поля, которые ничего не дают модели (pydantic-`title`,
дублирующий имя свойства, и описания вида «Latitude.», буквально повторяющие имя поля): без
обрезки схемы весили бы ~433 КБ вместо ~356 КБ, то есть экономия около 18% (на описания обрезка
не влияет). Самый тяжёлый отдельный тул — `discounts__calculate_loyalty_checkin`, 18.9 КБ схемы.
Из 236 методов **106 — read, 130 — write**; ошибок схематизации при интроспекции — 0,
регистрируются все.
Отсюда практический совет: **выбирайте `--domains` под конкретную задачу клиента**, а не
поднимайте сервер со всем каталогом — это и экономит контекст модели, и сокращает
поверхность записи.
## Установка
```bash
# как пакет в активированное окружение: даёт команду iikocloud-mcp на PATH
uv pip install "iikocloud-mcp @ git+https://github.com/UserVanya/Iikocloud-mcp.git"
# или для разработки: команда доступна только как `uv run iikocloud-mcp`,
# на PATH сама по себе она не появляется
git clone https://github.com/UserVanya/Iikocloud-mcp.git && cd Iikocloud-mcp && uv sync
```
Требуется Python 3.12+ и креды iikoCloud auth v2 (api_key, app_id, client_secret).
Примеры ниже написаны для второго (dev) пути, поэтому идут с префиксом `uv run`. Если пакет
установлен первым способом и его окружение активировано, префикс не нужен.
## Быстрый старт
```bash
# stdio: клиент запускает сервер как подпроцесс, креды — через env
IIKOCLOUD_API_KEY=key IIKOCLOUD_APP_ID=app IIKOCLOUD_CLIENT_SECRET=secret \
uv run iikocloud-mcp --transport stdio --domains organizations,menu
# HTTP: сервер на VPS, только чтение по организациям и меню.
# Слушаем 127.0.0.1 — TLS терминирует reverse-proxy на этом же хосте.
uv run iikocloud-mcp --transport http --host 127.0.0.1 --port 8000 \
--domains organizations,menu,dictionaries,addresses
```
`--host 0.0.0.0` оправдан **только** если TLS-прокси работает на другом хосте: сам сервер
говорит по HTTP без шифрования, а в каждом запросе едут `X-Iikocloud-Api-Key`,
`X-Iikocloud-App-Id` и `X-Iikocloud-Client-Secret`. Открытый в интернет порт — это те же
креды открытым текстом (см. [Безопасность](#безопасность)).
Тот же результат — через конфиг-файл (см. [`server.example.yml`](server.example.yml)):
```bash
cp server.example.yml server.yml # server.yml в .gitignore
uv run iikocloud-mcp --config server.yml
```
## Отбор тулов
Тул включается, если: **домен разрешён** И **тип операции разрешён** И (нет `allow` ИЛИ
имя совпало с `allow`) И имя **не** совпало с `deny`. `deny` всегда побеждает `allow`.
Имена тулов — `<домен>__<метод>` (например `menu__get_nomenclature`,
`deliveries__create_delivery_order`). 22 домена: `addresses`, `banquets`,
`customer_categories`, `customers`, `deliveries`, `deliveries_retrieve`,
`delivery_restrictions`, `dictionaries`, `discounts`, `drafts`, `employees`,
`invoice_processing`, `marketing_sources`, `menu`, `messages`, `notifications`,
`operations`, `orders`, `organizations`, `report`, `terminal_groups`, `webhooks`.
| Способ | CLI | env | YAML |
|--------|-----|-----|------|
| Домены | `--domains a,b` | `IIKOCLOUD_MCP_DOMAINS=a,b` | `domains: [a, b]` |
| Разрешить запись | `--allow-write` | `IIKOCLOUD_MCP_ALLOW_WRITE=1` | `operations: [read, write]` |
| Allowlist имён/glob | `--allow 'get_*' --allow '*_report'` | `IIKOCLOUD_MCP_ALLOW=get_*,*_report` | `allow: ["get_*"]` |
| Denylist имён/glob | `--deny 'delete_*'` | `IIKOCLOUD_MCP_DENY=delete_*` | `deny: ["delete_*"]` |
| Выключить подтверждение записи | `--no-confirm-writes` | `IIKOCLOUD_MCP_CONFIRM_WRITES=0` | `confirm_writes: false` |
| Фолбэк без elicitation | `--write-fallback open` | `IIKOCLOUD_MCP_WRITE_FALLBACK=open` | `write_fallback: open` |
| Лимит JSON-ответа (символы) | `--max-output-chars N` | `IIKOCLOUD_MCP_MAX_OUTPUT_CHARS=N` | `max_output_chars: N` |
| Таймаут вызова, с | `--call-timeout N` | `IIKOCLOUD_MCP_CALL_TIMEOUT=N` | `call_timeout: N` |
| Потолок окна лимитера, с | `--max-rate-window N` | `IIKOCLOUD_MCP_MAX_RATE_WINDOW=N` | `max_rate_window: N` |
| TTL кэша справочников, с | `--cache-ttl N` | `IIKOCLOUD_MCP_CACHE_TTL=N` | `cache_ttl: N` |
| Потолок записей кэша | `--cache-max-entries N` | `IIKOCLOUD_MCP_CACHE_MAX_ENTRIES=N` | `cache_max_entries: N` |
| Хост / порт | `--host` / `--port` | `IIKOCLOUD_MCP_HOST` / `IIKOCLOUD_MCP_PORT` | `host:` / `port:` |
| Транспорт | `--transport {stdio,http}` | `IIKOCLOUD_MCP_TRANSPORT` | `transport:` |
**Приоритет источников: CLI > env > YAML-файл.** Источник, задавший поле, заменяет его
целиком (списки не мержаются). Путь к YAML — `--config server.yml` или
`IIKOCLOUD_MCP_CONFIG`. См. [`server.example.yml`](server.example.yml).
`--allow-write` — это только включение записи со стороны CLI: чтобы выключить её обратно,
просто не передавайте флаг. У env-переменной `IIKOCLOUD_MCP_ALLOW_WRITE` есть и
включающее, и выключающее значение (`1`/`0`, `true`/`false` и т. п.).
Примеры:
```bash
# всё чтение по доставкам, но без карт лояльности
uv run iikocloud-mcp --transport http --domains deliveries,deliveries_retrieve \
--deny '*loyalty*'
# запись включена, но без операций очистки
uv run iikocloud-mcp --transport http --allow-write --deny '*__clear_*'
```
Пустое значение — это осознанный ноль, а не «без ограничения»: `--domains ''` (и `domains: []`
в YAML) даёт сервер без единого тула, а не со всеми 236. Так значение из файла можно очистить
из CLI. Про непонятое имя домена и про нулевую регистрацию сервер пишет в лог предупреждение.
## Передача учётных данных
Секреты идут **только по каналу транспорта**, не как аргументы тулов — модель их не видит.
| | HTTP-заголовок | env для stdio |
|---|---|---|
| API-ключ | `X-Iikocloud-Api-Key` | `IIKOCLOUD_API_KEY` |
| App ID | `X-Iikocloud-App-Id` | `IIKOCLOUD_APP_ID` |
| Client secret | `X-Iikocloud-Client-Secret` | `IIKOCLOUD_CLIENT_SECRET` |
Фолбэка на `Authorization: Basic` нет: он вмещает два секрета, а iikoCloud требует три.
**Для HTTP-транспорта обязателен TLS** — терминируйте HTTPS на reverse-proxy перед сервером,
заголовки с кредами передавайте только под ним. Отсюда и дефолт `--host 127.0.0.1`: сам
сервер шифрования не делает, поэтому наружу он должен смотреть только через прокси.
**stdio** — переменные окружения подпроцесса (см. выше), сервер как локальный процесс
отдельного TLS не требует.
Один сервер обслуживает несколько аккаунтов iikoCloud: экземпляр менеджера кэшируется по
отпечатку `sha1(api_key:app_id:client_secret)` (`ApiCredentials.key_id`). Все три секрета
обязательны в отпечатке: если бы он строился только по `api_key` и `app_id`, вызывающий с
верным ключом и app_id, но чужим или неверным `client_secret`, получил бы доступ к кэшу и
уже авторизованной сессии другого арендатора.
## Подключение MCP-клиента
**stdio** (например, конфиг Claude Desktop):
```json
{
"mcpServers": {
"iikocloud": {
"command": "iikocloud-mcp",
"args": ["--transport", "stdio", "--domains", "organizations,menu,dictionaries"],
"env": {
"IIKOCLOUD_API_KEY": "key",
"IIKOCLOUD_APP_ID": "app",
"IIKOCLOUD_CLIENT_SECRET": "secret"
}
}
}
}
```
`command` должен быть исполняемым файлом: голое `iikocloud-mcp` работает, только если пакет
установлен через `uv pip install` в окружение, видимое клиенту. Для dev-установки укажите
`"command": "uv"` и `"args": ["run", "--directory", "/путь/к/Iikocloud-mcp", "iikocloud-mcp",
"--transport", "stdio", ...]`.
**Удалённый HTTP:**
```json
{
"mcpServers": {
"iikocloud": {
"url": "https://mcp.example.com/mcp",
"headers": {
"X-Iikocloud-Api-Key": "key",
"X-Iikocloud-App-Id": "app",
"X-Iikocloud-Client-Secret": "secret"
}
}
}
}
```
## Программный API
```python
from iikocloud_mcp import ServerConfig, ToolFilter, create_server
cfg = ServerConfig(
# host="0.0.0.0" — только если TLS терминирует прокси на другом хосте
transport="http", host="127.0.0.1", port=8000,
tool_filter=ToolFilter(domains={"menu", "dictionaries"}, operations=frozenset({"read"})),
)
server = create_server(cfg) # FastMCP с зарегистрированными тулами
server.run(transport="streamable-http")
```
## Подтверждение операций записи
Write-тулы по умолчанию **требуют подтверждения пользователя** перед мутацией — сервер
вызывает MCP-elicitation и выполняет метод только при явном `accept`. Это серверный гейт, а
не просто хинт клиенту (`destructiveHint`): даже клиент с авто-подтверждением тулов не
выполнит запись без ответа пользователя. Политику задаёт **оператор при запуске** (не LLM):
- по умолчанию — подтверждение включено, фолбэк `closed`;
- `--write-fallback open` — если клиент не умеет elicitation, выполнять запись без
подтверждения (оператор берёт риск на себя); по умолчанию (`closed`) такая запись
**блокируется**;
- `--no-confirm-writes` — полностью отключить гейт (для доверенной автоматизации).
## Встроенные подсказки
Сервер обогащает описание и результат каждого тула, не полагаясь на память модели.
**Асинхронные команды.** 42 тула из 236 возвращают только `correlationId` — это квитанция
о принятой команде, самого результата в ответе нет. Описание такого тула получает пометку:
> ⏳ Асинхронная команда: ответ содержит только correlationId, результата в нём нет. Чтобы
> узнать исход, вызовите `operations__wait_command` с этим correlationId и organizationId.
Ещё у 8 write-тулов ответ содержательный, и сервер ищет в нём поле `creationStatus`: если оно
равно `InProgress`, к JSON-результату добавляется ключ `_iikocloudMcpHint` с той же инструкцией —
опросить `operations__wait_command`. Реально сработать подсказка может у **4** из них — тех, что
возвращают `OrderInfo`-подобную структуру: `deliveries__create_delivery_order`,
`drafts__commit_delivery_draft`, `orders__create_table_order`, `banquets__create_reserve`.
У остальных четырёх поля `creationStatus` в ответе нет вовсе: `drafts__create_delivery_draft` и
`drafts__save_delivery_draft` возвращают `CreateOrSaveDraftResponse` (`correlationId`, `orderId`),
`employees__open_personal_session` и `employees__close_personal_session` —
`correlationId` и `error`. Проверка идёт по значению в ответе, а не по типу, поэтому лишнего
она не добавляет.
**«Где взять ID».** Схема параметров каждого тула сверяется с курируемой картой полей вида
`organizationId → organizations__get_organizations`,
`terminalGroupId → terminal_groups__get_terminal_groups`,
`productId → menu__get_nomenclature` и т. д. Совпавшие поля попадают в описание тула строкой
«Где взять ID: …», так что модель не пытается угадывать идентификаторы.
**Как часто можно звать.** Если у метода есть запись в лимитере менеджера, описание получает
приписку вида «Троттлинг MCP-сервера (не лимит iikoCloud): не чаще N запрос(ов) за M с —
кэшируйте результат в диалоге». Приписка намеренно говорит о **клиентском троттлинге этого
сервера** (значения лимитера менеджера, уже с потолком `--max-rate-window`), а не о лимите
самого API: настоящие лимиты iikoCloud бывают жёстче и описаны в докстрингах SDK, которые
сервер не трогает (например, `webhooks__update_webhook_settings` — примерно 1 обновление в час).
У 17 тулов записи в лимитере нет: это тулы-обёртки вроде
`customers__get_customer_by_phone`, который под капотом зовёт лимитируемый
`customers__get_customer_info`. Молчание модель прочла бы как «ограничений нет», поэтому такие
тулы получают общую приписку: своего троттлинга нет, но вызов расходует квоту нижележащего
метода. Ручной карты «обёртка → метод» нет намеренно — она протухала бы при каждом изменении
менеджера.
**Версии внешнего меню.** У `menu__get_external_menu_by_id` результат — объединение
`ExternalMenuV2 | ExternalMenuV3 | ExternalMenuV4`: форма ответа зависит от параметра
`version`. Описание тула вручную перечисляет все переименования полей между версиями и
рекомендует явно указывать `version=4`.
**Адрес доставки.** У `deliveries__create_delivery_order` описание отдельно поясняет: формат
адреса задаёт сама организация (`addressFormatType`, значение — из
`organizations__get_organization_settings`), улицу можно передать и `id`
(`addresses__get_streets_by_city`), и просто `name` вместе с `city`, а город указывать нужно
всегда — он определяет разбор остального адреса.
## Кэш справочников
15 из 22 тулов-источников идентификаторов (тех самых, куда отправляют подсказки «где взять
ID») допускают не чаще одного запроса в 60 секунд — а диалог с моделью легко делает
несколько похожих запросов подряд. Поэтому ответы примерно **27 справочных read-тулов**
(организации, домены справочников, адреса, меню, курьеры и т. п.) кэшируются в памяти
процесса с TTL.
- `--cache-ttl` — время жизни записи в секундах, по умолчанию `300`; `0` полностью
выключает кэш.
- `--cache-max-entries` — потолок числа записей (LRU-вытеснение), по умолчанию `256`;
обязателен, потому что ответ `menu__get_nomenclature` может весить мегабайты.
Ключ кэша учитывает аккаунт (по хэшу кредов, не сами секреты) и аргументы вызова — на
HTTP-транспорте один процесс безопасно обслуживает разных арендаторов. Инвалидация — только
по TTL.
## Лимит размера ответа и таймаут вызова
По умолчанию лимита на размер ответа нет. Если установить положительный
`--max-output-chars`, слишком длинный результат вернётся не оборванным текстом, а корректным
JSON-объектом:
```json
{
"truncated": true,
"totalChars": 250000,
"limitChars": 100000,
"contentPrefix": "..."
}
```
`contentPrefix` — начало полного JSON-результата, так модель может распознать усечение и
сузить запрос вместо того, чтобы принять обрезанный ответ за полный.
`--call-timeout` (по умолчанию 120 с) ограничивает время одного вызова тула — лимитер
менеджера иначе просто блокирует запрос без таймаута. При превышении сервер тоже возвращает
корректный JSON, а не обрыв соединения:
```json
{
"timeout": true,
"tool": "menu__get_nomenclature",
"timeoutSeconds": 120,
"reason": "Вызов не уложился в лимит времени. Обычная причина — rate limit метода: лимитер ждёт освобождения окна. Повторите позже или сузьте запрос."
}
```
Текст `reason` у write-тулов **другой**, и это принципиально: `asyncio.wait_for` отменяет только
ожидание внутри сервера, а запрос в iikoCloud уже ушёл и мог там выполниться — вместе с
настоящим `correlationId`, который при таймауте теряется. Поэтому write-тул на таймауте
сообщает, что операция могла пройти, что повтор создаст дубль (второй заказ, вторую бронь,
второе пополнение) и что проверять надо чтением — для доставок семейством
`deliveries_retrieve__*`.
`--max-rate-window` (по умолчанию 120 с) отдельно зажимает окна лимитера методов сверху: у
методов с окном шире потолка (например 1 запрос/1800 с) окно укорачивается до потолка при
сохранении числа запросов — иначе `--call-timeout` по умолчанию не успел бы дождаться
собственного окна лимитера.
## Тесты
```bash
uv run pytest -m unit -v # 196 тестов, быстро, без сети и кредов
uv run pytest -m integration -v # 4 read-only теста; без креда IIKOCLOUD_TEST_CONFIG — skip, не fail
```
Интеграционный набор (`tests/integration/`) гоняет реальные тулы через `_make_tool` против
живого iikoCloud: получение организаций, camelCase-алиасы в ответе, попадание в кэш при
повторном вызове, усечение по `max_output_chars`. Он только читающий — write-тестов против
живого API нет и не будет: мутационный путь проверяется моками в `tests/test_server.py`.
Креды берёт из YAML по пути `IIKOCLOUD_TEST_CONFIG` (секция `read`) — см.
[`config.test.example.yml`](config.test.example.yml) и [`.env.example`](.env.example).
Скопируйте оба в `config.test.yml` и `.env`: оба файла в `.gitignore`, секреты в репозиторий
не попадают. Без `IIKOCLOUD_TEST_CONFIG` в окружении фикстура `read_creds` делает
`pytest.skip`, а не падение, — так что набор безопасно запускать и на машине без кредов.
## Безопасность
- Секреты **не** попадают в аргументы тулов (модель их не видит), **не** логируются, живут
только в памяти на время сессии.
- Для HTTP обязателен TLS (reverse-proxy). Заголовки с кредами — только под HTTPS.
- Дефолт и все примеры — `host: 127.0.0.1`. `0.0.0.0` открывает нешифрованный порт с
кредами в заголовках; он оправдан только когда TLS-прокси стоит на другом хосте.
- Своих гейтов по организациям или app_id сервер не вводит — доступ ограничивают настройки
самого iikoCloud-аккаунта.
- Дефолт read-only: мутации требуют явного `--allow-write`.
- Дефолт: write-тулы требуют подтверждения пользователя (elicitation, см. выше).
## Документация дизайна
- Спецификация: [`docs/superpowers/specs/`](docs/superpowers/specs/)
- План реализации: [`docs/superpowers/plans/`](docs/superpowers/plans/)
This server cannot be deployed
Maintenance
ActivitySlowing
ResponsivenessNo issues