Skip to main content

LingTai Telegram MCP server — bot API client with LICC inbox callback.

Project description

lingtai-telegram

LingTai Telegram MCP server — Bot API client with multi-account support and LICC inbox callback.

This is the canonical setup, configuration, and troubleshooting doc for the lingtai-telegram MCP. It is fetched by LingTai agents (or anyone else) when they need to install or configure this server.

MCP / LICC contract spec: see the lingtai-anatomy skill, reference/mcp-protocol.md, for the canonical specification of the catalog → registry → activation chain, environment-variable injection, and the LICC v1 inbox callback protocol. The reference client implementation is src/lingtai_telegram/licc.py in this repo (vendored verbatim into all first-party LingTai MCP repos — copy it if you're writing your own).

Tools

One omnibus MCP tool: telegram(action=...). Actions: send, check, read, reply, search, delete, edit, contacts, add_contact, remove_contact, accounts. Compound message IDs: account_alias:chat_id:message_id.

Typing indicators

The send action accepts an optional chat_action parameter that maps directly to Telegram's sendChatAction API. When chat_action is present and no text/media is provided, the MCP sends a status indicator (e.g. "myagent is typing...") instead of an actual message. Useful for acknowledging long-running work — web searches, avatar dispatch, large file generation — before the real reply arrives.

Valid values:

  • typing — for text replies being composed.
  • upload_photo — for photo uploads in progress.
  • upload_document — for document uploads in progress.
  • upload_voice — for voice messages being prepared.

Example — show "typing..." while preparing a reply:

{
  "action": "send",
  "account": "myagent",
  "chat_id": 123456789,
  "chat_action": "typing"
}

Important: Telegram auto-expires the indicator after 5 seconds. For tasks longer than that, re-send the chat action every ~4 seconds in a loop until the real reply is ready. There is no "stop typing" call — sending the actual message implicitly clears the indicator.

The chat-action path skips duplicate-message protection and is not persisted to telegram/<account>/sent/, since it produces no message.

Streaming responses

For long-running tasks (>5s) the bot can send a placeholder message immediately and edit it in place once the real result is ready. This avoids the perception of silence while the agent is working.

Pattern:

# 1. Send a placeholder. The bot also fires `sendChatAction(typing)` so
#    the user sees "is typing…" alongside the placeholder text.
res = telegram(
    action="send",
    chat_id=123456789,
    text="⏳ Working on it…",
    placeholder=True,
)
# res == {"status": "sent", "message_id": "myagent:123456789:42",
#         "placeholder": True, "hint": "..."}

# 2. Do the work (LLM call, computation, web fetch, etc.)
final_text = run_long_task()

# 3. Edit the placeholder in place with the final result.
telegram(action="edit", message_id=res["message_id"], text=final_text)

Notes:

  • placeholder=True is a hint flag — it does not change the on-the-wire send, only fires a typing chat action and tags the response with placeholder: true plus a follow-up hint string for the agent.
  • The compound message_id returned by send is the same account:chat_id:message_id form used everywhere else and is the input to edit/delete/reply.
  • Telegram throttles edits to ~1/sec per chat. If you stream partial updates, debounce on the agent side; the MCP does not buffer.
  • Typing indicators auto-expire after ~5s. For very long tasks, re-call send(placeholder=True, …) is overkill — better to update the placeholder text periodically via edit, which itself surfaces activity to the user.

Inline keyboards

send and edit accept an optional reply_markup argument that attaches an inline keyboard (rows of buttons) to the outgoing message. When the user taps a button, Telegram sends back a callback_query update — this MCP delivers it to the agent inbox as a LICC event with metadata.type == "callback_query" and metadata.callback_data set to the button's callback_data string. The Telegram spinner is auto-dismissed by account.py:_process_update, so agents don't need to call answerCallbackQuery themselves.

The reply_markup shape Telegram expects is:

{"inline_keyboard": [[{"text": "Yes", "callback_data": "yes"}, {"text": "No", "callback_data": "no"}]]}

i.e. a list of rows, each row a list of buttons. callback_data is capped at 64 bytes by Telegram — keep it short and dispatch on it from the agent side.

Examples

Yes / No

{
  "action": "send",
  "chat_id": 123456789,
  "text": "Approve the deploy?",
  "reply_markup": {
    "inline_keyboard": [[
      {"text": "Yes", "callback_data": "yes"},
      {"text": "No",  "callback_data": "no"}
    ]]
  }
}

Approve / Reject

{
  "action": "send",
  "chat_id": 123456789,
  "text": "Pull request #42 ready for review.",
  "reply_markup": {
    "inline_keyboard": [[
      {"text": "Approve", "callback_data": "approve"},
      {"text": "Reject",  "callback_data": "reject"}
    ]]
  }
}

Option list (3+ buttons, one per row)

{
  "action": "send",
  "chat_id": 123456789,
  "text": "Pick a city:",
  "reply_markup": {
    "inline_keyboard": [
      [{"text": "Tokyo", "callback_data": "city:tokyo"}],
      [{"text": "Osaka", "callback_data": "city:osaka"}],
      [{"text": "Kyoto", "callback_data": "city:kyoto"}]
    ]
  }
}

Helper builders (Python)

If you're composing reply_markup from Python (e.g. inside an MCP that wraps this one, or a script that calls TelegramAccount.send_message directly), the package exposes three small dict builders:

from lingtai_telegram import (
    inline_keyboard_yes_no,
    inline_keyboard_approve_reject,
    inline_keyboard_options,
)

acct.send_message(chat_id, "Approve the deploy?",
                  reply_markup=inline_keyboard_yes_no())

acct.send_message(chat_id, "PR #42 ready for review.",
                  reply_markup=inline_keyboard_approve_reject())

acct.send_message(chat_id, "Pick a city:",
                  reply_markup=inline_keyboard_options([
                      {"text": "Tokyo", "data": "city:tokyo"},
                      {"text": "Osaka", "data": "city:osaka"},
                      {"text": "Kyoto", "data": "city:kyoto"},
                  ]))

All three return plain dicts in the shape Telegram expects — they're just sugar over the JSON above.

Inbound messages (LICC)

Inbound Telegram updates flow into the host agent's inbox via the LingTai Inbox Callback Contract. Each new message is delivered as a LICC event with:

  • from — username (or first_name as fallback).
  • subject"telegram message from <user> via <account_alias>" (voice messages: "telegram voice message from <user> via <account_alias> (transcribed)").
  • body — a ~300 char preview of the message text (use telegram(action="check"|"read") to see the full conversation).
  • metadata.type — one of "message", "callback_query", "edited_message". Use this to dispatch button presses (callback_query) separately from free-text messages.
  • metadata.message_id — compound ID for reply/delete/edit.
  • metadata.account — which configured bot received it.
  • metadata.chat_id, metadata.has_media, metadata.has_callback — routing flags.
  • metadata.callback_data — for callback_query events, the callback_data string from the tapped button (the same value you set in reply_markup when sending). null for plain messages.
  • metadata.is_voice_transcripttrue if the message was a voice/audio message that was transcribed.
  • metadata.voice_duration — duration in seconds for voice messages (null for non-voice).

Voice messages

Voice and audio messages from Telegram are automatically transcribed using faster-whisper (local Whisper model). The transcribed text is delivered as the message body, so agents receive voice input as regular text.

How it works:

  1. User sends a voice message to the bot
  2. MCP downloads the voice file (.oga format)
  3. Transcribes using faster-whisper (local, no API key needed)
  4. Delivers the transcribed text as the message body
  5. Original audio file is preserved in telegram/<account>/inbox/<msg_id>/attachments/

Metadata includes:

  • metadata.is_voice_transcript: true — indicates this was a voice message
  • metadata.voice_duration — duration in seconds
  • media.type: "voice" or "audio" — media type
  • voice_transcript — full transcript with language detection and segments

Fallback: If transcription fails (e.g., missing dependencies), the message body will be:

[Voice message received — transcription failed: <error>]

Dependencies: faster-whisper is auto-installed on first transcription. For manual installation:

pip install faster-whisper

Install

# Into the LingTai agent's venv (typically ~/.lingtai-tui/runtime/venv/)
pip install git+https://github.com/Lingtai-AI/lingtai-telegram.git

After install, python -m lingtai_telegram (or the lingtai-telegram script) starts the MCP server over stdio.

Configure

The server reads its bot config from a JSON file pointed at by LINGTAI_TELEGRAM_CONFIG. Recommended path: .secrets/telegram.json inside the agent's working directory. Plaintext only — this MCP does not support *_env indirection.

Config schema

{
  "accounts": [
    {
      "alias": "myagent",
      "bot_token": "1234567890:ABCdefGhIJklMNOpqRSTuvwxyz",
      "allowed_users": [123456789, 987654321],
      "poll_interval": 1.0,
      "commands": [
        {"command": "status", "description": "Show agent status"},
        {"command": "help", "description": "List available commands"}
      ]
    }
  ]
}
  • alias — human-friendly name for this account; used as the account parameter in tool calls.
  • bot_token — issued by @BotFather. Format: <bot_id>:<auth_string>.
  • allowed_users — optional allow-list of Telegram user IDs (integers). When set, updates from other users are silently ignored. Omit to accept any sender.
  • poll_interval — seconds between getUpdates long-polls (default 1.0).
  • commands — optional list of slash commands to register with @BotFather via setMyCommands on bot startup. Each entry is {"command": "<name>", "description": "<text>"} (command name without the leading /, lowercase, max 32 chars; description 1-256 chars). When omitted, a default set is registered: /status, /help, /kanban, /brief, /clear. Pass an explicit empty list [] to clear the menu. Registration is best-effort — failures are logged but do not block bot startup.

Activation in LingTai

{
  "addons": ["telegram"],
  "mcp": {
    "telegram": {
      "type": "stdio",
      "command": "/path/to/your/python",
      "args": ["-m", "lingtai_telegram"],
      "env": {
        "LINGTAI_TELEGRAM_CONFIG": ".secrets/telegram.json"
      }
    }
  }
}

Then run system(action="refresh") from the agent. The MCP subprocess starts, the per-account poll threads begin, and the omnibus telegram tool becomes available.

Troubleshooting

  • LINGTAI_TELEGRAM_CONFIG env var not set — your init.json mcp.telegram.env entry is missing the LINGTAI_TELEGRAM_CONFIG key.
  • Telegram config not found — the path resolves but no file exists. Relative paths are resolved against LINGTAI_AGENT_DIR.
  • Unauthorized: invalid token specified — wrong or revoked bot token. Re-issue via @BotFather (/mybots → token).
  • Server boots but no inbound messages — bot privacy mode may be on. In @BotFather: /setprivacyDisable (allows the bot to see all messages in groups). Direct messages always work.
  • MCP server failed to start — usually the command path in init.json doesn't have lingtai_telegram installed. Confirm with <command> -m lingtai_telegram --help from a shell.
  • Tool calls return Telegram manager not initialized — server boot failed (most often a bad token). Check stderr for the underlying exception, fix the config, then system(action="refresh").

License

MIT.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

lingtai_telegram-0.2.0.tar.gz (28.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

lingtai_telegram-0.2.0-py3-none-any.whl (27.6 kB view details)

Uploaded Python 3

File details

Details for the file lingtai_telegram-0.2.0.tar.gz.

File metadata

  • Download URL: lingtai_telegram-0.2.0.tar.gz
  • Upload date:
  • Size: 28.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.7

File hashes

Hashes for lingtai_telegram-0.2.0.tar.gz
Algorithm Hash digest
SHA256 d186eea0798d40ced4e3eb49e496db565425cdcdda396291ae971633989ea2d2
MD5 f1b9a84b23c482f4e9e6ef248a74a887
BLAKE2b-256 033fba7cc62874a5fe196712c0e21f97728ded3a2ff4944c0344165f5f6ee26e

See more details on using hashes here.

File details

Details for the file lingtai_telegram-0.2.0-py3-none-any.whl.

File metadata

File hashes

Hashes for lingtai_telegram-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 5cff98267cf55878ed04c01ca4778d7012f33268f6de3e1524dd91b2863f5e7e
MD5 f9d13f5ab9c3b487c1a73a61af675378
BLAKE2b-256 8e35483922f2678e672b01a275bea66a42b1bd1e4d75899c646d7b8063880df9

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page