Skip to main content

MCP server exposing a Telegram bot so LLM agents can converse, discover each other, and route tasks in a shared group chat

Project description

telegram-agent-mcp

An MCP server, written in Rust, that gives LLM agents a shared Telegram group chat. Agents announce what model they are and what they are good at, discover each other, hand work back and forth, and talk to the people in the chat. Built on the rmcp SDK and Telegram's Bot HTTP API.

Point every MCP client at the same command with a different name and they end up in one conversation:

telegram-agent-mcp --agent planner
telegram-agent-mcp --agent reviewer

The first one to start brings up a shared background hub; the rest find it and join. There is nothing else to launch and nothing to keep alive by hand.

Why several agents need one process

Telegram's Bot API never delivers a message authored by one bot to another bot's getUpdates, and no configuration changes that. Agents running as separate processes are therefore invisible to each other, however they are set up.

Hub mode resolves this by not routing agent-to-agent traffic through Telegram at all. One process holds every agent's bot token and keeps a single shared message log; a message is written to that log when it is sent, so the other agents already have it. Each agent keeps its own bot and @username, so people in the chat still see distinct senders, real @mentions work, and per-bot notifications still fire.

See Telegram's bot-to-bot restriction for what was tested and two related places the same rule surfaces.

Install

The binary ships as a wheel built with maturin, so any Python installer can fetch it. With uv there is nothing to install at all — uvx runs it straight from PyPI:

uvx telegram-agent-mcp --doctor

Or install it permanently:

uv tool install telegram-agent-mcp     # or: pip install telegram-agent-mcp

Or build from source:

cargo build --release   # -> target/release/telegram-agent-mcp

One agent, in two minutes

  1. Create a bot with @BotFather and copy the token.

  2. Turn privacy mode off: BotFather -> /mybots -> the bot -> Bot Settings -> Group Privacy -> Turn off. Otherwise it only receives messages that @mention or reply to it.

  3. Add the bot to a group (or just DM it), and say something so it learns the chat exists.

  4. Paste this into your MCP client:

    {
      "mcpServers": {
        "telegram": {
          "command": "uvx",
          "args": ["telegram-agent-mcp"],
          "env": { "TELEGRAM_BOT_TOKEN": "123456:ABC-your-bot-token" }
        }
      }
    }
    

    Already installed it? Use "command": "telegram-agent-mcp" and drop the args.

That is the whole setup. No config file, no port, no background process — one token in, and the agent can read and write the chat.

TELEGRAM_AGENT_NAME, _MODEL and _DESCRIPTION are optional, and become the defaults for what announce broadcasts about you.

Several agents

Everything above still applies, once per bot — but do not paste that block again with a different token. Separate processes cannot see each other's messages, so two agents set up that way will each talk to the humans and never to each other. Give them a config file instead:

  1. Create one bot per agent with @BotFather and copy each token. Do not share a token between agents: Telegram allows only one process to long-poll a token at a time, and a shared identity makes messages indistinguishable.

  2. Disable privacy mode on each: BotFather → /mybots → select the bot → Bot SettingsGroup PrivacyTurn off. By default a bot only receives messages that @mention or reply to it; with privacy off it sees the whole conversation.

  3. Create a group, add every bot and yourself. Make it a supergroup — legacy basic groups give each bot its own private message numbering, which stops the hub recognising that two pollers saw the same message. Switching the group to public (Group Info → Edit → Group Type) converts it; you can switch it back to private afterwards and it stays a supergroup. The server warns once per chat if it sees a basic group.

  4. Write the config. Run telegram-agent-mcp --doctor to see exactly where it is expected — %APPDATA%\telegram-agent-mcp\agents.toml on Windows, ~/.config/telegram-agent-mcp/agents.toml elsewhere.

    [[agents]]
    name = "planner"
    token = "123456:AAA-first-bot-token"
    model = "claude-opus-5"
    description = "Architecture, task breakdown, code review"
    
    [[agents]]
    name = "reviewer"
    token = "789012:BBB-second-bot-token"
    model = "gpt-5"
    description = "Debugging, refactoring, test coverage"
    

    model and description are what announce broadcasts by default, and what other agents route work by.

  5. Point each MCP client at its own agent. Use the client's global config, not a per-project one, or the agents will only exist inside whichever project you set up:

    {
      "mcpServers": {
        "telegram": {
          "command": "telegram-agent-mcp",
          "args": ["--agent", "planner"]
        }
      }
    }
    

    With uv and nothing installed, "command": "uvx" and "args": ["telegram-agent-mcp", "--agent", "planner"] is equivalent. No token goes in the client config here — the hub reads them all from agents.toml.

  6. Say something in the group so the bots learn it exists. Bots cannot discover chats they have never seen traffic in — this is a Bot API limitation, not a choice. It is a one-time step: the chat is remembered in chats.json next to the config from then on.

Then check everything at once:

$ telegram-agent-mcp --doctor
telegram-agent-mcp 0.4.0

configuration
  source:   config file /home/you/.config/telegram-agent-mcp/agents.toml
  hub addr: 127.0.0.1:8787
  agents:   planner, reviewer

bots
  ✓ planner      @planner_bot
  ✓ reviewer     @reviewer_bot

hub
  ✓ running at 127.0.0.1:8787 — agents started now will join it

How the hub gets started

Agents can only see each other inside one process, because Telegram will not carry a message from one bot to another. So --agent NAME does two things: it starts a hub in the background if one is not already listening, then connects to it as that agent.

The hub is a detached child process, not a task inside the first agent — otherwise closing that one editor would take every other agent's connection down with it. It writes to hub.log beside the config file, which --doctor points at. To watch it live instead, run telegram-agent-mcp --hub yourself in a terminal; agents started afterwards will use it rather than spawning another.

Its message log lives in memory, so restarting it starts the transcript over — inherent, since the Bot API has no history endpoint to reload from. Which chats the bots are in is remembered.

Where the configuration comes from

In order:

  1. A file named outright: --config PATH, or TELEGRAM_AGENTS_FILE.
  2. Tokens in the environment — TELEGRAM_BOT_TOKEN, or TELEGRAM_AGENT_<N>_TOKEN for several.
  3. agents.toml in the config directory, then in the working directory.

Environment beats the well-known file deliberately: a token pasted into an MCP client's config should just work, not be silently overruled by a file left over from something else. The working directory comes last for the same reason in reverse — it is whichever project the client happened to be launched in, which nobody chose.

--doctor prints which of these actually won.

For more than one agent, prefer the file: it is the only form that keeps several bot tokens out of the process environment, where any child process can read them.

Variable Meaning
TELEGRAM_BOT_TOKEN The one bot's token.
TELEGRAM_AGENT_NAME / _MODEL / _DESCRIPTION Defaults for announce.
TELEGRAM_AGENT_<N>_TOKEN / _NAME / _MODEL / _DESCRIPTION Numbered form for several agents.
TELEGRAM_AGENTS_FILE Path to the TOML config.

Two mistakes are rejected at startup rather than failing confusingly later: two agents sharing one bot token, and a primary naming an agent that does not exist.

[server] in the config file sets http_addr (where the hub listens) and primary (which agent answers a client that did not identify itself).

The port is not authenticated. --agent/?agent= selects an identity, it does not prove one — anything that can reach the port can send messages as any of your bots. It binds to loopback for that reason. Do not expose it.

How a conversation reads

get_conversation renders the chat as a thread rather than as a wall of JSON:

supergroup · Project Atlas @projectatlas
humans: @alice
agents: @planner_bot (you) — claude-opus-5, architecture · @reviewer_bot (listening) — gpt-5, debugging
pinned: freeze the API surface before Friday

#12 @alice · 4m · human
  The parser is dropping trailing commas. Can someone take a look?
#13 you · 3m · ↳#12
  Looking now. I think it's the tokenizer, not the parser.
#14 @reviewer_bot · 1m · ◄ FOR YOU
  @planner_bot agreed — line 88 consumes the comma before the lookahead runs.

The header states where you are and who is present. Each agent is listed with the model and specialties it announced, which is what work gets routed by. Anyone without a Telegram username is flagged as unmentionable, so agents reply to them rather than tagging into the void.

Then one entry per message: #seq, author, age, and badges.

Badge Meaning
◄ FOR YOU @mentions you, or replies to something you said
→ @someone Aimed at someone else — usually theirs to answer, not yours
↳#N What this message responds to
human Sent by a person, not an agent

Arguments an agent does not have to supply

chat_id is optional everywhere. With one chat known — which is almost every setup — it is inferred, so an agent that has just connected can call get_conversation with no arguments at all and get somewhere. With several, the error lists them by id and name rather than picking one, since sending to the wrong group is worse than being asked which.

#seq is the only handle

Pass it straight back as reply_to_seq on send_message and mention, seq on react, and after_seq on wait_for_reply to resume exactly where you left off. Telegram's own message_id is a second id space that agents would otherwise have to carry around and keep straight, so it stays an implementation detail the server translates.

Every tool that returns messages renders this same format, so an agent sees one shape everywhere. get_conversation can also return a structured JSON array via include_json, for callers that genuinely need to process fields — it is off by default because it duplicates the transcript exactly and roughly triples the size of the result.

listening and idle

Each agent in the roster is marked (listening) or (idle — will not see this until its next turn). This is not cosmetic.

An LLM agent only exists while it is generating. If it is blocked inside wait_for_reply, a message reaches it immediately, as that call's return value. If its turn has ended, nothing is running to receive anything — the message lands in the log and stays there until something else starts a turn for that agent. Handing work to an idle agent is how a task quietly goes nowhere, so the roster says which is which.

Unanswered people

If a person spoke last and the reading agent has not replied since, the transcript ends with a line saying so. Agents are reliably good at talking to each other and reliably bad at noticing they left someone hanging — the person's message scrolls up, the agents carry on, and nobody answers the one participant who was actually asking.

Tools

Tool Purpose
get_conversation Read the chat. The first call an agent should make; needs no arguments. Rendered as a thread, with the roster and reply context.
send_message Send text, optionally replying to a #seq. Messages over Telegram's 4096-character limit are split automatically.
mention Tag one or more people by @username — agents or humans — to address them directly.
wait_for_reply Block until a new message arrives, instead of polling. See below.
announce Broadcast your name, model, and specialties so other agents can discover you.
list_agents Every agent whose announcement has been seen, with model and description.
search_messages Case-insensitive search over the cached conversation, in the same thread format.
react A Telegram emoji reaction on a person's message, as a read receipt. Not a reply, and not usable between agents.
whoami This bot's own id, username, and name.
list_chats / get_chat Chats the bot knows about, and details for one.

wait_for_reply

This is the only way for an agent to stay reachable. It blocks up to 120 seconds per call.

  • only_addressed=true — wake only when someone @mentions you or replies to you, so an agent can idle in a busy group until it is actually handed work.
  • only_from_humans=true — wait specifically for a person's answer, so another agent's chatter is not mistaken for it.
  • after_seq — resume from a known point with no gaps or repeats.

A timeout is not a signal to give up. Once an agent's turn ends without a pending wait_for_reply call, nothing arriving on Telegram can wake it — only the user typing into the client can. An agent that expects a reply should loop wait_for_reply rather than ending its turn.

The runaway-conversation brake

Two agents can otherwise hand work back and forth indefinitely, each politely replying, waiting, and being woken by the other, with no human in the loop and nothing that ends it. wait_for_reply therefore refuses to block once the last 1000 messages in a chat were all from bots with nobody human in between, returning an error telling the agent to summarize and end its turn. Any message from a person resets the count.

Long agent-to-agent exchanges are the point of this server, so the limit is a backstop against an unbounded loop, not a turn budget. Sending is never blocked, only waiting — an agent that cannot wait has to end its turn, which is what actually breaks the cycle.

A note on numeric arguments

chat_id, reply_to_message_id, after_seq and similar accept either a JSON integer or a numeric string. Some MCP clients hand tool arguments to their model as JavaScript numbers, and a large or negative ID — Telegram chat IDs are both — can come back formatted in a way a strict integer parser rejects, such as -5.309690856e+09 or a quoted string. Either form works.

Design notes and limitations

Telegram's bot-to-bot restriction

Telegram's Bot API does not deliver a message sent by one bot to another bot's getUpdates. This was established empirically against a clean-room supergroup with no prior history, testing every documented path — ambient traffic, an explicit @mention, and an explicit /command@BotName — with Bot-to-Bot ("Secretary") Mode enabled on both bots, both bots admin, and privacy mode disabled. System messages arrived; bot-authored text never did. A direct bot-to-bot DM is rejected outright with USER_BOT_TO_BOT_DISABLED.

Hub mode is the answer, as described above. Humans are unaffected either way — every bot sees every human message normally.

The same rule surfaces in two more places, both as Telegram validating a message id against the calling bot's own visibility:

  • Reactions. setMessageReaction rejects a reaction whose target belongs to another bot, so react works only on a person's message (or the bot's own) and otherwise errors, directing the agent to send a one-line reply instead. This deliberately has no workaround: posting the emoji as a message puts a bare "👀" in the transcript, spending a turn to say nothing. Between agents, a sentence is both cheaper and more useful.
  • Replies. sendMessage's reply_to_message_id is rejected the same way. send_message and mention retry as a plain send, but still record the intended reply target on the hub-side message — reply context and addressing are both driven by that stored field rather than by whether Telegram's UI drew a thread line, so the only loss is cosmetic.

No chat history API

Telegram's Bot API has no endpoint for fetching a chat's message history. Bots see messages only via getUpdates long polling, starting from when the bot was added or first messaged. The server therefore runs a background poller and keeps the most recent 2000 messages in memory. Consequently:

  • The server must be running to observe messages. Anything sent before it started, or while it was down, is unavailable.
  • Only one process may long-poll a given bot token at a time. Concurrent pollers disconnect each other, which is why each agent needs its own bot.
  • An edited message is kept alongside the original rather than replacing it, so a transcript shows both versions.

Full history or arbitrary DM access would require the MTProto user-account API instead of the Bot API — a materially different, higher-privilege approach not implemented here.

Reaching people who have no username

Telegram users who never set a username cannot be @mentioned by anyone. The roster at the top of get_conversation flags them, so agents reply to one of their messages instead. Set a username in Telegram settings to be taggable.

Rate limits

HTTP 429 is retried automatically using Telegram's retry_after hint. HTTP 409 — another process already polling the same token — fails with a specific error rather than a generic one.

Scope

Every agent must run on the same machine, because they only see each other by sharing one process. Reaching agents on a different machine would mean either exposing an unauthenticated port or relaying through a real Telegram user account via MTProto; neither is implemented, and the port is loopback-only for the reason above.

Operating notes

telegram-agent-mcp --doctor is the first thing to run when something is not working: it prints which config file is in use, whether each token authenticates, whether a hub is running, and where its log is.

Logs go to stderr; set RUST_LOG=debug for more detail. Stdout is reserved for the MCP protocol, so the background hub's output goes to hub.log beside the config file instead.

GET /health on the hub returns its version and agent list. Autostart uses it to tell a running hub from an unrelated service that happens to hold the port — a distinction that otherwise surfaces as an unexplained 404 during the MCP handshake.

telegram-agent-mcp --version prints the version, which is worth checking when a locally built binary and an installed wheel are both present.

License

MIT — see LICENSE.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

telegram_agent_mcp-0.4.0.tar.gz (90.5 kB view details)

Uploaded Source

Built Distributions

If you're not sure about the file name format, learn more about wheel file names.

telegram_agent_mcp-0.4.0-py3-none-win_amd64.whl (5.3 MB view details)

Uploaded Python 3Windows x86-64

telegram_agent_mcp-0.4.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (5.3 MB view details)

Uploaded Python 3manylinux: glibc 2.17+ x86-64

telegram_agent_mcp-0.4.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl (5.0 MB view details)

Uploaded Python 3manylinux: glibc 2.17+ ARM64

telegram_agent_mcp-0.4.0-py3-none-macosx_11_0_arm64.whl (4.9 MB view details)

Uploaded Python 3macOS 11.0+ ARM64

telegram_agent_mcp-0.4.0-py3-none-macosx_10_12_x86_64.whl (5.0 MB view details)

Uploaded Python 3macOS 10.12+ x86-64

File details

Details for the file telegram_agent_mcp-0.4.0.tar.gz.

File metadata

  • Download URL: telegram_agent_mcp-0.4.0.tar.gz
  • Upload date:
  • Size: 90.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.12.1 {"installer":{"name":"uv","version":"0.12.1","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for telegram_agent_mcp-0.4.0.tar.gz
Algorithm Hash digest
SHA256 292b803fd451d24db9d0cc6f7dcf426b6672627131b921de6159ccef16a4fb9d
MD5 e3a4d86f6129d1feff6c81da4668d807
BLAKE2b-256 f25406b91e75575aa1eca2d7dc8ab55349822bee3d15099732c7accaefd3ba87

See more details on using hashes here.

File details

Details for the file telegram_agent_mcp-0.4.0-py3-none-win_amd64.whl.

File metadata

  • Download URL: telegram_agent_mcp-0.4.0-py3-none-win_amd64.whl
  • Upload date:
  • Size: 5.3 MB
  • Tags: Python 3, Windows x86-64
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.12.1 {"installer":{"name":"uv","version":"0.12.1","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for telegram_agent_mcp-0.4.0-py3-none-win_amd64.whl
Algorithm Hash digest
SHA256 83bbce6ee466db8f39ac6103604f2105e271846195e0073fd3aae3ea5219ce03
MD5 b9d3cd56cf7f8f903805344eb221ca93
BLAKE2b-256 257dc7f30b3ee899c42be35f0dc0640a705257b59ee18dbc69741959f932ff55

See more details on using hashes here.

File details

Details for the file telegram_agent_mcp-0.4.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.

File metadata

  • Download URL: telegram_agent_mcp-0.4.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
  • Upload date:
  • Size: 5.3 MB
  • Tags: Python 3, manylinux: glibc 2.17+ x86-64
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.12.1 {"installer":{"name":"uv","version":"0.12.1","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for telegram_agent_mcp-0.4.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 d3587bc907d8452c24604c3a194f6134375a1317d8755b7daac65ee38fd1d462
MD5 957b749da1836f315c47be595c029c30
BLAKE2b-256 eec7beaa757fb0a5339aed6bf9871afc66b18f1cb5cf92b73c99e95ec50b21ed

See more details on using hashes here.

File details

Details for the file telegram_agent_mcp-0.4.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl.

File metadata

  • Download URL: telegram_agent_mcp-0.4.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
  • Upload date:
  • Size: 5.0 MB
  • Tags: Python 3, manylinux: glibc 2.17+ ARM64
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.12.1 {"installer":{"name":"uv","version":"0.12.1","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for telegram_agent_mcp-0.4.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Algorithm Hash digest
SHA256 c802a0adffc9a61d4f0a5f91ea7f32556afebb9a7fea31ef5c9a6108903769be
MD5 03927a7c230eae29d1702cefe1bbcdd2
BLAKE2b-256 3a9b2a1483131a2ab0f13de8ca8676ce1a9002d41cbde1119ea22ebda6e8ea46

See more details on using hashes here.

File details

Details for the file telegram_agent_mcp-0.4.0-py3-none-macosx_11_0_arm64.whl.

File metadata

  • Download URL: telegram_agent_mcp-0.4.0-py3-none-macosx_11_0_arm64.whl
  • Upload date:
  • Size: 4.9 MB
  • Tags: Python 3, macOS 11.0+ ARM64
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.12.1 {"installer":{"name":"uv","version":"0.12.1","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for telegram_agent_mcp-0.4.0-py3-none-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 a7073bb9bb046b083479c2863066463ac9a28dc93e8c08fb4b73035bfc09bbdd
MD5 f3deac9c3550187f6247506f952e174c
BLAKE2b-256 b2ce18bbf51514c70a58f09414c01bb277281393168dcca85275d49d6c7b15c9

See more details on using hashes here.

File details

Details for the file telegram_agent_mcp-0.4.0-py3-none-macosx_10_12_x86_64.whl.

File metadata

  • Download URL: telegram_agent_mcp-0.4.0-py3-none-macosx_10_12_x86_64.whl
  • Upload date:
  • Size: 5.0 MB
  • Tags: Python 3, macOS 10.12+ x86-64
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.12.1 {"installer":{"name":"uv","version":"0.12.1","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for telegram_agent_mcp-0.4.0-py3-none-macosx_10_12_x86_64.whl
Algorithm Hash digest
SHA256 bb12a346e78a9cb49f97fddbfbc6c53fa7559ea5df822edcbbf74f7a9faacf9b
MD5 b5c6c4480aa8e903306c0abc1814d83b
BLAKE2b-256 7f7d8a2de64df6d1a1a1bdc79b1b0a2094f69777505e70e4069adc4f9966a5ab

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page