Skip to main content
Glama
VladIvanchuk

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 `&lt;` `&gt;` `&amp;`.

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
```