
Telegram AI CLI MCP
io.github.stufentlyv0.1.1更新于 Oct 11, 2026
Read a personal Telegram account over MTProto; every write is a plan a human applies.
概览
让助手通过 MTProto 读取个人 Telegram 账号,所有写操作都先变成由人执行的计划。
- 功能
- 提供针对个人 Telegram 账号的读取与本地工具:列出聊天和文件夹、读取聊天记录与论坛话题、搜索消息、把用户名解析为对等 id、列出成员和管理员、查看草稿、定时消息和已登录会话、获取媒体、转写语音消息,以及同步或搜索本地 SQLite 归档。发送、回复、编辑、删除、转发、加入、邀请、封禁、提升或定时等写操作只以带类型的计划工具形式提供。没有任何工具会执行计划,需由人在终端运行应用命令。
- 适用场景
- 适合让智能体整理自己的 Telegram、起草回复、盯住某个聊天中的特定内容,或回答关于消息历史的问题,同时不赋予它直接发送、加入或邀请的长期权限。它面向个人账号,而不是机器人账号。
- 运行要求
- 作为本地 Python 程序通过 stdio 运行;文档给出的安装方式是从 Git 仓库执行的 uvx 命令,因为 PyPI 包尚未发布。账号必须先在终端登录,因为服务器和桌面扩展都无法提示输入登录验证码。配置位于 YAML 文件,由 TGAI_CONFIG 指定路径,TGAI_PROFILE 选择 readonly(默认)或 plan。会话材料保存在用户的本地状态目录中。
安装
在 SourceWeft 中
- 打开 控制台中的 Telegram AI CLI MCP,将其添加到工作区。
- 为需要使用其工具的对话启用该服务。
Desktop only,通过 STDIO。 STDIO 服务会启动本地进程,因此需要 SourceWeft 桌面宿主。
其他 MCP 客户端
参照 仓库 中的启动说明。
README
telegram-ai-cli-mcp — CLI and MCP server for AI agents on a personal Telegram account
[CI] [License: MIT] [Python] [MCP]
telegram-ai-cli-mcp is a CLI and MCP server, built from one Python codebase, for automating a personal Telegram account over MTProto. It gives Claude Code, Claude Desktop, Codex, Cursor and other MCP clients task-shaped access to Telegram — read chats, search messages, resolve who a username belongs to, check who's in a group — while anything that changes something on Telegram (sending a message, joining a chat, deleting, promoting an admin) goes through an explicit plan-and-apply step instead of running the moment a model asks for it.
It is for people who want an AI agent to have a hand on their own Telegram — triaging messages, drafting replies, watching a chat for something specific — without giving that agent the standing ability to send, join or invite on its own.
This automates a personal Telegram account (MTProto), not a bot account (Bot API). Telegram can limit or permanently ban accounts it judges to be running abusive automation — mass joins, high-volume sends, scraping members. Using this tool against your own account is at your own risk; see Why not another MTProto wrapper for why a personal account behaves differently from a bot in the first place.
Status: v0.1 release candidate. The repository-side implementation is complete and awaiting the external PyPI/GitHub release setup listed in
TASKS.md. Command names and flags may still move before the first tagged release; deliberate non-goals and accepted local-boundary risks are recorded indocs/product-boundaries.md.
Why not another MTProto wrapper
Handing a model raw Telethon or a thin MTProto wrapper hands it Telegram's sharp edges too:
- Usernames are not stable identifiers.
@someonecan change hands; a tool that plans against a handle and sends against the same handle later can address a different person than the one it was approved for. Every plan here resolves a target to a numeric peer id once, and re-checks that id — not the handle — at apply time. FloodWaitErroris not something to swallow. Telethon's defaultflood_sleep_thresholdsleeps and retries a failed call automatically, which is exactly the wrong behavior for a plan-and-apply tool: an automatic retry after a timeout is how one intended send becomes two. This project turns that default off and drives retries itself.- Reading a chat is not free of side effects by default. Most Telegram clients move the read pointer the moment a dialog is opened. A tool an agent can call to "just look" has to guarantee that looking never marks the other person's messages read — that's asserted by a dedicated test here, not left to documentation.
noforwards(protected content) is enforced by Telegram, not by your client. A chat marked content-protected refuses forwards at the server (CHAT_FORWARDS_RESTRICTED), and no client gets round that — somessage.forwardout of one fails, and this project's job is only to say so in those words instead of surfacing a Telethon class name. Downloading is not blocked here: Telegram asks clients to stop people saving protected attachments, which the official apps honour and a raw MTProto library does not — Telethon saves the file like any other, and this project adds no guard of its own rather than pretending to one. That means re-posting the bytes as a fresh upload is possible — it ismedia fetchfollowed bymessage send-file, two plans somebody approves separately — and there is deliberately no one-step feature for it, because a message you post yourself is your own act and should read like one.- A revoked session does not fail cleanly. Terminating a session from the phone's Settings → Devices doesn't raise a friendly "logged out" error on the next call — it raises
AuthKeyDuplicatedErrororSessionRevokedError, and a client that retries every disconnect the same way ends up looping against a session that is never coming back. - A user account is not a bot account. Bot API bots are rate-limited and sandboxed by Telegram on purpose, and can't read history the way a member can. A personal account can do far more — which is also why Telegram polices unusual activity on it more aggressively: mass joins, high-volume sends and member scraping draw limits and bans, not just error codes.
tdatais Telegram Desktop's own undocumented on-disk session format, not something Telegram publishes a schema for. Getting an import wrong doesn't fail loudly — see the tdata warning under Configure.
What it does
Safety
Read operations run immediately. Nothing else does — every operation that changes something on Telegram is a two-step plan, then apply.
A write command never sends anything by itself — it records a plan and tells you how to carry it out:
tg-ai plan show <id> prints exactly what applying it would do — every field already passed through the same sanitizer that protects the terminal from a hostile chat title or message body (see render.py) — and only tg-ai plan apply <id> (with a confirmation prompt, or --yes to skip it) actually carries it out.
The honest limit of this design, stated plainly: there is no MCP tool that applies a plan, on purpose — a confirmation an agent can send over MCP is a confirmation prompt injection can send. But that boundary is a property of the transport, not of the agent. An MCP client that has its own shell — Claude Code, Codex, and comparable coding agents — can simply run tg-ai plan apply <id> as a shell command, the same as a person would. Nothing in this project detects or blocks that, because nothing at the process level can tell "the person typed this command" apart from "the agent typed this command" once both share a terminal.
For that class of client, this design does not pretend to hold a hard line at plan/apply. The line it actually offers is: every attempt and every outcome is written to the audit log before and after the RPC (audit.py), rate limits persist across restarts so an agent (or an injection driving one) cannot out-run them by being restarted (limits.py), the hard denylist on 777000 (Telegram's login-code chat) and Saved Messages cannot be reconfigured away, and everything an agent is about to approve is sanitized before it reaches a terminal so the text being approved is the text that will actually send (render.py). If your MCP client can run shell commands, you are trusting the agent, and the safety net is visibility and limits, not an unbreakable gate. A profile named full — direct send with no plan step, from any caller — does not exist in this project; it was considered and rejected. A stronger boundary (a separate OS principal, or a TOTP-gated apply) is out of scope for v0.1 and is written up as a path forward in docs/threat-model.md.
Reading, chats and profiles
Groups and channels are readable as soon as an account is configured; direct messages are not readable until a chat id is explicitly allowlisted — an empty dms allowlist means none, not all. 777000 (Telegram Service Notifications, where login codes and 2FA resets arrive) and Saved Messages are closed in code and cannot be reopened by any configuration. Every write capability (send, admin, join, profile changes) is fail-closed the same way: an empty list means nothing is permitted until you add to it. Two profiles exist — readonly (default: only the read tools work) and plan (read, plus creating plans). See telegram_ai_cli_mcp/safety.py for the exact rule order.
Importing an existing session
Importing a tdata folder from Telegram Desktop is one way to authorize an account without a fresh login flow. Two things to know before you do it:
USE_CURRENT_SESSIONlogs the original Telegram Desktop out. Convertingtdatainto a usable MTProto session can either mint a brand-new session (the desktop app stays logged in) or take over the existing one (the desktop app is signed out the moment the imported session connects). If you want to keep using Telegram Desktop with the account you're importing, do not ask for the current-session mode.- Telegram treats a
.sessionfile as equivalent to holding the account's auth key. Anyone who obtains it controls the account until the session is revoked from a device. Store it accordingly — seedocs/threat-model.md.
Install
That one command is the server. It speaks MCP on stdin and waits; every client block below runs the same uvx command. Sign the account in first, from a terminal, because neither the server nor the Desktop extension can prompt for a login code:
--label names the account; put your own number in E.164 form; --phone is needed only on the first sign-in of that label. account login-qr --label work signs in by QR code instead, with no phone number.
The session is stored under ~/.local/state/telegram-ai-cli-mcp/. The server sends nothing by itself: a plan tool records an intention, and a person applies it with tg-ai plan apply.
Not on PyPI yet. The release pipeline is in place (Releasing) and publishes from a v* tag over Trusted Publishing, but it cannot run until the one-time registration on PyPI's side is done — and claiming a name on a public registry is permanent, so that step is the owner's to take deliberately. Until that registration exists, uvx telegram-ai-cli-mcp (a PyPI package name, with no --from) does nothing useful. The uvx --from git+… command above is the install path. A checkout still works, for development:
Give it an environment of its own — pipx, uv tool, or a plain
python3 -m venv — rather than a shared user site, and put the resulting
bin/tg-ai on PATH. The dependency floor here is deliberately current
(mcp>=2.0, telethon>=1.44), and a shared site-packages holds exactly one
version of a package for every program on the machine: anything pinned below
that floor loses, silently, the moment this project is installed or upgraded
alongside it.
That collision is live on the maintainer's host, where the tg-claude-userbot
project pins mcp==1.27.2 and telethon==1.43.2. Since 2026-09-03 tg-ai
lives in its own virtualenv there and the shared entry point is a symlink into
it:
Details of that layout, and what was left behind in the shared site, are in Operations.
Configure
Or sign in without typing anything, from a phone or desktop that is already logged in:
account login-qr draws a QR code in the terminal and waits for you to scan it in Telegram → Settings → Devices → Link Desktop Device. No phone number, no code: the code carries a one-shot login token, and the app that scans it authorizes the session. An unscanned code expires in well under a minute and is redrawn a few times before the command gives up; two-step verification is prompted for exactly as the phone login prompts for it. The raw tg://login?token=… URL is printed under the code as a fallback for terminals that can't draw block characters — treat it as a password: whatever opens it takes the account. It goes to your terminal and nowhere else — never to the log, the audit file, a command's output or stdout (so redirecting the command into a file can't capture it), and with no terminal at all the command refuses rather than minting a token nobody can see. A label that was never registered is enrolled by this command on its own, so account add first is optional.
Account material — the Telethon .session, the frozen device fingerprint and (if used) proxy credentials — lands under ~/.local/state/telegram-ai-cli-mcp/ with 0700/0600 permissions; api_hash and proxy secrets are encrypted at rest with a key you control (TGAI_SECRET_KEY, or a generated key file — see telegram_ai_cli_mcp/secretbox.py).
Everything else lives in one YAML file, ~/.config/telegram-ai-cli-mcp/tgai.yaml by default (TGAI_CONFIG to point elsewhere), overlaid by TGAI_-prefixed environment variables (TGAI_PROFILE, TGAI_SAFETY__WRITE__SEND__ALLOW, and so on — double underscore nests):
777000 and Saved Messages don't need — and can't get — an entry here; they're closed in telegram_ai_cli_mcp/config.py, not in this file.
Every key, its default and the rule order the safety kernel applies them in are in the configuration reference; every command and tool, with its arguments, is in the operations reference.
Example prompts
Ask in plain language. The server picks the tool.
- What did I miss in Telegram since yesterday morning?
- Who mentioned me in the project chat, and what did they actually say?
- Who is this username, and which groups do we have in common?
- Draft a reply to Alex's last message, but do not send it.
- Which devices is this account signed in on right now?
Add the MCP server to your AI client
The MCP client never handles a Telegram credential directly — the server resolves the account's session from local state, not from anything passed over MCP. Every JSON block below runs the same uvx command as Install. TGAI_PROFILE is readonly (the default; reads only) or plan (reads, plus preparing plans a person applies). There is no profile that sends.
Claude Code
Project file: .mcp.json in the project root.
Or from the terminal. Add --env TGAI_PROFILE=plan when the agent should be able to prepare plans (nothing is ever sent without tg-ai plan apply — see Safety for what that promises when the client is Claude Code itself):
Or as a plugin
The repository is also a Claude Code plugin: the same server, plus two skills — one for catching up on an account, one for the plan-and-apply boundary, which is the part an agent otherwise has to discover by looking for a send tool and not finding one.
The plugin supplies the server definition and the skills; it does not install
the program. tg-ai still has to be on PATH — until the first PyPI release
that means pipx install git+https://github.com/stufently/telegram-ai-cli-mcp, or
this repo checked out and installed — and the account still has to be signed in
with tg-ai account login, because a plugin cannot prompt for a login code.
The uvx blocks above do not put tg-ai on PATH; the plugin still needs one
of those installs.
Claude Desktop
File: claude_desktop_config.json — macOS
~/Library/Application Support/Claude/claude_desktop_config.json, Windows
%APPDATA%\Claude\claude_desktop_config.json, Linux
~/.config/Claude/claude_desktop_config.json.
One click, without editing JSON: download the .mcpb from
https://github.com/stufently/telegram-ai-cli-mcp/releases/latest
and open it. Claude Desktop installs the extension and asks for the profile
(readonly or plan). Sign in first with tg-ai account login in a terminal;
the extension cannot prompt for a login code, and the server sends nothing
by itself.
Cursor
File: ~/.cursor/mcp.json (every project) or .cursor/mcp.json (this project).
Windsurf
File: mcp_config.json — macOS and Linux ~/.config/devin/mcp_config.json
(or $XDG_CONFIG_HOME/devin/mcp_config.json), Windows
%APPDATA%\devin\mcp_config.json. Older builds read
~/.codeium/windsurf/mcp_config.json.
Zed
File: settings.json — Linux and macOS ~/.config/zed/settings.json.
Codex
VS Code and other stdio MCP clients
Same shape as the Claude Desktop block above, in whichever file the client reads its MCP servers from.
Docker
HTTP instead of stdio
Every block above launches the server over stdio, which is the default and needs no port. For a client that can only take a URL:
The client sends Authorization: Bearer $TGAI_HTTP_TOKEN. Two things are
refused at start-up rather than warned about: a bind address that is not a
loopback literal (0.0.0.0, a routable address, and a hostname — even
localhost, because what a name resolves to is not decided here), and a missing
token. There is no unauthenticated mode: a loopback port is reachable by every
other process and every other user on the machine. Put an SSH tunnel in front if
a remote client needs one. See
docs/configuration.md.
Sharing one account between several clients
A Telegram session is a single auth key, so a connected client holds an
exclusive lock on the account and everything else gets SESSION_LOCKED at once
rather than waiting — which bites as soon as a tg-ai watch is running or two
editors are open on the same account. Turn on daemon.enabled and run:
One process then holds the connection and callers queue behind each other. It is
opt-in, nothing supervises it, and a client that finds no daemon opens the
account itself exactly as before. See
docs/operations.md.
MCP tools
The server publishes immediate read/local tools and one typed plan tool per write operation, not a generic plan_create(operation, params), because an untyped params doesn't show a model the field schema and it starts inventing argument names. No tool applies a plan, and nothing about a plan's state is a tool either: tg-ai plan list and tg-ai plan show <id> are terminal commands, on the same side of the line as plan apply.
You can publish fewer of them. mcp.tools (TGAI_MCP__TOOLS='["telegram_chats","telegram_chat_read"]') lists the tools this server is allowed to publish; unset — the default — it publishes all of them, exactly as before. The point is narrow and worth stating plainly: a prompt injection cannot invoke a tool it never saw in the tool list. The gate applies to the call path too, since a tool name is a guessable string, and it only ever narrows — the profile, the chat allowlists and the hard denylist all still run underneath, and a name the registry does not publish is a startup error rather than a new tool. See docs/configuration.md.
The four telegram_archive_* tools are the way out of paying an RPC for every question. archive_sync copies one named chat — there is no daemon, no background sweep and no "archive everything" — into a SQLite file on your own disk, resuming from a stored watermark so a second run fetches only what is new. archive_search then answers offline, which is what makes regular expressions possible at all: Telegram's own search matches text, not patterns. Two properties keep that honest. The read allowlist is re-applied on the read, not remembered from the write, so a chat archived yesterday and closed today stops answering and is counted as withheld rather than quietly missing. And every archive answer carries meta.source: archive plus the timestamp of the oldest sync it covers — an archive result mistaken for a live one is stale state reported as current. The file is 0600, gitignored, and not encrypted: the .session file next to it already grants full live access to the account, so encryption would not raise the bar while it would make offline search impossible. Recognisable secrets are masked before they are written, and archive_forget erases a chat — including one the policy has since closed, because "you may not read it and you may not delete it" is the worst of both answers. See docs/operations.md.
telegram_folders reads the chat folders the user already arranged by hand, and telegram_chats / telegram_inbox take a folder argument that narrows a listing to one. A folder is the user's own sorting, never a permission: the filter runs after the policy, so a folder that names a chat this configuration may not enumerate does not make it appear.
Putting a chat into a folder (tg-ai folder add, planned) has a trap worth naming, because getting it wrong destroys data silently. Telegram has no "add one chat" call: UpdateDialogFilter replaces the entire folder, so an edit has to send back everything already in it — and on a real account that includes private chats this configuration is not allowed to enumerate and which telegram_folders therefore does not print. Rebuilding the folder from what the tool can see would delete exactly those, and the loss would appear in neither the plan nor the result. So nothing is rebuilt: the applier takes the raw filter Telegram just sent, appends one peer to it, and hands the same object back. What it cannot see, it cannot drop. Shareable folders, whose members follow an invite link rather than a list, are refused outright.
The five moderation tools exist so that every rights change has an undo. Granting admin rights was possible long before taking any back, which meant an agent could produce a state it had no way to reverse: plan_ban_user is paired with plan_unban_user, plan_promote_admin with plan_demote_admin, and plan_restrict_user either expires on its own or is lifted by the same unban — Telegram keeps a ban and a restriction in one set of rights. One member per plan, never a list: banning six people behind a single approval is exactly the blast radius the review step exists to bound. The preview names the person, the chat, each right being taken and how long it lasts, and for the two nobody on the receiving end can reverse — a ban and a kick — it says so in as many words.
tg-ai account add, tg-ai account login and tg-ai account login-qr are absent from this list on purpose, and the registry refuses to publish them: signing in puts something in front of a person — the code Telegram sent to their phone, or a QR code only they can hold a phone up to — and enrolling an account widens the very fleet every allowlist is written against. The QR login has a third reason of its own: its tg://login?token=… is the login, so a tool that could ask for one would hand a caller the account itself.
telegram_media_fetch looks like a read tool but isn't one — it writes a file, so it goes through the same server-controlled path handling as everything else that touches disk: the caller never supplies a path, the file lands under a download root with a generated name (O_CREAT|O_EXCL|O_NOFOLLOW, a size cap and a running quota), and only an opaque artifact_id comes back. If your MCP client advertises roots — the directories it sanctions — they are a ceiling on that one: a download directory outside every root is refused, by name, before the account is opened. It is never redirected to a directory the client would accept, because the configured path is what the quota is walked against and what the operator expects to find files in. The same ceiling covers the other two operations that write to this machine — archive_sync and archive_forget, judged by paths.archive rather than by a download directory they never touch. A client that doesn't implement roots constrains nothing; a client that advertises an empty list sanctions nothing, and those are different answers (docs/configuration.md).
telegram_media_transcribe turns a voice message into text without the audio leaving your machine. It is the one feature here that needs half a gigabyte of model weights, so it lives in a separate, optional Docker image: make build does not build it, the published package gains no dependency from it, and an installation that never runs make transcribe-image sees nothing but one extra tool that refuses with an explanation. Whisper (small) runs in that container with --network none and one file bind-mounted read-only — a process with no network interface cannot upload a recording, which is a stronger statement than a privacy policy. There is no cloud speech API here, not even as a fallback. The model is fetched once, by an explicit make transcribe-model, because that is the only invocation that ever has a network. And the transcript is treated exactly like a message body: it is somebody's words, an injection can simply be spoken aloud, so it comes back wrapped as untrusted content.
telegram_plan_send_file is the same rule pointed the other way, and it is the more dangerous direction: a caller that could name any path on the host would have a read of arbitrary bytes with a delivery mechanism attached — a private key, the .session file that is the account, somebody's documents — into a chat other people read. So a file is sent from one directory (paths.uploads), containment is decided after symlinks are resolved, and the size ceiling is answered from stat() rather than from a transfer that fails halfway. The plan's summary names the file, its size, its type and the form it arrives in, because "send photo.jpg" hides the difference between a re-encoded picture and the original file. See docs/operations.md. telegram_plan_set_chat_photo publishes a chat photo through that same rule rather than a second copy of it — the weaker of two rules for "which local file may leave" is the one that becomes the hole.
No read tool ever marks a chat read — mark_read only exists as an explicit plan operation, because an agent asked to "just look" at a chat should never have a side effect on what the other person sees as unread. That includes the read-state block telegram_chat_read returns: it comes from a call that describes a dialog's read pointers without acknowledging anything in it.
telegram_watch is the alternative to asking telegram_inbox again in a loop. It blocks until a message arrives in a chat the policy already permits, and hands back the whole burst in one answer: four fast replies wake the caller once, not four times — polling costs a turn (and the system prompt with it) whether or not anything happened, and re-creating that cost inside the waiting tool would defeat the point. The wait is capped at five minutes by the schema itself, because an MCP client cannot abandon a call it is waiting on; coming back empty at the ceiling is a result with a waited_sec on it, not an error. Messages from chats the configuration refuses are not reported at all — not even as the fact that something happened, which would say a specific conversation was active at a specific second. It holds that account's session lock for the duration: one auth key allows one connection, so nothing else can use the same account until the wait returns. See docs/operations.md.
telegram_sessions answers the question the rest of this README keeps assuming somebody can ask — which devices is this account signed in on, and is one of them not mine? It reads and nothing else: no operation in this project ends a session, deliberately, because a read tool that can log a device out can log the owner's own phone out with no plan step in the way. The row carries the device, the app, the country and the dates in full; the IP address is cut to its network (198.51.x.x) and the authorisation hash — the handle a terminating call would take — is not returned at all. The reasoning is in docs/operations.md.
telegram_drafts and telegram_scheduled cover what a history read cannot see: text that was started and never sent, and messages queued to go out later. Both are read-only in the same strong sense — nothing clears a draft or cancels a send. Drafts are filtered chat by chat against the read policy, and a draft in Saved Messages or Service Notifications is not listed or counted, since a withheld tally would still say one exists there.
telegram_mentions is the other half of that promise, and the one place where getting it wrong is invisible. Telegram counts unread mentions and unread reactions separately from plain unread, and Telethon's namespace puts GetUnreadMentionsRequest one letter from ReadMentionsRequest — the first asks which mentions are unread, the second clears them on every device the owner has. Only the Get pair is ever issued, and the test asserts on the whole list of requests the operation made rather than on its answer. Those same two counters rank telegram_inbox: a chat where somebody called your name outranks a chat that is merely busy.
telegram_plan_schedule_message is the one write whose approval outlives this tool. Applying it puts the message in Telegram's own scheduled queue, where the owner can see it in the app and cancel it there — from a phone, with no agent running and no terminal open. The time has to carry an explicit UTC offset: a naive one is refused rather than guessed, because a summary that says "09:00" without saying whose is one nobody can check. It also has to be at least two minutes out, because Telegram sends a nearly-due schedule immediately and the applier spends real time getting there. The exception is "send when they are next online", which skips the queue altogether if the other person is already there — the summary says so rather than promising a cancel button that will not exist. telegram_plan_archive_chat and telegram_plan_mute_chat sit at the opposite end of the blast radius — they change this account's own chat list and its own notifications, nobody else can observe either, and both summaries say so in words, because "mute" and "ban" are one word apart in a review queue.
telegram_message_reactions reports counts, not people. Telegram can name everyone who reacted; that request is never made, and where the roster is unavailable the payload says so rather than leaving a gap. Reacting is a write, so it is planned rather than performed: telegram_plan_react_message and telegram_plan_unreact_message record the intent, and a person applies it. Telegram's reaction call takes the account's whole list for a message rather than a delta, so reacting replaces whatever this account had reacted with unless keep_existing is set — the plan summary says which of the two is about to happen, and the applier refuses if the list moved while the plan sat in the queue.
telegram_plan_pin_message is the loudest thing here short of sending. By default every member of the chat gets a notification and the banner appears at the top of their window; silent suppresses the notification, not the banner, and the summary says which one it is. In a one-to-one chat it pins on this side only unless both_sides is set. telegram_plan_unpin_message is quieter but no less visible, and it is the one operation here that routinely undoes somebody else's decision — the preview says whose message it is. Both are judged by the admin policy rather than send, because a pin changes what everyone in the chat sees. These four, and the message operations generally, take a t.me/…/123 permalink in chat and read the message number out of it.
JSON contract
Every command and every tool call returns the same envelope, whether it's the CLI or the MCP server answering (telegram_ai_cli_mcp/envelope.py) — that's what keeps the MCP server a thin adapter instead of a second implementation.
untrusted_content: true marks any response carrying text that came from Telegram — a message body, a chat title, a display name. It's written by strangers; a model reading it should treat it as data, never as an instruction.
The flag says a response contains such text. It doesn't say where, and "somewhere in this document" isn't a boundary — so the values themselves are delimited:
A sender can't close that wrapper: ⟦ and ⟧ are replaced with [ and ] inside wrapped content, unconditionally, so no spelling of ⟦/untrusted⟧ in a message body ends the frame — it comes out as inert [/untrusted], still readable, no longer a marker. Strings that aren't wrapped are defanged the same way, so the delimiters are this project's alone whatever the field list forgets. Ids, dates, counts, links and username are never wrapped, so parsers keep working; the delimiters are published in meta.untrusted_markers and untrusted.unwrap() strips them. Full rationale: the trust boundary.
retryable answers exactly one question: may the caller send the identical request again? A policy refusal is never retryable, no matter how many times it's attempted; a flood wait is, once the wait has elapsed. Error codes are a stable enum (telegram_ai_cli_mcp/errors.py) — renaming one is a breaking change, so they're not string literals scattered through the codebase.
Documentation
- Operations reference — every command and tool, its arguments and what it consults
- Configuration reference — the full
tgai.yaml, every environment variable, and what cannot be configured - Security policy
- Threat model
- Design spec
- Changelog
- Contributing
Releasing
Pushing a v* tag runs .github/workflows/release.yml: the full CI matrix, then a build, then an upload to PyPI, then a draft GitHub release carrying the same artifacts and their SHA-256 sums. Nothing else triggers it — an ordinary push to main cannot publish anything.
There is no PyPI API token anywhere, and there is not meant to be one. The upload authenticates with Trusted Publishing: GitHub mints a short-lived OIDC token that identifies this workflow file in this repository in this environment, and PyPI exchanges it for a one-off upload credential. Nothing to leak, nothing to rotate, nothing to store.
That trust is registered against five exact values, and renaming any of them silently breaks it:
Before the first release, the repository owner does three things by hand — none of them is something a commit can do:
- Register the pending publisher on PyPI. pypi.org/manage/account/publishing → Add a new pending publisher, with the five values in the table above. "Pending" is the form used for a project that does not exist yet: the first successful upload creates it.
- Create the
pypienvironment on GitHub. Settings → Environments → New environment, named exactlypypi. Worth adding a required reviewer on it: the environment gate is what turns "somebody pushed a tag" into "somebody approved a publish", and it is the last point at which a release can be stopped. - Push the tag. The version is taken from
pyproject.toml, and the workflow refuses to build unless the tag says the same thing — a tag and aversionthat disagree would put one number on PyPI under a release everybody reads as another, permanently.
Then review the draft release GitHub created and publish it.
What the pipeline checks before it uploads, because a version on PyPI can never be replaced or re-uploaded: the tag matches pyproject.toml; the built file names carry that version; every .py file in telegram_ai_cli_mcp/ is present and non-empty in both the wheel and the sdist (an unanchored .gitignore pattern has shipped an empty package from this repo before); py.typed is in the wheel, since Typing :: Typed is a lie without it; and twine check --strict accepts the metadata PyPI is about to read.
FAQ
How do I let Claude read my Telegram?
Run tg-ai mcp and register it as an MCP server with Claude Code or Claude Desktop (see Add the MCP server to your AI client). By default the readonly profile is active and groups/channels are readable immediately; direct messages stay closed until you add specific chat ids to safety.read.dms.allow in the config.
Is there an MCP server for Telegram user accounts?
Yes — this one. It's built on MTProto through Telethon, the same protocol Telegram's own apps use, not the Bot API — so it can read existing chat history, resolve usernames and see who's in a group the way a human member's client can, none of which a Bot API bot is able to do.
Can an AI agent send messages on my Telegram account through this?
Only after a two-step plan-and-apply. From an MCP client without shell access, no — there's no tool that applies a plan. From an MCP client that also has shell access (Claude Code, Codex and similar), it can run tg-ai plan apply itself, the same as a person typing the same command would; see Safety for exactly what that does and doesn't protect against.
Is this a Telegram bot?
No. It automates a personal user account over MTProto. A Bot API bot is a different kind of Telegram entity with different limits and different visibility into chat history; see Why not another MTProto wrapper.
Will Telegram ban my account for using this?
Telegram can limit or ban any account it judges to be running abusive automation — regardless of what tool sent the requests. Ordinary reading and occasional, allowlisted sending is the kind of usage this tool is built around; mass joins, high-volume sends and scraping members are not something this tool tries to make safe, because Telegram itself treats them as abuse.
Does importing tdata log me out of Telegram Desktop?
It can. Converting Telegram Desktop's tdata folder into a usable session either mints a fresh session (desktop stays logged in) or takes over the existing one (desktop is logged out the moment the import connects) — the choice is USE_CURRENT_SESSION. See Importing an existing session.
What happens if my session gets revoked from my phone?
The account is moved to a revoked status rather than looped through automatic reconnect attempts — a revoked session does not come back on its own, and retrying it the way a normal disconnect is retried just produces a busy loop against a dead session.
Which Python version do I need?
3.12 or newer; 3.12–3.14 are tested in CI. See Compatibility.
Does this work without an AI client at all?
Yes. tg-ai is a complete CLI on its own, with human-readable output and a --json form for scripting, independent of whether anything is talking to it over MCP.
Compatibility
License
MIT. See LICENSE.
Telegram is a trademark of Telegram FZ-LLC / Telegram Messenger Inc. This project is an independent open-source project and is not affiliated with or endorsed by Telegram.
来源:README.md,提交 8516834
工具
0版本历史
1- v0.1.1最新Oct 11, 2026

