This guide walks you through creating a Telegram bot and configuring it for use with Telegram Bridge MCP.
An AI assistant can read this resource (telegram-bridge-mcp://setup-guide) and walk you through setup step-by-step.
This section is not optional. An unsecured bot token is a public endpoint — anyone who finds your bot can message it and inject responses into the agent's decision stream.
The server enforces security at two independent layers:
Your numeric Telegram user ID. When set:
- Every update (message, button press) is checked against this ID before it is returned to the agent.
- Updates from any other sender are silently consumed and discarded — they advance the offset so the queue stays clean, but the agent never sees them.
- Without this, a second person messaging your bot could feed the agent arbitrary responses.
| Threat | Mitigated by |
|---|---|
| Stranger messages bot to inject replies | ALLOWED_USER_ID |
| Agent redirected to message a different chat | No chat_id parameter — target is always ALLOWED_USER_ID |
| Token leak → someone sends messages as bot | Rotate via /revoke in BotFather |
| Token in version control | .env is git-ignored; never put it in config files |
Startup behaviour: If ALLOWED_USER_ID is not set the server starts but emits a warning to stderr. Set it before using the bot in any real workflow.
-
Open Telegram and search for @BotFather (official, has a blue checkmark).
-
Send
/newbot. -
When prompted, enter a display name (e.g.
My Coding Assistant). -
Enter a username — must end in
bot(e.g.mycodingassistant_bot). -
BotFather replies with your HTTP API token — a string like:
123456789:AABBCCDDEEFFaabbccddeeff-1234567890Copy it. Treat it like a password — never commit it to git.
Copy .env.example to .env in the project root (already git-ignored), then fill in your values:
BOT_TOKEN=123456789:AABBCCDDEEFFaabbccddeeff-1234567890
# Strongly recommended — see Security Model above
ALLOWED_USER_ID=<your numeric user ID>Or pass both as environment variables in your MCP host config (see Step 5).
The bot needs your numeric Telegram user ID for ALLOWED_USER_ID. For private 1-on-1 bots, your chat ID equals your user ID — no separate config needed.
-
Search for your bot by @username in Telegram and start a chat.
-
Send any message (e.g.
/start). -
In a browser, open:
https://api.telegram.org/bot<YOUR_TOKEN>/getUpdates -
In the JSON response, find:
{ "message": { "from": { "id": 123456789 } } }message.from.id→ your user ID → use asALLOWED_USER_ID.
Tip:
pnpm pairautomates this step — it polls for the pairing code and writesALLOWED_USER_IDto.envautomatically.
Use action(type: "chat/info") to verify the bot connection. It should return chat info confirming the session is active.
If you get a 401 Unauthorized error, the token is wrong — regenerate it with /revoke in BotFather.
Run one server instance and connect any number of editors or Claude Code sessions. Each client gets its own MCP session with an isolated queue — no getUpdates conflicts.
1. Start the server in its own terminal window:
pnpm start -- --httpThe --http flag enables Streamable HTTP mode on the port configured in .env
(MCP_PORT, default 3099). All other config comes from .env — no credentials
in your editor settings.
Resilience tip: Run the server in a standalone terminal window outside your editor. If the editor crashes, the MCP server stays up and agent sessions survive. On Windows, you can spawn an independent window from PowerShell:
Start-Process pwsh -ArgumentList "-NoExit","-Command","cd 'path/to/telegram-bridge-mcp'; pnpm start -- --http"
2. Point your MCP hosts at it:
VS Code (.vscode/mcp.json or user settings):
{
"servers": {
"telegram": {
"type": "streamable-http",
"url": "http://127.0.0.1:3099/mcp"
}
}
}Claude Code (.mcp.json):
{
"mcpServers": {
"telegram": {
"type": "streamable-http",
"url": "http://127.0.0.1:3099/mcp"
}
}
}Claude Desktop (claude_desktop_config.json): same shape as Claude Code.
Cursor (.cursor/mcp.json in your project root):
{
"mcpServers": {
"telegram": {
"type": "streamable-http",
"url": "http://127.0.0.1:3099/mcp"
}
}
}Scope tip: A project-scoped config (
.mcp.json,.vscode/mcp.json) keeps Telegram tools out of unrelated sessions. A global config works fine with Streamable HTTP or the launcher bridge — each session gets its own isolated queue. Avoid global configs with raw stdio (dist/index.js) though — multiple instances will fight overgetUpdates.
stdio mode spawns a dedicated process per host. Only one host can connect at a time — multiple instances will fight over getUpdates.
VS Code (.vscode/mcp.json):
{
"servers": {
"telegram": {
"type": "stdio",
"command": "node",
"args": ["dist/index.js"],
"cwd": "/path/to/telegram-bridge-mcp",
"env": {
"BOT_TOKEN": "YOUR_TOKEN_HERE",
"ALLOWED_USER_ID": "123456789"
}
}
}
}Claude Desktop (claude_desktop_config.json):
{
"mcpServers": {
"telegram": {
"command": "node",
"args": ["/absolute/path/to/telegram-bridge-mcp/dist/index.js"],
"env": {
"BOT_TOKEN": "YOUR_TOKEN_HERE",
"ALLOWED_USER_ID": "123456789"
}
}
}
}Claude Code (.mcp.json): same shape as Claude Desktop.
Cursor (.cursor/mcp.json):
{
"mcpServers": {
"telegram": {
"command": "node",
"args": ["/absolute/path/to/telegram-bridge-mcp/dist/index.js"],
"env": {
"BOT_TOKEN": "YOUR_TOKEN_HERE",
"ALLOWED_USER_ID": "123456789"
}
}
}
}A dist/launcher.js convenience script is also available — it auto-starts the HTTP server if none is running, then bridges stdio ↔ HTTP. This lets you use a stdio config while still benefiting from a shared server.
Launcher bridge (auto-starts the HTTP server):
Instead of starting the server manually, use dist/launcher.js as a drop-in stdio replacement. It auto-starts the HTTP server on first use and bridges stdin/stdout ↔ HTTP for all subsequent connections. Credentials come from .env — no need to duplicate them in editor config.
VS Code (.vscode/mcp.json):
{
"servers": {
"telegram": {
"type": "stdio",
"command": "node",
"args": ["dist/launcher.js"],
"cwd": "/absolute/path/to/telegram-bridge-mcp"
}
}
}Claude Desktop (claude_desktop_config.json) / Claude Code (.mcp.json):
{
"mcpServers": {
"telegram": {
"command": "node",
"args": ["/absolute/path/to/telegram-bridge-mcp/dist/launcher.js"]
}
}
}Cursor (.cursor/mcp.json):
{
"mcpServers": {
"telegram": {
"command": "node",
"args": ["/absolute/path/to/telegram-bridge-mcp/dist/launcher.js"]
}
}
}Voice messages are auto-transcribed before delivery using a bundled ONNX Whisper model. No external API or ffmpeg required.
WHISPER_MODEL=onnx-community/whisper-base # default; swap for a larger model for better accuracy
WHISPER_CACHE_DIR=/path/to/cache # optional — cache model files heresend(type: "text", audio: "...") picks a TTS provider in priority order:
| Priority | Env var | Provider |
|---|---|---|
| 1 | TTS_HOST |
Any OpenAI-compatible /v1/audio/speech endpoint (Kokoro, Ollama, etc.) |
| 2 | OPENAI_API_KEY |
api.openai.com |
| 3 | (neither) | Bundled ONNX model — zero config, lower quality |
Kokoro is the recommended local TTS option — high-quality output, 25+ voices, runs in Docker with no API key.
docker run -d --name kokoro -p 8880:8880 ghcr.io/hexgrad/kokoro-onnx-server:latestTTS_HOST=http://localhost:8880
TTS_FORMAT=ogg
TTS_VOICE=af_heart # default voice; send /voice in Telegram to browse all 25+Kokoro voices follow a {prefix}_{name} pattern — af_ (American female), am_ (American male), bf_ (British female), bm_ (British male). Examples: af_heart, am_onyx, bf_emma, am_michael.
Send /voice in your Telegram chat to browse and preview all available voices interactively.
Agents can set a per-session TTS voice with action(type: "profile/voice"), overriding the global default without affecting other sessions. Pass an empty string to clear the override and revert to the global default.
- The server started without a token. Check the
envblock in your MCP config or that.envexists.
- An inbound update arrived from a user who is not
ALLOWED_USER_ID. - This is the security filter working correctly — no action needed.
- If you sent the message yourself and still see this,
ALLOWED_USER_IDis set to the wrong value. Re-check it againstmessage.from.idingetUpdates.
ALLOWED_USER_IDis not configured. Set it in your.envor MCP host config.
- The
chat_idis wrong, or the bot has never been added to that chat. - For DMs: you must message the bot first (Telegram requires users to initiate).
- For groups: the bot must be a member.
- The user has blocked the bot. They must unblock it in Telegram settings, or use
/startagain.
- The bot needs admin rights for pin/delete operations.
- In the group: Telegram → Group info → Administrators → Add the bot as admin.
- HTML parse mode: ensure all tags are properly closed (
<b>bold</b>, not<b>bold). - MarkdownV2: these characters must be escaped with
\:. ! - = ( ) [ ] { } ~ # > + |
- Telegram limits bots to ~30 messages/second globally, ~1 message/second per chat.
- The error includes
retry_after— wait that many seconds before retrying.
- Messages can only be edited within 48 hours of sending.
- Only the bot's own messages can be edited.
- Telegram silently ignores edits where the text is identical to the current content.
- This is not an error — the message is already up to date.
{ empty: true }— expected whentimeoutis 0 (instant poll) and there are no pending updates.{ timed_out: true }— expected when a blocking wait (default 300 s) expires with no updates. Call again immediately.- Use
dequeue()with no arguments to block up to 300 s for the next update.
- Only one process can poll
getUpdatesper bot token. If multiple MCP instances share the same token, they race for updates — most sessions receive nothing. - Common cause: the Telegram MCP server is configured globally (e.g.
~/.claude.jsonmcpServersfor Claude Code, or Claude Desktop's global config) and multiple sessions are open. - Fix: move the config to a project-scoped file (
.mcp.jsonfor Claude Code,.vscode/mcp.jsonfor VS Code) so the server only runs in one session at a time.
- This doesn't happen by default. Bots do not receive updates for messages they sent.
-
If you previously set a webhook on this token,
getUpdateswill fail. -
Clear it by calling:
https://api.telegram.org/bot<TOKEN>/deleteWebhook
| Action | Permission needed |
|---|---|
| Send messages to a group | Must be a member |
| Read group messages | Must be a member, or have can_read_all_group_messages set by BotFather |
| Delete messages | Admin with "Delete messages" right |
| Pin messages | Admin with "Pin messages" right |
| Get chat member info | Admin or member |
1. action(type: "chat/info") → confirm bot/session identity
2. send(type: "notification", text: "MCP Online") → confirm message delivery
3. send(type: "question", choose: [{label:"OK",value:"ok"}], text: "Test?") → confirm interactivity
If all three succeed, the integration is working correctly.