Skip to main content
Glama
imejaikin

bitrix-chat-mcp

by imejaikin
README.md
# bitrix-chat-mcp

**Локальное зеркало истории чатов Bitrix24 с точным поиском, доступное AI-ассистентам через MCP.**

> *A local mirror of your Bitrix24 chat history with fast full-text search, exposed to AI assistants
> over MCP. Read-only against Bitrix: it never posts anything back.*

Решает конкретную боль: важные инженерные договорённости живут **в чатах**, а не в задачах и не в
документации. Найти их через живой API тяжело — приходится выкачивать тысячи строк и грепать.
Здесь они ищутся за миллисекунды и не теряются.

Всё держится на машине: SQLite рядом с кодом, никакого облака, никаких эмбеддингов. Node 22+,
две зависимости (`@modelcontextprotocol/sdk`, `zod`).

**В Bitrix этот сервер не пишет никогда.** Единственная запись — локальные закладки в своей же базе.

## Как это соотносится с `bitrix24-local-mcp`

Инструменты дополняют друг друга, у них разные задачи:

| | `bitrix24-local-mcp` | `bitrix-chat-mcp` (этот) |
|---|---|---|
| Про что | **живое состояние**: задачи, свежие сообщения, пользователи | **память**: накопленная история чатов |
| Пишет в Bitrix | да (создать задачу, отправить сообщение) | **никогда** |
| Поиск по истории | тяжело: постранично через API | быстро, SQLite FTS5 |
| Без сети | не работает | работает |

**Связаны они тремя вещами, без общего кода:**

1. **Общий креденшл.** Читаем `BITRIX24_WEBHOOK_URL` из своего `.env`, а если его нет —
   из `../bitrix24-local-mcp/.env` (сосед опционален, без него всё работает). Один секрет
   на две тулзы, но процессы независимы:
   обновление или падение одного не ломает другой.
2. **Общий словарь идентификаторов.** Зеркало возвращает `dialog_id` и `message_id` — те же,
   что понимает живой MCP. Это и есть точка стыковки:
   ```
   chat_search("ai-rules-check")  →  msg 322265, chat10833
     ↓ те же идентификаторы
   im_chat_messages(chat10833)    →  свежий контекст
   im_send_message(chat10833, …)  →  ответить
   ```
3. **Подсказки в описаниях инструментов** — в них прямо сказано, когда переходить в живой MCP.

## Установка

Нужен входящий вебхук Bitrix24 (`Приложения` → `Разработчикам` → `Входящий вебхук`) с правом
**Мессенджер (`im`)** и **Пользователи (`user`)**. URL выглядит так:
`https://ВАШ-ПОРТАЛ.bitrix24.ru/rest/USER_ID/СЕКРЕТНЫЙ_КОД/`.

```bash
make install
cp .env.example .env     # вписать BITRIX24_WEBHOOK_URL
make ui                  # выбрать чаты -> http://127.0.0.1:7625
make sync                # выкачать историю
make status              # что получилось
```

> ⚠️ Вебхук — это полный доступ к порталу под правами его владельца. Храните его как пароль:
> он лежит в `.env`, который под `.gitignore`.

`config.json` (какие чаты зеркалим) в репозитории **не хранится** — по одному этому списку видно,
с кем человек переписывается. В git лежит только `config.example.json`; свой создаётся через
`make ui`.

Подключение в `~/.mcp.json` (или в конфиге вашего MCP-клиента):

```json
{
  "mcpServers": {
    "bitrix-chat": { "command": "node", "args": ["/путь/к/bitrix-chat-mcp/src/server.mjs"] }
  }
}
```

После правки конфига Claude Code нужно перезапустить.

## Настройка через UI

`make ui` поднимает страницу выбора чатов: командные каналы и личные переписки разделены,
видно объём и глубину уже зеркалируемого. Отметили → «Сохранить» → `make sync`.

Ограничения страницы намеренные:
- слушает **только `127.0.0.1`** — это не сетевой сервис;
- **не показывает и не принимает вебхук** — секрет остаётся в `.env`;
- ничего не пишет в Bitrix, только правит локальный `config.json`.

> **Про личные переписки.** Зеркалирование складывает сообщения на диск в открытом виде.
> В личках нередко передают доступы и ключи — отмечайте их осознанно. По умолчанию
> в конфиге только командные каналы, вложения не выкачиваются вообще.

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

| Инструмент | Зачем |
|---|---|
| `chat_search` | найти, где обсуждали решение или замечание; отдаёт `dialog_id` + `message_id` |
| `chat_context` | сообщение с соседними — понять, чем закончилось обсуждение |
| `chat_list` | какие чаты в зеркале, объём и глубина |
| `chat_pin` | отметить сообщение как договорённость с пояснением (локально) |
| `chat_pins` | все отмеченные договорённости |
| `answer_pending` | вопросы ко мне, после которых я в том же чате ничего не написал |
| `my_promises` | мои же «сделаю / отпишу / пришлю» — и писал ли я в чате после |

Закладки (`chat_pin`) — это ответ на «помнить важные замечания»: помеченное не растворяется
в истории и достаётся одним вызовом.

### Что просело между чатами

`answer_pending` и `my_promises` отвечают на два вопроса, которые теряются одинаково —
сообщение прочитано, ответ отложен «на потом», и «потом» не наступает.

Что считать «моей темой», задаётся в `config.json` полем `topics` (например
`["биллинг", "касса", "отчёты"]`): по этим словам вопрос считается адресованным вам даже без
прямого упоминания. Пустой список — только прямые упоминания и личные диалоги.

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

Что выяснилось на живых данных (две недели переписки, 372 сообщения):

* **Тему надо искать по началу слова, а не подстрокой.** «бот» внутри «работал» и «чек»
  внутри «человек» затаскивали в выдачу чужие ежедневные отчёты: 24 «вопроса» вместо 9.
* **Знак вопроса из ссылки — не вопрос.** `…/compare/a...b?from_project_id=26` держал
  в списке сообщение, где вопроса нет вообще.
* **«Сделаю» и «сделал» отличаются одной буквой** и противоположны по смыслу, поэтому
  шаблоны обещаний перечислены точно, без основ слов.

`my_promises` не решает, выполнено обещание или нет: «сделаю» закрывается коммитом, а не
сообщением. Он помечает `silent_since` — после этого обещания я в чате молчу.

Свой `user_id` берётся из самого вебхука (`/rest/45/секрет/`) — без обращения к сети
и без вывода секрета наружу.

## Синхронизация

```bash
make sync   # инкрементально: тянет только то, что появилось после прошлого раза
```

Первый прогон по чату забирает историю вглубь (ограничение — `maxPagesPerChat` в `config.json`),
дальше докачиваются только новые сообщения.

Вместе с чатами забираются **треды** — комментарии к сообщению. В Bitrix тред это отдельный
скрытый чат, связь с родительским сообщением лежит в `commentInfo`. Ветка перекачивается,
только если выросла: счётчик из `commentInfo` сравнивается с сохранённым. В поиске такое
сообщение показывается как «Родительский чат · тред», а не голым `chat11115`.

Про API: у `im.dialog.messages.get` неочевидная семантика, проверенная экспериментально —
`FIRST_ID` отсутствует → последняя страница; `FIRST_ID: 0` → **самые старые**;
`FIRST_ID: <id>` → сообщения **новее** указанного. Поэтому курсор двигается по максимальному id,
и один код работает и для первой закачки, и для докачки.

## Что не индексируется

Системные события и сообщения без текста (звонки, «X вступил в чат», голые вложения) —
они только зашумляют поиск. Файлы не выкачиваются.

Из-за этого число сохранённых сообщений ветки меньше, чем `messageCount` в ответе Bitrix.
Признак «ветка выросла» строится на самом `messageCount`, а не на подсчёте своих строк —
иначе каждый прогон перекачивает ветки заново.

## Структура

```
src/
  server.mjs      MCP-сервер (stdio)
  sync.mjs        инкрементальная синхронизация
  ui.mjs          локальная страница настройки
  status.mjs      состояние зеркала
  lib/bitrix.mjs  клиент REST + чтение креденшла
  lib/db.mjs      схема, FTS5, помощники
  lib/attention.mjs  вопросы без ответа и мои обещания (чистые функции)
test/             node --test, запуск: npm test
config.json       какие чаты зеркалим (правится через UI)
data/             БД зеркала (в git не хранится)
```

## Тесты

```bash
npm test
```

Чистые функции разбора (`src/lib/attention.mjs`) покрыты тестами без обращения к базе и к сети.

## Лицензия

MIT — см. [LICENSE](LICENSE).