watush-mcp
This MCP server allows agents to interact with you and your named contacts over Telegram through a bot. It provides:
Send: Send messages to yourself, specific named individuals (e.g., "Alice"), or groups tagged with labels like "family" or "partner"; the owner gets an audit copy when messaging others.
Edit: Rewrite previously sent messages in place, ideal for live status updates.
Delete: Remove sent messages to reduce clutter (Telegram allows deletion within 48 hours).
Ask: Pose questions with tap-to-answer buttons or free-text input; the agent blocks until the recipient responds, and the message updates with the answer.
List Recipients: Discover valid names and tags (chat IDs are never exposed).
Markdown: Format messages with bold, italic, code, links, etc., with automatic plain-text fallback.
Configuration: Define bot token, owner, recipients, and audit preferences via
config.yamlor environment variables.Runtime: Runs on-demand via stdio—no service needed—and coordinates multiple pending questions across agent sessions.
Allows sending Telegram messages to configured recipients (owner, friends, family) via a Telegram bot. Supports tagging and audit copies to the owner.
Click on "Install 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., "@watush-mcpsend a telegram to Alice: meeting at 3pm"
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.
watush-mcp
A robust Telegram MCP server for agents. Any agent — Claude Code, Claude Desktop, or Codex — can use it to message, send files, update, clean up after, and ask questions of you or named people (partner, family, friends) through your own Telegram bot.
So you can say:
"message me on telegram once the build is done" "send me a screenshot of the dashboard" "telegram me the report as a PDF when it's written" "keep me updated on the GPU temps in one telegram message" "ping the family: dinner at 7" "message me on watush if you need anything while I'm out"
…and it routes to the right person. You (the owner) always get an audit note for anything sent to, edited for, deleted from, or asked of someone else.
Configure several bot tokens and you get several channels — separate Telegram threads you can mute or read independently ("use the 2nd bot for the noisy stuff") — plus automatic failover when one bot is rate-limited or its token dies. See Channels.
It cannot browse your chats: the only thing it ever reads is the reply to a question it is actively waiting on.
How it works
Seven tools are exposed to the agent:
send_telegram_message({ text, to?, channel? })— send a message.tois a person's name or a tag (case-insensitive). Omitto(or use"me") to message yourself. A tag shared by several people fans out to all of them.channelpicks which bot to send through. Returns a handle like4021@main.send_telegram_file({ path, caption?, to?, channel?, as? })— send any file: screenshots, images, video, PDFs,.md/.docx/.csv, logs, archives.pathis a file on the machine running the server (or anhttp(s)URL for Telegram to fetch). The kind is picked from the extension — images preview inline,.mp4plays, everything else arrives as a downloadable attachment — andasoverrides that. See Sending files.edit_telegram_message({ message_id, text, to? })— rewrite a sent message in place. Perfect for live status messages: one message that keeps updating instead of a wall of notifications. No time limit. On a message sent bysend_telegram_file, this rewrites its caption.delete_telegram_message({ message_id, to? })— remove a sent message to cut noise. Telegram allows this within 48 hours of sending.ask_telegram_question({ question, options?, to?, timeout_seconds? })— ask a question with tap-to-answer buttons (or free-text replies — you can always just type instead). The agent blocks until you answer; your tap or reply is returned to it directly. The question message updates itself to show what you answered.list_recipients()— lets the agent discover who/what tags it can address (names + tags only; chat IDs are never exposed).list_channels()— lets the agent discover which bots exist, their positions and tags, and which are currently rate-limited or unable to reach someone (tokens are never exposed).
Markdown formatting works in all message/question text:
**bold**,*italic*,`code`, fenced blocks,[links](url),~~strike~~, headers. Rendered as native Telegram formatting, with an automatic plain-text fallback so a message is never lost to a formatting error.Timing is the agent's job. "in 30 minutes" is handled by the agent that calls the tool; the tool itself always sends now.
No background process. Stdio MCP servers are spawned on demand by each client, so there's nothing to "start on login" — installing simply registers the server with your agents so it's available whenever they run.
Related MCP server: Telegram Agent for Codex
Setup
1. Install (build + wire all agents)
cd ~/Code/watush-mcp
./install.shThis installs dependencies, compiles the TypeScript, creates config.yaml (if
missing), and registers the server with Claude Code, Codex, and Claude Desktop —
whichever are present. Existing agent configs are backed up first. Re-run it any
time (e.g. after a Node version change).
2. Configure config.yaml
config.yaml holds your secrets and is git-ignored — it is never committed.
Copy the example if you don't have one yet:
cp config.example.yaml config.yaml && chmod 600 config.yamldefaultChannel: "main"
channels: # one bot per channel; order = fallback order
- name: "main"
token: "123456789:ABC..." # from @BotFather
tags: ["general"]
- name: "backup1"
token: "987654321:XYZ..."
tags: ["backup"]
owner:
name: "Owner"
chatId: "111111" # your numeric Telegram ID (gets audit copies)
recipients:
- name: "Alice"
chatId: "222222"
tags: ["partner", "household"]
- name: "Bob"
chatId: "333333"
tags: ["friend", "team"]
auditToOwner: true3. Find chat IDs
Everyone you want to message (including you) must DM the bot once — and with
several channels configured, once per bot. Chat IDs are global to Telegram,
but a bot cannot open a conversation on its own, so a perfectly valid chat ID
still fails on a bot that person never started. npm run doctor shows exactly
which pairs are missing.
You have two ways to capture the numeric IDs — neither runs in the background:
a) On-demand /chatinfo listener (recommended)
npm run chatinfoThis starts a temporary listener on every channel at once, so one pass
confirms who has started which bot. Each person DMs a bot /chatinfo and
that bot replies with their own chat ID (it's also printed in your terminal,
prefixed with the channel). Press Ctrl-C when you're done — it does not keep
running.
npm run chatinfo -- --once # auto-stop after the first /chatinfo
npm run chatinfo -- --timeout=60 # auto-stop after 60 seconds
npm run chatinfo -- --channel=backup1 # listen on one channel onlyb) One-shot snapshot
npm run chatsPrints the chat ID + name of everyone who recently messaged any of your bots, and which bots saw each chat. (Reads only — never writes your config.)
Copy the right IDs into config.yaml, then verify the whole matrix:
npm run doctorCHANNEL BOT TOKEN Mike (you) Meg&Mike
1. main * @watushbot ok ok ok
2. backup1 @watush1bot ok ok MISSING
3. backup2 @watush2bot ok ok MISSINGMISSING means that bot cannot message that chat yet — press Start in a DM with
it, or add it to that group.
4. Restart Claude Desktop
If you use Claude Desktop, restart it so it loads the new server. Claude Code and Codex pick it up on their next run.
Usage
Just ask, in any agent:
"message me on telegram: deploy finished ✅" "text my partner: leaving now" "notify the family: flight landed"
Live-updating messages — one message, continuously rewritten:
"track the download and update me in a single telegram message" "keep a running status of the migration on watush"
Cleanup — the agent can delete its own noise:
"delete those progress messages now that it's done"
Questions — the agent asks, you tap (or type), the agent continues:
"message me on watush if you need anything" → later, your phone shows "Deploy to prod now, or wait for the tests?" with [Deploy now] [Wait] buttons. Tap one and the agent carries on with your answer.
Behind the scenes the agent calls send_telegram_message,
edit_telegram_message, delete_telegram_message, or
ask_telegram_question. Anything involving a person other than you also
sends you an audit note like:
🔔 Audit — sent to Alice: leaving now
✏️ Audit — edited message to Alice: updated ETA 18:30
❓ Audit — asked Alice: pizza or sushi?Sending files
"send me a screenshot of the desktop" "telegram me that chart" "write the summary to a .md and send it over"
The agent takes the screenshot or writes the document first, then passes the path. The file has to live on the machine running the server — each machine runs its own copy, so "send me a screenshot" means that machine's screen.
Extension | Arrives as | Method |
| inline photo (recompressed) |
|
| looping animation |
|
| inline video player |
|
| audio track |
|
| voice note |
|
everything else — | downloadable file |
|
asoverrides detection.as: "document"keeps an image at full quality (Telegram recompresses photos);as: "photo"forces an inline preview.Limits. Telegram caps bot uploads at 10 MB for photos and 50 MB for everything else. An oversized image is downgraded to a document automatically; over 50 MB is refused with a clear message.
Nothing gets stuck. If Telegram rejects a file as a photo/video (odd dimensions, an unsupported codec), it is retried once as a document — but only when a different kind could actually help, never on a block or a rate limit.
Captions take Markdown, and are trimmed to Telegram's 1024-character cap.
URLs work for images, video, audio, PDF and ZIP. Telegram will not fetch other file types by URL — download it locally first, then send the path.
Audit copies. A file sent to someone else is
copyMessaged into your chat, so you get the actual media without a second upload.
How questions work (and their limits)
The question arrives as a normal Telegram message from your bot, with one button per option. Tapping answers instantly; typing a message answers free-form. With several questions pending in one chat, swipe-reply to a specific question message to target it; a plain message answers the newest.
The asking agent waits as long as its
timeout_seconds(default 15 min). In Claude Code, long waits are automatically moved to a background task, so the agent keeps working until your answer arrives.Several agent sessions can wait on questions at once — watush coordinates them so only one process polls Telegram (Telegram allows a single poller per bot token), and answers are routed to whichever session asked.
One machine at a time. The coordination state is per-machine, but the bot token's update stream is global. If agents on two different machines wait on questions at the same moment, one machine's poller can consume (and lose) the other machine's answers, and they compete for the single polling slot. Keep simultaneous questions on one machine, or give each machine its own bot token (each token gets its own state dir automatically).
Channels (several bots)
Each channel is its own Telegram bot, which means its own conversation thread. Give the noisy things their own bot and you can mute that thread without missing an alert. Configure as many as you like — @BotFather allows 20 per account.
defaultChannel: "main"
channels:
- name: "main"
token: "..."
tags: ["general"]
- name: "backup1"
token: "..."
tags: ["backup"]
- name: "backup2"
token: "..."
tags: ["backup"]
# fallback: false # keep this one out of automatic failoverOrder matters. The list order is the failover order. Channel 1 is the
default unless defaultChannel names another — and a channel is always
reachable by its own name, even one literally named main or default.
Addressing a channel
You say… |
| Resolves to |
"send it on backup1" |
| the channel named |
"use the 2nd bot" |
| channel at position 2 |
also | the same, by position | |
"use a backup" |
| the first channel tagged |
(omitted) | — |
|
A name always wins over a position reading, so backup1 means the channel
named backup1 — even though it sits at position 2.
Failover
A send walks the chain until a bot gets through, and the result says so:
Sent to you — message_id 4@backup1. (watush: Unauthorized — fell back to backup1.)It falls over on rate limits (429), dead tokens (401/404), server errors (5xx), network failures, and "this bot can't reach that chat". It does not fall over on content errors — bad markup or an over-long message fails identically on every bot, so retrying would only spend a second bot's quota to reproduce the error. At most 6 bots are tried per delivery, and if that cap is reached the result names the channels it never got to.
Messages are at-least-once. Telegram has no idempotency keys, so a send whose
response times out may already have arrived; retrying it on the next bot can
duplicate a notification. For messages and files that is the intended trade — a
rare double ping beats a silently lost alert, which is the entire reason for
having spare bots. Where a duplicate would be actively harmful the code does
not retry an ambiguous failure: ask_telegram_question will not re-post a
question that may already be on screen with a live keyboard, and a media audit
copy that times out is not re-sent as a text note.
Edits and deletes never fail over. A message_id belongs to exactly one
(bot, chat) pair, so 4021@main can only be edited by main. If that channel is
down the call fails and says which channel owns the message — quietly re-posting
through another bot would strand the original and split a live-progress thread
across two chats mid-run.
Handles
send_telegram_message returns 4021@main, not a bare number, because with
several bots the same message_id genuinely exists in several threads — three
fresh bots will each mint message_id 2 in your chat. Pass the handle back to
edit_telegram_message / delete_telegram_message and the right bot is used
automatically. A bare number is still accepted and means the default channel.
Health memory
Failures are remembered in ~/.watush/state/channels.json so the next send
routes around a known problem instead of rediscovering it:
Channel cooldown — the bot itself is in trouble (flood-wait, dead token, 5xx, no network). Time-based, honouring Telegram's own
retry_after.Unreachable pair — the bot is fine but cannot reach that one chat (nobody pressed Start; it was never added to the group). Recorded per (channel, chat), so a bot missing from one group is never treated as down for your DMs. Expires after 6 h, so adding the bot to a group heals routing without running anything.
Nothing is ever dropped on the basis of this memory — unhealthy channels sink to the back of the queue rather than being removed, so if every bot is flood-waiting, the one recovering soonest is still tried. A successful send clears both kinds of memory for that pair.
Configuration reference
Key | Meaning |
| Channel name, addressable as |
| That channel's bot token from @BotFather. |
| Aliases like |
|
|
| Channel used when the agent names none. Defaults to the first. |
| Legacy single-bot form; migrated to a one-entry channel list. |
| Your display name. |
| Your numeric Telegram ID. Receives audit copies. |
| A person's name (addressable as |
| Their numeric Telegram ID. |
| Aliases like |
|
|
Environment overrides (handy for CI / secrets managers):
TELEGRAM_BOT_TOKEN (replaces the default channel's token), TELEGRAM_CHAT_ID
(owner), TELEGRAM_RECIPIENTS and WATUSH_CHANNELS (JSON arrays merged over the
file, by name), and WATUSH_CONFIG (alternate config path).
Resolving to
You say… |
| Goes to |
"message me" | (omitted) | owner (you) |
"message Alice" |
| Alice |
"message my partner" |
| whoever is tagged |
"message the family" |
| everyone tagged |
"message Dana" (unknown) |
| nothing — returns a helpful error listing known names/tags |
Uninstall
./uninstall.sh # unregister from all agents (keeps config.yaml)
./uninstall.sh --purge # also remove dist/ and node_modules/Security
config.yamlis git-ignored andchmod 600. The token and chat IDs never leave your machine and are never committed.No chat browsing: there is no tool to read message history or list chats. Incoming messages are only read while a question is pending, and only to resolve that question.
Question state (pending questions, answers, poll coordination) lives in
~/.watush/state/<token-hash>/, keyed by a hash of the bot token; override the location withWATUSH_STATE_DIR.If your token ever leaks, rotate it in @BotFather (
/revoke) and paste the new one intoconfig.yaml.
On send_telegram_file and file access. The tool will read any path the
agent hands it, deliberately: restricting it to a workspace root would break its
whole purpose (screenshots land in temp dirs, documents live anywhere), and it
would buy little — an agent that can read a file could already paste the
contents through send_telegram_message. The file tool is a nicer envelope for
that same trust, not a new one. Two things are refused outright:
watush's own
config.yaml, so the bot token can never be uploaded to a chat.UNC paths to hosts outside the LAN (
\\evil.example\share\x.png). On Windows, merely stat-ing one performs an SMB handshake that hands the remote server a crackable NTLM challenge-response. LAN shares (\\COVE\Archive, private IPs) still work.
Note that sending a file does upload its bytes to Telegram's servers. If you keep data deliberately off third-party infrastructure, that applies here.
Development
npm install
npm run build # tsc -> dist/
npm test # offline unit tests (resolution, config, fallback, audit logic)
npm run doctor # check every token + who each bot can actually reach
npm start # run the MCP server over stdioProject layout:
src/config.ts load YAML config + resolve recipients (pure, tested)
src/channels.ts channel resolution (name/position/tag) + handles (pure, tested)
src/health.ts per-channel cooldowns + per-chat reachability memory (tested)
src/format.ts Markdown → Telegram HTML + plain-text fallback (pure, tested)
src/telegram.ts Telegram Bot API client: send/edit/delete/callbacks/updates
src/deliver.ts resolve + send/edit/delete + audit orchestration (pure, tested)
src/questions.ts question engine: shared state, poll lock, answer spool
src/mcp-server.ts the MCP server (seven tools)
src/doctor.ts "npm run doctor" token + reachability matrix
src/list-chats.ts read-only "npm run chats" helper
src/listen.ts on-demand "npm run chatinfo" listener
bin/watush-mcp-server launcher that resolves node for GUI apps / nvm
scripts/ config editors used by install/uninstallMIT licensed.
Available Tools
2 toolslist_recipientsList Telegram recipientsA
List who can be messaged on Telegram: the owner (you) and configured recipients with their names and tags. Use this to map a phrase like 'my partner' to a valid 'to' value for send_telegram_message. Chat IDs are never returned.
| Name | Required | Description | Default |
|---|---|---|---|
No parameters | |||
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations provided, the description carries the full burden. It discloses a key behavioral trait: 'Chat IDs are never returned.' This is important for privacy and security. It also implies the output contains recipient names and tags, which is sufficient for this simple tool.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is two sentences, front-loaded with purpose and followed by usage guidance and a key constraint. Every sentence is informative and earns its place with no wasted words.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
Given the tool has no parameters, no output schema, and no annotations, the description provides all necessary context: purpose, usage guidance, and a critical behavioral detail (no chat IDs). It is fully complete for an agent to understand and select the tool.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
The input schema has zero parameters, so the description provides all meaningful semantics. It explains what the tool returns (names and tags) and explicitly states what it does not return (chat IDs). This adds complete value beyond the empty schema.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description clearly states the tool lists who can be messaged (owner and configured recipients) with names and tags. It uses the verb 'List' and the resource 'Telegram recipients', and distinguishes from the sibling tool send_telegram_message by explaining its purpose for mapping phrases to valid 'to' values.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The description explicitly tells when to use this tool: to map a phrase like 'my partner' to a valid 'to' value for send_telegram_message. It provides clear context for usage, though it does not include explicit when-not scenarios, which is acceptable given its straightforward purpose.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
send_telegram_messageSend Telegram messageA
Send a Telegram message to the user (yourself) or to a named/tagged person such as a partner, family member, or friend. Use whenever the user asks to be messaged, pinged, notified, texted, or alerted on Telegram, or to message someone like a partner or a named contact, or to deliver a result/reminder. Omit 'to' (or use 'me') to message the user themselves. The user (owner) always receives an audit copy of messages sent to other people.
| Name | Required | Description | Default |
|---|---|---|---|
| text | Yes | The message text to send. | |
| to | No | Who to send to: a person's name (e.g. 'Alex') or a tag (e.g. 'partner', 'friend', 'family'). Omit or use 'me' to message the user (owner). Call list_recipients to see valid names and tags. |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
No annotations are provided, so the description carries the full burden. It discloses that the user always receives an audit copy of messages sent to others, which is a key behavioral detail. However, it does not address error conditions or prerequisites like authentication.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is concise (three sentences), front-loaded with the main purpose, and every sentence adds essential information. No wasteful repetition or fluff.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
The tool has no output schema, so the description should ideally mention the return value or success indication. It covers the main behavior and the audit copy, but omits what the agent can expect as a response (e.g., success message, error handling). For a simple send tool, it is minimally adequate.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema coverage is 100%, so the baseline is 3. The description adds value by explaining that omitting 'to' or using 'me' messages the user, and by directing to list_recipients for valid names/tags, which goes beyond the schema's property descriptions.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description clearly states the tool sends a Telegram message to the user or a named/tagged person. It specifies the verb 'send' and the resource 'Telegram message', and distinguishes itself from the sibling tool list_recipients by explaining how to specify recipients.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The description provides explicit guidance on when to use the tool (e.g., when the user asks to be messaged, pinged, notified, etc.) and mentions using list_recipients to see valid recipients. It also explains the behavior of the 'to' parameter. While it lacks explicit 'when not to use' instructions, the context is clear.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
2 tool updates
v1.0.0- First observed
list_recipients - First observed
send_telegram_message
TDQS
Scored across 2 tools
The two tools are clearly distinct: one lists recipients, the other sends messages. No overlap or ambiguity.
Both tools follow a consistent verb_noun pattern (list_recipients and send_telegram_message), making the naming predictable.
Two tools are exactly appropriate for a focused Telegram messaging server. Each tool has a clear, indispensable role.
The toolset covers the full lifecycle of sending a Telegram message: discover recipients and send messages. No obvious gaps given the intended scope.
Maintenance
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
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.
141Unified inbox MCP for WhatsApp, Telegram, Email, voice — read/send messages, search, AI agents.
Hosted MCP messaging across owners, tools, and machines, with readable transcripts.
Related MCP Servers
- AlicenseBqualityDmaintenanceA Telegram MCP server that connects agents to a real Telegram user account via MTProto, enabling reading, searching, sending, moderating, and managing Telegram chats through natural language or automated tool calls.1007729MIT
- AlicenseNot gradedqualityCmaintenancePrivacy-first Telegram MCP server enabling maintainers to triage chats, inspect context, search messages, draft replies, and send authorized messages locally without a cloud relay.301MIT
- AlicenseNot gradedqualityBmaintenanceMCP server for Telegram integration with Command Code, enabling AI agents to send messages, photos, files, and read updates via Telegram.2MIT
- FlicenseNot gradedqualityCmaintenanceMCP server that enables AI agents to send messages, photos, polls, and receive replies via Telegram with persistence and rate limiting.27-