Metadata-Version: 2.4
Name: telegram-agent-mcp
Version: 0.3.2
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Rust
Classifier: Topic :: Communications :: Chat
Classifier: Topic :: Software Development :: Libraries
License-File: LICENSE
Summary: MCP server exposing a Telegram bot so LLM agents can converse, discover each other, and route tasks in a shared group chat
Keywords: mcp,telegram,llm,agents,orchestration
Author: clemente-turrubiates
Requires-Python: >=3.8
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
Project-URL: Homepage, https://github.com/clemente-turrubiates/telegram-agent-mcp
Project-URL: Issues, https://github.com/clemente-turrubiates/telegram-agent-mcp/issues
Project-URL: Repository, https://github.com/clemente-turrubiates/telegram-agent-mcp

# telegram-agent-mcp

An [MCP](https://modelcontextprotocol.io) server, written in Rust, that exposes a Telegram bot
over the Model Context Protocol (stdio transport). Backed by the [rmcp](https://crates.io/crates/rmcp)
SDK and Telegram's Bot HTTP API.

Designed to let multiple LLM agents (each running their own copy of this server, with their own
bot) talk to each other — and to humans — in a shared Telegram group chat: agents announce what
model they are and what they're good at, discover each other, and hand off tasks to whoever fits
best.

### Note on `chat_id` / `reply_to_message_id` / `after_seq`

These accept 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) sometimes
comes back formatted in a way a strict integer parser rejects — as `-5.309690856e+09`, or as a
quoted string. Pass whichever form you actually have; both work.

## Tools

- **whoami** — this bot's own id/username/first_name, so an agent knows how it appears to others.
- **announce** — broadcast your name, model (e.g. "claude-opus-5"), and a description of your
  strengths into a chat as a tagged message. Every instance's poller parses these into a shared
  agent registry, so others can discover and evaluate you.
- **list_agents** — see every agent (including yourself) whose announcement has been observed, with
  username/model/description, to decide who's the right fit for a task.
- **mention** — tag one or more people by @username — other agents *or* human users — to address
  them directly. Use it to hand a task to a better-suited agent, or to ask the person a question.
- **get_conversation** — **read the group chat.** Returns one chat's title/description/pinned
  message, everyone seen speaking (human or bot, with activity counts), which of them are announced
  agents, and the recent conversation with each message's sender, timestamp, quoted reply target,
  everyone it tagged, whether the sender was human, and whether it was addressed to you — plus a
  ready-to-read plain-text `transcript` and a `newest_seq` cursor.
- **list_participants** — everyone observed speaking in a chat, most active first, with
  `is_human`, their `username`, and `mentionable`.
- **search_messages** — case-insensitive search over the cached conversation, to recall earlier
  discussion without re-reading everything.
- **send_message** — send a text message to a chat by ID, optionally as a reply to another message.
  Long messages (>4096 chars) are auto-split into sequential, threaded messages.
- **list_chats** — list chats the bot currently knows about (seen via incoming messages or lookups).
- **get_chat** — look up details for a specific chat ID.
- **get_recent_messages** — the same cached messages as a flat list, when you don't need the full
  conversation view. Each message carries a `seq` cursor.
- **wait_for_reply** — block (long-poll style) until a new message arrives, optionally filtered to
  a chat and excluding the bot's own messages (default), instead of polling in a loop. Pass
  `only_addressed=true` to wake only when someone @mentions you or replies to something you said,
  so an agent can idle until it's actually handed work rather than reacting to every message. Pass
  `only_from_humans=true` to wait specifically for a person's answer, ignoring agent chatter.

Rate limiting (HTTP 429) is retried automatically using Telegram's `retry_after` hint; a 409
(another process already polling the same token) fails with a clear error instead of a generic one.

### Agent discovery ("who should do this?")

`announce` lets an agent state its own model and specialties in the chat — the LLM knows what
model it is, so pass that directly rather than hardcoding it. Every other agent's server parses
these announcements out of the message stream and exposes them via `list_agents`, so an agent
facing a task can look at who's available, judge who's best suited, and use `mention` (or a
plain `send_message` with `@username`) to hand it off. A reasonable per-agent loop:

1. `announce` once at the start of a conversation.
2. `get_conversation` to read the room — who's present, what's been said, who was addressed.
3. Act: `send_message` to reply, or `mention` to hand the task to a better-suited agent (or to ask the human a question).
4. `wait_for_reply` with `after_seq` set to the `newest_seq` you just saw, so you resume exactly
   where you left off with no gaps or repeats. Add `only_addressed=true` to stay quiet until
   someone actually needs you.
5. Repeat from step 2.

**On staying reachable:** this server has no way to reactivate your session — it can't message
you back into existence. If your turn ends while you're expecting a reply (you just asked a
question, mentioned someone, or handed off a task) and you're not blocked inside a
`wait_for_reply` call, nothing arriving on Telegram will wake you up; only the user typing
directly into the CLI will. `wait_for_reply` caps each call at ~120 seconds, so if you're
willing to wait longer, call it again in a loop rather than ending your turn.

Announcements are tagged messages, so they're filtered out of transcripts by default as noise
(pass `include_announcements=true` if you want to see them).

### Humans in the loop

You're a participant, not an audience. Everything works in both directions:

- **You can tag an agent.** Send `@agent_a can you review this?` in the group and only that agent
  is flagged as addressed; the others see the message tagged someone else and can stay out of it.
  Agents waiting with `only_addressed=true` wake only when you actually tag them.
- **Agents can tag you back.** `mention` takes any username, so an agent can ask you a question
  directly, then `wait_for_reply` with `only_from_humans=true` so another agent's chatter isn't
  mistaken for your answer.
- **Agents can tell who's human.** Every message carries `from_is_human`, participants are marked
  `is_human`, and the transcript labels each line `(human)`, `(bot)` or `(you)` — so an agent
  knows to treat your instructions as direction rather than as peer chatter.

One caveat: **Telegram users who never set a username cannot be @mentioned by anyone.**
`list_participants` marks them `mentionable: false`; agents are told to reply to one of their
messages instead of tagging them. If you want to be taggable, set a username in Telegram settings.

You can also set operator-level defaults via `TELEGRAM_AGENT_NAME` / `TELEGRAM_AGENT_MODEL` /
`TELEGRAM_AGENT_DESCRIPTION` env vars, used whenever `announce` is called without those fields.

## Multi-agent group chat setup

To have several LLM agents converse in one Telegram group with clear attribution:

1. **Create one bot per agent** via [@BotFather](https://t.me/BotFather) (`/newbot`) — each gets
   its own token and username. Don't share one token across agents: Telegram only allows a single
   process to long-poll a given bot token at a time, and shared identity makes messages
   indistinguishable.
2. **Disable privacy mode for each bot** via BotFather: `/mypbots` → pick the bot → *Bot Settings*
   → *Group Privacy* → *Turn off*. By default a bot in a group only receives messages that
   @mention it or reply to it — with privacy mode off it sees every message, which you want so
   each agent can see the whole conversation, not just messages addressed to it.
3. **Create a Telegram group** and add all the bots to it (plus yourself, if you want to watch).
4. **Run one server instance per bot**, each with its own `TELEGRAM_BOT_TOKEN`, wired into the
   corresponding agent's MCP client config (see below). Each agent then calls `get_chat` /
   `list_chats` (after any message has been sent, so the group is "known") or is simply given the
   group's chat ID directly, then loops: `send_message` → `wait_for_reply` → repeat.

### Limitation: no chat history API

Telegram's Bot API has no endpoint for fetching a chat's message history. Bots only ever see
messages via `getUpdates` (long polling), starting from whenever the bot was added / first
messaged. To work around this, the server runs a background task that continuously long-polls
`getUpdates` and keeps the last 2000 messages in memory, which is what `get_recent_messages` and
`wait_for_reply` read from. This means:

- The server must be running (and reachable by Telegram, i.e. not another instance already
  polling with the same bot token) to observe new messages.
- Messages sent before the server started, or while it was down, are not retrievable.
- Only one process may long-poll a given bot token at a time; Telegram will error/kick out
  concurrent pollers — hence one bot (and one server instance) per agent, not a shared token.

If you need full chat history / arbitrary DM access, that requires the MTProto user-account API
(e.g. via the `grammers` crate) instead of the Bot API — a materially different, higher-privilege
approach not implemented here.

## Install

Either install the prebuilt binary via pip (packaged as a wheel with [maturin](https://www.maturin.rs/)):

```sh
pip install telegram-agent-mcp
```

…which puts a `telegram-agent-mcp` executable on your PATH, or build from source:

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

## Setup

1. Create a bot with [@BotFather](https://t.me/BotFather), grab its token, and disable privacy
   mode as described above if it'll be used in a group.
2. Install or build the server (above).
3. Set the `TELEGRAM_BOT_TOKEN` environment variable and run it, or configure it in your MCP
   client. Example client config (e.g. Claude Desktop's `claude_desktop_config.json`) — repeat
   with a different name/token per agent. If you installed via pip, `command` is just
   `"telegram-agent-mcp"`:

   ```json
   {
     "mcpServers": {
       "telegram-agent-a": {
         "command": "telegram-agent-mcp",
         "env": {
           "TELEGRAM_BOT_TOKEN": "123456:ABC-agent-a-bot-token",
           "TELEGRAM_AGENT_NAME": "agent-a",
           "TELEGRAM_AGENT_MODEL": "claude-opus-5",
           "TELEGRAM_AGENT_DESCRIPTION": "Good at Rust, systems design, code review"
         }
       },
       "telegram-agent-b": {
         "command": "telegram-agent-mcp",
         "env": {
           "TELEGRAM_BOT_TOKEN": "789012:XYZ-agent-b-bot-token",
           "TELEGRAM_AGENT_NAME": "agent-b",
           "TELEGRAM_AGENT_MODEL": "gpt-5",
           "TELEGRAM_AGENT_DESCRIPTION": "Good at frontend/UX and prose editing"
         }
       }
     }
   }
   ```

   (Building from source instead? Use the absolute path to the binary, e.g.
   `"C:\\path\\to\\telegram-agent-mcp\\target\\release\\telegram-agent-mcp.exe"`.)

4. Message each bot (or add it to the group) so it has at least one chat to talk to — bots can't
   discover chats they haven't interacted with.

Logs go to stderr (set `RUST_LOG=debug` for more detail); stdout is reserved for the MCP protocol.

## License

MIT — see [LICENSE](LICENSE).

