letopis-mcp
by Toligrim
README.md
<div align="center">
# 📜 Летопись (Letopis)
**Движок архива и умного поиска по истории Telegram-чатов**
Сырые сообщения в JSONL · полнотекстовый поиск с русской морфологией · качалка с менеджером
[](https://www.python.org/)
[](https://github.com/LonamiWebs/Telethon)
[](https://www.sqlite.org/fts5.html)
[]()
</div>
---
## Идея
Летопись — это не бот и не сервис, а **CLI-инструмент**, заточенный под то, чтобы
LLM-агент (в первую очередь [Claude Code](https://claude.com/claude-code)) мог
читать историю ваших Telegram-чатов и отвечать по ней на вопросы, как по обычной
базе знаний.
Архив хранится как обычные файлы — `.jsonl`, по одному на чат и месяц,
append-only. Поверх них строится индекс SQLite с полнотекстовым поиском (FTS5),
который понимает русские словоформы: запрос «хостинг» находит сообщения со
словом «хостингами». К поиску подключены расшифровки голосовых, имена файлов
и текст опросов.
```
$ ./tg search переезд хостинг --chat devops --from 2025-06
```
> **Движок и данные разделены.** Этот репозиторий содержит только код —
> сами архивы чатов, `config.toml`, `.env` и сессия Telegram живут в отдельном
> приватном репозитории, который держит вас в контроле над тем, что публично,
> а что нет. Подробнее — в разделе [«Устройство»](#-устройство).
---
## ✨ Возможности
| | |
|---|---|
| 🔎 **Полнотекстовый поиск** | SQLite FTS5 + `pymorphy3`: ищет по леммам, а не только точным словам |
| 📦 **Архив как файлы** | `archive/<chat_id>/<YYYY-MM>.jsonl`, append-only, ничего не переписывается задним числом |
| ⬇️ **Качалка с менеджером** | `download` / `sync` докачивают только новое; `manifest.json` помнит, что отслеживается |
| 🎙️ **Транскрипция голосовых** | локально (faster-whisper), через Telegram Premium или OpenAI Whisper API |
| 🌐 **Веб-просмотрщик** | чаты → топики-чипы, бесконечная прокрутка, фильтры, плеер голосовых, прыжки по реплаям |
| ⌨️ **TUI-просмотрщик** | тот же функционал в терминале (`textual`) |
| 👥 **Несколько аккаунтов** | разные чаты можно скачивать разными Telegram-аккаунтами |
| 🤖 **Заточен под агентов** | JSON-вывод, компактные короткие форматы, стабильный CLI-контракт |
---
## 🚀 Быстрый старт
```sh
git clone https://github.com/Toligrim/letopis.git
cd letopis
python3 -m venv .venv && .venv/bin/pip install -e .
.venv/bin/pip install faster-whisper # опционально: локальная транскрипция голосовых
```
Летопись — это только движок. Чтобы подключить его к конкретному архиву, создайте
**отдельный репозиторий с данными** и положите туда обёртку `./tg`:
```sh
#!/bin/sh
exec "$HOME/projects/letopis/.venv/bin/tg" "$@"
```
Дальше всё запускается из корня репозитория с данными:
```sh
chmod +x tg
./tg login # авторизация Telegram-сессии (телефон / код / 2FA)
./tg download --chat mychat --media all
./tg index && ./tg meta
./tg search привет
```
Движок сам находит корень с данными: при запуске `tg` ищет от текущей директории
вверх папку, где рядом лежат `config.toml` и `archive/` (либо это явно задаётся
переменной окружения `TG_ROOT`).
## 🤖 Read-only MCP для ChatGPT
Letopis может работать как **read-only MCP retrieval gateway** для ChatGPT:
сервер использует тот же `data/index.db`, что и обычный поиск, но отдаёт только
пять безопасных retrieval-инструментов — обзор архива, поиск, агрегаты, выборку
сообщений и локальный контекст. MCP-процесс не синхронизирует Telegram, не
скачивает файлы и не меняет индекс.
### Установка и запуск
Установите MCP SDK и тестовые зависимости в окружение движка:
```sh
.venv/bin/pip install -e ".[mcp,test]"
```
Запуск через entrypoint:
```sh
.venv/bin/letopis-mcp
```
Альтернативная форма — `.venv/bin/python -m tgarchive.mcp.server`. По умолчанию
сервер слушает `http://127.0.0.1:8765/mcp` и принимает только loopback-адреса.
Для production задайте стабильный секрет курсоров и путь к индексу в окружении
процесса, например:
```sh
export LETOPIS_MCP_DB=/srv/letopis-data/data/index.db
export LETOPIS_MCP_CURSOR_SECRET='случайный-длинный-секрет'
.venv/bin/letopis-mcp
```
### Переменные окружения
| Переменная | По умолчанию | Назначение |
|---|---:|---|
| `LETOPIS_MCP_DB` | значение `[general].db` из `config.toml`, обычно `data/index.db` | Путь к SQLite-индексу; относительный путь считается от корня проекта. |
| `LETOPIS_MCP_CURSOR_SECRET` | нет; временный случайный секрет на процесс | HMAC-SHA256 для непрозрачных курсоров. **Обязателен в production**: без него курсоры не переживают рестарт процесса. |
| `LETOPIS_MCP_HOST` | `127.0.0.1` | Loopback-адрес bind; приложение отклоняет нелокальные адреса. |
| `LETOPIS_MCP_PORT` | `8765` | TCP-порт Streamable HTTP endpoint. |
| `LETOPIS_MCP_LOG_LEVEL` | `INFO` | Уровень структурированных логов (`DEBUG`, `INFO`, `WARNING`, `ERROR`, `CRITICAL`). |
| `LETOPIS_MCP_MAX_CONCURRENCY` | `8` | Максимум конкурентных операций с read-only БД. |
| `LETOPIS_MCP_QUERY_TIMEOUT_SECONDS` | `30.0` | Дедлайн SQLite-запроса и ожидания слота concurrency. |
| `LETOPIS_MCP_ROLLING_CALLS_MAX` | `60` | Максимум завершённых вызовов в глобальном rolling-окне. |
| `LETOPIS_MCP_ROLLING_CHARS_MAX` | `250000` | Максимум отданных символов в том же окне. |
| `LETOPIS_MCP_ROLLING_WINDOW_SECONDS` | `600` | Длина rolling-окна в секундах. |
Rate limit намеренно глобальный для одного процесса: в v1 нет OAuth и
идентифицированных principals, поэтому это не per-user ACL. MCP читает эти
переменные как конфигурацию процесса и не загружает `.env` автоматически.
### Подключение к ChatGPT
Проверенный production-паттерн оставляет MCP-сервер на loopback и публикует
наружу только HTTPS-эндпоинт через Cloudflare Tunnel:
```text
ChatGPT → https://your-domain.example.com/mcp?k=<секрет>
→ Cloudflare WAF → Cloudflare Tunnel → 127.0.0.1:8765/mcp
```
В этом варианте не используется «OpenAI Secure MCP Tunnel»: публичной
self-serve документации OpenAI по настройке серверной стороны этого механизма
сейчас нет. Пункт «Туннель» в интерфейсе ChatGPT — отдельный нативный механизм;
ниже описан рабочий вариант с обычным публичным HTTPS-туннелем.
#### ⚙️ systemd-сервис
Запустите `letopis-mcp` как отдельный systemd unit под непривилегированным
пользователем. Файл unit можно разместить, например, в
`/etc/systemd/system/letopis-mcp.service`:
```ini
[Unit]
Description=Letopis read-only MCP server
After=network.target
[Service]
Type=simple
User=<непривилегированный-пользователь>
WorkingDirectory=<корень-репозитория-с-данными>
EnvironmentFile=/etc/default/letopis-mcp
ExecStart=<venv>/bin/letopis-mcp
Restart=always
RestartSec=5
NoNewPrivileges=true
ProtectSystem=full
[Install]
WantedBy=multi-user.target
```
Файл `/etc/default/letopis-mcp` должен находиться вне git-репозитория и
содержать как минимум конфигурацию SQLite и стабильный секрет курсоров:
```sh
LETOPIS_MCP_DB=<корень-репозитория-с-данными>/data/index.db
LETOPIS_MCP_CURSOR_SECRET=<стабильный-секрет>
```
Замените плейсхолдеры на значения своей установки и закройте файл от лишних
пользователей. После установки unit активируйте его так:
```sh
sudo systemctl daemon-reload
sudo systemctl enable --now letopis-mcp.service
```
Дополнительный `ReadWritePaths` не нужен: сервер сам открывает SQLite в режиме
`mode=ro`, поэтому ему нужны только права чтения на индекс и необходимые
SQLite sidecar-файлы.
#### 🌐 Cloudflare Tunnel и HTTPS
Настройте `cloudflared` на проксирование публичного имени к loopback-порту
MCP-сервера. Существенная часть `config.yml` выглядит так:
```yaml
ingress:
- hostname: your-domain.example.com
service: http://127.0.0.1:8765
originRequest:
httpHostHeader: "127.0.0.1:8765"
- service: http_status:404
```
> **Важно про `Host` и DNS-rebinding-защиту.** MCP SDK по умолчанию включает
> защиту от DNS rebinding, когда сервер привязан к loopback-адресу
> (`127.0.0.1`/`localhost`): заголовок `Host` проверяется по списку
> `127.0.0.1:*`, `localhost:*`, `[::1]:*`. Cloudflare Tunnel по умолчанию
> передаёт на origin внешний `Host`, например
> `your-domain.example.com`, поэтому без переписывания сервер отвечает
> `421 Invalid Host header`. Эта проверка реализована в
> `mcp/server/transport_security.py` MCP SDK.
>
> Параметр `originRequest.httpHostHeader` переписывает заголовок только при
> передаче запроса к локальному origin и сохраняет защиту SDK включённой.
> Порт после адреса обязателен: `"127.0.0.1"` без `:8765` не пройдёт проверку,
> потому что шаблон `127.0.0.1:*` требует порт. Это правильное решение,
> ослаблять DNS-rebinding-защиту в приложении не нужно.
#### 🛡️ Авторизация на границе через Cloudflare WAF
MCP v1 не реализует аутентификацию (см. раздел
[«Безопасность deployment»](#безопасность-deployment)), поэтому публичный
эндпоинт нужно защищать до того, как запрос попадёт в приложение. Cloudflare
Access — один из вариантов, но включение Zero Trust-плана требует
привязки карты даже на бесплатном тарифе. Проверенная бескарточная альтернатива
— **WAF Custom Rule**, доступная на бесплатном плане (до пяти правил).
В Cloudflare откройте **Security → Security rules → Custom rules** и создайте
правило:
- условие: `Hostname equals your-domain.example.com` **AND** `URI Query String does not contain "k=<секрет>"`;
- действие: `Block`.
В URL ChatGPT передайте тот же WAF-секрет query-параметром:
```text
https://your-domain.example.com/mcp?k=<секрет>
```
Секрет WAF — отдельное значение; не переиспользуйте для него
`LETOPIS_MCP_CURSOR_SECRET`. При такой проверке запрос без `k` получает `403`
ещё на границе Cloudflare, и MCP-сервер его не видит. Запрос с параметром
доходит до сервера и обрабатывается нормально.
Это грубее полноценной per-request-аутентификации: статичный секрет живёт в
URL и может попасть в историю или логи. Но для сценария «URL случайно узнали
или endpoint просканировали» такой edge-gate закрывает доступ без изменений в
сервере и без дополнительных затрат. При утечке секрет следует заменить и
обновить WAF-правило и URL коннектора.
#### 🔌 Подключение в ChatGPT
В ChatGPT создайте custom connector / плагин в режиме разработчика:
1. Откройте **Developer mode → Settings → Plugins → `+`**.
2. Для `Connection` выберите **«URL-адрес сервера»**, а не **«Туннель»**.
3. Укажите URL `https://your-domain.example.com/mcp?k=<секрет>`.
4. Для `Authentication` выберите **«Без авторизации»**. Это означает, что
MCP-протокол не использует OAuth: защита уже выполнена правилом Cloudflare
WAF. В форме также могут быть варианты OAuth и «Смешанные».
5. Создайте подключение.
В этой схеме ChatGPT успешно обнаруживает все пять retrieval-инструментов.
### Безопасность deployment
MCP-процессу нужны только `data/index.db` и необходимые SQLite sidecar-файлы
`data/index.db-wal` / `data/index.db-shm`. Ему не должны быть доступны
`.env`, `telegram.session*`, `archive/`, media или manifest. Запускайте сервер
под отдельным Unix-пользователем с минимальными правами, а синхронизацию и
индексацию выполняйте отдельным процессом с нужными правами записи.
MCP v1 не реализует аутентификацию: endpoint сам не проверяет OAuth или другой
identity пользователя. Поэтому при публичной публикации нужна внешняя защита
на границе, например описанная выше WAF Custom Rule.
---
## 🗂 Устройство
```
репозиторий с данными/
├── config.toml # настройки: аккаунты, транскрипция, веб-порт
├── .env # api_id / api_hash Telegram
├── telegram.session # сессия аккаунта (и доп. сессии из [accounts])
├── tg -> letopis/.venv/bin/tg # обёртка-энтрипоинт
├── data/
│ └── index.db # SQLite + FTS5 — производный, пересобирается
└── archive/
├── manifest.json # какие чаты отслеживаем, каким аккаунтом, какие медиа качаем
└── <chat_id>/
├── 2025-06.jsonl # сырые сообщения этого месяца — источник истины
├── 2025-07.jsonl
├── transcripts.jsonl # расшифровки голосовых/кружков
├── media_index.jsonl # реестр скачанных файлов
└── media/ # сами файлы
```
- **JSONL — источник истины.** Файлы по месяцам, `sync` только дописывает новые
сообщения, старые никогда не трогает.
- **`index.db` — производный слой.** Можно удалить и пересобрать (`./tg index --rebuild`)
в любой момент без потери данных.
- **`manifest.json` — менеджер.** Переживает пересборку индекса; хранит, какие
чаты/топики отслеживаются и какие типы медиа для них качать.
Такое разделение (движок в git, открыто · данные — отдельно, приватно) позволяет
свободно развивать и делиться кодом, не рискуя утечкой переписки.
---
## 🧭 Команды
### Поиск — основное для агента
| Команда | Что делает |
|---|---|
| `tg search <слова…>` | Полнотекстовый поиск. Флаги: `--any` (OR вместо AND), `--chat`, `--topic`, `--sender`, `--from` / `--to`, `--media`, `--around N` (контекст вокруг находок), `--count`, `--by-chat` / `--by-topic` / `--by-sender` (агрегаты), `--rank` (по релевантности), `--json`, `--short N`, `--limit N\|0` |
| `tg dump --chat X [--topic N]` | Хронологический кусок переписки целиком |
| `tg context --chat X --id N` | Сообщения вокруг конкретного (`--before` / `--after` / `--whole-chat`) |
| `tg chats` | Обзор чатов в архиве |
| `tg topics --chat X` | Топики форум-чата |
| `tg status` | Состояние архива и индекса |
### Просмотрщики — ручной режим
| Команда | Что делает |
|---|---|
| `tg web` | Локальный веб-интерфейс: чаты → топики-чипы, бесконечная прокрутка, поиск с фильтрами, прыжок к дате, фильтр по автору (клик по нику), фото/видео инлайн, плеер голосовых с расшифровкой, реплаи с прыжком по треду, ссылки `t.me`. Порт — в `config.toml [web]` |
| `tg tui` | То же самое в терминале: `/` поиск · `g` дата · `s` автор · `o`/`n` старее/новее · `c` контекст · `m` открыть в Telegram · `f` открыть файл · `Esc` назад · `q` выход |
### Качалка и менеджер
| Команда | Что делает |
|---|---|
| `tg dialogs` | Все чаты аккаунта (✓ — уже в архиве) |
| `tg download --chat <имя\|id\|@user>` | Скачать чат/топики и поставить на отслеживание. Флаги: `--topic N`, `--from 2025-01`, `--media photo,voice\|all\|none` |
| `tg sync [--chat X]` | Докачать новые сообщения всех отслеживаемых чатов |
| `tg media --chat X --media voice` | Докачать файлы для уже скачанных сообщений |
| `tg transcribe [--provider …]` | Расшифровать голосовые в текст, чтобы он попал в поиск |
| `tg untrack --chat X` | Снять чат с отслеживания (файлы остаются на диске) |
| `tg meta` / `tg index` | Обновить названия чатов / доиндексировать архив |
| `tg login [--account имя]` | Авторизовать Telegram-сессию (телефон / код / 2FA) |
Чат можно указывать как `id`, алиас из `config.toml`, часть названия, `@username`
или ссылку `t.me/...`. Новые сообщения скачиваются со всеми реакциями, опросами
и сервисными событиями.
---
## 🎙 Транскрипция голосовых
Провайдер задаётся в `config.toml [transcription]`:
| Провайдер | Стоимость | Требования |
|---|---|---|
| `whisper-local` | бесплатно, локально | `faster-whisper`, модель `small` по умолчанию |
| `telegram` | бесплатно | Telegram Premium на аккаунте |
| `openai` | платно (Whisper API) | `OPENAI_API_KEY` в `.env` |
---
## 👥 Несколько аккаунтов
```toml
[accounts]
default = "telegram.session"
backup = "sessions/backup.session"
```
`tg login --account backup` авторизует новую сессию. У `download` / `sync` /
`dialogs` / `meta` есть флаг `--account`. Каждый чат в манифесте закреплён за
своим аккаунтом.
---
## 🗺 Статус
| Этап | Статус | Что входит |
|---|---|---|
| A | ✅ готово | Индекс, поиск, CLI, интеграция с Claude Code |
| B | ✅ готово | Качалка (`download`/`sync`, append-only), медиа по настройкам, бэкфилл, транскрипция, менеджер (`manifest.json`), несколько аккаунтов, FloodWait-защита |
| C | ✅ готово | Просмотрщики: `tg web` (браузер, медиа) и `tg tui` (терминал) |
**Дальше:** автосинк по расписанию, OCR картинок, Azure Speech-провайдер, экспорт выборок.
---
## 🔒 Безопасность
`telegram.session` и `.env` дают **полный доступ** к Telegram-аккаунту.
Держите их в отдельном приватном репозитории с данными, не коммитьте в этот
и никуда не публикуйте.
---
<div align="center">
Сделано для того, чтобы агент помнил переписку лучше, чем вы сами.
</div>
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues