Skip to main content
Glama
mitetenov

Bedolaga MCP Server

by mitetenov

Bedolaga MCP Server

An MCP server for retrieving user facts from Bedolaga Bot by Telegram ID or internal user_id.

The server is read-only: via Bedolaga MCP you cannot change the balance, create or renew subscriptions, apply promo codes, issue refunds, withdraw referral funds, or perform any other actions on behalf of the user.

Breaking migration (1.0.0)

As of version 1.0.0, the public tool contract has changed and the old names have been removed. Update your client configuration:

Old tool

Replacement

bedolaga_balance

replaced by bedolaga_user_get

bedolaga_transactions

replaced by bedolaga_billing_get

bedolaga_subscription

has no equivalent in Bedolaga MCP. The actual subscription status and VPN panel state are checked via a separate mcp-remnawave, not through this server

Also in 1.0.0, the legacy HTTP path /mcp has been removed: sessionful Streamable HTTP is now served at the root endpoint /, as with mcp-remnawave. Each image release gets three tags: :latest, :{version}, and :{sha}.

Version 1.0.0 is the first contract with correct API routes, structured results, and an explicit boundary of responsibility with Remnawave.

Related MCP server: Monobank MCP Server

Tools

The server provides exactly eight tools available via the MCP protocol. All tools are readonly — no data is modified.

Identity contract

Each tool accepts exactly one of two fields:

  • telegram_id — an integer, the user's Telegram ID (positive);

  • user_id — an integer, the user's internal ID in Bedolaga (positive), used for email-only account tickets.

If neither field is passed or both are passed, the tool returns an invalid_input error. Identity is never taken from the model: supportBot always pins the actual sender — a positive telegram_id from the authenticated Telegram update, or the internal account user_id for an email-only ticket.

bedolaga_user_get

Get the account and balance of the current Bedolaga user.

Parameters:

Parameter

Type

Required

Description

telegram_id

int

exactly one of two

The user's Telegram ID

user_id

int

exactly one of two

The user's internal Bedolaga ID (email-only ticket)

Response JSON fields (data):

Field

Type

Description

found

bool

Whether the user was found

telegram_id

int | null

The user's Telegram ID

display_name

string | null

Safe display name

status

string | null

Bedolaga account status

balance_kopeks

int | null

Balance in kopecks

balance_rubles

float | null

Balance in rubles (always kopeks / 100)

has_made_first_topup

bool | null

Whether the first top-up was made in the past

has_had_paid_subscription

bool | null

Whether there was a paid purchase in the past

referral_code

string | null

Referral code

was_referred

bool | null

Whether the user came by invitation

promo_group

object | null

Promo group name and discount percentages

created_at / last_activity

string | null

Creation and last activity dates

The promo_group field contains only name, server_discount_percent, traffic_discount_percent, device_discount_percent.

Example interpretation (synthetic): balance_kopeks: 350000 and balance_rubles: 3500.0 mean a balance of 3,500 rubles. has_had_paid_subscription: false means there have been no paid purchases yet.

bedolaga_billing_get

With a single call, show the balance, recent financial events, and internal Bedolaga purchase records — to distinguish a top-up from a purchase.

Parameters:

Parameter

Type

Required

Description

telegram_id

int

exactly one of two

The user's Telegram ID

user_id

int

exactly one of two

The user's internal Bedolaga ID (email-only ticket)

limit

int

No

Limit of operations in the list (default 20, maximum 50)

Response JSON fields (data):

Field

Type

Description

balance_kopeks / balance_rubles

int / float | null

Current balance

transactions

array

Operations from newest to oldest, no more than limit

latest_completed_deposit

object | null

Summary of the latest completed deposit

latest_completed_subscription_purchase

object | null

Summary of the latest completed subscription purchase

purchased_after_latest_deposit

bool | null

A completed purchase after the latest completed deposit

bot_subscriptions

array

Internal Bedolaga subscription records

meta

string

Fixed note “deposit ≠ purchase”

Each operation in transactions:

Field

Type

Description

id

number | null

Internal transaction ID

category

string

Normalized category: deposit, subscription_purchase, gift_purchase, withdrawal, refund, failed_refund, referral_reward, poll_reward, unknown

direction

string

credit / debit / unknown

raw_type

string | null

Original safe type name

amount_kopeks / amount_rubles

int / float | null

Absolute amount

payment_method

string | null

Payment method

is_completed

bool | null

Whether the operation is completed

description

string | null

Description

created_at / completed_at

string | null

Creation and completion time

Each record in bot_subscriptions contains id, bot_record_status, bot_record_effective_status, is_trial, tariff_id, tariff_name, start_date, end_date, autopay_enabled, autopay_days_before, and a fixed note. The server prefers the full upstream subscriptions list, removes duplicate records by id, and keeps a fallback to the single legacy subscription field. The field is named bot_record_status deliberately: it is an internal Bedolaga record, not the VPN panel status. bot_record_effective_status is also a bot-side effective status (computed by the bot from status and end_date), not the panel state.

Example interpretation (synthetic): latest_completed_deposit: {amount_kopeks: 350000} and purchased_after_latest_deposit: false — the money has been credited to the balance, but a separate purchase after the deposit has not been completed.

bedolaga_referrals_get

Get the referral summary of the current user.

Parameters:

Parameter

Type

Required

Description

telegram_id

int

exactly one of two

The user's Telegram ID

user_id

int

exactly one of two

The user's internal Bedolaga ID (email-only ticket)

Response JSON fields (data):

Field

Type

Description

referral_code

string | null

Account owner's referral code

was_referred

bool | null

Owner joined via an invitation

effective_referral_commission_percent

number | null

Effective commission

invited_count

int | null

Total invited

active_referrals

int | null

Active referrals

total_earned_kopeks / total_earned_rubles

int / float | null

Lifetime earnings

month_earned_kopeks / month_earned_rubles

int / float | null

Current month earnings

recent_referral_rewards

array

Owner's recent rewards

meta

string

Fixed note

Only the account owner's statistics are returned. Telegram IDs, internal IDs, usernames, names, balances, and activity of referred users are never returned.

bedolaga_subscription_get

Get bot-side subscription records and lifecycle dates (created_at, start_date, end_date, is_trial, autopay_enabled).

Parameters: telegram_id or user_id (exactly one).

Returns has_subscription_records, active_record_count, a subscriptions list, and a fixed meta. The bot_record_status field is the bot's internal record, not the VPN panel status (actual state is checked via Remnawave MCP).

bedolaga_tickets_get

Get a summary of the owner's own support tickets (id, title, status, priority, creation/update/closure dates) without message texts or media.

Parameters: telegram_id or user_id (exactly one), limit (default 10, max 50).

bedolaga_payment_status_get

Get the history of financial operations and completion status in the bot's accounting system (completed / not_completed / unknown).

Parameters: telegram_id or user_id (exactly one), limit (default 5, max 20).

The not_completed status only means the operation has not been completed in the bot's billing, not a failure or pending state on the payment gateway side.

bedolaga_promocode_check

Check the global promocode definition, validity period, activity, bonus, and remaining uses.

Parameters: code (required), telegram_id or user_id (exactly one for identity pinning).

Returns a masked code (code_masked), a globally_valid flag, reason_code (not_found, inactive, not_yet_valid, expired_or_exhausted, lookup_incomplete), and user_eligibility: "unknown".

bedolaga_gifts_get

Get the account owner's gift purchase history.

Parameters: telegram_id or user_id (exactly one), limit (default 20, max 50).

It only shows the fact of gift purchases (accounting); gift tokens, recipients, and activation status are not disclosed.

Decision table

How the LLM (supportBot) should use Bedolaga and Remnawave data by scenario:

Scenario

What is visible in Bedolaga MCP

LLM action

Deposit without purchase

deposit present, purchased_after_latest_deposit: false

Explain that the money has been credited to the balance, but a separate purchase has not been completed; direct the user to complete the purchase from the balance. Do not claim the subscription is broken

Purchase with working panel

Completed subscription_payment present

Check the actual panel state via Remnawave MCP

Purchase without panel record

Completed subscription_payment present

Escalate as a confirmed discrepancy with a brief factual summary

No deposit

No deposit

Do not claim the payment provider did not charge the money (Bedolaga only confirms the absence of a credit in its own accounting system); escalate if the user reports an actual charge

Referral question

bedolaga_referrals_get

Route only to Bedolaga MCP

Node / HWID question

Route only to Remnawave MCP (Bedolaga does not know node and device state)

Result format

Each tool returns JSON in the text MCP content with a unified envelope:

  • success: ok: true, source: "bedolaga-mcp", tool, data, meta;

  • error: ok: false, source, tool, error.code, safe error.message, error.retryable.

The raw Bedolaga API response body and Python model exceptions are not returned. Tools do not return email, subscription link, crypto link, keys, external payment IDs, receipt identifiers, Remnawave identifiers, or personal data of referrals.

Error codes

Code

Retryable

When it occurs

invalid_input

no

Both or neither identity field provided; invalid value

not_configured

no

Missing/incorrect environment configuration

identity_unavailable

no

Identity cannot be matched to a Bedolaga user

user_not_found

no

User not found (upstream 404)

unauthorized

no

Invalid/missing API credentials (upstream 401/403)

rate_limited

yes

Rate limit reached (upstream 429)

upstream_timeout

yes

Timeout or network failure before response

upstream_unavailable

yes

Upstream unavailable (5xx or unrecoverable error)

invalid_upstream_response

no

Response body is invalid JSON or not an object

internal_error

no

Unexpected internal error

The user-facing message is built only from the safe error.message and never exposes the HTTP body or internal URL.

Transports

The server supports two transports on a single server factory and a single tool registry:

Transport

Launcher

Port

Protocol

Streamable HTTP (primary)

http_server.py

3100 by default

Dual-era MCP on / (see below), GET /health, DELETE / (legacy sessions only)

Stdio

bedolaga_server.py

MCP stdio handshake (same factory)

The / endpoint is the only one, but it serves two protocol eras simultaneously; SDK v2 itself determines which era each request belongs to via the MCP-Protocol-Version header:

  • Modern protocol 2026-07-28 — stateless/sessionless. Each POST to / is self-contained: the server never issues Mcp-Session-Id and does not store state between requests. Official MCP SDK v2 clients (see "Official SDK v2 client" below) use this mode automatically.

  • Legacy clients with initialize handshake (protocols up to 2025-11-25, including 2024-11-05) receive the Mcp-Session-Id header in response to initialize and must pass it in all subsequent requests. DELETE / with this header terminates exactly that session; it does not affect other sessions or modern clients.

GET /health returns process liveness and the server version without exposing configuration or secrets.

Version compatibility

Component

Version

Bedolaga Bot API (upstream)

commit 49b05d5, app 4.1.0

bedolaga-mcp

1.2.0

Python MCP SDK (mcp)

2.0.0

Supported MCP protocols

2026-07-28 (modern, stateless) + legacy initialize-handshake up to 2025-11-25

supportBot

2.0.1

mcp-remnawave

v3.2.1

The tool contract is verified against the specified upstream commit and the mcp-remnawave v3.2.1 reference.

Requirements

  • Python 3.11+

  • Docker (optional)

  • A deployed Bedolaga Bot with Web API

  • An API key from Bedolaga (issued in the bot's admin panel)

Quick start

1. Clone

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

2. Configure

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

3. Run

Streamable HTTP (recommended):

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

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

The server will listen on http://0.0.0.0:3100, MCP endpoint is the root /.

Stdio:

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

Via Docker:

docker compose up -d

The Docker image by default runs the Streamable HTTP server on port 3100.

Connecting as an MCP server

Streamable HTTP

The server is available over HTTP on port 3100, endpoint is the root / (http://localhost:3100).

Hermes Agent

# ~/.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

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

Cursor / VS Code

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

Checking via curl (legacy compatibility check)

Raw JSON-RPC via curl uses the legacy initialize-handshake (protocol 2024-11-05) — this is a manual backward-compatibility check, not how modern clients operate. Modern MCP SDK v2 clients negotiate protocol 2026-07-28 automatically and do not receive Mcp-Session-Id (see "Official SDK v2 client (modern protocol)" below).

# 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>"

Official SDK v2 client (modern protocol)

The official client from Python MCP SDK v2 (mcp==2.0.0) negotiates the protocol itself — 2026-07-28 if the server supports it, otherwise legacy handshake — without manually constructing _meta or headers:

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" is the same negotiation that supportBot uses: the client itself decides whether it is dealing with a modern server or a legacy one, and does not require the calling code to know the protocol era in advance.

Stdio transport

Hermes Agent

# ~/.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

{
  "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

Add to .cursor/mcp.json or settings.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"
      }
    }
  }
}

Session management

The Streamable HTTP transport is dual-era, and sessions apply to only one of the two eras:

  • Legacy initialize-handshake (protocols up to 2025-11-25): after initialize, the server returns the Mcp-Session-Id header, which the client must send in all subsequent requests. DELETE / with this header terminates only the specified session; one client cannot terminate or reuse another client's session.

  • Modern protocol 2026-07-28: stateless/sessionless — the server never issues Mcp-Session-Id, and DELETE / is neither needed nor used for such clients.

Environment variables

Variable

Purpose

BEDOLAGA_API_URL

Bedolaga Web API URL

BEDOLAGA_API_KEY

Bedolaga API key (passed upstream in X-API-Key)

MCP_HTTP_HOST

Address to bind (default: 0.0.0.0)

MCP_HTTP_PORT

HTTP server port (default: 3100)

BEDOLAGA_TIMEOUT_MS

Upstream timeout in milliseconds (default: 10000)

For compatibility, the legacy variables HOST/PORT are accepted if MCP_HTTP_HOST/MCP_HTTP_PORT are not set.

Upstream API

Bedolaga Web API: X-API-Key in the header. Routes used:

  • GET /users/by-telegram-id/{telegram_id} — user by Telegram ID;

  • GET /users/{user_id} — user by internal ID (email-only tickets);

  • GET /transactions?user_id=... — transactions with filters and pagination;

  • GET /partners/referrers/{user_id} — referral card.

More details: https://docs.bedolagam.ru

First-version limitations

  • No provider-specific payment attempts. Bedolaga returns only transactions that became records in the shared transactions table. Raw payment provider attempts that did not become a record are unavailable.

  • No reading of the user's Redis cart. The current Web API does not provide a safe read-only endpoint for this. The current "top-up made, but no purchase" issue is reliably diagnosed by the difference between deposit and subscription_payment (see decision table).

  • Email-only lookup is supported. For cabinet tickets without a Telegram ID, the server accepts an internal user_id (a positive integer) and resolves it via GET /users/{user_id}. supportBot pins the cabinet's internal user_id (the absolute value of the negative synthetic conversation key) — for such tickets, Bedolaga data is available, while Remnawave tools return identity_unavailable because such a user has no Telegram identity and no proven record in the panel.

Rollback

Setting BEDOLAGA_MCP_ENABLED=false in supportBot returns it to Remnawave-only mode: Bedolaga MCP is not connected, its tools disappear from the allowlist, and webhook/poller ticket handling (BEDOLAGA_ENABLED) remains independent. The rollback does not touch the user base or financial data — Bedolaga MCP is read-only and stores no state.

Rolling back the bedolaga-mcp image to tag 1.1.0 (the last release before the migration to MCP SDK v2, legacy-era Streamable HTTP only) is also safe: the supportBot client on MCP SDK v2 automatically falls back (auto-fallback) to the legacy initialize-handshake if the server does not respond to the modern 2026-07-28 protocol, so Bedolaga MCP tools remain available without additional configuration.

Related MCP Connectors

Related MCP Servers

  • F
    license
    Not graded
    quality
    Not graded
    maintenance
    Provides user balance information by connecting to a backend service through the users_balance tool. Built with TypeScript and Express for retrieving financial data.
    -
  • A
    license
    A
    quality
    A
    maintenance
    Enables integration with Monobank API to check currency exchange rates, view account balances, and retrieve transaction statements through natural language queries.
    3
    29 npm
    9
    MIT
  • A
    license
    Not graded
    quality
    D
    maintenance
    Enables interaction with Telegram Bot API for sending messages, photos, editing messages, answering callbacks, and fetching updates.
    MIT