Skip to main content
Glama

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 shell

The 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-server

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 serve

Restart Claude Code (or /mcp → reconnect) and the tools appear. To check:

claude mcp list        # tgread: … - ✔ Connected
tgread status          # account, session validity, tool surface

Those 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 + JSON

Any 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 serve

Why 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 (telethon, pyaes, pyasn1, rsa, itself)

MCP layer

mcp SDK → starlette, uvicorn, pydantic, pyjwt[crypto], opentelemetry

~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:#fff

Three layers, in increasing order of how much they'd survive a bug:

  1. No write tool is advertised. An injected instruction has nothing to call.

  2. Every payload is wrapped in an UNTRUSTED CONTENT banner naming it as data, not instructions — including chat titles and bios, which are attacker-controlled too.

  3. The transport guard. Telethon funnels every outbound TL request through TelegramClient._call (68 internal call sites reach it via await self(req), and __call__ is a one-line delegate). ReadOnlyClient overrides 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:#fff

tgread 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

whoami

which account this server is signed in as

list_chats

dialogs, filterable by name and by kind

search_public_chats

Telegram's public directory — finds channels never joined, split into already-known and strangers

list_folders

chat folders (dialog filters) and their sizes

list_contacts

saved contacts

Reading

Tool

What

read_chat

history, oldest-first, paged by message id or date

get_messages

specific ids, or a window around one — what a search hit was replying to

read_thread

replies under a post: a channel item's comment section

list_pinned

pinned messages — usually a chat's rules and announcements

Search

Tool

What

search_messages

full text, in one chat or across everything the account sees

list_media

history filtered by attachment kind, via Telegram's server-side index

People and metadata

Tool

What

chat_info

kind, member count, description, verified/scam flags

list_members

participants with roles, searchable, admins_only

get_reactions

who reacted to a message, and with what

common_chats

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_media returns captions and metadata; fetching attachments would mean writing attacker-chosen bytes to the agent's disk.

  • Reading never marks as read. messages.ReadHistory is 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_chats finds channels without joining them; get_reactions reads 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   # + research

Restricted 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

TGREAD_TOOLS here

KEY=VALUE

Advertised, denied

Claude Code settings.jsonpermissions.deny: ["mcp__tgread__search_messages"]

JSON

Per-subagent

.claude/agents/*.md frontmatter tools:

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-mcp is not this, and not chigwell's either. That PyPI name belongs to an unrelated project; passing TELEGRAM_API_ID / TELEGRAM_API_HASH to 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, holding config.env (0600) and tgread.session (0600). One directory to chmod, to back up, to destroy. tgread status flags it if the permissions drift.

  • install.sh uses uv sync --frozen — it installs exactly the versions in the committed uv.lock and 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

tgread.py

the whole server: guard, tools, JSON-RPC loop, CLI

bin/tgread

launcher — execs the pinned venv's interpreter

pyproject.toml, uv.lock

the pin

install.sh

venv, symlink, claude mcp add --scope user

test-tgread.sh

45 offline tests — guard, catalogue, protocol, CLI, hygiene

test-tgread-live.sh

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 runs

Two 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 session

The 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.

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections. Dates show when Glama detected each change.

No tool schema history has been recorded yet.

Maintenance

ActivityMaintained
ResponsivenessNo issues

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    C
    maintenance
    Enables AI agents to interact with Telegram accounts through MCP, supporting messaging, contacts, groups, media, and admin functions.
    4
    Apache 2.0
  • A
    license
    Not graded
    quality
    C
    maintenance
    A read-only MCP server that lets AI agents read personal Telegram chats from an allowlist of folders, with no send/edit/delete capability.
    53
    MIT
  • F
    license
    Not graded
    quality
    D
    maintenance
    Enables users to read, search, and manage Telegram messages in channels, groups, and private chats through MCP tools.
    -
  • A
    license
    Not graded
    quality
    B
    maintenance
    Provides read-only access to Telegram chats, allowing AI agents to list chats, read messages, and search within chats via local MCP.
    MIT

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/demian-overflow/tgread'

If you have feedback or need assistance with the MCP directory API, please join our Discord server