Skip to content
MCP Shelf

Telegram MCP

View on GitHub

Your own Telegram account, as an assistant can use it: chats, search, sending and groups.

  • Needs your setup first
  • ★ 1.9K GitHub stars
  • Last commit 24 Oct 2026

What is Telegram MCP?

Telegram MCP is an open source MCP server that signs in to Telegram as your own user account, so AI assistants can read, search and send messages in your chats, groups and channels.

This server by chigwell uses Telethon to sign in as your personal Telegram account, not as a bot. That means the model sees what you see: every chat, group and channel, with tools to read history, search, send and edit messages, react, forward, manage contacts, run polls and administer groups. There are nearly 150 tools in total, and voice messages can be transcribed through Groq, an OpenAI-compatible endpoint or a local Whisper model.

Acting as you is the reason to set it up carefully. A read-only mode registers only the tools that cannot change anything, you can add back just the ones you want, and an allowlist can limit it to particular chats.

Made by
chigwell and contributors
Licence
Apache-2.0
GitHub stars
1.9K
Last commit
24 Oct 2026
Tools
147
Runs on
Your computer
Setup checked
27 Oct 2026
Page updated
27 Oct 2026

Our check: It needs something our Linux test machine does not have (an account, a desktop app, a Mac or a running service), so on 27 Oct 2026 we read its tools from the source code instead of running this config. The shared database gave "npx telegram-mcp", which is an npm package by someone else, and the telegram-mcp name on PyPI belongs to another project too. Run this one from its repository.

MCP Shelf take

Telegram needs something our Linux test machine does not have (an account, an app or a service), so on 27 Oct 2026 we read its source and counted 147 tools. 65 of the 147 only read; the other 82 can change things. Other directories point at a package with the same name that belongs to someone else; the config on this page installs the real one. It sits 11th of 16 in its category on our shelf by GitHub stars (1.9K).
MCP Shelf tests: numbers from our own runs, not the README · tested on 27 Oct 2026

What can Telegram MCP read and change?

It can read

  • Every chat, group and channel your account can see, with their history and media
  • Your contacts, folders, profile and privacy settings

It can change

  • Messages sent, edited, forwarded or deleted as you
  • Groups and channels you create or administer, their members and admins
  • Your contacts, folders, profile and privacy settings

Safer setup: Start with TELEGRAM_EXPOSED_TOOLS=read-only and add back only the tools you need, and set TELEGRAM_ALLOWED_CHAT_IDS to limit which chats it can reach.

What's in the pod

What tools does Telegram MCP have?

Telegram MCP groups its tools like this, and which ones load depends on how you set it up.

Messages
Reading, searching, sending, editing, forwarding, scheduling, pinning, reactions, polls and drafts: 37 tools.
Groups and channels
Creating groups and channels, members, admins, bans, permissions, invite links and the admin log: 26 tools.
Chats
Listing and opening chats, forum topics, muting, archiving and public search: 20 tools.
Contacts
Contacts, blocking, importing and exporting, and remembered names for people: 17 tools.
Media and custom emoji
Files, albums, voice notes, stickers, GIFs, photos, documents and custom emoji packs: 17 tools.
Folders, profile and accounts
Chat folders, your profile, privacy settings, bot info and the list of configured accounts: 23 tools.
Events
Waiting for new messages and the incoming-message feed: 7 tools.

We found 147 tools in its source code on 27 Oct 2026. 65 only read. The rest can change things, so keep your app's "ask before each tool call" setting on for them.

How we tested it: Tool list read from the source at commit 43e4eb3. The server signs in to a real Telegram account, so we did not start it. TELEGRAM_EXPOSED_TOOLS=read-only registers only the read-only ones.

Show all 147 tools

Changes things82 tools · Can create, edit, send or delete.

ToolWhat it is for
add_contactAdds a contact.
archive_chatMoves a chat to the archive.
ban_userBans a user from a group or channel.
block_userBlocks a user.
create_channelCreates a channel or supergroup.
create_folderCreates a chat folder.
create_groupCreates a new group with the people you choose.
create_pollCreates a poll in a chat.
delete_messageDeletes a message.
download_mediaSaves a photo, video or document from a message to your machine.
edit_messageEdits a message you sent.
enable_incoming_feedStarts writing incoming messages to a local file a client can watch (Claude Code).
export_unread_messagesExports the unread messages of one or more chats to a JSON file.
forward_messageForwards a message to another chat.
invite_to_groupAdds people to a group or channel.
join_chat_by_linkJoins a chat from an invite link.
leave_chatLeaves a group or channel.
mark_as_readMarks a chat as read.
mute_chatMutes a chat's notifications.
pin_messagePins a message in a chat.
promote_adminMakes a member an admin.
reply_to_messageReplies to a particular message.
save_draftSaves a draft in a chat without sending it.
send_fileSends a file from an allowed folder on your machine.
send_messageSends a message to a chat.
send_reactionAdds an emoji reaction to a message.
send_scheduled_messageSchedules a message to send later.
update_profileChanges your name or bio.
  • add_chat_to_folder
  • clear_draft
  • create_forum_topic
  • delete_chat_history
  • delete_chat_photo
  • delete_contact
  • delete_contact_alias
  • delete_folder
  • delete_forum_topic
  • delete_messages_bulk
  • delete_profile_photo
  • delete_scheduled_message
  • demote_admin
  • disable_incoming_feed
  • edit_admin_rights
  • edit_chat_about
  • edit_chat_photo
  • edit_chat_title
  • edit_forum_topic
  • enable_forum_topics
  • export_chat_invite
  • forward_messages
  • get_invite_link
  • import_chat_invite
  • import_contacts
  • press_inline_button
  • remove_chat_from_folder
  • remove_reaction
  • remove_user
  • reorder_folders
  • send_album
  • send_contact
  • send_gif
  • send_sticker
  • send_typing
  • send_voice
  • set_bot_commands
  • set_contact_alias
  • set_default_chat_permissions
  • set_default_send_as
  • set_privacy_settings
  • set_profile_photo
  • subscribe_public_channel
  • toggle_slow_mode
  • unarchive_chat
  • unban_user
  • unblock_user
  • unmute_chat
  • unpin_all_messages
  • unpin_message
  • update_folder
  • upgrade_group_to_supergroup
  • upload_file
  • vote_poll

Only reads65 tools · Looks things up and changes nothing.

ToolWhat it is for
get_chatReturns the details of one chat.
get_chatsLists your chats a page at a time.
get_historyReads a chat's history, up to a limit.
get_meShows the account the server is signed in as.
get_message_contextShows the messages around one message.
get_messagesReads messages from a chat, a page at a time.
get_participantsLists the members of a group or channel.
get_privacy_settingsReads your privacy settings.
list_accountsLists the Telegram accounts configured for the server, when you use more than one.
list_chatsLists chats with filters such as type, unread or archived.
list_contactsLists your contacts.
list_foldersLists your chat folders.
list_messagesReads messages with filters such as date, sender or search text.
resolve_usernameLooks up the user or chat behind an @username.
search_contactsFinds a contact by name, username or phone.
search_globalSearches across all your chats.
search_messagesSearches the messages in one chat.
search_public_chatsFinds public channels, groups and users by name.
transcribe_voiceTurns a voice message into text with the engine you set up.
wait_for_new_messageWaits for a new private message from a person, then lists the chats with messages waiting.
  • export_contacts
  • get_admins
  • get_banned_users
  • get_blocked_users
  • get_bot_info
  • get_chatlist_invite
  • get_common_chats
  • get_contact_chats
  • get_contact_ids
  • get_custom_emoji_pack
  • get_custom_emoji_packs
  • get_direct_chat_by_contact
  • get_drafts
  • get_folder
  • get_folder_limits
  • get_folder_snapshot
  • get_full_chat
  • get_full_user
  • get_gif_search
  • get_last_interaction
  • get_media_info
  • get_member_admin_status
  • get_message_link
  • get_message_reactions
  • get_message_read_by
  • get_messages_after
  • get_photo_sheet
  • get_pinned_messages
  • get_recent_actions
  • get_scheduled_messages
  • get_send_as
  • get_sticker_sets
  • get_user_photos
  • get_user_status
  • incoming_feed_status
  • inspect_document
  • list_contact_aliases
  • list_forum_topic_icons
  • list_inline_buttons
  • list_photos
  • list_topics
  • open_photo
  • preview_custom_emoji
  • wait_for_events
  • wait_for_settled_message

Grouped and described by us. The README on GitHub lists the inputs each tool takes.

How do I install Telegram MCP?

Install steps from the Telegram MCP README

The project's own instructions, copied from its README on GitHub on 27 Oct 2026 under its Apache-2.0 licence. They are the source of truth; the ready-made configs further down are built from them. The rest of the README is in the README tab.

Quick Start

Do not install this server with uvx telegram-mcp, uvx --from telegram-mcp, or pip install telegram-mcp. The telegram-mcp name on PyPI is currently owned by a different project and does not install this repository. Passing TELEGRAM_API_ID, TELEGRAM_API_HASH, or TELEGRAM_SESSION_STRING to that package can expose Telegram account credentials to unrelated third-party code.

1. Clone and Install
git clone https://github.com/chigwell/telegram-mcp.git
cd telegram-mcp
uv sync
2. Generate a Session String
uv run session_string_generator.py

Follow the prompts. Save the generated session string securely. A session string is convenient for initial setup and portable deployments; for a long-running server, migrate it to a persistent file session after configuring your environment (see below).

For scripted setup or operational runbooks, choose the login method explicitly:

# QR login, recommended when you already have Telegram open on another device
uv run session_string_generator.py --qr

# Phone number + verification code login
uv run session_string_generator.py --phone

Without a flag, the generator keeps the interactive method prompt.

3. Configure Environment

Copy the example file and fill in your real values:

cp .env.example .env

Single-account setup:

TELEGRAM_API_ID=your_api_id_here
TELEGRAM_API_HASH=your_api_hash_here
TELEGRAM_SESSION_STRING=your_session_string_here
Recommended: Persistent Session for Long-Running Servers

StringSession stores the Telegram authorization key, but not Telethon's update state. After a restart, a busy account can therefore receive a large backlog of updates. A local SQLite session persists the update cursor and entity cache, so subsequent reconnects receive only the missed delta.

Stop all running telegram-mcp processes, then migrate the string configured in .env:

uv run telegram-mcp-migrate-session

The command verifies the new session with Telegram and prints the exact setting to use. Update .env as instructed, for example:

# Remove or comment this out; it takes precedence when both are present.
# TELEGRAM_SESSION_STRING=your_session_string_here
TELEGRAM_SESSION_NAME=/absolute/path/to/telegram_mcp_session

The resulting .session file contains account credentials. Keep it private and do not commit it. The repository's .gitignore already excludes session files.

For a labeled account, use --account with the same label:

uv run telegram-mcp-migrate-session --account work

This reads TELEGRAM_SESSION_STRING_WORK and writes the corresponding TELEGRAM_SESSION_NAME_WORK instruction. Use --target PATH to choose another destination. Do not run the migration while another process is using the source session.

By default, all Telegram MCP tools are exposed. If you want to prevent MCP clients from sending messages or performing chat/account mutations, set TELEGRAM_EXPOSED_TOOLS=read-only to expose only tools annotated with readOnlyHint=True:

TELEGRAM_EXPOSED_TOOLS=read-only

If read-only is too strict but all is too broad, append + and a comma-separated list of tool names to also expose those specific write tools. Every other write tool stays unregistered:

TELEGRAM_EXPOSED_TOOLS=read-only+send_message,reply_to_message,send_file

To hide specific tools instead, append - and a comma-separated list to either base. It works after a + list too:

TELEGRAM_EXPOSED_TOOLS=all-delete_chat_history,delete_messages_bulk
TELEGRAM_EXPOSED_TOOLS=read-only-get_participants
TELEGRAM_EXPOSED_TOOLS=read-only+send_message,reply_to_message-get_participants

An unknown name in either list aborts startup, so a typo cannot silently produce a different surface that looks like it worked. Startup also aborts on an empty list, a name in both lists, or a - name the base already hides (such as read-only-send_message), which would be a no-op. To turn off voice transcription, set TELEGRAM_TRANSCRIBE=off rather than -transcribe_voice: hiding the tool leaves listings' pending-transcript hints and auto prefetch in place.

At startup the server prints the names of the tools the mode hides to stderr; those are the names the + list accepts. Tools hidden by the - list are printed on a separate line.

This is an MCP tool-surface restriction, not a Telegram session sandbox or reduced Telegram account permission. The Telegram session string still has its normal authority inside the server process; read-only mode only prevents non-read-only tools from being registered and exposed through MCP. Accepted values are all (the default), read-only, and read-only+<tool>,<tool>, each optionally followed by -<tool>,<tool>.

A separate, hardcoded allowlist restricts send_voice, send_sticker, set_profile_photo, and edit_chat_photo to their expected file extensions; send_file and upload_file accept any extension by default. Use TELEGRAM_FILE_EXTENSIONS to add allowlists for those two, or to tighten or replace any of the hardcoded ones, in the same tool:.ext,.ext shape as TELEGRAM_EXPOSED_TOOLS, with entries separated by ;:

TELEGRAM_FILE_EXTENSIONS=send_file:.pdf,.png,.jpg;upload_file:.pdf,.png

Naming a tool that already has a hardcoded default replaces that tool's whole set rather than adding to it; any tool left unnamed keeps its default (so leaving this unset keeps today's behavior unchanged). Extensions are case-insensitive and the leading dot is optional (pdf and .pdf are equivalent). An unknown tool name, a malformed extension, or the same tool named twice aborts startup, the same way a typo in TELEGRAM_EXPOSED_TOOLS does.

This is defence in depth against accidents, not a security boundary. The check reads the final suffix, so an allowlist of .pdf still accepts payload.exe.pdf, and it says nothing about the bytes in the file. What it does reliably block is the careless case: extensionless secrets such as id_rsa or a bare .env. Files with ordinary extensions — .pem, .key, a .json token file, a .sqlite cookie store — are not covered unless you leave them out of the list yourself.

Voice transcription (see Voice transcription above) is off by default in the sense that no transcript is ever fetched unless you ask for one — transcribe_voice is always available, and listings only pick up already-cached transcripts. Enable prefetching or pick an engine explicitly:

TELEGRAM_TRANSCRIBE=on-demand       # off / on-demand (default) / auto
TELEGRAM_TRANSCRIBE_ENGINE=groq     # groq (default), telegram, openai or whisper
GROQ_API_KEY=your_groq_api_key_here # required whenever engine=groq is used

engine=groq requires GROQ_API_KEY; engine=telegram requires Telegram Premium on the account; engine=openai requires TELEGRAM_TRANSCRIBE_OPENAI_URL; engine=whisper requires the whisper extra. TELEGRAM_TRANSCRIBE_MAX_VOICES (default 5) and TELEGRAM_TRANSCRIBE_MAX_SECONDS (default 300) bound how much auto mode prefetches per listing call; TELEGRAM_TRANSCRIPT_CACHE_DIR (default data/transcripts) sets where the SQLite cache is written; TELEGRAM_TRANSCRIBE_GROQ_MAX_MB (default 25) is the largest recording the groq engine will upload.

Telegram rate limits (FloodWaitError) are surfaced transparently to MCP clients and LLM agents with the exact wait duration and an explicit instruction not to retry immediately.

Use TELEGRAM_FLOOD_SLEEP_THRESHOLD to configure Telethon's internal silent-sleep threshold (default: 60 seconds). Set to 0 to disable silent sleeping and let the agent manage all backoff pacing:

TELEGRAM_FLOOD_SLEEP_THRESHOLD=60   # Max seconds Telethon sleeps silently on FloodWait (default 60)

Run the server locally:

uv run main.py

What you need first

  • An API ID and hash from my.telegram.org/apps
  • A session string, made with the script in the repository
  • Python 3.10 or newer and uv

Steps

  1. Clone https://github.com/chigwell/telegram-mcp and run "uv sync" inside it.
  2. Create an app at my.telegram.org/apps to get your API ID and hash.
  3. Run "uv run session_string_generator.py" (add --qr to sign in by scanning a QR code) and copy the session string it prints.
  4. Add the config below with your values. Remove TELEGRAM_EXPOSED_TOOLS, or use "read-only+send_message,reply_to_message", when you want it to send.

Add Telegram MCP to your app

Pick your app for a ready-to-paste snippet and where it goes, built from the steps above.

Where it goes: Your terminal, in the project folder

Claude Code
claude mcp add telegram -e TELEGRAM_API_ID=YOUR_API_ID -e TELEGRAM_API_HASH=YOUR_API_HASH -e TELEGRAM_SESSION_STRING=YOUR_SESSION_STRING -e TELEGRAM_EXPOSED_TOOLS=read-only -- uv --directory /full/path/to/telegram-mcp run main.py

Only Claude Code can use the incoming event feed: enable_incoming_feed writes new messages to a file that a Monitor watches, so Claude wakes on replies instead of waiting in a blocking call.

Checked against the project's own install docs on 27 Oct 2026: chigwell/telegram-mcp README.

New to this? See the setup guides for Claude Code, Cursor, Claude Desktop and ChatGPT.

Telegram MCP not working? Common fixes

The session is not authorized
Generate a new session string with uv run session_string_generator.py and put it in your config.
AuthKeyDuplicatedError
The same session was used from two places at once. Make a fresh session for this server and do not share it with another script.
database is locked
Two copies of the server are using the same session file. Close the other one, or use a session string instead of TELEGRAM_SESSION_NAME.
Slow replies after many requests
Telegram rate-limits accounts. TELEGRAM_FLOOD_SLEEP_THRESHOLD controls how long the server waits quietly before reporting a FloodWait.

How do I configure Telegram MCP?

Names are exactly as the project spells them. Required ones are marked.

SettingWhat it does
TELEGRAM_API_IDEnvironment variable · requiredYour app's API ID from my.telegram.org/apps.1234567
TELEGRAM_API_HASHEnvironment variable · requiredYour app's API hash from the same page.
TELEGRAM_SESSION_STRINGEnvironment variable · requiredThe signed-in session made by session_string_generator.py. Treat it like a password: it gives full access to your account.
TELEGRAM_SESSION_NAMEEnvironment variableAn absolute path to a session file, used instead of the session string. The author recommends it for servers that run for a long time./Users/you/telegram_mcp_session
TELEGRAM_EXPOSED_TOOLSEnvironment variableall (the default), read-only, or read-only plus named tools, such as read-only+send_message,reply_to_message. Tools left out are never registered.read-only
TELEGRAM_ALLOWED_CHAT_IDSEnvironment variableA list of chat IDs the server may touch. Every other chat is out of reach.
TELEGRAM_TRANSCRIBE_ENGINEEnvironment variableHow voice messages are transcribed: groq (the default, needs GROQ_API_KEY), telegram, openai or whisper (local).whisper

Telegram MCP FAQ

How do I set up Telegram MCP?

Clone chigwell/telegram-mcp and run uv sync, create an app at my.telegram.org/apps for an API ID and hash, run uv run session_string_generator.py to sign in, then add the config with those three values.

How do I add Telegram MCP to Claude Code?

From the clone, run claude mcp add telegram with -e flags for TELEGRAM_API_ID, TELEGRAM_API_HASH and TELEGRAM_SESSION_STRING, then -- uv --directory /full/path/to/telegram-mcp run main.py.

Is Telegram MCP a bot?

No. It signs in as your own user account through Telethon, so it can do what you can do in the Telegram app, including reading private chats.

Can I install Telegram MCP with pip or uvx?

No. The telegram-mcp names on PyPI and npm belong to other projects, and passing your Telegram credentials to them could expose your account. Run it from the repository.

Is Telegram MCP safe?

Start with TELEGRAM_EXPOSED_TOOLS=read-only, add back only the tools you need, and set TELEGRAM_ALLOWED_CHAT_IDS to limit which chats it can reach. Keep the session string as secret as a password.

Can Telegram MCP get my account banned?

Telegram can limit accounts that behave like spam. Use it for your own chats and replies, not for mass messaging, and slow down if replies start lagging after many requests.

Is Telegram MCP free?

Yes, the code is Apache 2.0 and costs nothing to run. Voice transcription through Groq or OpenAI is billed by them; the local Whisper option is free.

Why does Telegram MCP say the session is not authorized?

The session string expired or was revoked, for example after logging out other sessions. Run session_string_generator.py again and update TELEGRAM_SESSION_STRING.

Telegram MCP README

This is the project's own README from chigwell/telegram-mcp, written by chigwell and contributors, and shared under its Apache-2.0 licence. We copied it on 27 Oct 2026, so the version on GitHub may be newer.

A Telegram integration for Claude, Cursor, and other MCP-compatible clients. It exposes Telegram account, chat, message, contact, media, folder, and admin operations through the Model Context Protocol using Telethon.

🤖 MCP in Action

Basic Telegram MCP usage in Claude:

Telegram MCP in action

Asking Claude to analyze chat history and send a response:

Telegram MCP Request

Message sent successfully:

Telegram MCP Result

Contents

  • Skills & Practical Workflows
  • What It Can Do
  • Requirements
  • Quick Start
  • MCP Client Configuration
  • Multi-Account Setup
  • Device Identity
  • Expected Account Check
  • Proxy Support
  • File Path Security
  • Chat Access Privacy (Allowlist)
  • Docker
  • Development
  • Security Notes
  • Troubleshooting
  • License

Skills & Workflows

Looking for ready-to-use workflows, prompt examples, or integration recipes? Explore the Skills Documentation for step-by-step guides on:

What It Can Do

The server currently includes 80+ MCP tools grouped into these areas:

  • Accounts: list configured accounts and route tool calls by account label.
  • Chats and groups: list chats, inspect metadata, create groups/channels, upgrade basic groups to supergroups, join or leave chats, invite or remove users, manage admins, bans, default permissions, slow mode, topics and their allowed icons, invite links, common chats, read receipts, and message links.
  • Messages: send, schedule, edit, delete, forward, pin, unpin, mark read, reply, search, inspect context, create polls, manage reactions, inspect inline buttons, and press inline callbacks. send_message, reply_to_message, and edit_message support classic formatting (parse_mode='md'/'html') and server-side rich formatting (parse_mode='rich'/'rich_markdown'/'rich_html' — full Markdown/HTML with tables, headings, formulas, and collapsible sections). Rich modes require Telegram Premium on the account; Premium is re-checked on every call, and without it nothing is sent — the tool returns a structured telegram_premium_required result so the agent can reformat with classic modes and retry. send_message, reply_to_message, and edit_message also accept format_date to render a date as a tappable chip. edit_message reports an edit that changes nothing as not modified instead of an error.

list_inline_buttons describes each button by row, column and kind (what pressing it does), for inline buttons and reply keyboards. press_inline_button presses callback and game buttons, returns the bot's answer with alert and url, and refuses buttons that need the 2FA password.

get_message_reactions returns an empty list for a message with no reactions. To reuse a custom reaction, pass the returned custom:<document_id> value to send_reaction.

vote_poll(chat_id, message_id, option_indices, account=None) votes in an existing poll. Read the poll with get_messages first, then select zero-based answer indices: [0] for the first answer, or [0, 2] for the first and third in a multiple-choice poll. This may replace an existing vote; empty selections, duplicate or out-of-range indices, closed polls, and multiple answers in single-choice polls are rejected. Telegram enforces voting permissions and restrictions on changing quiz votes.

In supergroups where you can post as more than one identity (for example as a channel you own), get_send_as(chat_id) lists the allowed senders and set_default_send_as(chat_id, send_as) saves which one your later messages use: "me" for your own account, or the ID or username of an allowed sender. The saved sender applies in all your Telegram clients. Anonymous posting as an admin is an admin right and is not changed by it.

  • Contacts: list, search, add, delete, block, unblock, import, export, inspect direct chats, find recent contact interactions, and remember contacts by the names you actually use (see below).

Typing indicators

send_typing(chat_id, seconds=5, topic_id=None, account=None) sends a temporary typing indicator without sending a message. Duration must be 0.5 to 45 seconds; the tool blocks until it finishes, so call it before send_message or reply_to_message, not in parallel. It refreshes the indicator at intervals of at most four seconds and attempts to clear it on completion, failure, or cancellation. topic_id targets a specific forum topic.

Overlapping calls for the same account, peer, and topic are rejected rather than cancelling each other's indicators. Flood waits are not slept through automatically for these RPCs; the typing phase has a 50-second timeout and cleanup a separate three-second timeout. A failed cleanup is reported explicitly. Successful RPC responses confirm Telegram accepted the requests, not that another client's UI displayed them. This is a write operation and respects the existing account routing and chat allowlist.

Remembered contacts

set_contact_alias teaches the server what you call someone, and every tool that takes a chat_id understands it from then on — send_message("андрей бекендер", ...) just works. A contact can carry any number of aliases, which is how tags work: save both андрей бекендер and бекендер for the same person and either resolves.

Only an exact saved wording ever sends. Similar wording (Андрею бекендеру for a saved андрей бекендер) is matched too, but only to suggest: the tool sends nothing and asks you to confirm the contact by name. This is deliberate — Лена/Леня and Иван/Иванов differ exactly as much as a case ending does, so a matcher confident enough to handle declensions is also confident enough to message the wrong person whenever the one you meant is not saved yet. Confirming saves that wording as its own alias, so each new phrasing costs one yes/no the first time and nothing ever again. Set TELEGRAM_CONTACT_FUZZY=0 to drop the suggestions too.

When a reference is unknown, resembles one contact, matches several, or points at a contact that no longer resolves, tools send nothing and return a structured instruction telling the agent exactly what to ask you, to save the answer with set_contact_alias, and to retry once. list_contact_aliases shows one row per person with all their aliases (use it to spot a wrong memory), delete_contact_alias forgets one, and repointing an alias at someone else requires replace=True. The save path itself refuses a target it would have to guess at: contacts are saved by @username, phone, numeric ID, or an alias already confirmed for them.

Aliases live in ${XDG_STATE_HOME:-~/.local/state}/telegram-mcp/aliases.json (owner-only, written atomically); TELEGRAM_ALIASES_FILE overrides the path, and a pre-existing aliases.json next to the code is still read as a fallback.

  • Media: send files, download media, upload files, send voice notes, stickers, GIFs, inspect message media, and transcribe voice messages/video notes (see below).

Voice transcription

transcribe_voice(chat_id, message_id, engine=None) turns a voice message or video note into text. Four engines are available:

  • groq (default): uploads the recording to Groq's hosted whisper-large-v3-turbo. Leaves the server and costs a download+upload per call, but doesn't drop the recording's last few words the way native transcription does. Requires GROQ_API_KEY. Groq caps the size of a single upload, so a recording above TELEGRAM_TRANSCRIBE_GROQ_MAX_MB (default 25, the free-tier limit) is refused locally with a too_large error naming its size instead of being downloaded and rejected by the API. Raise the limit if your Groq tier allows bigger files, or transcribe that message with engine='telegram', which has no such cap.
  • telegram: native Telegram Premium transcription (messages.TranscribeAudioRequest). Free and never leaves Telegram, but empirically drops the last speech segment in roughly 2 of 3 recordings and requires Telegram Premium on the account. Long recordings come back pending and are polled automatically.
  • openai: any OpenAI-compatible /audio/transcriptions endpoint — OpenAI itself, a self-hosted Parakeet or speaches server, LocalAI, a vLLM Whisper deployment, and so on. Set TELEGRAM_TRANSCRIBE_OPENAI_URL to the API base URL (e.g. https://api.openai.com/v1; a full .../audio/transcriptions URL also works), TELEGRAM_TRANSCRIBE_OPENAI_API_KEY for the bearer token (optional for keyless local servers), and TELEGRAM_TRANSCRIBE_OPENAI_MODEL (default whisper-1). Size cap: TELEGRAM_TRANSCRIBE_OPENAI_MAX_MB (default 25). For Parakeet use TELEGRAM_TRANSCRIBE_OPENAI_URL=http://localhost:5092/v1, the API key only if the server sets PARAKEET_API_KEY, and set TELEGRAM_TRANSCRIBE_LANGUAGE for anything that isn't English — Parakeet assumes en when no language is sent.
  • whisper: a local faster-whisper model loaded inside the MCP server process. The audio never leaves the machine. Install the extra with pip install 'telegram-mcp[whisper]' (or uv sync --extra whisper). TELEGRAM_TRANSCRIBE_WHISPER_MODEL picks the model (default small; e.g. large-v3-turbo for better quality), TELEGRAM_TRANSCRIBE_WHISPER_DEVICE (auto/cpu/cuda), TELEGRAM_TRANSCRIBE_WHISPER_COMPUTE_TYPE (e.g. int8 on CPU) and TELEGRAM_TRANSCRIBE_WHISPER_MODEL_DIR (where models are downloaded) tune it. The model is loaded once on first use and recordings are transcribed one at a time. Not available in the Alpine Docker image; use openai against a Parakeet or other OpenAI-compatible server next to the container instead.

TELEGRAM_TRANSCRIBE_LANGUAGE (ISO-639-1, e.g. nl) is passed as a language hint to every engine except telegram; unset, the engines auto-detect. TELEGRAM_TRANSCRIBE_TIMEOUT (default 120 seconds) bounds a single request to the HTTP engines (groq, openai).

The engine is chosen per call via the engine argument, or otherwise defaults to TELEGRAM_TRANSCRIBE_ENGINE (groq, telegram, openai or whisper). Results are cached by (chat_id, message_id, engine) in a local SQLite file so repeat reads and repeat listings never re-transcribe the same message. Concurrent requests for the same uncached recording are collapsed too: the second one waits for the first and returns its transcript, so a burst of callers costs one paid call, not one per caller. Every transcript is returned with a note marking it as a machine transcript, not a verbatim quote — treat it as a paraphrase, not exact wording.

get_history, get_messages, list_messages, search_messages, search_global, and get_message_context fill in already-cached transcripts for voice messages instead of leaving the text empty, controlled by TELEGRAM_TRANSCRIBE:

  • off: transcription is disabled at runtime. The transcribe_voice tool stays registered and returns {"transcribed": false, "reason": "transcription_disabled"} instead of transcribing, and listings never show transcripts. Use TELEGRAM_EXPOSED_TOOLS to hide the tool itself.
  • on-demand (default): listings show cached transcripts but never spend an API call fetching a new one.
  • auto: get_history, get_messages, list_messages, and search_messages also prefetch missing transcripts, bounded per call by TELEGRAM_TRANSCRIBE_MAX_VOICES/TELEGRAM_TRANSCRIBE_MAX_SECONDS (Groq isn't free, so this prefetch is budgeted rather than unbounded).

The cache lives in TELEGRAM_TRANSCRIPT_CACHE_DIR (default data/transcripts in the working directory, or ~/.cache/telegram-mcp/transcripts when that is not writable), written as a 700 directory / 600 file since it holds personal-chat text in plaintext — see Docker for why this needs its own volume mount in a container.

  • Profile and privacy: get your own account info, update profile fields, set or delete profile photos, inspect privacy settings, get user info/photos/status, and manage bot commands.

  • Folders and drafts: list, create, update, reorder, and delete Telegram folders; preview a shared folder invite (t.me/addlist/...) without joining it; save, list, and clear drafts.

    • get_folder_limits reports current Premium-aware folder, explicit-chat and pin limits from Telegram app config; unavailable limits are null.
    • get_folder_snapshot captures folder definitions/order, including title entities, display fields, flags, stable peer IDs and ordered pins, without access hashes or chat contents. Its private-folder definition objects can be passed as restoration patches; shared invites and account/chat state are outside this snapshot's scope.
    • update_folder(folder_id, patch, expected_revision?) edits an existing private folder in place, preserving omitted fields. Peer lists replace only the specified list; [] clears it. For example, {"title": "Projects", "exclude_archived": false} renames the folder and allows archived chats through its rules without unarchiving anything. Save a snapshot before editing and use its revision to reject stale plans. Shared/system folders are refused; avoid concurrent folder editors because Telegram has no atomic compare-and-swap.
  • Events: wait for incoming messages with debounce (wait_for_new_message, wait_for_settled_message), optionally for one chat only via chat_id — without it any unrelated conversation wakes the wait — or enable the opt-in incoming event feed for callback-style delivery (see below). Integrators that follow chats (such as chat bridges) use the per-message event log instead (see below).

All tool results that include Telegram user-controlled content are sanitized and, where practical, returned as structured JSON.

Reusing custom emoji

get_history, list_messages, search_messages, search_global, get_message_context (including replied_message), get_pinned_messages, and get_drafts include custom_emojis when the message contains custom emoji. get_messages and get_scheduled_messages include the same metadata as a JSON list in their text output. Ordinary messages keep their existing output.

{"text": "🍷 News", "custom_emojis": [{"emoji": "🍷", "id": "5368324170671202286"}]}

Each entry contains the fallback emoji and its Telegram document ID as a string. Repeated IDs appear once per message; different IDs remain separate even when their fallback emoji looks identical. This is a list of reusable emoji variants, not a map of their positions or a copy of all message formatting. Extraction uses the original Telegram entities, including TextCustomEmoji nodes in block-format rich_message content, and makes no additional API requests. Emoji joiners and flag tag characters are preserved in the fallback text.

To reuse an entry, set parse_mode="html" in send_message, reply_to_message, or edit_message, and insert <tg-emoji emoji-id="ID">EMOJI</tg-emoji> using its id and emoji. HTML-escape the fallback and other literal text. For example, the entry above becomes <tg-emoji emoji-id="5368324170671202286">🍷</tg-emoji>. Telegram's account restrictions still apply to sending custom emoji.

For plain-text messages, pass the entries themselves as custom_emojis instead, with no HTML and no escaping; the text is sent verbatim. For example, send_message(chat_id, "🍷 News for List<int> & co", custom_emojis=[{"emoji": "🍷", "id": "5368324170671202286"}]). The same parameter works in reply_to_message and edit_message. Every standalone occurrence of each emoji becomes that custom emoji, including ones inside URLs or code; to keep one plain, use parse_mode="html" with <tg-emoji> instead. An emoji inside a longer sequence (such as 👍 in 👍🏽) is not a match. Nothing is sent when an emoji is not found or does not look like an emoji (pure ASCII/Latin punctuation such as # is refused), an id is not a positive 64-bit integer, two marked spans overlap, or parse_mode is set. It combines with format_date.

send_file and send_album take the same parse_mode and custom_emojis for their caption, which for a media group sits on its first item. Without parse_mode a caption is still parsed as Markdown, as before; parse_mode="plain" sends it verbatim, the rich modes are not supported for captions, and custom_emojis needs a caption.

Tappable dates and times

Passing format_date to send_message, reply_to_message, or edit_message renders a tappable date/time chip — the same entity Telegram's apps attach when you type a recognizable date. Give the date text exactly as it appears in the message: '13/09', '13/09/2026', or '13/09 17:00'. The chip opens copy-date / add-to-calendar / reminder actions. Plain-text messages only — omit parse_mode. For example, send_message(chat_id, "Lunch 13/09 13:00", format_date="13/09 13:00") sends a message whose date opens that menu.

Incoming Event Feed (callback mode, Claude Code only)

By default, an agent waits for replies by calling wait_for_settled_message, which blocks up to the MCP tool timeout and must be re-called — that works everywhere (Codex, Cursor, etc.) and is unchanged.

Clients that can wake an agent on external output (Claude Code's persistent Monitor on tail -f) can switch to callback mode instead:

  1. The agent calls enable_incoming_feed (or set TELEGRAM_EVENT_FEED=1 in the environment to auto-enable). Each settled incoming burst is appended as one JSON line to ${XDG_STATE_HOME:-~/.local/state}/telegram-mcp/incoming_feed.jsonl, created owner-only (0600). Override the path with TELEGRAM_EVENT_FEED_FILE — an explicit path's directory must already exist. incoming_feed_status reports the effective path and a ready-to-use watch command.
  2. The agent arms a persistent Monitor with the watch_command returned by the tool. Every new line re-invokes the agent with the burst summary; no blocking tool call is held open, and the chat stays free.

disable_incoming_feed switches back; incoming_feed_status reports the current mode. While the feed is enabled it consumes settled bursts, so don't combine it with wait_for_settled_message. Feed lines contain user-generated name fields — treat them as untrusted data.

Per-message events (chat bridges)

Every new message of a private chat, basic group or supergroup gets one numbered event in a bounded in-memory log (TELEGRAM_EVENT_LOG_SIZE, default 2000; 0 turns it off). Incoming messages count, and so do the owner's own messages sent from another device; messages this server sent itself do not. Broadcast channels are left out, and so are chats outside TELEGRAM_ALLOWED_CHAT_IDS when it is set. Events carry no text and nothing is written to disk.

  • wait_for_events(chat_id?, after_seq, boot?, timeout, limit, account?) long-polls: it returns as soon as there are events after after_seq (at most 100 s, under typical proxy timeouts; keep timeout under your MCP client's request timeout). after_seq=-1 starts from now without replaying the log. An event has seq, chat_id, chat_type, message_id, sender_id, sender_name, sender_bot, out, addressed (private chats always, groups when Telegram marks the message as mentioning the account or replying to it; never the owner's own), topic (the forum topic's id, 1 for General; absent outside forums), reply_to, service and date; with several accounts it also names its account. Always pass back boot and last_seq; reset (the server restarted) or gap (the log overflowed; global, not per chat) means: read every chat you follow from your last message id, then continue.
  • get_messages_after(chat_id, after_id?, limit) returns the messages after an id, oldest first, as the list_messages records plus out, mentioned, addressed, date, topic in forums and action for service messages. The result has chat_id, chat_type, has_more (newer messages remain with after_id, older ones exist without it), messages and, in a forum, one topics map of topic id to title. A negative after_id is rejected.
  • send_message takes reply_to (a topic id posts in that forum topic) and names the sent message: plain sends end with message_id=<id>, rich sends (rich* parse modes) return JSON with a message_id key (null when Telegram's answer does not name it), so a bridge can recognise its own message when it comes back in history.

The debounce tools above keep their private-only behaviour.

Requirements

  • Python 3.10+
  • Telegram API credentials from my.telegram.org/apps
  • A Telegram session string or file-based session
  • An MCP client such as Claude Desktop, Cursor, or another MCP-compatible host
  • Optional: uv for local development

Quick Start

Do not install this server with uvx telegram-mcp, uvx --from telegram-mcp, or pip install telegram-mcp. The telegram-mcp name on PyPI is currently owned by a different project and does not install this repository. Passing TELEGRAM_API_ID, TELEGRAM_API_HASH, or TELEGRAM_SESSION_STRING to that package can expose Telegram account credentials to unrelated third-party code.

1. Clone and Install

git clone https://github.com/chigwell/telegram-mcp.git
cd telegram-mcp
uv sync

2. Generate a Session String

uv run session_string_generator.py

Follow the prompts. Save the generated session string securely. A session string is convenient for initial setup and portable deployments; for a long-running server, migrate it to a persistent file session after configuring your environment (see below).

For scripted setup or operational runbooks, choose the login method explicitly:

# QR login, recommended when you already have Telegram open on another device
uv run session_string_generator.py --qr

# Phone number + verification code login
uv run session_string_generator.py --phone

Without a flag, the generator keeps the interactive method prompt.

3. Configure Environment

Copy the example file and fill in your real values:

cp .env.example .env

Single-account setup:

TELEGRAM_API_ID=your_api_id_here
TELEGRAM_API_HASH=your_api_hash_here
TELEGRAM_SESSION_STRING=your_session_string_here

Recommended: Persistent Session for Long-Running Servers

StringSession stores the Telegram authorization key, but not Telethon's update state. After a restart, a busy account can therefore receive a large backlog of updates. A local SQLite session persists the update cursor and entity cache, so subsequent reconnects receive only the missed delta.

Stop all running telegram-mcp processes, then migrate the string configured in .env:

uv run telegram-mcp-migrate-session

The command verifies the new session with Telegram and prints the exact setting to use. Update .env as instructed, for example:

# Remove or comment this out; it takes precedence when both are present.
# TELEGRAM_SESSION_STRING=your_session_string_here
TELEGRAM_SESSION_NAME=/absolute/path/to/telegram_mcp_session

The resulting .session file contains account credentials. Keep it private and do not commit it. The repository's .gitignore already excludes session files.

For a labeled account, use --account with the same label:

uv run telegram-mcp-migrate-session --account work

This reads TELEGRAM_SESSION_STRING_WORK and writes the corresponding TELEGRAM_SESSION_NAME_WORK instruction. Use --target PATH to choose another destination. Do not run the migration while another process is using the source session.

By default, all Telegram MCP tools are exposed. If you want to prevent MCP clients from sending messages or performing chat/account mutations, set TELEGRAM_EXPOSED_TOOLS=read-only to expose only tools annotated with readOnlyHint=True:

TELEGRAM_EXPOSED_TOOLS=read-only

If read-only is too strict but all is too broad, append + and a comma-separated list of tool names to also expose those specific write tools. Every other write tool stays unregistered:

TELEGRAM_EXPOSED_TOOLS=read-only+send_message,reply_to_message,send_file

To hide specific tools instead, append - and a comma-separated list to either base. It works after a + list too:

TELEGRAM_EXPOSED_TOOLS=all-delete_chat_history,delete_messages_bulk
TELEGRAM_EXPOSED_TOOLS=read-only-get_participants
TELEGRAM_EXPOSED_TOOLS=read-only+send_message,reply_to_message-get_participants

An unknown name in either list aborts startup, so a typo cannot silently produce a different surface that looks like it worked. Startup also aborts on an empty list, a name in both lists, or a - name the base already hides (such as read-only-send_message), which would be a no-op. To turn off voice transcription, set TELEGRAM_TRANSCRIBE=off rather than -transcribe_voice: hiding the tool leaves listings' pending-transcript hints and auto prefetch in place.

At startup the server prints the names of the tools the mode hides to stderr; those are the names the + list accepts. Tools hidden by the - list are printed on a separate line.

This is an MCP tool-surface restriction, not a Telegram session sandbox or reduced Telegram account permission. The Telegram session string still has its normal authority inside the server process; read-only mode only prevents non-read-only tools from being registered and exposed through MCP. Accepted values are all (the default), read-only, and read-only+<tool>,<tool>, each optionally followed by -<tool>,<tool>.

A separate, hardcoded allowlist restricts send_voice, send_sticker, set_profile_photo, and edit_chat_photo to their expected file extensions; send_file and upload_file accept any extension by default. Use TELEGRAM_FILE_EXTENSIONS to add allowlists for those two, or to tighten or replace any of the hardcoded ones, in the same tool:.ext,.ext shape as TELEGRAM_EXPOSED_TOOLS, with entries separated by ;:

TELEGRAM_FILE_EXTENSIONS=send_file:.pdf,.png,.jpg;upload_file:.pdf,.png

Naming a tool that already has a hardcoded default replaces that tool's whole set rather than adding to it; any tool left unnamed keeps its default (so leaving this unset keeps today's behavior unchanged). Extensions are case-insensitive and the leading dot is optional (pdf and .pdf are equivalent). An unknown tool name, a malformed extension, or the same tool named twice aborts startup, the same way a typo in TELEGRAM_EXPOSED_TOOLS does.

This is defence in depth against accidents, not a security boundary. The check reads the final suffix, so an allowlist of .pdf still accepts payload.exe.pdf, and it says nothing about the bytes in the file. What it does reliably block is the careless case: extensionless secrets such as id_rsa or a bare .env. Files with ordinary extensions — .pem, .key, a .json token file, a .sqlite cookie store — are not covered unless you leave them out of the list yourself.

Voice transcription (see Voice transcription above) is off by default in the sense that no transcript is ever fetched unless you ask for one — transcribe_voice is always available, and listings only pick up already-cached transcripts. Enable prefetching or pick an engine explicitly:

TELEGRAM_TRANSCRIBE=on-demand       # off / on-demand (default) / auto
TELEGRAM_TRANSCRIBE_ENGINE=groq     # groq (default), telegram, openai or whisper
GROQ_API_KEY=your_groq_api_key_here # required whenever engine=groq is used

engine=groq requires GROQ_API_KEY; engine=telegram requires Telegram Premium on the account; engine=openai requires TELEGRAM_TRANSCRIBE_OPENAI_URL; engine=whisper requires the whisper extra. TELEGRAM_TRANSCRIBE_MAX_VOICES (default 5) and TELEGRAM_TRANSCRIBE_MAX_SECONDS (default 300) bound how much auto mode prefetches per listing call; TELEGRAM_TRANSCRIPT_CACHE_DIR (default data/transcripts) sets where the SQLite cache is written; TELEGRAM_TRANSCRIBE_GROQ_MAX_MB (default 25) is the largest recording the groq engine will upload.

Telegram rate limits (FloodWaitError) are surfaced transparently to MCP clients and LLM agents with the exact wait duration and an explicit instruction not to retry immediately.

Use TELEGRAM_FLOOD_SLEEP_THRESHOLD to configure Telethon's internal silent-sleep threshold (default: 60 seconds). Set to 0 to disable silent sleeping and let the agent manage all backoff pacing:

TELEGRAM_FLOOD_SLEEP_THRESHOLD=60   # Max seconds Telethon sleeps silently on FloodWait (default 60)

Run the server locally:

uv run main.py

MCP Client Configuration

Claude Desktop extension (.mcpb)

The repository ships a manifest.json, so it can be packed into a Claude Desktop extension and installed without editing any JSON:

npx @anthropic-ai/mcpb pack . telegram-mcp.mcpb

Open the resulting telegram-mcp.mcpb with Claude Desktop (or drag it onto Settings → Extensions) and fill in the API ID, API hash and session string (see Quick Start for generating one). Claude Desktop installs Python and the dependencies itself via uv, and masks the API hash and session string as sensitive fields. The allowed-chats and files-directory fields map to TELEGRAM_ALLOWED_CHAT_IDS and TELEGRAM_ALLOWED_ROOTS. .mcpbignore keeps .env, session files and logs out of the bundle.

Manual configuration

For Claude Desktop or Cursor, point the MCP server at a cloned checkout of this project:

{
  "mcpServers": {
    "telegram-mcp": {
      "command": "uv",
      "args": [
        "--directory",
        "/full/path/to/telegram-mcp",
        "run",
        "main.py"
      ],
      "env": {
        "TELEGRAM_API_ID": "your_api_id_here",
        "TELEGRAM_API_HASH": "your_api_hash_here",
        "TELEGRAM_SESSION_STRING": "your_session_string_here"
      }
    }
  }
}

To expose only read-only tools in Claude Desktop or Cursor, add this to the server env block:

"TELEGRAM_EXPOSED_TOOLS": "read-only"

Or keep read-only as the baseline and allow a few write tools on top:

"TELEGRAM_EXPOSED_TOOLS": "read-only+send_message,reply_to_message"

Alternatively, install this repository directly from GitHub into a virtual environment using a specific release tag or commit:

python -m venv .venv
. .venv/bin/activate
pip install "git+https://github.com/chigwell/telegram-mcp.git@<tag-or-commit>"

Then configure your MCP client to run the installed console script:

{
  "mcpServers": {
    "telegram-mcp": {
      "command": "/full/path/to/.venv/bin/telegram-mcp",
      "env": {
        "TELEGRAM_API_ID": "your_api_id_here",
        "TELEGRAM_API_HASH": "your_api_hash_here",
        "TELEGRAM_SESSION_STRING": "your_session_string_here"
      }
    }
  }
}

Generate a session string without cloning the repo by sourcing this repository from GitHub explicitly:

uvx --from "git+https://github.com/chigwell/telegram-mcp.git@<pinned-release-tag-or-commit>" telegram-mcp-generate-session

Transports

The server speaks three MCP transports, selected with MCP_TRANSPORT:

Value Transport Use case
stdio stdio (default) One dedicated server process per MCP client
http streamable HTTP One shared server for many clients (Claude Code, Codex, Cursor)
sse SSE (legacy HTTP) Clients that only support the deprecated SSE transport

For http and sse, the server binds MCP_HOST:MCP_PORT (default 127.0.0.1:8765); the streamable HTTP endpoint is /mcp, the SSE endpoint is /sse.

If the server is reachable via a domain (e.g. behind a reverse proxy) rather than only 127.0.0.1/localhost, set MCP_ALLOWED_HOSTS (and optionally MCP_ALLOWED_ORIGINS) to enable DNS-rebinding protection and allow that Host header, e.g. MCP_ALLOWED_HOSTS=mcp.example.com. Comma-separated; supports a :* suffix to allow any port. Left unset, DNS-rebinding protection stays off (the historical default).

Prefer http when more than one MCP client (or many coding-agent sessions) will use the server: a single long-lived process holds one Telegram connection, instead of every client spawning its own Telethon session — Telegram throttles and may flag accounts that open many parallel sessions.

Every tool call also has a server-side ceiling of 55 seconds, configured with TELEGRAM_TOOL_TIMEOUT_SECONDS. A timed-out Telegram request returns an explicit MCP error instead of leaving the client waiting indefinitely. The error says that completion is unknown: a write may already have succeeded, so check the destination before retrying. Set the value to 0 only for a deliberately unbounded operator session.

Register the shared server with clients:

# Claude Code
claude mcp add --transport http telegram http://127.0.0.1:8765/mcp

# Codex
codex mcp add telegram --url http://127.0.0.1:8765/mcp

For stdio-only clients, bridge with mcp-remote:

{
  "mcpServers": {
    "telegram-mcp": {
      "command": "npx",
      "args": ["-y", "mcp-remote", "http://127.0.0.1:8765/mcp"]
    }
  }
}

Multi-Account Setup

Use suffixed session variables to configure multiple Telegram accounts:

TELEGRAM_API_ID=your_api_id_here
TELEGRAM_API_HASH=your_api_hash_here
TELEGRAM_SESSION_STRING_WORK=session_string_for_work
TELEGRAM_SESSION_STRING_PERSONAL=session_string_for_personal

Labels are lowercased and become the account parameter value in tools.

If TELEGRAM_SESSION_NAME is set as well (the Docker image and .env.example set it), that session becomes an extra default account when it is logged in; when it is not, the server skips it with a warning on stderr instead of refusing to start.

  • In single-account mode, account is optional.
  • In multi-account mode, write tools require account.
  • Read-only tools fan out to all accounts when account is omitted.

Example prompts:

  • "List my accounts"
  • "Show unread messages from all accounts"

Session pool (one account, several concurrent clients)

To run several MCP clients against the same Telegram account at once (for example the desktop app and a terminal CLI), give each client its own authorized session. Telegram forbids one session (auth key) being used from two IPs simultaneously, so on a VPN or dual-stack host two local clients can collide with AuthKeyDuplicatedError. List several interchangeable session strings in TELEGRAM_SESSION_STRINGS (separated by whitespace, comma or semicolon); each process claims a free one via an advisory file lock, so clients deterministically pick distinct sessions:

TELEGRAM_SESSION_STRINGS=<session A> <session B> <session C>

Generate extra sessions with uv run session_string_generator.py. The pool takes precedence over TELEGRAM_SESSION_STRING for the default account. As an extra safety net, a transient AuthKeyDuplicatedError at connect time (e.g. during a VPN reconnect) is retried with backoff before the server gives up.

Size the pool to the number of clients you actually run concurrently. If every slot is already claimed, the server refuses to start with an explicit error rather than reusing a session another client holds — reuse would make Telegram permanently invalidate that session for both clients.

  • "Send this from my work account to @example"

Sharing one session from one host

Before connecting, every server process takes a per-session lock (see the AuthKeyDuplicatedError entry under Troubleshooting). By default it is exclusive: a second instance for the same session waits briefly for the first to exit and otherwise refuses to start. That lock can only see processes on the same host, and Telegram's rule is about IPs, not processes: instances on one host only collide when they reach Telegram from different IPs (dual-stack or split-tunnel VPN hosts). When they all share one egress IP — a laptop running several MCP clients — you can let them share the session instead of pooling:

TELEGRAM_SESSION_LOCK=shared

Shared instances coexist with each other but never overlap an exclusive one. If you are not sure every client egresses from the same IP, use the pool.

Device Identity

These optional variables control how the client appears in Telegram under Settings > Devices (the active-sessions list):

TELEGRAM_DEVICE_MODEL=Telegram MCP
TELEGRAM_SYSTEM_VERSION=1.0
TELEGRAM_APP_VERSION=1.0

If left unset, Telethon falls back to the host platform (for example arm64). Because these values are re-sent on every connection, a long-running server would otherwise overwrite the name chosen during login on each reconnect, so set them to keep a stable, recognisable device name. The same variables are read both by the session string generator (at login) and by the server (on every connect), so set them in the same place as your other credentials.

Expected Account Check

Set TELEGRAM_EXPECTED_USERNAME (or TELEGRAM_EXPECTED_USERNAME_<LABEL> per account) and the server refuses to start when the session belongs to a different account. The comparison is case-insensitive and ignores a leading @. Unset means no check.

Proxy Support

Route Telegram traffic through a proxy by setting the TELEGRAM_PROXY_* environment variables. Supported types are socks5, socks4, http, and mtproxy.

SOCKS and HTTP proxies require the optional python-socks package:

uv sync --extra proxy
# or
pip install python-socks

Single-account configuration:

TELEGRAM_PROXY_TYPE=socks5
TELEGRAM_PROXY_HOST=127.0.0.1
TELEGRAM_PROXY_PORT=1080
TELEGRAM_PROXY_USERNAME=optional_user
TELEGRAM_PROXY_PASSWORD=optional_pass
TELEGRAM_PROXY_RDNS=true

MTProxy:

TELEGRAM_PROXY_TYPE=mtproxy
TELEGRAM_PROXY_HOST=mtproxy.example
TELEGRAM_PROXY_PORT=443
TELEGRAM_PROXY_SECRET=ee0123456789abcdef...

Per-account overrides use the same _<LABEL> suffix as session variables and take precedence over the unsuffixed defaults:

TELEGRAM_PROXY_TYPE=socks5
TELEGRAM_PROXY_HOST=127.0.0.1
TELEGRAM_PROXY_PORT=1080

TELEGRAM_PROXY_TYPE_WORK=http
TELEGRAM_PROXY_HOST_WORK=proxy.work.example
TELEGRAM_PROXY_PORT_WORK=3128

Misconfigured proxy settings (unknown type, missing host/port, invalid port, missing MTProxy secret, or a missing python-socks package) cause the server to fail fast at startup with a clear error message instead of silently bypassing the proxy.

File Path Security

File-path and media tools (send_file, download_media, upload_file, send_voice, send_sticker, set_profile_photo, edit_chat_photo, and send_album) are registered and supported, but remain strictly disabled by default until allowed roots are configured (safe-by-default to prevent unauthorized file access).

Allowed roots can come from:

  • Environment Variable (TELEGRAM_ALLOWED_ROOTS): Semicolon- (;) or comma- (,) separated paths (also supports colon : on POSIX systems). Ideal for Docker, headless daemon setups, and MCP clients that pass settings via environment.
  • Server CLI arguments: Positional folder paths passed to main.py, used as a server-side fallback.
  • MCP client Roots: Configured directly in supported MCP clients (roots/list).

Security behavior:

  • Client MCP Roots replace server CLI roots when available.
  • Some clients (notably Cursor) return workspace roots as bare absolute paths instead of file:// URIs. That breaks MCP SDK validation of list_roots; the server recovers those absolute paths from the validation error so file-path tools keep working.
  • Empty client Roots are treated as deny-all by default. Some clients implement the Roots capability but advertise an empty list, which disables file tools even when server CLI roots are configured. Set TELEGRAM_ALLOW_SERVER_ROOTS_FALLBACK=1 to fall back to the server CLI roots in that case (opt-in; the default stays deny-all). The same opt-in also applies when list_roots fails unexpectedly and no client paths could be recovered.
  • roots/list times out after TELEGRAM_ROOTS_TIMEOUT_SECONDS (default 10). With server roots configured, TELEGRAM_SERVER_ROOTS_ONLY=1 uses them directly and skips roots/list; client Roots are then ignored.
  • Paths are resolved through real paths and must stay inside an allowed root.
  • Traversal, wildcard-like, shell-like, and null-byte path patterns are rejected.
  • Relative paths resolve under the first allowed root.
  • Downloads default to <first_root>/downloads/ (or directly into <first_root>/ if the root folder is already named downloads / Downloads). Configurable via TELEGRAM_DOWNLOAD_SUBDIR.
  • Size and extension limits are enforced for sensitive media tools. send_file and upload_file have no extension limit by default; see TELEGRAM_FILE_EXTENSIONS above to add one.

Run with allowed roots via environment variable:

# Semicolon- or comma-separated roots
TELEGRAM_ALLOWED_ROOTS="/data/telegram,/tmp/telegram-mcp" uv run main.py

Or run with allowed roots via CLI positional arguments:

uv run main.py /data/telegram /tmp/telegram-mcp

From an MCP client configuration, you can pass roots via env or as arguments after main.py:

{
  "mcpServers": {
    "telegram-mcp": {
      "command": "uv",
      "args": [
        "--directory",
        "/full/path/to/telegram-mcp",
        "run",
        "main.py",
        "/data/telegram",
        "/tmp/telegram-mcp"
      ],
      "env": {
        "TELEGRAM_API_ID": "your_api_id_here",
        "TELEGRAM_API_HASH": "your_api_hash_here",
        "TELEGRAM_SESSION_STRING": "your_session_string_here"
      }
    }
  }
}

Chat Access Privacy (Allowlist)

To restrict AI agents or MCP clients to specific chats only (preventing access to all private or corporate conversations), configure TELEGRAM_ALLOWED_CHAT_IDS:

# Comma-separated list of allowed chat IDs, supergroup IDs, or usernames
TELEGRAM_ALLOWED_CHAT_IDS=12345678,-100123456789,@allowed_channel

When TELEGRAM_ALLOWED_CHAT_IDS is set:

  • list_chats / get_chats: Only chats matching the allowlist are returned; all other conversations remain completely invisible to the agent.
  • Messaging & Chat Tools (send_message, get_messages, get_chat, create_poll, etc.): Any attempt to interact with a chat outside the allowlist is rejected with a structured error message: Access to chat '<chat_id>' is restricted by privacy policy (TELEGRAM_ALLOWED_CHAT_IDS).
  • Search & Drafts (search_global, get_drafts): Global searches and draft listings omit unallowed conversations.
  • Contact, folder & group-admin tools: a chat named by contact_id or group_id (get_last_interaction, get_contact_chats, invite_to_group, promote_admin, demote_admin) is checked like chat_id; get_common_chats, get_contact_chats, get_direct_chat_by_contact and get_folder leave unallowed chats out of their results; create_folder refuses unallowed chat_ids.
  • Incoming Events: Notifications and debounce feeds only process events from allowed chats.

If TELEGRAM_ALLOWED_CHAT_IDS is unset or empty, the server operates in unrestricted mode (default), preserving 100% backward compatibility.

Docker

Build the image:

docker build -t telegram-mcp:latest .

Shared server (recommended)

Run one long-lived container serving streamable HTTP, and point every MCP client at it (see Transports for client registration):

docker run -d --name telegram-mcp --restart unless-stopped \
  --env-file .env \
  -e MCP_TRANSPORT=http \
  -e MCP_HOST=0.0.0.0 \
  -p 127.0.0.1:8765:8765 \
  telegram-mcp:latest

MCP_HOST=0.0.0.0 binds inside the container so the published port works; -p 127.0.0.1:8765:8765 keeps the server reachable only from the local machine — the endpoint is unauthenticated, so never publish it on a public interface.

The bundled Compose file runs the same setup:

docker compose up --build -d

It also mounts ./transcript_cache into the container at /app/data/transcripts so the voice-transcription SQLite cache (see Voice transcription) survives a rebuild instead of living in the container's writable layer. Create it once, owned by the container's appuser (uid 1000), before starting:

mkdir -p ./transcript_cache && chown 1000:1000 ./transcript_cache

One container per client (stdio)

Alternatively, an MCP client can spawn a dedicated container itself:

{
  "mcpServers": {
    "telegram-mcp": {
      "command": "docker",
      "args": ["run", "-i", "--rm", "--env-file", "/full/path/to/.env", "telegram-mcp:latest"]
    }
  }
}

This is fine for a single client, but with several clients (or coding agents that spawn subagent sessions) each one starts its own container and its own Telegram session, which Telegram throttles; a client that exits uncleanly can also leave its container running. Prefer the shared server above in those setups.

For multiple accounts, pass variables such as TELEGRAM_SESSION_STRING_WORK and TELEGRAM_SESSION_STRING_PERSONAL.

Development

The implementation is split into a small compatibility entrypoint and modular package code:

main.py                    # historical entrypoint and compatibility exports
telegram_mcp/runtime.py    # shared MCP setup, account routing, validation, file safety
telegram_mcp/runner.py     # application startup
telegram_mcp/tools/        # tool modules grouped by domain
sanitize.py                # output sanitization helpers
tests/                     # pytest suite

Run tests:

uv run pytest

Run tests with coverage:

uv run pytest --cov --cov-report=term-missing --cov-report=xml

Coverage is configured in pyproject.toml with an 80% minimum gate for deterministic unit-testable core modules. GitHub Actions runs the same coverage command and uploads coverage.xml.

Run formatting checks:

uv run black --check .
uv run flake8 .

Security Notes

  • Never commit .env, session strings, or .session files.
  • A Telegram session string grants access to the account it belongs to.
  • The telegram-mcp package name on PyPI is not controlled by this project. Avoid PyPI-based telegram-mcp install commands unless ownership changes and the package is verified.
  • This repository includes a best-effort startup guard that refuses installed telegram-mcp distributions without a source checkout or direct git/file install record. That guard cannot run when the unrelated PyPI package itself is launched, so use clone-based or explicit git installs.
  • Prefer session strings over file sessions when running multiple server instances.
  • By default, Telegram API calls go directly from your machine/container to Telegram. If TELEGRAM_PROXY_* is configured, Telegram traffic is routed through the configured SOCKS/HTTP/MTProxy proxy instead.
  • User-generated Telegram content is sanitized before being returned to MCP clients.

Prompt Injection Protection

Telegram messages, display names, chat titles, and button labels are untrusted content. The server mitigates prompt-injection risk with:

  • Structured JSON output for user-controlled data where practical.
  • sanitize_user_content(), sanitize_name(), and sanitize_dict() for control-character stripping, invisible-character stripping, and length limits.
  • MCP content annotations marking returned content as user audience data.
  • Tool descriptions that warn clients not to treat returned Telegram fields as model instructions.
  • No brittle keyword-based filtering.

Troubleshooting

  • No Telegram session configured: set TELEGRAM_SESSION_STRING, TELEGRAM_SESSION_NAME, or suffixed multi-account variants.
  • Session is not authorized: run uv run session_string_generator.py --qr outside the MCP server when you can scan from an existing Telegram app, or uv run session_string_generator.py --phone when you need phone-code login. Then set TELEGRAM_SESSION_STRING in .env. The MCP server does not perform interactive phone-code login over stdio, including after a reconnect: a session terminated while the server runs fails with "not authorized after reconnect".
  • Invalid API credentials: verify TELEGRAM_API_ID and TELEGRAM_API_HASH at my.telegram.org/apps.
  • Database is locked: make sure no other process is using the same file session.
  • AuthKeyDuplicatedError / "Another telegram-mcp process is already connected with this session": two processes tried to connect the same Telegram session at once (e.g. an MCP client restarted the connector before the old process exited), which Telegram rejects and can invalidate the session for both. The server now takes an exclusive lock per session before connecting; a second concurrent launch waits briefly (default 20s, override with TELEGRAM_LOCK_GRACE_SECONDS) for the first to release it and otherwise exits without ever calling connect(), instead of racing into a duplicate connection. Retry once only one instance is running — the refusal names the PID holding the lock. If several instances on this host are meant to share one session (all reaching Telegram from the same IP), set TELEGRAM_SESSION_LOCK=shared; see Sharing one session from one host.
  • File tools are disabled: pass allowed roots or configure MCP Roots in your client.
  • File tools wait for roots/list: configure server roots and set TELEGRAM_SERVER_ROOTS_ONLY=1.
  • Path rejected: ensure the path is inside an allowed root and does not use traversal or wildcard patterns.
  • Auth errors after password changes: regenerate your session string.
  • Bot-only tool rejected: regular user accounts cannot manage bot command settings.
  • Need details: check your MCP client logs, terminal output, and mcp_errors.log.

Contributing

  1. Fork and clone the repository.
  2. Install dependencies and git hooks:
    • uv sync
    • uv run pre-commit install --hook-type pre-commit --hook-type pre-push
  3. Create a focused branch.
  4. Add or update tests when behavior changes.
  5. Run checks locally:
    • uv run pre-commit run --all-files
    • uv run pre-commit run --hook-stage pre-push --all-files
  6. Open a pull request with a concise description.

License

This project is licensed under the Apache 2.0 License.

Acknowledgements

Maintained by @chigwell and @l1v0n1. PRs welcome.

Star History

Contributors

Two peas, one chat

Chat with AI about Telegram

Opens a chat primed with Telegram’s README and setup docs from this page, or add it in Cursor to use its tools.

The Cursor button opens Cursor and asks before it adds the server. Fill in any keys it needs in Cursor’s MCP settings.

Peas in a pod

What are the alternatives to Telegram MCP?

How Telegram MCP lines up against its neighbours on the shelf. See every knowledge and work apps server.

  • Telegram MCPThis pea

    Your own Telegram account, as an assistant can use it: chats, search, sending and groups.

    Tools
    147
    Runs on
    Your computer
    Licence
    Apache-2.0
    Stars
    1.9K
  • Slack MCP Server

    Read and search your Slack channels, threads and DMs, with posting off by default.

    Tools
    22
    Runs on
    Your computer
    Licence
    MIT
    Stars
    1.9K
  • iMCP

    A Mac menu bar app that shares Calendar, Messages, Notes, Reminders and more with your assistant.

    Tools
    56
    Runs on
    Your computer
    Licence
    MIT
    Stars
    1.8K
  • OpenLore

    One folder of Markdown that every agent on a team can read, with access rules.

    Tools
    2
    Runs on
    Hosted
    Licence
    Apache-2.0
    Stars
    1.7K
  • Minutes

    Records and transcribes meetings on your device, then lets an assistant search them.

    Tools
    34
    Runs on
    Your computer
    Licence
    MIT
    Stars
    1.6K
  • Garmin Connect MCP

    Ask an assistant about your Garmin activities, sleep, recovery and training.

    Tools
    20
    Runs on
    Your computer
    Licence
    MIT
    Stars
    1.4K
Full docs on GitHubchigwell/telegram-mcp

Something wrong on this page? Tell us.