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-anatomyskill,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 issrc/lingtai_telegram/licc.pyin 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=Trueis a hint flag — it does not change the on-the-wire send, only fires a typing chat action and tags the response withplaceholder: trueplus a follow-uphintstring for the agent.- The compound
message_idreturned bysendis the sameaccount:chat_id:message_idform used everywhere else and is the input toedit/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 viaedit, 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 (usetelegram(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 forreply/delete/edit.metadata.account— which configured bot received it.metadata.chat_id,metadata.has_media,metadata.has_callback— routing flags.metadata.callback_data— forcallback_queryevents, thecallback_datastring from the tapped button (the same value you set inreply_markupwhen sending).nullfor plain messages.metadata.is_voice_transcript—trueif 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:
- User sends a voice message to the bot
- MCP downloads the voice file (
.ogaformat) - Transcribes using faster-whisper (local, no API key needed)
- Delivers the transcribed text as the message body
- Original audio file is preserved in
telegram/<account>/inbox/<msg_id>/attachments/
Metadata includes:
metadata.is_voice_transcript: true— indicates this was a voice messagemetadata.voice_duration— duration in secondsmedia.type: "voice"or"audio"— media typevoice_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 theaccountparameter 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 viasetMyCommandson 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— yourinit.jsonmcp.telegram.enventry is missing theLINGTAI_TELEGRAM_CONFIGkey.Telegram config not found— the path resolves but no file exists. Relative paths are resolved againstLINGTAI_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:
/setprivacy→Disable(allows the bot to see all messages in groups). Direct messages always work. MCP server failed to start— usually thecommandpath ininit.jsondoesn't havelingtai_telegraminstalled. Confirm with<command> -m lingtai_telegram --helpfrom 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, thensystem(action="refresh").
License
MIT.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d186eea0798d40ced4e3eb49e496db565425cdcdda396291ae971633989ea2d2
|
|
| MD5 |
f1b9a84b23c482f4e9e6ef248a74a887
|
|
| BLAKE2b-256 |
033fba7cc62874a5fe196712c0e21f97728ded3a2ff4944c0344165f5f6ee26e
|
File details
Details for the file lingtai_telegram-0.2.0-py3-none-any.whl.
File metadata
- Download URL: lingtai_telegram-0.2.0-py3-none-any.whl
- Upload date:
- Size: 27.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5cff98267cf55878ed04c01ca4778d7012f33268f6de3e1524dd91b2863f5e7e
|
|
| MD5 |
f9d13f5ab9c3b487c1a73a61af675378
|
|
| BLAKE2b-256 |
8e35483922f2678e672b01a275bea66a42b1bd1e4d75899c646d7b8063880df9
|