Skip to main content
Glama
mitetenov

Bedolaga MCP Server

by mitetenov
README.md
# Bedolaga MCP Server

MCP-сервер для получения пользовательских фактов из [Bedolaga Bot](https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot) по Telegram ID или внутреннему `user_id`.

Сервер **read-only**: через Bedolaga MCP нельзя менять баланс, создавать или продлевать подписки, применять промокоды, оформлять возвраты, выводить реферальные средства или выполнять иные действия от имени пользователя.

## Breaking migration (1.0.0)

Начиная с версии **1.0.0** публичный контракт инструментов изменён, а старые имена **удалены**. Обновите конфигурацию клиента:

| Старый инструмент | Что вместо него |
|---|---|
| `bedolaga_balance` | заменён на `bedolaga_user_get` |
| `bedolaga_transactions` | заменён на `bedolaga_billing_get` |
| `bedolaga_subscription` | **не имеет аналога в Bedolaga MCP**. Фактический статус подписки и состояние VPN-панели проверяются через отдельный [mcp-remnawave](https://github.com/mitetenov/mcp-remnawave), а не через этот сервер |

Также в 1.0.0 удалён устаревший HTTP-путь `/mcp`: sessionful Streamable HTTP теперь обслуживается на корневом endpoint `/`, как у mcp-remnawave. Каждая публикация образа получает три тега: `:latest`, `:{version}` и `:{sha}`.

Версия 1.0.0 — первый контракт с корректными API routes, structured результатами и явной границей ответственности с Remnawave.

## Инструменты (Tools)

Сервер предоставляет ровно восемь инструментов, доступных через MCP-протокол. Все инструменты **readonly** — данные не изменяются.

### Контракт идентичности

Каждый инструмент принимает **ровно одно** из двух полей:

- `telegram_id` — целое число, Telegram ID пользователя (положительное);
- `user_id` — целое число, внутренний ID пользователя в Bedolaga (положительное), используется для email-only тикетов кабинета.

Если не передано ни одного поля или переданы оба — инструмент возвращает ошибку `invalid_input`. Идентичность никогда не берётся от модели: supportBot всегда пинит фактического отправителя — положительный `telegram_id` из аутентифицированного Telegram update, либо внутренний `user_id` кабинета для email-only тикета.


### `bedolaga_user_get`

Получить аккаунт и баланс текущего пользователя Bedolaga.

**Параметры:**

| Параметр | Тип | Обязательный | Описание |
|---|---|---|---|
| `telegram_id` | `int` | ровно одно из двух | Telegram ID пользователя |
| `user_id` | `int` | ровно одно из двух | Внутренний ID пользователя Bedolaga (email-only тикет) |

**JSON-поля ответа (`data`):**

| Поле | Тип | Описание |
|---|---|---|
| `found` | `bool` | Признак, что пользователь найден |
| `telegram_id` | `int` \| `null` | Telegram ID пользователя |
| `display_name` | `string` \| `null` | Безопасное отображаемое имя |
| `status` | `string` \| `null` | Статус аккаунта Bedolaga |
| `balance_kopeks` | `int` \| `null` | Баланс в копейках |
| `balance_rubles` | `float` \| `null` | Баланс в рублях (всегда `kopeks / 100`) |
| `has_made_first_topup` | `bool` \| `null` | Признак первого пополнения в прошлом |
| `has_had_paid_subscription` | `bool` \| `null` | Признак наличия платной покупки в прошлом |
| `referral_code` | `string` \| `null` | Реферальный код |
| `was_referred` | `bool` \| `null` | Пользователь пришёл по приглашению |
| `promo_group` | `object` \| `null` | Название промогруппы и проценты скидок |
| `created_at` / `last_activity` | `string` \| `null` | Даты создания и последней активности |

Поле `promo_group` содержит только `name`, `server_discount_percent`, `traffic_discount_percent`, `device_discount_percent`.

**Пример интерпретации (синтетический):** `balance_kopeks: 350000` и `balance_rubles: 3500.0` означают баланс 3 500 рублей. `has_had_paid_subscription: false` означает, что платных покупок ещё не было.

### `bedolaga_billing_get`

Одним вызовом показать баланс, недавние финансовые события и внутренние записи покупок Bedolaga — чтобы отличить пополнение от покупки.

**Параметры:**

| Параметр | Тип | Обязательный | Описание |
|---|---|---|---|
| `telegram_id` | `int` | ровно одно из двух | Telegram ID пользователя |
| `user_id` | `int` | ровно одно из двух | Внутренний ID пользователя Bedolaga (email-only тикет) |
| `limit` | `int` | Нет | Лимит операций в списке (по умолчанию 20, максимум 50) |

**JSON-поля ответа (`data`):**

| Поле | Тип | Описание |
|---|---|---|
| `balance_kopeks` / `balance_rubles` | `int` / `float` \| `null` | Текущий баланс |
| `transactions` | `array` | Операции от новых к старым, не более `limit` |
| `latest_completed_deposit` | `object` \| `null` | Сводка последнего завершённого пополнения |
| `latest_completed_subscription_purchase` | `object` \| `null` | Сводка последней завершённой покупки подписки |
| `purchased_after_latest_deposit` | `bool` \| `null` | Завершённая покупка позже последнего завершённого пополнения |
| `bot_subscriptions` | `array` | Внутренние записи подписок Bedolaga |
| `meta` | `string` | Фиксированное пояснение «deposit ≠ purchase» |

Каждая операция в `transactions`:

| Поле | Тип | Описание |
|---|---|---|
| `id` | `number` \| `null` | Внутренний ID транзакции |
| `category` | `string` | Нормализованная категория: `deposit`, `subscription_purchase`, `gift_purchase`, `withdrawal`, `refund`, `failed_refund`, `referral_reward`, `poll_reward`, `unknown` |
| `direction` | `string` | `credit` / `debit` / `unknown` |
| `raw_type` | `string` \| `null` | Исходное безопасное имя типа |
| `amount_kopeks` / `amount_rubles` | `int` / `float` \| `null` | Абсолютная сумма |
| `payment_method` | `string` \| `null` | Платёжный метод |
| `is_completed` | `bool` \| `null` | Завершённость операции |
| `description` | `string` \| `null` | Описание |
| `created_at` / `completed_at` | `string` \| `null` | Время создания и завершения |

Каждая запись в `bot_subscriptions` содержит `id`, `bot_record_status`, `bot_record_effective_status`, `is_trial`, `tariff_id`, `tariff_name`, `start_date`, `end_date`, `autopay_enabled`, `autopay_days_before` и фиксированную `note`. Сервер предпочитает полный upstream-список `subscriptions`, удаляет повторяющиеся записи по `id` и сохраняет fallback на одиночное legacy-поле `subscription`. Поле называется `bot_record_status` намеренно: это внутренняя запись Bedolaga, а **не** статус VPN-панели. `bot_record_effective_status` — тоже бот-сторонний эффективный статус (вычисленный ботом из `status` и `end_date`), а не состояние панели.

**Пример интерпретации (синтетический):** `latest_completed_deposit: {amount_kopeks: 350000}` и `purchased_after_latest_deposit: false` — деньги зачислены на баланс, но отдельная покупка после пополнения не завершена.

### `bedolaga_referrals_get`

Получить реферальную сводку текущего пользователя.

**Параметры:**

| Параметр | Тип | Обязательный | Описание |
|---|---|---|---|
| `telegram_id` | `int` | ровно одно из двух | Telegram ID пользователя |
| `user_id` | `int` | ровно одно из двух | Внутренний ID пользователя Bedolaga (email-only тикет) |

**JSON-поля ответа (`data`):**

| Поле | Тип | Описание |
|---|---|---|
| `referral_code` | `string` \| `null` | Реферальный код владельца аккаунта |
| `was_referred` | `bool` \| `null` | Владелец пришёл по приглашению |
| `effective_referral_commission_percent` | `number` \| `null` | Эффективная комиссия |
| `invited_count` | `int` \| `null` | Всего приглашено |
| `active_referrals` | `int` \| `null` | Активных приглашённых |
| `total_earned_kopeks` / `total_earned_rubles` | `int` / `float` \| `null` | Заработок за всё время |
| `month_earned_kopeks` / `month_earned_rubles` | `int` / `float` \| `null` | Заработок за текущий месяц |
| `recent_referral_rewards` | `array` | Последние начисления владельца |
| `meta` | `string` | Фиксированное пояснение |

Возвращается статистика **только владельца аккаунта**. Telegram ID, внутренние ID, username, имена, баланс и активность приглашённых пользователей никогда не возвращаются.

### `bedolaga_subscription_get`

Получить бот-сторонние записи подписок и даты жизненного цикла (`created_at`, `start_date`, `end_date`, `is_trial`, `autopay_enabled`).

**Параметры:** `telegram_id` или `user_id` (ровно одно).

Возвращает `has_subscription_records`, `active_record_count`, список `subscriptions` и фиксированный `meta`. Поле `bot_record_status` — внутренняя запись бота, а **не** статус VPN-панели (фактическое состояние проверяется через Remnawave MCP).

### `bedolaga_tickets_get`

Получить сводку собственных обращений в поддержку (`id`, `title`, `status`, `priority`, даты создания/обновления/закрытия) без текстов сообщений и медиа.

**Параметры:** `telegram_id` или `user_id` (ровно одно), `limit` (по умолчанию 10, максимум 50).

### `bedolaga_payment_status_get`

Получить историю финансовых операций и статус завершённости в учётной системе бота (`completed` / `not_completed` / `unknown`).

**Параметры:** `telegram_id` или `user_id` (ровно одно), `limit` (по умолчанию 5, максимум 20).

Статус `not_completed` означает лишь отсутствие завершения операции в биллинге бота, а **не** сбой или ожидание на стороне платёжного шлюза.

### `bedolaga_promocode_check`

Проверить глобальное определение промокода, срок действия, активность, бонус и остаток применений.

**Параметры:** `code` (обязательно), `telegram_id` или `user_id` (ровно одно для пиннинга идентичности).

Возвращает маскированный код (`code_masked`), признак `globally_valid`, `reason_code` (`not_found`, `inactive`, `not_yet_valid`, `expired_or_exhausted`, `lookup_incomplete`) и `user_eligibility: "unknown"`.

### `bedolaga_gifts_get`

Получить историю покупок подарков владельца аккаунта.

**Параметры:** `telegram_id` или `user_id` (ровно одно), `limit` (по умолчанию 20, максимум 50).

Отображает только факт покупки подарков (бухгалтерия); токены подарков, получатели и статус активации не раскрываются.

## Decision table

Как LLM (supportBot) должен использовать данные Bedolaga и Remnawave по сценариям:

| Сценарий | Что видно в Bedolaga MCP | Действие LLM |
|---|---|---|
| Deposit без покупки | `deposit` есть, `purchased_after_latest_deposit: false` | Объяснить, что деньги зачислены на баланс, но отдельная покупка не завершена; направить завершить покупку из баланса. Не заявлять о неисправной подписке |
| Покупка с рабочей панелью | Завершённая `subscription_payment` есть | Проверить фактическое состояние панели через Remnawave MCP |
| Покупка без записи в панели | Завершённая `subscription_payment` есть | Эскалировать как подтверждённое расхождение с кратким factual summary |
| Нет пополнения | `deposit` нет | Не заявлять, что платёжный провайдер не списал деньги (Bedolaga подтверждает только отсутствие зачисления в своей учётной системе); эскалировать, если пользователь сообщает о фактическом списании |
| Реферальный вопрос | `bedolaga_referrals_get` | Маршрутизировать только в Bedolaga MCP |
| Вопрос о ноде / HWID | — | Маршрутизировать только в Remnawave MCP (состояние нод и устройств Bedolaga не знает) |

## Формат результата

Каждый инструмент возвращает JSON в текстовом MCP content с единой оболочкой:

- успех: `ok: true`, `source: "bedolaga-mcp"`, `tool`, `data`, `meta`;
- ошибка: `ok: false`, `source`, `tool`, `error.code`, безопасный `error.message`, `error.retryable`.

Сырое тело ответа Bedolaga API и исключения Python модели не возвращаются. Инструменты не возвращают email, ссылку подписки, crypto link, ключи, внешние платёжные ID, receipt-идентификаторы, Remnawave-идентификаторы и персональные данные рефералов.

### Error codes

| Код | Retryable | Когда возникает |
|---|---|---|
| `invalid_input` | нет | Переданы оба или ни одного identity-поля; недопустимое значение |
| `not_configured` | нет | Отсутствует/некорректна конфигурация окружения |
| `identity_unavailable` | нет | Идентичность нельзя сопоставить с пользователем Bedolaga |
| `user_not_found` | нет | Пользователь не найден (upstream 404 исключительно при поиске пользователя) |
| `unauthorized` | нет | Неверные/отсутствующие API-креденшелы (upstream 401/403) |
| `rate_limited` | да | Достигнут rate limit (upstream 429) |
| `upstream_timeout` | да | Таймаут или сбой сети до ответа |
| `upstream_unavailable` | да | Upstream недоступен (5xx, сбой запроса или upstream 404 для непользовательских ресурсов — не означает отсутствие аккаунта) |
| `invalid_upstream_response` | нет | Тело ответа — невалидный JSON или не объект |
| `internal_error` | нет | Непредвиденная внутренняя ошибка |

Пользовательское сообщение строится только из безопасного `error.message` и никогда не раскрывает HTTP body или внутренний URL. Ошибка `user_not_found` возникает только при прямом поиске учётной записи (`get_user_by_telegram_id` / `get_user_by_id`). При статусе 404 для зависимых ресурсов (транзакции, рефералы, подписки, тикеты, промокоды) возвращается `upstream_unavailable`, что указывает на недоступность конкретного ресурса и не свидетельствует об отсутствии аккаунта пользователя.

## Транспорты

Сервер поддерживает два транспорта на одном server factory и одном реестре инструментов:

| Транспорт | Launcher | Порт | Протокол |
|---|---|---|---|
| **Streamable HTTP** (основной) | `http_server.py` | 3100 по умолчанию | Dual-era MCP на `/` (см. ниже), `GET /health`, `DELETE /` (только legacy-сессии) |
| Stdio | `bedolaga_server.py` | — | MCP stdio handshake (тот же factory) |

Эндпоинт `/` — единственный, но обслуживает **две эры протокола** одновременно; SDK v2 сам определяет, к какой эре относится каждый запрос, по заголовку `MCP-Protocol-Version`:

- **Современный протокол `2026-07-28`** — stateless/sessionless. Каждый POST на `/` самодостаточен: сервер никогда не выдаёт `Mcp-Session-Id` и не хранит состояние между запросами. Официальные клиенты MCP SDK v2 (см. «Официальный клиент SDK v2» ниже) используют этот режим автоматически.
- **Legacy-клиенты с initialize-handshake** (протоколы вплоть до `2025-11-25`, включая `2024-11-05`) получают заголовок `Mcp-Session-Id` в ответ на `initialize` и обязаны передавать его во всех последующих запросах. `DELETE /` с этим заголовком завершает именно эту сессию; на другие сессии и на современных клиентов это не влияет.

`GET /health` отдаёт liveness процесса и версию сервера, не раскрывая конфигурацию и секреты.

## Версионная совместимость

| Компонент | Версия |
|---|---|
| Bedolaga Bot API (upstream) | commit `49b05d5`, приложение `4.1.0` |
| bedolaga-mcp | `1.2.0` |
| Python MCP SDK (`mcp`) | `2.0.0` |
| Поддерживаемые протоколы MCP | `2026-07-28` (современный, stateless) + legacy initialize-handshake вплоть до `2025-11-25` |
| supportBot | `2.0.1` |
| mcp-remnawave | `v3.2.1` |


Контракт инструментов проверен против указанного upstream-коммита и эталона mcp-remnawave v3.2.1.

## Требования

- Python 3.11+
- Docker (опционально)
- Развёрнутый Bedolaga Bot с Web API
- API-ключ от Bedolaga (выдаётся в админ-панели бота)

## Быстрый старт

### 1. Клонировать

```bash
git clone https://github.com/mitetenov/bedolaga-mcp.git
cd bedolaga-mcp
```

### 2. Настроить

```bash
cp .env.example .env
# Заполнить BEDOLAGA_API_URL и BEDOLAGA_API_KEY
```

### 3. Запустить

**Streamable HTTP (рекомендуется):**

```bash
# Установить зависимости
pip install -r requirements.txt

# Запустить HTTP-сервер
BEDOLAGA_API_URL=https://your-bot.example.com \
BEDOLAGA_API_KEY=your-key \
python3 http_server.py
```

Сервер будет слушать на `http://0.0.0.0:3100`, MCP endpoint — корень `/`.

**Stdio:**

```bash
BEDOLAGA_API_URL=https://your-bot.example.com \
BEDOLAGA_API_KEY=your-key \
python3 bedolaga_server.py
```

**Через Docker:**

```bash
docker compose up -d
```

Docker-образ по умолчанию запускает Streamable HTTP сервер на порту 3100.

## Подключение как MCP-сервер

### Streamable HTTP

Сервер доступен по HTTP на порту 3100, endpoint — корень `/` (`http://localhost:3100`).

#### Hermes Agent

```yaml
# ~/.hermes/config.yaml
mcp_servers:
  bedolaga:
    transport: streamable-http
    url: "http://localhost:3100"
    env:
      BEDOLAGA_API_URL: "https://your-bot.example.com"
      BEDOLAGA_API_KEY: "your-api-key"
```

#### Claude Desktop

```json
{
  "mcpServers": {
    "bedolaga": {
      "type": "streamableHttp",
      "url": "http://localhost:3100"
    }
  }
}
```

#### Cursor / VS Code

```json
{
  "mcpServers": {
    "bedolaga": {
      "transport": "streamable-http",
      "url": "http://localhost:3100"
    }
  }
}
```

#### Проверка через curl (legacy compatibility check)

Сырой JSON-RPC через curl использует **legacy initialize-handshake** (протокол `2024-11-05`) — это ручная проверка обратной совместимости, а не то, как ходят современные клиенты. Современные клиенты MCP SDK v2 согласовывают протокол `2026-07-28` автоматически и `Mcp-Session-Id` не получают (см. «Официальный клиент SDK v2 (современный протокол)» ниже).

```bash
# Liveness
curl -s http://localhost:3100/health

# Legacy initialize handshake (получить session ID; работает для протоколов вплоть до 2025-11-25)
curl -s -X POST http://localhost:3100/ \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{"jsonrpc":"2.0","method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{}},"id":1}' \
  -D - | grep -i mcp-session-id

# Список инструментов (с session ID)
curl -s -X POST http://localhost:3100/ \
  -H "Content-Type: application/json" \
  -H "Mcp-Session-Id: <SESSION_ID>" \
  -d '{"jsonrpc":"2.0","method":"tools/list","id":2}'

# Вызов инструментов
# Пользователь и баланс
curl -s -X POST http://localhost:3100/ \
  -H "Content-Type: application/json" \
  -H "Mcp-Session-Id: <SESSION_ID>" \
  -d '{"jsonrpc":"2.0","method":"tools/call","params":{"name":"bedolaga_user_get","arguments":{"telegram_id":123456789}},"id":3}'

# Биллинг (операции и внутренние записи покупок)
curl -s -X POST http://localhost:3100/ \
  -H "Content-Type: application/json" \
  -H "Mcp-Session-Id: <SESSION_ID>" \
  -d '{"jsonrpc":"2.0","method":"tools/call","params":{"name":"bedolaga_billing_get","arguments":{"telegram_id":123456789,"limit":20}},"id":4}'

# Реферальная сводка
curl -s -X POST http://localhost:3100/ \
  -H "Content-Type: application/json" \
  -H "Mcp-Session-Id: <SESSION_ID>" \
  -d '{"jsonrpc":"2.0","method":"tools/call","params":{"name":"bedolaga_referrals_get","arguments":{"telegram_id":123456789}},"id":5}'

# Завершение legacy-сессии (для современного протокола 2026-07-28 не требуется и не применяется)
curl -s -X DELETE http://localhost:3100/ \
  -H "Mcp-Session-Id: <SESSION_ID>"
```

#### Официальный клиент SDK v2 (современный протокол)

Официальный клиент из Python MCP SDK v2 (`mcp==2.0.0`) сам согласовывает протокол — `2026-07-28`, если сервер его поддерживает, иначе legacy-handshake — без ручного построения `_meta` или заголовков:

```python
import asyncio

from mcp.client.client import Client


async def main() -> None:
    async with Client("http://localhost:3100/", mode="auto") as client:
        print("negotiated protocol:", client.protocol_version)  # "2026-07-28" against this server

        tools = await client.list_tools()
        print([tool.name for tool in tools.tools])

        result = await client.call_tool(
            "bedolaga_user_get", {"telegram_id": 123456789}
        )
        print(result.content)


asyncio.run(main())
```

`mode="auto"` — это то же самое согласование, которое использует supportBot: клиент сам решает, современный сервер перед ним или legacy, и не требует от вызывающего кода знать протокольную эру заранее.

### Stdio транспорт

#### Hermes Agent

```yaml
# ~/.hermes/config.yaml
mcp_servers:
  bedolaga:
    command: "python3"
    args: ["/path/to/bedolaga-mcp/bedolaga_server.py"]
    env:
      BEDOLAGA_API_URL: "https://your-bot.example.com"
      BEDOLAGA_API_KEY: "your-api-key"
```

#### Claude Desktop

```json
{
  "mcpServers": {
    "bedolaga": {
      "command": "python3",
      "args": ["/path/to/bedolaga-mcp/bedolaga_server.py"],
      "env": {
        "BEDOLAGA_API_URL": "https://your-bot.example.com",
        "BEDOLAGA_API_KEY": "your-api-key"
      }
    }
  }
}
```

#### Cursor / VS Code

Добавить в `.cursor/mcp.json` или `settings.json`:

```json
{
  "mcpServers": {
    "bedolaga": {
      "command": "python3",
      "args": ["/path/to/bedolaga-mcp/bedolaga_server.py"],
      "env": {
        "BEDOLAGA_API_URL": "https://your-bot.example.com",
        "BEDOLAGA_API_KEY": "your-api-key"
      }
    }
  }
}
```

## Управление сессиями

Streamable HTTP транспорт — dual-era, и сессии применимы только к одной из двух эр:

- **Legacy initialize-handshake** (протоколы вплоть до `2025-11-25`): после `initialize` сервер возвращает заголовок `Mcp-Session-Id`, который клиент должен передавать во всех последующих запросах. `DELETE /` с этим заголовком завершает только указанную сессию; один клиент не может завершить или переиспользовать чужую сессию.
- **Современный протокол `2026-07-28`**: stateless/sessionless — сервер никогда не выдаёт `Mcp-Session-Id`, и `DELETE /` для таких клиентов не нужен и не применяется.

## Переменные окружения

| Переменная | Назначение |
|---|---|
| `BEDOLAGA_API_URL` | URL Bedolaga Web API |
| `BEDOLAGA_API_KEY` | API-ключ Bedolaga (передаётся upstream в `X-API-Key`) |
| `MCP_HTTP_HOST` | Адрес для bind (по умолчанию: `0.0.0.0`) |
| `MCP_HTTP_PORT` | Порт HTTP-сервера (по умолчанию: `3100`) |
| `BEDOLAGA_TIMEOUT_MS` | Таймаут upstream в миллисекундах (по умолчанию: 10000) |

Для совместимости принимаются legacy-переменные `HOST`/`PORT`, если `MCP_HTTP_HOST`/`MCP_HTTP_PORT` не заданы.

## Upstream API

Bedolaga Web API: `X-API-Key` в заголовке. Используемые маршруты:

- `GET /users/by-telegram-id/{telegram_id}` — пользователь по Telegram ID;
- `GET /users/{user_id}` — пользователь по внутреннему ID (email-only тикеты);
- `GET /transactions?user_id=...` — операции с фильтрами и пагинацией;
- `GET /partners/referrers/{user_id}` — реферальная карточка.

Подробнее: https://docs.bedolagam.ru

## Ограничения первой версии

- **Нет provider-specific попыток платежей.** Bedolaga возвращает только операции, ставшие записями в общей таблице transactions. Сырые попытки платёжного провайдера, которые не стали записью, недоступны.
- **Нет чтения Redis-корзины пользователя.** Актуальный Web API не предоставляет безопасный read-only endpoint для этого. Текущую проблему «пополнил, а покупки нет» достоверно диагностируют по разнице между `deposit` и `subscription_payment` (см. decision table).
- **Email-only lookup поддерживается.** Для тикетов кабинета без Telegram ID сервер принимает внутренний `user_id` (положительное целое) и резолвит его через `GET /users/{user_id}`. supportBot пинит внутренний `user_id` кабинета (абсолютное значение отрицательного synthetic conversation key) — для таких тикетов Bedolaga-данные доступны, а Remnawave-инструменты возвращают `identity_unavailable`, потому что у такого пользователя нет Telegram-идентичности и доказанной записи в панели.

## Откат (rollback)

Выключение `BEDOLAGA_MCP_ENABLED=false` в supportBot возвращает его в Remnawave-only режим: Bedolaga MCP не подключается, его инструменты исчезают из allowlist, а вебхук/poller-обработка тикетов (`BEDOLAGA_ENABLED`) остаётся независимой. Откат не трогает пользовательскую базу и финансовые данные — Bedolaga MCP read-only и не хранит состояние.

Откат образа `bedolaga-mcp` до тега `1.1.0` (последний релиз до миграции на MCP SDK v2, только legacy-эра Streamable HTTP) тоже безопасен: клиент supportBot на MCP SDK v2 автоматически переходит (auto-fallback) на legacy initialize-handshake, если сервер не отвечает на современный протокол `2026-07-28`, поэтому инструменты Bedolaga MCP остаются доступны без дополнительной настройки.