telegram-mcp: an unauthenticated endpoint and a version a major behind
Telegram MCP server powered by Telethon to let MCP clients read chats, manage groups, and send/modify messages, media, contacts, and settings.
At a glance
- What is it?
- telegram-mcp gives an MCP client 80+ tools over a real Telegram account through Telethon, covering messages, groups, contacts, media and voice transcription. The compose file publishes an unauthenticated HTTP endpoint and says so in a comment, the packaging metadata declares 2.0.1 while the tags sit at 3.2.62, and the contact matcher is built so that only an exact saved wording ever sends.
- Who is it for?
- Understand what you are installing before anything else. This server puts a full Telegram account, including the power to send messages as you and administer groups you own, behind an MCP endpoint that the compose file itself calls unauthenticated, with a loopback publish on 127.0.0.1:8765 as the only protection.
- Can I use it commercially?
- Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 2 days ago.
- What is it written in?
- Mainly Python, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 4, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The packaging says 2.0.1 while the tags are at 3.2.62
`pyproject.toml` declares `version = "2.0.1"` and classifies the project as `Development Status :: 4 - Beta`. The releases recorded for this project are v3.2.62 and v3.2.61, both on 2026-10-03 and about five minutes apart, and v3.2.60 on 2026-09-30. So the declared package version and the tag stream are a major version apart, and the cadence is high-frequency patch bumping rather than the occasional release.
Whatever version number the build system uses, the repository is unambiguously the newer thing, and the gap is worth resolving deliberately rather than discovering it from a pip install resolving 2.0.1 while your checkout is three minor lines ahead. The other version-shaped detail in the same file is the classifier set: `requires-python` is `>=3.10` and the classifiers name Python 3.10, 3.11 and 3.12. There is no 3.13 classifier, while the production image is built `FROM python:3.13-alpine`, so the version the container runs is one above the highest version the packaging claims to support.
The project URLs are also inward-facing, naming a Homepage and a Bug Tracker that both point back at the repository rather than at a documentation site, and no separate homepage is recorded for the project.
The image installs requirements.txt, so neither optional extra is in it
`pyproject.toml` defines two optional dependency groups with comments explaining exactly when each is needed. The `proxy` group is required only when `TELEGRAM_PROXY_TYPE` is set to socks5, socks4 or http, and brings `python-socks`. The `whisper` group is required only for the local transcription engine selected by `TELEGRAM_TRANSCRIBE_ENGINE=whisper`, and brings `faster-whisper` together with a ceiling on `av`.
The container never installs either one. Its builder stage does this:
COPY requirements.txt ./
RUN pip install --no-cache-dir --upgrade pip && \
pip install --no-cache-dir --prefix=/install -r requirements.txtand `requirements.txt` is a plain transcription of the eight core dependencies in `pyproject.toml`: dotenv, httpx, mcp with the cli extra, pillow, python-dotenv, python-json-logger, qrcode and telethon. No extras, no groups. So proxy support and local transcription are features you can install with pip and then find missing from a container built from this repository, and the two requirements files have to be kept in step by hand because nothing checks they agree.
The `av<19` ceiling in the whisper group is worth reading on its own. The comment records that faster-whisper 1.2.1 passes `av.open(metadata_errors=...)`, which PyAV 19 removed, that 18.1.0 is the newest version that works, and that the pin should be dropped once a faster-whisper release includes SYSTRAN/faster-whisper#1495. A transitive dependency pinned below a major release, with the upstream issue named, is a fair summary of what local Whisper support costs right now.
The endpoint is unauthenticated and the only guard is a loopback publish
The compose file states the security posture in a comment rather than leaving you to infer it:
environment:
MCP_TRANSPORT: http
MCP_HOST: 0.0.0.0
# The endpoint is unauthenticated — publish on localhost only.
ports:
- "127.0.0.1:8765:8765"Two things are happening at once. Inside the container the server binds `0.0.0.0`, because the comment explains that is required for a published port to work at all. Outside it, Docker publishes only on `127.0.0.1`, which is what keeps the endpoint off the network. Remove that host binding and the same server becomes reachable from the LAN with no credential of any kind.
The transport choice has its own reason. A second comment explains that streamable HTTP is served so one long-lived container can be shared by all MCP clients, as opposed to a per-client stdio process. That is a sensible design for multi-client use and it is also what makes the missing authentication matter: with stdio the client spawns the server and inherits its environment, while here any process on the host can open a connection. The service also carries `restart: unless-stopped`, so the exposure, once created, persists across reboots until the compose file changes.
The transcript cache is plaintext personal chat and asks for its own retention
One volume is mounted by default, `./transcript_cache:/app/data/transcripts`, and the comment above it is unusually specific about what lives there. It is a SQLite cache of voice and video-note transcripts, and it holds plaintext personal-chat transcripts, so the directory is told to get its own backup and retention policy rather than ride along with a general backup. The host directory has to be readable and writable by uid 1000, the `appuser` the production image runs as, and the suggested setup is a `mkdir -p` plus a `chown 1000:1000`.
The consequence of not mounting it is stated too: without the mount the cache lives only in the container's writable layer and is lost on every `docker compose build`. So the choice is plaintext transcripts surviving rebuilds, or transcripts that do not.
A second persistence concern sits commented out, and it is a different one. The `./telegram_sessions:/app` volume is there for persisting the Telegram session file, and the comment says to mount it if you are not using `TELEGRAM_SESSION_STRING`. The session file is the account credential and the transcript cache is derived content, and they are kept in different places on purpose. The repository backs this up with a session-string generator exposed as a console script, a migration module, and a sanitization module, all four listed as top-level modules in the packaging configuration.
Only an exact saved contact wording ever sends
This is the most carefully argued design decision in the project, and the reasoning is stated rather than assumed. `set_contact_alias` teaches the server what you call someone, and every tool that takes a `chat_id` understands that name from then on. A contact can carry any number of aliases, so saving both `андрей бекендер` and `бекендер` makes either resolve.
Only an exact saved wording sends anything. A wording that merely resembles a saved alias, `Андрею бекендеру` against a saved `андрей бекендер`, is matched too, but only to make a suggestion: the tool sends nothing and asks you to confirm the contact by name. The stated reason is that `Лена` and `Леня`, or `Иван` 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 or no the first time and nothing after. `TELEGRAM_CONTACT_FUZZY=0` removes the suggestions too.
The other failure cases all send nothing and return a structured instruction telling the agent what to ask you, what to save with `set_contact_alias`, and to retry once: an unknown reference, one that resembles a single contact, one that matches several, and one pointing at a contact that no longer resolves. `list_contact_aliases` shows one row per person with all their aliases so a wrong memory is visible, `delete_contact_alias` forgets one, and repointing an alias at someone else requires `replace=True`. The save path itself refuses anything it would have to guess at, accepting only an @username, a phone, a numeric ID, or an alias already confirmed for that person.
Rich formatting needs Premium and fails without sending anything
`send_message`, `reply_to_message` and `edit_message` take two families of formatting. The classic modes are `parse_mode='md'` and `parse_mode='html'`. The server-side rich modes are `parse_mode='rich'`, `rich_markdown` and `rich_html`, giving full Markdown and HTML with tables, headings, formulas and collapsible sections.
The rich modes require Telegram Premium on the account, Premium is re-checked on every call, and without it nothing is sent. Instead of a partial or mangled message the tool returns a structured `telegram_premium_required` result, which is the shape an agent needs in order to reformat with the classic modes and retry on its own. Re-checking rather than caching the entitlement means an account that gains or loses Premium changes behaviour immediately, at the cost of a round trip per message. All three tools also accept `format_date` to render a date as a tappable chip.
A smaller contract in the same area is worth knowing because it is the kind of thing an agent misreads. `get_message_reactions` returns an empty list for a message that has no reactions, rather than null or an error, and reusing a custom reaction means passing the `custom:<document_id>` value it returned straight back to `send_reaction`.
Four transcription engines with different privacy and different failure rates
`transcribe_voice(chat_id, message_id, engine=None)` offers four engines, and the trade between them is spelled out rather than left to inference.
`groq` is the default. It uploads the recording to Groq's hosted `whisper-large-v3-turbo`, which leaves the server and costs a download plus upload on every call, and it is chosen because it does not drop the last few words the way native transcription does. It needs `GROQ_API_KEY`. Groq caps a single upload, so a recording larger than `TELEGRAM_TRANSCRIBE_GROQ_MAX_MB`, default 25 and described as the free-tier limit, is refused locally with a `too_large` error that names its size, rather than being downloaded and then rejected by the API.
`telegram` is native Telegram Premium transcription. It is free, it never leaves Telegram, and it empirically drops the last speech segment in roughly two of three recordings. Long recordings come back `pending` and are polled automatically. `openai` targets any OpenAI-compatible `/audio/transcriptions` endpoint, including OpenAI itself, a self-hosted Parakeet or speaches server, LocalAI, or a vLLM Whisper deployment, configured through `TELEGRAM_TRANSCRIBE_OPENAI_URL`, which accepts either an API base such as `https://api.openai.com/v1` or a full transcriptions URL. The fourth is the local engine, selected by `TELEGRAM_TRANSCRIBE_ENGINE=whisper` and installed through the optional group.
Editorial conclusion
Understand what you are installing before anything else. This server puts a full Telegram account, including the power to send messages as you and administer groups you own, behind an MCP endpoint that the compose file itself calls unauthenticated, with a loopback publish on 127.0.0.1:8765 as the only protection. Run it that way, and understand that anything on your machine can reach it. Past that, the design decisions are careful in ways worth knowing: contact aliases send only on an exact saved wording and offer declension matches as suggestions rather than actions, rich formatting is re-checked for Premium on every call and returns a structured error instead of sending something partial, and a recording too large for your transcription tier is refused locally with its size rather than uploaded and thrown away by the API. Check the version you actually run, since the packaging metadata says 2.0.1 and the tags are at 3.2.62, and check whether your install has the extras, because the container builds from requirements.txt and picks up neither the proxy support nor the local whisper engine. And if you take the HTTP transport off loopback, you are publishing your Telegram account, so put your own authentication in front of it before you do.
Frequently asked questions
How does telegram-mcp decide which contact to send a message to?
Only an exact saved wording sends. A wording that merely resembles a saved alias, such as a declined form of a name, is matched only to make a suggestion, so the tool sends nothing and asks you to confirm the contact by name. Confirming saves that wording as its own alias, and TELEGRAM_CONTACT_FUZZY=0 drops the suggestions too.
Does telegram-mcp need Telegram Premium?
For two things. Server-side rich formatting modes require Premium and are re-checked on every call, returning a structured telegram_premium_required result instead of sending anything. Native Telegram transcription also requires it, is free, never leaves Telegram, and empirically drops the last speech segment in roughly two of three recordings.
How is the telegram-mcp HTTP endpoint secured?
The compose file states that the endpoint is unauthenticated and publishes it on 127.0.0.1:8765 only, while setting MCP_HOST to 0.0.0.0 inside the container so the published port works. It serves streamable HTTP so that one long-lived container can be shared by all MCP clients.
Where does telegram-mcp store contact aliases?
In ${XDG_STATE_HOME:-~/.local/state}/telegram-mcp/aliases.json, written owner-only and atomically. TELEGRAM_ALIASES_FILE overrides the path, and a pre-existing aliases.json next to the code is still read as a fallback.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/chigwell-telegram-mcp)