Skip to content
Conduit

mcp server

telegram-read-mcp

A minimal read-only MCP server that exposes Telegram chat and forum-topic reading via Streamable HTTP

sargue/telegram-read-mcpcommunitylistedunreviewed

Set up

Runs on your computer. Pick your agent and operating system.

Before you start

  • uv (`curl -LsSf https://astral.sh/uv/install.sh | sh`)
~/Library/Application Support/Claude/claude_desktop_config.json
{
  "mcpServers": {
    "telegram-read-mcp": {
      "command": "uvx",
      "args": [
        "telegram-read-mcp"
      ]
    }
  }
}

same format Cursor, GitHub Copilot CLI, Junie, JetBrains AI Assistant, Augment (Auggie CLI), Factory Droid, Kiro, Devin CLI +12

Values in angle brackets are placeholders. Restart Claude Desktop after editing its config.Claude Desktop docs

README

synced Oct 21, 2026

telegram-read-mcp

A minimal read-only MCP server that exposes Telegram chat and forum-topic reading via Streamable HTTP. It uses Telethon user sessions to authenticate with Telegram's API and FastMCP to serve tools over the Model Context Protocol. The server is read-only by default — it never sends messages or modifies any Telegram data. The one exception is an opt-in mark_read tool, which is only available when you set TG_ALLOW_MARK_READ=true (see Marking Chats as Read). Another opt-in tool, get_message_image, saves message images to a local directory so an agent on the same host can open them (see Message Images).

Prerequisites

Installation

git clone <repo>
cd telegram-read-mcp
uv sync

First Login

Telegram requires an authenticated user session before the server can read any data.

  1. Copy .env.example to .env and fill in your credentials:

    cp .env.example .env
    # edit .env and set TG_API_ID and TG_API_HASH
    chmod 600 .env
    
  2. Run the login script:

    uv run python scripts/first-login.py
    
  3. Choose a login method:

    • QR code (recommended): The script displays a QR code in your terminal. On your phone, go to Telegram > Settings > Devices > Link Desktop Device and scan it. This is the most reliable method.
    • Phone number + code: Enter your phone number (with country code, e.g. +34...). Telegram sends a code to your Telegram app (check the "Telegram" service chat). If you have 2FA enabled, you'll also be prompted for your cloud password.
  4. A session file is saved to disk and reused on every subsequent server start. You do not need to log in again unless the session expires or the file is deleted. Restrict it to the current user:

    chmod 600 telegram_read_mcp.session*
    

Running the Server

HTTP mode (default):

scripts/run-http.sh
# or
uv run telegram-read-mcp

Stdio mode (for MCP clients that speak stdio):

scripts/run-stdio.sh
# or
uv run python -m telegram_read_mcp.server --stdio

The HTTP server listens at http://127.0.0.1:8765/mcp by default. The host and port are configurable via environment variables (see Configuration).

MCP Tools Reference

ToolParametersDescription
list_chatsinclude_archived=false, limit=200, query=nullList joined groups and supergroups
get_chatchat_idGet metadata for one chat (includes is_forum flag)
list_topicschat_id, limit=100, query=nullList forum topics (returns is_forum=false for non-forum chats)
get_messageschat_id, limit=50, before_message_id=null, allow_forum_root=falseRead messages from a regular group (rejects forum chats by default)
get_topic_messageschat_id, topic_id, limit=50, before_message_id=nullRead messages from a specific forum topic
search_messageschat_id, query, limit=50Server-side full-text search within a chat
search_topic_messageschat_id, topic_id, query, limit=50Search within a specific forum topic (client-side filtered)
get_message_imagechat_id, message_id, topic_id=nullSave the photo or image document in one message to a local file and return its path. Opt-in: only registered when TG_IMAGE_DIR is set
mark_readchat_id, topic_id=null, max_message_id=nullMark a chat, one forum topic, or all forum topics as read. Opt-in: only registered when TG_ALLOW_MARK_READ=true

Topic Semantics

Understanding how Telegram forum topics map to the API is important for using the topic-related tools correctly.

Forums are supergroups with threads, not separate chats. A Telegram forum supergroup is a single chat entity. Topics are threads that live inside it — they are not independent chats and do not have their own chat IDs.

topic_id comes from list_topics. Call list_topics with the forum's chat_id to discover available topics and their IDs. The topic_id values correspond to Telegram's internal forum topic identifiers.

Message retrieval uses thread/reply semantics. Fetching messages for a topic uses Telethon's reply_to parameter, which maps to Telegram's GetRepliesRequest under the hood. This retrieves messages that are part of a specific thread.

The General topic (topic_id=1) is a special case. The reply_to API may not work for the General topic (the default topic that exists in every forum). When topic_id=1 is requested, the server falls back to filtering messages by reply_to_top_id instead.

Safety scan cap. For the General topic fallback path and for search_topic_messages (which performs client-side filtering), the server scans at most 5000 messages to prevent hanging indefinitely on large chats. Results may be incomplete if there are more than 5000 messages in scope.

Service messages and edge cases. Forum topics may contain service messages (topic creation, title changes, etc.) that appear alongside normal messages. Some edge cases exist around pinned messages and topic-level events.

get_messages rejects forum supergroups by default. Calling get_messages on a forum chat returns an error to prevent accidentally reading the unstructured root stream. Pass allow_forum_root=true to override this and read messages from the forum root anyway.

Message Images

get_message_image gives an agent the pixels of an image it found with get_messages or get_topic_messages. It saves the image to a file in a fixed local directory and returns the absolute path. It never returns the image bytes in the MCP response. Set TG_IMAGE_DIR and restart the server to register it.

Precondition: the path is only useful to a process on the same host that can read TG_IMAGE_DIR, normally because its user is in the directory's group. A remote MCP client gets a path it cannot open.

What it accepts. Pass the chat_id and message_id of a message whose media_type is MessageMediaPhoto or MessageMediaDocument. Photos are downloaded at their largest size. Documents are accepted only when the MIME type is image/jpeg, image/png, image/webp or image/gif and they are not stickers, animations, videos or audio. The downloaded bytes are checked again, and the file extension and mime_type come from that check, not from the sender's file name or MIME type. Web page previews (MessageMediaWebPage), games, invoices, stories and self-destructing media are rejected even when they carry a photo, so the server never fetches anything outside the message itself.

topic_id (optional). For a forum, the message must belong to that topic, using the same rule as the client-side topic filters (see Topic Semantics). Otherwise the tool returns MESSAGE_NOT_FOUND.

No side effects in Telegram. The tool only fetches the message by ID and downloads the file (channels.getMessages or messages.getMessages, then upload.getFile). Nothing is marked as read, sent or edited.

Example call (tools/call arguments):

{"chat_id": -1001234567890, "message_id": 48211, "topic_id": 1873}

Result:

{
  "chat_id": -1001234567890,
  "topic_id": 1873,
  "message_id": 48211,
  "media_type": "MessageMediaPhoto",
  "mime_type": "image/jpeg",
  "size": 183422,
  "width": 1280,
  "height": 960,
  "path": "/var/cache/telegram-read-mcp/tg-3f9a0c1e5b7d4a2e8c6f1b0d9e7a5c3b.jpg",
  "expires_at": "2026-10-02T14:05:31.218Z"
}

Errors come back as {"error": code, "message": ...} like the other tools: NO_IMAGE, UNSUPPORTED_IMAGE_FORMAT, IMAGE_TOO_LARGE, MESSAGE_NOT_FOUND, INVALID_MESSAGE_ID, RATE_LIMITED, and so on (see Error Codes).

Image directory

PropertyValue
PathTG_IMAGE_DIR, resolved once at startup. Every returned path is that directory plus a name the server generates
DirectoryOwned by the server user, group = the readers, mode 2750. The setgid bit makes new files inherit the readers' group
Filestg-<32 random hex>.<jpg|png|webp|gif>, mode 0640. Written as a hidden .part file and renamed only after the content check
TTLTG_IMAGE_TTL_SECONDS (default 3600). The server sweeps every minute, at startup and before each download
LimitsTG_IMAGE_MAX_BYTES per image (checked against the declared size before downloading, and on every chunk), TG_IMAGE_DIR_MAX_BYTES in total, TG_IMAGE_MAX_CONCURRENT downloads at once, TG_IMAGE_RATE_PER_MINUTE calls

The server refuses to write if the directory is not owned by its user, is writable by anyone else, or is accessible to all users, and the sweep only deletes files that match its own naming pattern. It sets file modes explicitly, so a service UMask=0077 does not hide images from the group. Don't put the directory under /tmp (systemd PrivateTmp=true gives the service its own /tmp) or under a home directory the reader cannot traverse. See Systemd Deployment for the setup.

Marking Chats as Read

mark_read is the only tool that changes Telegram state, so it is off by default. Set TG_ALLOW_MARK_READ=true in .env and restart the server to register it. The tool is annotated as not read-only, so MCP clients that ask before non-read-only calls will prompt for it.

What it marks depends on the chat and on topic_id:

Chattopic_idEffectTelegram method
Regular groupomittedWhole chat read up to the latest messagechannels.readHistory / messages.readHistory
ForumsetThat topic read up to its latest message (General is topic_id=1)messages.readDiscussion
ForumomittedEvery topic with unread messagesmessages.readDiscussion per topic

This matches what Telegram Desktop does for "Mark as read". Passing topic_id for a non-forum chat returns NOT_A_FORUM.

max_message_id avoids marking messages you haven't seen. Messages can arrive between reading a chat and marking it read. Pass the newest message ID you actually read, and only messages up to and including that ID are marked. In "all topics" mode the cap applies to every topic.

Partial failures in "all topics" mode. Each topic is a separate request. If one fails, the rest are still marked and the failure is listed in failed. The tool makes no separate calls to clear mention or reaction badges.

Read receipts. In small groups, senders can see that you have read their messages.

Example result for a forum with no topic_id:

{
  "chat_id": -1001234567890,
  "is_forum": true,
  "marked": [
    {"topic_id": 1, "read_max_id": 48211},
    {"topic_id": 1873, "read_max_id": 48207}
  ],
  "failed": []
}

For a regular group, marked has one entry with topic_id: null. It is empty if the chat has no messages or all topics are already read.

Error Codes

CodeMeaning
CHAT_NOT_FOUNDChat ID not found or not accessible with the current session
NOT_A_FORUMThe chat exists but is not a forum-enabled supergroup
TOPIC_NOT_FOUNDTopic ID not found in the specified forum
INVALID_LIMITLimit is out of range — must be between 1 and 200
TELEGRAM_ERRORAn error was returned by the Telegram API
TELEGRAM_AUTH_REQUIREDThe session is not authorized — run first-login.py
FORUM_ROOT_REJECTEDTried to read a forum chat with get_messages without setting allow_forum_root=true
INVALID_MESSAGE_IDmax_message_id (mark_read), or message_id or topic_id (get_message_image), is not a positive message ID
MESSAGE_NOT_FOUNDThe message does not exist in the chat, or is not in the given topic_id
NO_IMAGEThe message has no photo or image document (text, web page preview, video, sticker, self-destructing media, ...)
UNSUPPORTED_IMAGE_FORMATThe image is not JPEG, PNG, WebP or GIF, or the downloaded bytes are not a valid image of those types
IMAGE_TOO_LARGEThe image is larger than TG_IMAGE_MAX_BYTES
IMAGE_STORAGE_FULLSaved images already use TG_IMAGE_DIR_MAX_BYTES. Retry after older images expire
IMAGE_DIR_UNAVAILABLETG_IMAGE_DIR is missing, not writable, or has unsafe ownership or permissions
RATE_LIMITEDMore than TG_IMAGE_RATE_PER_MINUTE image requests in the last minute, or Telegram asked to wait (flood wait)

Configuration

All configuration is via environment variables, typically stored in .env.

VariableRequiredDefaultDescription
TG_API_IDYes—Telegram API ID from my.telegram.org
TG_API_HASHYes—Telegram API hash from my.telegram.org
TG_SESSION_PATHNotelegram_read_mcpPath to the session file, without the .session extension
MCP_HOSTNo127.0.0.1Address the HTTP server binds to
MCP_PORTNo8765Port the HTTP server listens on
TG_ALLOW_MARK_READNofalseSet to true to register the mark_read tool, the only tool that changes Telegram state
TG_IMAGE_DIRNoemptyAbsolute path of the image directory. Setting it registers get_message_image
TG_IMAGE_TTL_SECONDSNo3600Seconds before a saved image is deleted
TG_IMAGE_MAX_BYTESNo10485760Largest image to download (10 MiB)
TG_IMAGE_DIR_MAX_BYTESNo268435456Total size of saved images (256 MiB)
TG_IMAGE_MAX_CONCURRENTNo2Image downloads running at the same time
TG_IMAGE_RATE_PER_MINUTENo30get_message_image calls allowed per minute

Systemd Deployment

To run the server as a system service:

  1. Create a dedicated system user:

    sudo useradd -r -s /bin/false telegram-mcp
    
  2. Copy only the tracked project files to /opt/telegram-read-mcp:

    sudo mkdir -p /opt/telegram-read-mcp
    git archive --format=tar HEAD | sudo tar -x -C /opt/telegram-read-mcp
    sudo chown -R telegram-mcp:telegram-mcp /opt/telegram-read-mcp
    

    This avoids copying local-only files such as .env, .venv, .git, and *.session.

  3. Sync the project environment:

    cd /opt/telegram-read-mcp
    sudo -u telegram-mcp uv sync --frozen
    
  4. Copy your .env file and run the first login as the service user:

    sudo cp .env /opt/telegram-read-mcp/.env
    sudo chown telegram-mcp:telegram-mcp /opt/telegram-read-mcp/.env
    sudo chmod 600 /opt/telegram-read-mcp/.env
    cd /opt/telegram-read-mcp
    sudo -u telegram-mcp uv run python scripts/first-login.py
    sudo chmod 600 /opt/telegram-read-mcp/*.session*
    
  5. Install the systemd unit:

    sudo cp systemd/telegram-read-mcp.service /etc/systemd/system/
    sudo systemctl daemon-reload
    
  6. Optional: set up the image directory for get_message_image. Create a group for the users allowed to read the images and add the MCP client's user to it (or use that user's own primary group, which avoids restarting the client to pick up a new group):

    sudo groupadd --system telegram-mcp-images
    sudo usermod -aG telegram-mcp-images <client-user>
    sudo cp systemd/telegram-read-mcp.tmpfiles /etc/tmpfiles.d/telegram-read-mcp.conf
    sudo systemd-tmpfiles --create /etc/tmpfiles.d/telegram-read-mcp.conf
    echo 'TG_IMAGE_DIR=/var/cache/telegram-read-mcp' | sudo tee -a /opt/telegram-read-mcp/.env
    

    The unit already lists /var/cache/telegram-read-mcp in ReadWritePaths=, which ProtectSystem=strict requires. For a unit installed before this, add it with a drop-in instead of editing the unit:

    sudo mkdir -p /etc/systemd/system/telegram-read-mcp.service.d
    printf '[Service]\nReadWritePaths=-/var/cache/telegram-read-mcp\n' | sudo tee /etc/systemd/system/telegram-read-mcp.service.d/images.conf
    sudo systemctl daemon-reload
    
  7. Enable and start the service:

    sudo systemctl enable --now telegram-read-mcp
    

Check status with sudo systemctl status telegram-read-mcp and follow logs with journalctl -u telegram-read-mcp -f.

Tests

uv run pytest

The unit tests need no credentials. tests/test_get_message_image_integration.py runs against a running server and is skipped unless you point it at one. Run it as the user that will open the images, on the server host, with the IDs of an existing photo message:

TG_TEST_MCP_URL=http://127.0.0.1:8765/mcp \
TG_TEST_IMAGE_CHAT_ID=<chat id> TG_TEST_IMAGE_MESSAGE_ID=<message id> \
TG_TEST_IMAGE_TOPIC_ID=<topic id, optional> \
uv run pytest tests/test_get_message_image_integration.py -v

It only reads: it never calls mark_read, and it checks that the chat's unread count did not drop.

uv Notes

  • uv sync creates and maintains the local .venv for this project.
  • uv.lock should be committed so deploys can use uv sync --frozen.
  • uvx is not the primary deployment path here. The server uses a project-local environment because it depends on a persistent session file and is intended to run as a long-lived service.
↑↓ navigate openesc close