tgread
Provides read-only access to a Telegram account's channels, groups, and direct messages, with tools for listing chats, reading message history, searching messages, and viewing chat metadata. Deliberately exposes no write, edit, delete, or admin capabilities.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@tgread summarize recent messages from my Telegram channel"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
tgread
A read-only Telegram reader — a CLI first, and an MCP server second. Fifteen tools for reading your channels, groups and DMs, and not one that can write to them.
./install.sh # pinned venv + launcher + MCP registration
tgread login # api_id/api_hash, phone, code, 2FA
tgread tools # the catalogue
tgread read_chat --chat @durov --limit 5 # …and every tool runs from a shellThe CLI comes first
Every tool is a subcommand; the MCP server is a second front end onto the same handlers. That ordering is the point. A tool you can only reach through an MCP client is a tool you cannot test against a real account, and "the code type-checks" is not evidence that a Telegram result unpacks the way you assumed.
tgread tools # catalogue, with required flags
tgread read_chat --help # one tool's arguments
tgread read_chat --chat @durov --limit 5 # JSON on stdout
tgread search_messages --query "postgres" | jq '.messages[].url'Flags are generated from each tool's inputSchema — the same object advertised
over MCP — so the two surfaces cannot drift. Adding a property to a schema adds
a flag; there is no second place to update. Types come from the schema too:
--limit 5 is coerced to an integer, --ids 1,2,3 to an array, --admins-only
is a bare boolean, and --kind banana is rejected against the enum.
exit | meaning |
0 | success; a JSON document on stdout, diagnostics on stderr |
1 | the tool ran and failed — not logged in, chat not visible, no such thread |
2 | usage error — unknown flag, bad type, missing required argument |
3 | a read tool attempted a write. Should be unreachable; it means a bug |
4 | an unexpected error escaped a tool — also a bug, never a stack trace |
CLI output carries no UNTRUSTED CONTENT banner. That envelope exists to frame
the payload for a model; a human or a jq pipeline wants the document itself.
TGREAD_TOOLS restricts what an agent sees, not what you can run — the person
at the terminal is not the threat it exists for. tgread tools marks anything
currently hidden from agents.
Related MCP server: Telegram MCP
As an MCP server
You never run tgread serve yourself. Claude Code spawns it as a child
process and speaks JSON-RPC over its stdin/stdout, dispatching to the same
handlers the CLI calls; install.sh registers it so that happens automatically:
claude mcp add --scope user tgread -- ~/.local/bin/tgread serveRestart Claude Code (or /mcp → reconnect) and the tools appear. To check:
claude mcp list # tgread: … - ✔ Connected
tgread status # account, session validity, tool surfaceThose two answer different questions, and the difference bites people:
Connected only means the process started and completed the MCP handshake.
It says nothing about whether you are logged in — connection to Telegram is
lazy, on the first tool call. tgread status is the one that talks to Telegram.
sequenceDiagram
participant C as Claude Code
participant S as tgread serve
participant T as Telegram
C->>S: spawn (stdio)
C->>S: initialize / tools/list
S-->>C: 4 read tools
Note over S,T: no connection yet — login is not needed to start
C->>S: tools/call read_chat
S->>T: connect, reuse stored auth_key
T-->>S: history
S-->>C: UNTRUSTED envelope + JSONAny other MCP client works too — it is a plain stdio server:
{ "mcpServers": { "tgread": { "command": "/home/you/.local/bin/tgread",
"args": ["serve"] } } }To drive it by hand while debugging, pipe JSON-RPC at it (TGREAD_LOG=DEBUG
puts diagnostics on stderr; stdout stays pure protocol):
printf '%s\n' \
'{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{}}}' \
'{"jsonrpc":"2.0","id":2,"method":"tools/list"}' | tgread serveWhy not one of the existing ones
There are good community servers — chigwell/telegram-mcp has 1.5k stars, 30 contributors and real release hygiene. The reason to write this one is not distrust of that code. It is that the honest gain from self-writing is tool surface and reviewability, not dependency count, and those are the two things that matter when the process holds a Telegram session and feeds an agent attacker-controlled text.
community server | tgread | |
MTProto | Telethon | Telethon — same, and rightly so |
resolved packages | 44 | 5 ( |
MCP layer |
| ~200 lines of stdio JSON-RPC in this repo |
read tools | ~15 | 15 |
runnable without an MCP client | no | yes — every tool is a subcommand |
write tools | send, edit, delete, forward, react, join, admin | none |
write enforcement | by convention | at the transport chokepoint |
code to review before trusting it | ~3,000 lines across 30 contributors | one file you can read in a sitting |
Writing MTProto by hand would be reckless — Telethon is the crypto, the DC migration and the reconnect logic. So it stays. Everything above it is ours.
The threat model
Reading channels means text an attacker chose enters an agent that holds a shell — and, usually, whatever else you have connected: mail, notes, cloud credentials. That is the dominant risk here, and it is not fixed by who wrote the server. Choosing a server with no write surface is one of the few mitigations that does not depend on the model behaving well.
flowchart TD
A["hostile channel post<br/>'ignore previous instructions…'"] --> B["tgread read_chat"]
B --> C["UNTRUSTED envelope<br/>wrapped around every payload"]
C --> D["agent context"]
D --> E{"agent tries to act on it"}
E -->|"send / delete / join"| F["no such tool exists<br/>tools/call → isError"]
E -->|"raw TL request"| G["guard at _call → WriteBlocked"]
E -->|"summarise for the user"| H["fine — this is the intended path"]
style F fill:#1f6f43,color:#fff
style G fill:#1f6f43,color:#fffThree layers, in increasing order of how much they'd survive a bug:
No write tool is advertised. An injected instruction has nothing to call.
Every payload is wrapped in an
UNTRUSTED CONTENTbanner naming it as data, not instructions — including chat titles and bios, which are attacker-controlled too.The transport guard. Telethon funnels every outbound TL request through
TelegramClient._call(68 internal call sites reach it viaawait self(req), and__call__is a one-line delegate).ReadOnlyClientoverrides it. A bug in this file still cannot mutate the account.
The guard fails closed: a request is refused unless its TL class name starts
with Get/Search/Resolve/Check/Find, or is on a nine-entry
infrastructure allowlist. Three read-shaped requests are denied by name
because they have effects other people can observe — GetMessagesViews
(bumps the public view counter), GetBotCallbackAnswer (presses an inline
button), GetInlineBotResults (queries a bot as you). Nested requests are
walked, so a write cannot ride inside an allowed InvokeWithLayer wrapper.
flowchart LR
R["TL request"] --> W["walk nested .query"]
W --> D{"in EXPLICIT_DENY?"}
D -->|yes| X["WriteBlocked"]
D -->|no| I{"in INFRA_ALLOW?"}
I -->|yes| P["to the wire"]
I -->|no| V{"starts with Get/Search/<br/>Resolve/Check/Find?"}
V -->|yes| P
V -->|"no — incl. every<br/>name we've never seen"| X
style X fill:#8b2020,color:#fff
style P fill:#1f6f43,color:#ffftgread check runs this offline: 26 write requests blocked, 17 reads allowed,
unknown names fail closed, nesting checked. No network, no session, no
credentials.
Tools
The surface is as wide as MTProto's read paths allow — narrowing it is the
deployment's job, not the server's (see below). Every tool takes a chat as a
@username, a t.me link, or a numeric id from list_chats.
Identity and discovery
Tool | What |
| which account this server is signed in as |
| dialogs, filterable by name and by kind |
| Telegram's public directory — finds channels never joined, split into already-known and strangers |
| chat folders (dialog filters) and their sizes |
| saved contacts |
Reading
Tool | What |
| history, oldest-first, paged by message id or date |
| specific ids, or a window around one — what a search hit was replying to |
| replies under a post: a channel item's comment section |
| pinned messages — usually a chat's rules and announcements |
Search
Tool | What |
| full text, in one chat or across everything the account sees |
| history filtered by attachment kind, via Telegram's server-side index |
People and metadata
Tool | What |
| kind, member count, description, verified/scam flags |
| participants with roles, searchable, |
| who reacted to a message, and with what |
| groups and channels shared with a given user |
Three things these deliberately do not do, despite being adjacent to tools that would:
No media download.
list_mediareturns captions and metadata; fetching attachments would mean writing attacker-chosen bytes to the agent's disk.Reading never marks as read.
messages.ReadHistoryis a write and the guard blocks it, so the unread badges on your phone stay exactly as they were.Nothing joins, reacts or presses.
search_public_chatsfinds channels without joining them;get_reactionsreads reactions without adding one;GetBotCallbackAnswer— which would press an inline button — is denied by name even though it is spelled like a read.
Restricting the tool surface
The server implements everything; the deployment decides what an agent sees. Those are separate concerns, so they are separate settings — a tool you might want next month should not have to be a tool your agent can reach today.
Set TGREAD_TOOLS in config.env (or the environment) to a comma-separated
allowlist. Unset means all fifteen, which is the right default: the cost of
a read tool is context, not risk, and the risky operations were never
implemented.
TGREAD_TOOLS=list_chats,read_chat # just follow some channels
TGREAD_TOOLS=list_chats,read_chat,search_messages,get_messages # + researchRestricted tools are not advertised in tools/list and are refused again
on the call path. That ordering matters: a denied-but-advertised tool still
costs context and is still something a model can be argued into attempting,
whereas a tool that was never listed does not exist as far as the model is
concerned. Same reason there are no write tools at all — absence beats denial.
An unknown name is fatal at startup, not a warning. A typo that silently fell back to "all tools" would widen the surface at exactly the moment someone was trying to narrow it.
There is no YAML config, deliberately: a YAML parser means PyYAML, and
5 dependencies is the point of this repo. config.env is KEY=VALUE.
Three places can narrow what an agent reaches, weakest last:
Layer | Where | Format |
Not advertised |
|
|
Advertised, denied | Claude Code | JSON |
Per-subagent |
| YAML |
Operational notes
Use a secondary account. Userbots (any MTProto client that is not the official app) are ToS-ban-able. This risk is identical for every server here.
The session file is a bearer token for the whole account. Changing your Telegram password does not invalidate it. Only
tgread logout— which revokes server-side before deleting locally — or Settings → Devices does. Treat it like an SSH private key.pip install telegram-mcpis not this, and not chigwell's either. That PyPI name belongs to an unrelated project; passingTELEGRAM_API_ID/TELEGRAM_API_HASHto it would hand your credentials to third-party code. Nothing here is published to PyPI on purpose.State lives in one directory —
$TGREAD_STATE_DIR, default~/.local/state/tgread, mode 0700, holdingconfig.env(0600) andtgread.session(0600). One directory to chmod, to back up, to destroy.tgread statusflags it if the permissions drift.install.shusesuv sync --frozen— it installs exactly the versions in the committeduv.lockand fails rather than re-resolving. A resolver that quietly picks up a fresh upstream release is how a compromised package reaches a process holding your session.
Layout
Path | What |
| the whole server: guard, tools, JSON-RPC loop, CLI |
| launcher — execs the pinned venv's interpreter |
| the pin |
| venv, symlink, |
| 45 offline tests — guard, catalogue, protocol, CLI, hygiene |
| every tool against the real account; skips cleanly with no session |
Commands
tgread tools the catalogue, and what agents see of it
tgread <tool> [--flag value ...] run one tool, JSON on stdout
tgread <tool> --help that tool's arguments
tgread login interactive: API credentials, phone, login code, 2FA
tgread status who am I, is the session valid, are permissions sane
tgread logout revoke server-side, then delete locally
tgread check offline self-test of the read-only guard (no network)
tgread serve speak MCP over stdio — what Claude Code runsTwo test suites, split by what they can actually prove:
./test-tgread.sh # 45 tests, no network/session/credentials needed
./test-tgread-live.sh # every tool against your account; skips without a sessionThe offline suite covers the guard, the catalogue, the protocol and the CLI — the properties that must hold before this is pointed at an account. The live suite covers the half it structurally cannot: that a TL result unpacks the way the code assumes. It is read-only like everything else, so it is safe to re-run.
It earned its keep on the first run, catching get_reactions crashing with
BroadcastForbiddenError — broadcast channels expose per-emoji totals but not
who reacted. Telegram declining a read is a normal answer, so every tool now
funnels through one seam that turns any RPC refusal into a readable error.
WriteBlocked is deliberately not caught there: that one really is a bug.
login and logout use a plain client, not the guarded one: the guard exists
to constrain the agent, not the human at the terminal establishing or
revoking the session. Everything the MCP server touches goes through
ReadOnlyClient.
Available Tools
15 toolschat_infoA
Metadata for one chat: kind, member count, description, verification flags. Useful for judging whether a channel is what it claims to be. Read-only.
Inputs: chat (required).
common_chatsA
Groups and channels the account shares with a given user. Read-only.
Inputs: user (required), limit.
get_messagesA
Fetch specific messages by id, or the conversation around one — use around with context to see what a search hit was replying to. Deleted or inaccessible ids are simply absent from the result. Read-only.
Inputs: chat (required), ids, around, context.
get_reactionsA
Who reacted to a message, and with which emoji. Read-only — it adds no reaction.
Inputs: chat (required), message_id (required), limit.
list_chatsA
List the Telegram chats, channels and groups this account can see, newest activity first. Use it to find the id or @username to pass to the other tools. Read-only.
Inputs: kind, limit, query.
list_contactsA
The account's saved contacts, optionally filtered by name. Read-only.
Inputs: limit, query.
list_foldersA
The account's chat folders (Telegram 'dialog filters') and their sizes. Read-only.
Inputs: no named parameters.
list_mediaA
Messages in a chat filtered by attachment kind, using Telegram's own server-side index. Returns metadata and captions only — there is no download tool. Read-only.
Inputs: chat (required), kind (required), limit.
list_membersA
Participants of a group or channel, searchable, with their roles. Broadcast channels hide this from non-admins — admins_only still works there. Read-only.
Inputs: chat (required), limit, query, admins_only.
list_pinnedB
Pinned messages in a chat — usually its rules, links or announcements. Read-only.
Inputs: chat (required), limit.
read_chatA
Read recent messages from one channel, group or conversation, returned oldest-first. Accepts a @username, a t.me link, or a numeric id from list_chats. Read-only: reading here does not mark anything as read.
Inputs: chat (required), limit, since, before, +2 more.
read_threadA
The replies under one post — a channel item's comment section, or a reply chain in a group. Only posts with a linked discussion group have one. Read-only.
Inputs: chat (required), message_id (required), limit.
search_messagesA
Full-text search over message history. With chat, searches that one chat; without it, searches every chat this account can see. Read-only.
Inputs: query (required), chat, limit, since, +1 more.
search_public_chatsA
Search Telegram's public directory by name — finds channels and groups the account has never joined. Results are split into ones already known to this account and strangers. Read-only; does not join anything.
Inputs: query (required), limit.
whoamiA
The Telegram account this server is signed in as. Read-only.
Inputs: no named parameters.
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
Loading tool schema changelog…
TDQS
Scored across 15 tools
Each tool targets a distinct read operation or resource: chat metadata, message lists, specific message fetches, threads, media, members, reactions, contacts, folders, and shared chats. Even the message-related tools (read_chat, get_messages, read_thread) are clearly separated by scope and purpose.
The vast majority of tools follow a clear verb_noun pattern (list_*, get_*, read_*, search_*), with three exceptions: chat_info, whoami, and common_chats. These deviations are minor and still readable, but they prevent a perfect consistency score.
At 15 tools, the server sits at the upper edge of the well-scoped range, and every tool earns its place for a read-only Telegram interface. The count is substantial but not bloated, with each tool covering a distinct aspect of the domain.
The read surface is remarkably comprehensive: chat enumeration, message retrieval, search, threads, pinned messages, media, members, reactions, contacts, folders, public directory search, and account info are all present. The only excluded capability, media downloading, is explicitly documented as out of scope, so there are no hidden gaps.
Related MCP Connectors
Telegram bridge for your MCP-compatible agent. Bidirectional, no LLM in our stack.
Unofficial Telegram MCP server — read, search, reply and react in your own Telegram account.
WhatsApp and Telegram for AI agents, behind a policy gateway. OAuth or access key.
Run a Telegram channel from your AI agent. Posts go out through your own bot, not your account.
Related MCP Servers
- AlicenseNot gradedqualityDmaintenanceEnables AI agents to interact with Telegram accounts through MCP, supporting messaging, contacts, groups, media, and admin functions.4Apache 2.0
- AlicenseBqualityCmaintenanceProvides read-only access to Telegram chats, allowing AI agents to list chats, read messages, and search within chats via local MCP.14MIT
- AlicenseNot gradedqualityBmaintenanceThis MCP server enables agents to connect to Telegram accounts and bots to search chats, review unread messages, build sourced digests, inspect media and documents, and prepare or execute confirmed actions with scoped permissions.1MIT
- AlicenseAqualityCmaintenanceEnables agents to perform read-only access to explicitly permitted chats on Telegram Web, including history, native search, post comments, photos and files, multi-chat context collection, and persistent local conversation history via a dedicated browser profile without Telegram API registration.10MIT