Skip to main content
Glama
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/)

TDQS

B3/5.0

Scored across 106 tools

Disambiguation2/5

Many tools are duplicated with two arg styles (e.g., dictionaries__get_cancel_causes vs dictionaries__get_cancel_causes_by_organization), making it hard to pick the right one. Customer lookup tools also overlap heavily, and deliveries_retrieve has several similar retrieval variants. Descriptions are detailed, but boundaries between many tools remain unclear.

Naming Consistency4/5

Tools follow a consistent category__verb_noun snake_case pattern throughout (e.g., customers__get_customer_by_id, invoice_processing__list_inventory_writeoff_documents). Minor inconsistencies exist such as get_delivery_by_id vs get_deliveries_by_id and mixed singular/plural forms, but the overall convention is predictable and readable.

Tool Count1/5

106 tools is far beyond a manageable MCP surface, and many are thin wrappers or duplicate variants of the same operation. Even for a broad iikoCloud API, this is an extreme count that will overwhelm an agent and increase misselection.

Completeness2/5

The toolset is overwhelmingly read-only: there are no create/update/delete tools for customers, orders, deliveries, menu, or webhooks, despite including operations__wait_command that references write-tools. Gaps like send_sms (only checks) and no mutation endpoints mean agents cannot complete lifecycle workflows. The read side is broad, but the surface is incomplete for real operations.

Maintenance

ActivityInactive
ResponsivenessNo recent community issues