# tgclient-mcp

Telegram-клиент формата MCP для экосистемы Gnexus (конвенции
[gnexus-handbook](https://git.gnexus.space/git/root/gnexus-handbook)):
мульти-аккаунтный MTProto юзер-клиент (Telethon), MCP-сервер для ИИ-агентов
(`/mcp-protocol/`, alias `/mcp`) и SPA-админка, закрытая SSO gnexus-auth,
с персональными MCP-токенами.

## Возможности

- **Мульти-аккаунтность**: каждый SSO-пользователь логинит свои Telegram-аккаунты
  (phone → код → 2FA в SPA-визарде или текстом через MCP-тулы) и работает только со
  своими; админ видит все.
- **MCP-каталог тулов** (`/mcp-protocol/`, streamable HTTP + Bearer `mcp_*`):
  - ядро: `accounts_list`, `me_get`, `dialogs_list`, `messages_history`,
    `messages_read`, `messages_search`, `message_send/reply/edit/delete`;
  - медиа: `download_media` (base64), `upload_file`; **голосовые** (`upload_voice`
    ogg-opus + waveform 63×5 бит) и **кружки** (`upload_round`, mp4-квадрат ≤60 с);
    каждый upload-тул принимает и base64, и `file_path` — имя файла внутри
    каталога сервера `TGCLIENT_UPLOAD_DIR` (пути запрещены, по умолчанию выкл.);
  - **звонки**: `call_start/call_status/call_discard` — сигналинг MTProto
    (`phone.requestCall` → ринг у абонента, DH + confirm по voice-calls-спеке);
    **голос не передаётся** (Telethon без tgcalls): принявший вызов получает
    моментальный hangup; статус в RAM (после рестарта сервиса — потерян);
  - справочники: `contacts_list`, `chat_info`, `chat_participants`;
  - текстовый логин: `account_login_start/code/password/status/cancel`.
- **Ошибки — данные** (`{"error": code, "detail": ...}`), перс. ключи по канону
  `mcp.md` (≤10 на юзера, снейпшот роли, plaintext один раз, ревок вместо архива),
  блокировка юзера гасит ключи тем же 401, rate-limit мутаций и логинов,
  Synapse-уведомления (логин/логаут/auth-lost/флуд).

## Стек

FastAPI + uvicorn, aiosqlite (WAL, одна БД SQLite в `/data`), Telethon
(только `StringSession`), `mcp` python-sdk (FastMCP), gnexus-gauth SDK (OAuth
PKCE + cookie-сессии + вебхуки), gnexus-synapse (нотификации), Vue 3 + Vite +
gnexus-ui-kit (SPA).

## Запуск

```bash
cp .env.example .env && chmod 600 .env
# заполнить: TGCLIENT_API_ID/_API_HASH (my.telegram.org), TGCLIENT_ADMIN_TOKEN,
# TGCLIENT_AUTH_* ( gnexus-auth приложение), TGCLIENT_SYNAPSE_* (опционально)
docker compose up -d --build
# http://<host>:TGCLIENT_PORT — SPA; MCP: http://<host>:TGCLIENT_PORT/mcp-protocol/
```

### Подключение агента

```
claude mcp add --transport http tgclient https://<host>/mcp-protocol/ \
  --header "Authorization: Bearer mcp_<персональный ключ>"
```

Ключ выпускается на странице «MCP-ключи» сервиса; показывается один раз.
Статический `TGCLIENT_ADMIN_TOKEN` — супер-бэкдор для скриптов (роль superadmin).

## События в Synapse (`10-platform/notifications.md`)

Конверты v1, fire-and-forget, клиент `gnexus-synapse`; конфиг —
`TGCLIENT_SYNAPSE_URL/_API_KEY` (+ `_DEFAULT_SOURCE=tgclient-mcp`).
Кому и куда доставить — решает маршрутизация Synapse; в конверте только
факт. `payload.user_id` — `sub` gnexus-auth.

| событие | subject/action | prio | dedup | когда |
|---|---|---|---|---|
| `tg_account_logged_in` | account/logged_in | high | день+аккаунт | Telegram-сессия добавлена (новая или повторная) |
| `tg_account_logged_out` | account/logged_out | normal | — | логаут аккаунта из SPA |
| `tg_account_auth_lost` | account/auth_lost | high | день+аккаунт | MTProto-ключ умер (сессия отозвана в ТГ) |
| `tg_login_failed` | account/login_failed | normal | день+логин | 3 неверных кода / 2 пароля — логин отменён |
| `tg_flood_wait` | api/flood_wait | high | день+аккаунт | длинный FloodWait (>60 с) в Telethon |
| `mcp_token_issued` | token/issued | normal | — | выпуск персонального MCP-ключа |
| `mcp_token_revoked` | token/revoked | normal | — | ревок ключа (свой или админский) |
| `tg_message_received` | message/received | normal | — | новое сообщение (text + media-мета: voice/round/duration/waveform) |
| `tg_message_edited` | message/edited | normal | — | правка сообщения |
| `tg_message_deleted` | message/deleted | low | — | удаление (ids; ttl 1 ч) |

Пуш-события сообщений — то, на что реагирует ИИ-агент/скрипт: подписка через
**Synapse Target** (webhook) — MCP подписок не даёт (stateless request/response).
Включается env `TGCLIENT_NOTIFY_MESSAGES=1` (payload несёт личные переписки —
по умолчанию выключено). Plaintext MCP-ключа в конверты не уходит.

## Разработка (без docker)

```bash
cd backend && python3 -m venv .venv && .venv/bin/pip install -e .
TGCLIENT_DB_PATH=./data/tgclient.db .venv/bin/uvicorn app.main:app --reload
# SPA: cd frontend && npm ci && npm run dev (vite proxy /api,/auth → 8710)
```

- `TGCLIENT_AUTH_CLIENT_ID` пуст → **auth-off**: всё принадлежит служебному
  юзеру `local`, MCP открыт без ключей (только для разработки!).
- `/api/v1/health` → `{status, version, accounts:{active,connected,pending_logins}, db}`.

## Структура

```
backend/app/
  config.py db.py schema.sql security.py auth.py locales.py synapse_report.py errors.py
  main.py                       # lifespan (telethon-пул, GC, persist), роуты, /mcp, SPA
  api/   auth_routes accounts mcp_tokens admin
  tg/    manager login_flow limits waveform
  mcp/   server context tools serializers
frontend/src/  App.vue router.js api.js i18n/ pages/
```

## Pitfalls (из плана)

1. Telethon-сессии — только `StringSession` в БД (SQLiteSession конфликтует с aiosqlite).
2. TL-объекты сериализуются только через `mcp/serializers.py`, никогда `to_dict()`;
   наружу — `id` в bot-API-маркировке (юзер >0, группа `-id`, канал `-100id`).
3. Кружок требует mp4 H.264/AAC квадрат ≤60 с, иначе уйдёт как обычное видео;
   waveform голосового считаем сами (63 семпла × 5 бит).
4. base64 в JSON ≈ +33%: cap `TGCLIENT_MEDIA_MAX_BYTES` (по умолчанию 20 MB).
5. Повторный `send_code_request` на живую pending-сессию не дёргается; код/пароль
   не логируются и не хранятся.
6. FloodWait ≤60 с Telethon спит сам; длинный — 429 как данные + Synapse-репорт.