rules-mcp
by kopyshok
README.md
# rules-mcp — правила обмена 1С в чате
MCP-сервер, который разбирает боевые правила обмена 1С (Конвертация данных 2.0)
и отвечает на вопросы о них прямо в чате Open WebUI: как работает конкретный
обмен, что создаётся у приёмника, где расхождение с конфигурацией.
Правила обмена — это XML на десятки мегабайт со встроенным кодом обработчиков.
Прочитать их целиком нельзя, искать по ним вслепую — терять каскады и
протекающие ветки. Сервер разбирает их один раз в индекс и отвечает точечно.
Разбор — обычная программа, языковая модель в нём не участвует и токены на него
не тратятся. Токены уходят только на то, что инструмент вернул в чат.
* Поддерживается **КД 2.0** (`ExchangeRules.xml` + `RegistrationRules.xml`).
Конвертация данных 3 (EnterpriseData) — другой формат, здесь не поддерживается.
* Только стандартная библиотека Python для разбора, одна зависимость для MCP.
* Индекс живёт в памяти, базы данных нет. 36 МБ правил разбираются за секунду.
---
## Что умеет
| Инструмент | Отвечает на вопрос |
|---|---|
| `exchange_overview` | Какие есть направления, документы, процессы |
| `trace_document` | Что происходит с документом при обмене — от регистрации до приёмника; где стоит каждый вызов правила; что из этого при обмене через план не сработает |
| `get_conversion_rule` | Одно правило конвертации целиком: признаки, реквизиты, табличные части, каскады, замечания проверки |
| `search_rules` | Где вообще упоминается реквизит, алгоритм или фрагмент логики |
| `get_rule_handler` | Полный текст обработчика правила, реквизита, табличной части или общего события конвертации — постранично, с номерами строк |
| `get_rule_queries` | Запросы обработчика: пакет, параметры, временные таблицы |
| `check_rules` | Что в правилах сломано или при обмене через план не сработает — без конфигураций |
| `receiver_fields` | Что правила ждут от конфигурации приёмника — для сверки |
| `refresh_rules` | Забрать свежие правила из репозитория сейчас, не дожидаясь часа |
Инструменты намеренно общие, а не готовые отчёты: пользователь спрашивает как
угодно, а ответ складывает модель.
`search_rules` отвечает отдельными строками, и по одной строке нельзя судить о
том, при каких условиях ветка выгрузки выбирает правило конвертации. Поэтому у
каждого попадания есть номер строки, а рядом стоит `get_rule_handler`: он отдаёт
весь обработчик целиком, страницами, с теми же номерами строк. Если показано не
всё, в ответе будет «следующая строка» — молча обрезанного текста не бывает.
`trace_document` сразу говорит, где стоит вызов каждого правила конвертации:
обработчик и номер строки в той же нумерации. Закомментированные вызовы не
показываются; правило, заданное в свойствах ветки, а не вызовом в коде, так и
помечено. Для вызовов через общие алгоритмы — обе строки: где ветка зовёт
алгоритм и где в алгоритме стоит вызов. Рядом с процессами ветки — откуда
каждая метка: своя у ветки или пришла от вызываемого правила. Метка чужого
правила не значит, что ветка выгружает этот процесс.
Отбор по правилу в `search_rules` понимает код, наименование и имя объекта —
как `get_rule_handler`. Коды правил регистрации числовые, поэтому их удобнее
называть именем документа; если правил у документа несколько, ищется по всем.
`get_rule_queries` делает механическую часть, в которой легко ошибиться руками:
делит пакет на отдельные запросы, сопоставляет `Результат[n]` с номером запроса в
пакете, показывает, где создаётся каждая временная таблица и от каких она зависит.
Текст, собранный не из строкового литерала, а выражением, полным не выдаётся —
такой запрос помечается неполным. Вычисляемый номер элемента пакета не
угадывается, а выносится отдельно как неопределённый.
Инструменты видят не только правила, но и **общие алгоритмы** с **именованными
запросами**: ветка выгрузки часто зовёт `Выполнить(Алгоритмы.Имя)`, а сам вызов
правила конвертации стоит внутри алгоритма. В правилах УПП → ЕРП так спрятаны
34 вызова, и без обхода алгоритмов часть документов приёмника просто не видна.
И ещё **обработчики отдельных реквизитов** правил конвертации. Там лежит условие
заполнения реквизита — не то же самое, что условие выбора правила: признак может
влиять на то, чем заполнится склад, и никак не влиять на то, какое правило
сработает. Таких обработчиков по корпусу 2115, они есть у 489 правил конвертации.
Карточка правила (`get_conversion_rule`) показывает у реквизита с обработчиком его
код, перечень обработчиков в `get_rule_handler` перечисляет такие реквизиты, а
прочитать обработчик можно тем же `get_rule_handler` с параметром `prop` — кодом
реквизита или именем приёмника. Имя приёмника внутри правила бывает не
единственным (в шапке и в табличной части), тогда в ответе перечень — выберите код.
`search_rules` ищет и по ним; у такого попадания есть поля «реквизит» и «код
реквизита».
### Проверка правил и поведение БСП
`check_rules` находит в правилах то, что видно без конфигураций и баз:
- **ошибки**: обращение к алгоритму, которого нет, или к алгоритму другого
режима (выгрузки вместо загрузки); ссылку на несуществующее правило
конвертации; повтор кода;
- **ветки, которые при обмене через план работают не так, как выглядят**: вторая
ветка на тот же тип (не исполняется — БСП берёт первую), произвольный отбор,
выборка и «Отказ» в «перед обработкой правила» (не исполняются), ветка на
объект, который не регистрируется к обмену;
- **правила конвертации**, на которые никто не ссылается, и объекты, которые
уходят только ссылкой (в приёмник попадают лишь поля поиска);
- **формы поиска в приёмнике**, дающие дубли или слияние объектов;
- **запись объекта в «после загрузки»** — проведение дважды.
У каждого замечания — адрес для `get_rule_handler` (правило, обработчик, строка,
код реквизита) и чем оно грозит. Замечание в алгоритме, который никто не
вызывает, понижено: на обмен оно не влияет.
Проверки перенесены из [kd2-rules-mcp](https://github.com/egordoronchenko/kd2-rules-mcp)
(Егор Доронченко, MIT) — те, которым не нужны структуры конфигураций. Там же —
обоснования строками кода БСП 3.1.12. Утверждения о ветках выгрузки мы
дополнительно сверили с кодом БСП 2.3. Где наша реализация отличается, она
точнее на этом корпусе: закомментированный вызов и ссылка из отключённого
реквизита вызовом не считаются, явная выгрузка из обработчика реквизита — это
выгрузка объекта целиком. Справочник поведения БСП для модели —
[BSP_BEHAVIOR.md](BSP_BEHAVIOR.md).
Из той же работы — что разбор видит весь код правил: обработчик поиска,
обработчики реквизитов «при выгрузке» и «после выгрузки», обработчики табличных
частей и общие события конвертации (в УПП → ЕРП одно из них — 9 167 строк).
До версии 3.0.0 они в индекс не попадали.
### Ради чего всё затевалось
Правила целиком состоят из ссылок на реквизиты и типы **двух** конфигураций.
Проверить их в одиночку нельзя. Но если рядом подключён сервер, читающий
конфигурации 1С (например [rlm-tools-bsl](https://github.com/Dach-Coin/rlm-tools-bsl)),
появляется проверка, которую иначе делают руками:
```
1. rules-mcp → «правила пишут вот в эти реквизиты приёмника, вот их типы»
2. сервер конфигураций → «а вот что реально есть у этого объекта»
3. модель → сравнивает и показывает расхождения
```
Реквизита нет, он переименован или сменил тип — обмен на этом не падает, он
молча не переносит данные. Это самая дорогая и самая незаметная поломка обмена.
Серверы друг друга не знают и ничего друг о друге не предполагают —
разворачиваются и чинятся независимо. Связывает их модель: когда и как идти во
второй сервер, написано в [PROMPT.md](PROMPT.md) и в разделе «Сверка с
конфигурацией» [KNOWLEDGE.md](KNOWLEDGE.md).
---
## Как должны лежать правила
Папка на направление обмена, внутри выгрузка из Конвертации данных:
```
<корень или подкаталог репозитория>/
├── УПП_ERP/
│ ├── ExchangeRules.xml правила выгрузки (ПВД) и конвертации (ПКО)
│ ├── RegistrationRules.xml правила регистрации (ПРО)
│ └── CorrespondentExchangeRules.xml не читается
├── ERP_УПП/
│ └── …
└── Розница_ERP/
└── …
```
Направление определяется по файлу `ExchangeRules.xml`; папка без него молча
пропускается. Имена папок могут быть на кириллице или латиницей — стороны
обмена берутся из самих правил, а не из имени папки, поэтому «УПП → ЕРП»,
«упп erp», «из розницы в ерп» и «УПП_ERP» распознаются одинаково.
**Порядок сторон значим:** `УПП_ERP` и `ERP_УПП` — разные направления с разными
правилами, и сервер их не путает.
## Как выкладывать новые правила
Сервис читает **одну ветку** репозитория — заданную в `GITLAB_BRANCH` — и в ней
**одну папку**, заданную в `RULES_SUBDIR`. Чего нет в этой ветке и этой папке,
для сервиса не существует.
### Новое направление
1. Выгрузить из Конвертации данных `ExchangeRules.xml` и `RegistrationRules.xml`.
2. Положить в новую папку **прямо в каталоге правил**, без вложенности:
`src/rules/Розница_ERP/ExchangeRules.xml`. Папку глубже сервис не увидит.
3. Влить в боевую ветку.
4. Подождать до часа или попросить в чате «обнови правила».
### Обновление существующего направления
Заменить файлы **в той же папке**. Не заводить рядом `УПП_ERP_новые` или
`УПП_ERP_v2`: на две папки с одним направлением сервис отвечает «направление
понято неоднозначно», а старая так и остаётся в ответах.
### Ветки в git
* Рабочую ветку под новые правила заводить можно — сервис её не увидит, пока
изменения не влиты в боевую. Это и есть защита от черновиков в чате.
* Переключить сервис на другую ветку — поменять `GITLAB_BRANCH` в `.env` и
пересоздать контейнер: `docker compose up -d rules-mcp`. Клон удалять не надо.
* Смотреть в чате и боевые правила, и черновые — второй экземпляр сервиса: своя
ветка, свой том, своё подключение и отдельная модель в Open WebUI. Один
экземпляр на две ветки не настраивается.
### Проверить, что подхватилось
Спросить в чате «какие есть направления обмена». Новое направление в списке,
в «версии правил» — нужная ветка и последний коммит из GitLab, поля «внимание»
нет.
| Что видно | Причина |
|---|---|
| Коммит последний, а направления нет | Папка не в каталоге правил или вложена глубже; файл назван не `ExchangeRules.xml` — регистр букв важен |
| Коммит старый, «внимания» нет | Правила лежат в другой ветке и не влиты в боевую; либо не прошёл час |
| «внимание: не прочитаны: <папка> — …» | Файл битый или пересохранён в другой кодировке. По этой папке отвечают прежние правила, остальные обновились |
| «направление понято неоднозначно» | Две папки с одним и тем же направлением |
| «без версии (каталог не под git)» | Сервис не подключён к репозиторию: не задан `GITLAB_REPO` |
---
## Установка
### Вариант 1. Docker рядом с Open WebUI (рекомендуется)
Добавьте в ваш `docker-compose.yml` (или возьмите готовый фрагмент из
[docker-compose.yml](docker-compose.yml)):
```yaml
services:
rules-mcp:
build: ./rules-mcp # или image: ghcr.io/…, если соберёте свой
environment: # значения — в .env, здесь не править
GITLAB_REPO: ${GITLAB_REPO:-}
GITLAB_TOKEN: ${GITLAB_TOKEN:-}
GITLAB_BRANCH: ${GITLAB_BRANCH:-}
RULES_SUBDIR: ${RULES_SUBDIR:-}
REFRESH_INTERVAL: ${REFRESH_INTERVAL:-3600}
volumes:
- rules-clone:/data # клон переживает перезапуск контейнера
restart: unless-stopped
networks: [ai]
open-webui:
# ваш существующий сервис; нужно добавить только это
environment:
WEBUI_SECRET_KEY: ${WEBUI_SECRET_KEY}
networks: [ai]
volumes:
rules-clone:
networks:
ai:
```
```bash
git clone https://github.com/kopyshok/rules-mcp.git
cat rules-mcp/.env.example >> .env # дописать к своему .env; заполнить адрес, токен, ветку
docker compose up -d --build rules-mcp
docker compose logs -f rules-mcp
```
Секции `ports` намеренно нет: наружу сервис публиковать не надо, Open WebUI
дотянется до него по внутренней сети. Это же снимает вопрос доступа извне.
### Вариант 2. Без Docker
```bash
git clone https://github.com/kopyshok/rules-mcp.git && cd rules-mcp
python -m venv .venv && . .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -r requirements.txt
export RULES_DIR=/путь/к/каталогу/с/направлениями
export REFRESH_INTERVAL=0 # работать с тем, что уже на диске
python server.py
```
Нужен Python 3.10+ и установленный `git`, если правила берутся из репозитория.
---
## Настройки
Все настройки — через переменные окружения. Образец: [.env.example](.env.example).
| Переменная | По умолчанию | Смысл |
|---|---|---|
| `GITLAB_REPO` | пусто | Адрес репозитория с правилами по HTTPS. Пусто — работать с тем, что уже лежит в `RULES_DIR` |
| `GITLAB_TOKEN` | пусто | Токен на чтение репозитория |
| `GITLAB_BRANCH` | пусто | Ветка с правилами. Пусто — ветка по умолчанию |
| `RULES_SUBDIR` | пусто | Подкаталог внутри репозитория, если папки направлений лежат не в корне |
| `REFRESH_INTERVAL` | `3600` | Период опроса, секунды. `0` — не опрашивать |
| `RULES_DIR` | `/data/rules` | Каталог с направлениями. При работе из репозитория клон кладётся в `/data/checkout` |
| `HOST` / `PORT` | `0.0.0.0` / `8000` | Адрес прослушивания |
Несмотря на имена, `GITLAB_*` работают с любым git-сервером — GitHub, Gitea,
Bitbucket. Имена оставлены такими, потому что писалось под GitLab.
---
## Подключение к GitLab
### 1. Токен на чтение
Лучше **deploy-токен**: он привязан к репозиторию, а не к человеку, и не
отвалится, когда сотрудник уйдёт.
**Settings → Repository → Deploy tokens → Add token**
* Name: `rules-mcp`
* Expires: можно оставить пустым
* Scopes: **только `read_repository`**
GitLab покажет имя пользователя и сам токен **один раз**. Сервису нужен только
токен — имя пользователя он подставляет сам.
Личный токен (Settings → Access Tokens, область `read_repository`) тоже
подойдёт, если deploy-токены запрещены политикой.
### 2. Адрес репозитория
Только HTTPS. SSH не поддерживается — в контейнере нет ключей и известных
хостов, и заводить их ради чтения одного репозитория не стоит.
```
GITLAB_REPO=https://gitlab.example.ru/exchange/rules.git
GITLAB_TOKEN=glpat-xxxxxxxxxxxxxxxxxxxx
```
Токен **не надо** вписывать в адрес — сервис подставит его сам при обращении.
Так он не попадает ни в логи, ни в вывод команд.
Если GitLab на нестандартном порту, укажите его в адресе как обычно:
`https://gitlab.example.ru:8443/exchange/rules.git`.
### 3. Ветка с правилами
```
GITLAB_BRANCH=main
```
**Указывайте ветку явно.** Если оставить пусто, берётся ветка по умолчанию — а
она в репозиториях с правилами часто оказывается черновой. Аналитики будут
читать не то, что стоит на продуктиве, и не узнают об этом.
Заведите отдельную ветку под боевые правила (`prod`, `production`, `release`) и
укажите её. Сервис работает ровно с ней: `git fetch` и `git reset` идут по
указанной ветке, изменения в других ветках его не трогают.
Ветка видна в ответе каждого инструмента — поле «версия правил»:
```
ветка prod, коммит d54877b от 08.09.2026
```
Так человек в чате всегда видит, на какой версии правил построен ответ.
### 4. Правила лежат не в корне
```
RULES_SUBDIR=exchange/rules
```
Клонируется репозиторий целиком, но папки направлений ищутся в этом
подкаталоге. Путь указывается от корня репозитория, через `/`.
### 5. Проверка
```bash
docker compose logs rules-mcp # клонирование и старт
```
Если токена не хватает или ветки нет, сервис не падает: он продолжает отвечать
на прежнем индексе, а в каждый ответ добавляется поле «внимание» с текстом
ошибки. Это видно прямо в чате — молчаливой рассинхронизации не будет.
---
## Подключение к Open WebUI
Родная поддержка MCP появилась в Open WebUI **0.6.31**. На версиях старее
понадобится обновление либо прослойка [mcpo](https://github.com/open-webui/mcpo).
**Настройки → Админ → Интеграции → Внешние серверы инструментов → Добавить:**
| Поле | Значение |
|---|---|
| Тип | **MCP (Streamable HTTP)** |
| Адрес | `http://rules-mcp:8000/mcp` |
| Авторизация | **None** |
Три места, где спотыкаются:
1. **Тип должен быть MCP, не OpenAPI.** Если вписать MCP-настройки в
подключение типа OpenAPI, интерфейс уходит в бесконечную загрузку. Лечится
удалением подключения и повторным добавлением.
2. **Адрес — имя контейнера, а не `localhost`.** Глобальные подключения
выполняются самим Open WebUI, и `localhost` внутри его контейнера означает
его же, а не сервер.
3. **Авторизация именно None.** Выбранный «Bearer» с пустым ключом шлёт пустой
заголовок, и соединение рвётся.
В Open WebUI должен быть задан `WEBUI_SECRET_KEY`, иначе подключения слетают
при каждом пересоздании контейнера.
### Чтобы люди не включали инструмент вручную
Глобальный инструмент в чате сам не появляется — каждому пришлось бы включать
его в меню интеграций. Вместо этого соберите модель:
**Рабочая область → Модели → Создать**
* базовая модель — ваша обычная;
* системная подсказка — готовый текст в [PROMPT.md](PROMPT.md);
* знания — [KNOWLEDGE.md](KNOWLEDGE.md) (инструменты, порядок вызова, разобранный
пример) и [BSP_BEHAVIOR.md](BSP_BEHAVIOR.md) (как БСП исполняет правила), плюс
методичка и карта процессов, если они у вас есть;
* инструменты — `rules-mcp` и сервер конфигураций, включены принудительно;
* доступ — нужной группе.
Пользователь выбирает модель из списка, и у него уже всё настроено.
---
## Обновление правил
Три пути, все работают вместе:
* **Автоматически** раз в `REFRESH_INTERVAL` секунд. Если коммит не сменился,
индекс не пересобирается — проверка стоит несколько килобайт трафика.
* **Из чата** — попросить «обнови правила», модель вызовет `refresh_rules`. Нужен,
когда только что выложили релиз и ждать час не хочется. Пароля нет: если коммит
не сменился, вызов ничего не пересобирает, поэтому лишний вызов безвреден.
* **Перезапуском контейнера** — индекс собирается заново при старте.
Индекс подменяется целиком и только после того, как новый успешно собран. Пока
он строится, сервер продолжает отвечать на старом. Если забрать правила не
удалось, старый индекс остаётся в работе.
Если не читается одна папка (битый XML, не та кодировка), остальные
направления всё равно обновляются. По битой остаётся прежняя версия, а в поле
«внимание» указано, какая папка и что с ней.
---
## Если что-то не работает
| Симптом | Причина |
|---|---|
| Open WebUI не видит сервер | Адрес `localhost` вместо имени контейнера; контейнеры в разных сетях |
| Интерфейс виснет при добавлении | Выбран тип OpenAPI вместо MCP |
| Соединение сразу рвётся | Авторизация «Bearer» с пустым ключом; нужна None |
| Подключения слетают после пересоздания | Не задан `WEBUI_SECRET_KEY` в Open WebUI |
| «направлений: 0» | Не тот `RULES_SUBDIR`, не та ветка, или в папках нет `ExchangeRules.xml` |
| В ответах поле «внимание» | Не проходит обновление: протух токен, нет ветки, недоступна сеть — или не читается папка направления. Текст ошибки там же |
| «версия правил: без версии (каталог не под git)» | Не задан `GITLAB_REPO`: сервис читает папку на диске и из репозитория не обновляется |
| Модель не зовёт инструменты | Вопрос к модели, а не к серверу. Проверьте на модели с надёжным вызовом инструментов |
Посмотреть, что сервер вообще отдаёт:
```bash
docker compose exec rules-mcp python -c "
import asyncio
from fastmcp import Client
async def m():
async with Client('http://127.0.0.1:8000/mcp') as c:
for t in await c.list_tools(): print(t.name)
print((await c.call_tool('exchange_overview', {})).data)
asyncio.run(m())"
```
---
## Как устроено внутри
* [rules_index.py](rules_index.py) — разбор XML и индекс. Только стандартная
библиотека. Правила регистрации, ветки выгрузки, правила конвертации со всеми
свойствами и табличными частями, каскады в подчинённые правила, функциональные
блоки (процессы), которыми разработчики помечают ветки, общие алгоритмы и
именованные запросы.
* [query_extract.py](query_extract.py) — разбор запросов в коде обработчиков:
многострочные литералы 1С, деление пакета, параметры, временные таблицы и их
зависимости. Тоже только стандартная библиотека, от индекса не зависит.
* [checks.py](checks.py) — проверки правил для `check_rules`. Только стандартная
библиотека, работает поверх индекса; замечания считаются один раз на версию
правил.
* [server.py](server.py) — инструменты, работа с git, фоновое обновление. Тонкая
обёртка: разрешить направление, вызвать функцию индекса, добавить версию правил.
Вся логика лежит в `rules_index.py`, чтобы её можно было проверять тестами без
установленного `fastmcp`.
Отбор регистрации разворачивается в читаемое дерево условий вместо XML:
```
(ИЛИ:
(И:
ВидОперации = ПокупкаКомиссия
ВидПоступления = ПоОрдеру
Контрагент.НеЯвляетсяРезидентом = Истина
)
…
)
```
Ответы по умолчанию сжатые: тексты обработчиков отдаются только по запросу
(`get_conversion_rule` с `verbose=true`), состав справочников приёмника — отдельным
вызовом. Иначе один ответ съедает контекст чата.
Исключение — `get_rule_handler`: он для того и сделан, чтобы отдавать текст целиком.
Половина обработчиков короче 170 знаков и приходит одним куском; крупных, больше
20 000 знаков, во всём корпусе четыре десятка, и только им нужны страницы. Самый
большой — около 100 000 знаков.
## Проверка
```bash
python test_rules_index.py /путь/к/каталогу/с/направлениями
python test_reading.py
python test_query_extract.py
python test_checks.py
```
`test_rules_index.py` проверяет на настоящих правилах: направления распознаются с
учётом порядка сторон, ветки выгрузки не зовут несуществующих правил конвертации,
отбор регистрации разбирается, каскады не зацикливаются, справочники без своей
ветки выгрузки находятся через подчинённые правила.
`test_reading.py` — чтение и поиск: все крупные обработчики читаются страницами и
собираются знак в знак, номер строки из поиска годится для чтения, закомментированный
вызов правила в выдачу не попадает, многозначное имя правила даёт ошибку с перечнем,
а не молчаливый выбор. То же для обработчиков реквизитов: все читаются и
собираются, находятся по коду и по имени приёмника, повторяющееся имя приёмника
даёт ошибку с перечнем, поиск по ним отсеивает закомментированные строки.
Места вызовов правил в `trace_document` по всему корпусу указывают на живую
строку с вызовом и не теряют ни одного вызова; метки процессов ветки делятся
на свои и пришедшие без потерь; отбор поиска по имени объекта даёт то же, что
по кодам его правил.
Там же с 3.0.0: каждый текст с кодом из XML правил находится среди
обработчиков индекса — пропущенный вид обработчика больше не пройдёт молча.
`test_checks.py` — проверки: на маленьких искусственных правилах каждая
срабатывает где надо и молчит рядом (закомментированное, строка в кавычках,
отключённое); на боевых — известные факты корпуса, сверенные с файлами вручную.
`test_query_extract.py` — разбор запросов по всему корпусу без исключений, номера
элементов пакета совпадают с присваиваниями `Результат[n]`, вычисляемые номера не
угадываются, у неполного запроса текста нет.
## Версии
Номер версии — три числа через точку: `основная.дополнительная.исправление`.
* **основная** растёт, когда ответ инструмента меняется так, что прежняя
системная подсказка или база знаний перестают ему соответствовать;
* **дополнительная** — новый инструмент или параметр, старое работает как раньше;
* **исправление** — ошибка исправлена, поведение по описанию не менялось.
Текущая версия записана в `server.py` и видна в каждом ответе сервиса («версия
сервиса») — так из чата видно, что именно развёрнуто. История — в
[CHANGELOG.md](CHANGELOG.md).
## Лицензия
MIT — см. [LICENSE](LICENSE).
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues