telegram-mcp
by VladIvanchuk
README.md
# Telegram MCP Server for Claude Cowork
Minimal MCP server with a single tool: `send_telegram_message`.
No third-party intermediaries (Composio, etc.) — just your code, your bot, your server.
## Step 1. Create a Bot in Telegram
1. Open a chat with **@BotFather** in Telegram.
2. Send `/newbot`.
3. Provide a name (anything) and a username ending in `bot` (for example, ``reminders_my_bot`).
4. BotFather will send a token like `123456789:AAHfiqksKZ8...` — save it, this is your `TELEGRAM_BOT_TOKEN`.
## Step 2. Find Your chat_id
1. Find your bot in Telegram by its username and send it any message (for example, "hello").
2. Open in your browser (substitute your token):
```
https://api.telegram.org/bot<TELEGRAM_BOT_TOKEN>/getUpdates
```
3. In the response, find `"chat":{"id": NUMBER, ...}` — this is your `TELEGRAM_CHAT_ID`.
## Step 3. Create a Secret
Set any long random string — this is your `MCP_SHARED_SECRET`.
It secures the server: without it, no one (even if they find the URL) can send messages through your bot. You can generate one with:
```bash
openssl rand -hex 32
```
## Step 4. Deploy the Server
The easiest options are **Render.com** (free tier available) or **Railway.app**.
General steps for any of these platforms:
1. Push this directory (`telegram-mcp/`) to a GitHub repository (can be private).
2. On Render: New → Web Service → connect the repository.
- Build command: `npm install`
- Start command: `npm start`
3. In the Environment section, add three environment variables:
- `TELEGRAM_BOT_TOKEN` = token from Step 1
- `TELEGRAM_CHAT_ID` = id from Step 2
- `MCP_SHARED_SECRET` = secret from Step 3
4. Deploy. Render will give you a public URL like `https://telegram-mcp-xxxx.onrender.com`.
5. Your MCP endpoint is `https://telegram-mcp-xxxx.onrender.com/mcp`.
> Note: Render's free tier "sleeps" after a period of inactivity and takes ~30 seconds to wake up on the first request. For a "once a week" task, this is not an issue.
## Step 5. Connect to Claude
1. `Customize` → `Connectors` → `+` → `Add custom connector`.
2. Name: `Telegram Notify` (or whatever you like).
3. Remote MCP server URL: put the secret straight into the path —
`https://your-domain/mcp/<your MCP_SHARED_SECRET>`
4. Leave the OAuth Client ID / Secret fields under **Advanced settings** empty — this server doesn't use OAuth.
5. Click **Add**.
The server also accepts the secret as a header (`Authorization: Bearer <MCP_SHARED_SECRET>`
against `https://your-domain/mcp`), which is the cleaner option if your client can send
custom request headers. The connector dialog currently offers only OAuth fields, so the
URL-path form above is the one that works there.
> The secret in the URL is as sensitive as a password: don't paste that URL into
> screenshots, issues, or chats. If it leaks, change `MCP_SHARED_SECRET` in Render's
> Environment section and update the connector URL.
## Step 6. Use in a Scheduled Task
In Cowork:
```
/schedule
```
and describe the task, for example:
> Every Friday at 9:00 AM check [event source] and send me a short digest via send_telegram_message.
Before scheduling, test it manually with a one-off task to make sure the message actually arrives.
## Topics (routing each automation to its own tab)
Each automation can post into its own topic. Two places can host those topics, and
the server code is identical for both — only `TELEGRAM_CHAT_ID` differs.
**Option A — private chat with the bot ("Threaded Mode").** Added in Bot API 9.3
(2025-12-31): enable it via @BotFather → your bot → Threaded Mode, then create the
topics in the chat itself. `TELEGRAM_CHAT_ID` stays your personal chat id.
> Between the Bot API 10.0 rollout (2026-05-08) and some point before 2026-08-07,
> sending with `message_thread_id` to a *private* chat returned
> `400: message thread not found` — see
> [tdlib/telegram-bot-api#847](https://github.com/tdlib/telegram-bot-api/issues/847).
> Verified working again on 2026-08-07. If that error comes back, option B is
> unaffected by it.
**Option B — forum supergroup.** Unaffected by the above.
1. Create a group, add your bot, promote it to admin.
2. Group settings → **Topics** → on. Create one topic per automation.
3. Set `TELEGRAM_CHAT_ID` to the group id (looks like `-1001234567890`).
**Getting the ids (either option):** post a message in each topic, then open
`https://api.telegram.org/bot<TOKEN>/getUpdates` and read `"chat":{"id": ...}` and
`"message_thread_id"`. In Telegram Desktop/Web you can instead copy a message link
from the topic — `t.me/c/<chat>/<thread>/<message>`.
Then add the map on Render:
```
TELEGRAM_TOPICS = {"events":2,"news":8,"jobs":14}
```
The tool exposes a `topic` parameter restricted to those names, so each scheduled
task states where its output belongs. Omitting it posts to General.
Invalid JSON or a non-integer id fails at startup rather than silently misrouting
messages days later.
## Message Formatting
Messages are parsed as **Telegram HTML**. The supported tags are:
```
<b>bold</b> <i>italic</i> <u>underline</u> <s>strikethrough</s>
<code>inline code</code> <pre>code block</pre>
<a href="https://example.com">link</a>
<blockquote>quote</blockquote> <tg-spoiler>hidden</tg-spoiler>
```
That's the whole list. There is no `<br>`, no headings, no `<ul>`/`<li>`, no tables —
use real newlines for line breaks and emoji or `•` for bullets. Literal `<`, `>`, `&`
in prose must be escaped as `<` `>` `&`.
HTML is used instead of MarkdownV2 because MarkdownV2 requires escaping over a dozen
characters (`.`, `-`, `!`, `(`, `)`, …) and a single missed one rejects the message.
If the markup is invalid anyway, the server retries once without parsing so the text
still arrives — unformatted, with a note in the tool result. Pass `plain: true` to skip
parsing deliberately (useful when the body contains raw angle brackets).
Link previews are disabled, so a message with a URL stays a one-liner.
## Local Launch (for testing)
```bash
npm install
TELEGRAM_BOT_TOKEN=xxx TELEGRAM_CHAT_ID=xxx MCP_SHARED_SECRET=xxx npm start
```
This server cannot be deployed
Maintenance
ActivitySlowing
ResponsivenessNo issues