The Telegram plugin bridges Telegram bots to Claude Code via an MCP server. "The MCP server logs into Telegram as a bot and provides tools to Claude to reply, react, or edit messages. When you message the bot, the server forwards the message to your Claude Code session." (README.md) Users pair via 6-character codes, switch to allowlist mode for security, and support single-user DMs plus group mentions.
Plugin manifest (.claude-plugin/plugin.json):
{
"name": "telegram",
"description": "Telegram channel for Claude Code — messaging bridge with built-in access control. Manage pairing, allowlists, and policy via /telegram:access.",
"version": "0.0.6",
"keywords": ["telegram", "messaging", "channel", "mcp"]
}Executable: server.ts (1032 lines, shebang #!/usr/bin/env bun)
Runtime: Bun — configured in .mcp.json:
{
"mcpServers": {
"telegram": {
"command": "bun",
"args": ["run", "--cwd", "${CLAUDE_PLUGIN_ROOT}", "--shell=bun", "--silent", "start"]
}
}
}The server is invoked via bun start, which runs bun install --no-summary && bun server.ts (from package.json). It runs as a stdio MCP server, expecting input on stdin and writing JSON-RPC on stdout. Life cycle: token validation (lines 42–52), token holder cleanup (lines 56–69), error handlers for unhandled rejections (lines 71–78), MCP server setup, Telegram bot start, and graceful shutdown on stdin EOF (lines 630–664).
MCP SDK: @modelcontextprotocol/sdk v1.0.0 (server.ts:12–13)
import { Server } from '@modelcontextprotocol/sdk/server/index.js'
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'Telegram client: grammy v1.21.0 (server.ts:19)
import { Bot, GrammyError, InlineKeyboard, InputFile, type Context } from 'grammy'Four tools exposed to Claude (server.ts:432–504):
-
reply— Send text + files to a chat. Chunks long text (up to 4096 chars per Telegram limit), routes images as photos, docs as documents. Optionalreply_to(message ID) for threading,format('text' or 'markdownv2'). Returns sent message ID(s). (server.ts:510–576) -
react— Add emoji reaction (Telegram's fixed whitelist only: 👍 👎 ❤ 🔥 👀 🎉 etc). (server.ts:577–582) -
download_attachment— Fetch a file from Telegram byfile_id(from inbound meta), download via HTTP, write to~/.claude/channels/telegram/inbox/. Returns local path. Caps at 20MB. (server.ts:584–600) -
edit_message— Edit a message the bot sent. Text-only, no file changes. Useful for "working..." → result updates. (server.ts:602–613)
Tool handler: CallToolRequestSchema (server.ts:506) dispatches by name and wraps responses with error handling.
Messages → Claude via MCP notification notifications/claude/channel (server.ts:957–979). Metadata includes:
chat_id,message_id,user,user_id,ts(ISO 8601)image_path(for photos, downloaded eagerly)attachment_kind,attachment_file_id,attachment_size,attachment_mime,attachment_name(for documents, voice, audio, video, stickers; file_id left for Claude to download)
Handlers cover: text, photo, document, voice, audio, video, video_note, sticker (server.ts:781–877). Photos auto-download on receipt; other media lazy-load via download_attachment tool.
MCP notification handler for notifications/claude/channel/permission_request (server.ts:405–430) broadcasts Telegram inline keyboards to allowlisted users. Callback query handler (server.ts:725–779) intercepts "perm:allow/deny/more" button taps and emits notifications/claude/channel/permission with behavior and request_id. Text-reply intercept (server.ts:921–936) also recognizes "yes/no XXXXX" (5 lowercase a-z minus 'l') and emits same notification.
.claude-plugin/plugin.json (11 lines) — plugin metadata. See above.
.mcp.json (8 lines) — MCP server launch config. See above.
package.json (14 lines):
{
"name": "claude-channel-telegram",
"version": "0.0.1",
"license": "Apache-2.0",
"type": "module",
"bin": "./server.ts",
"scripts": {
"start": "bun install --no-summary && bun server.ts"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.0.0",
"grammy": "^1.21.0"
}
}-
State directory (server.ts:26–29):
~/.claude/channels/telegram/— holds.env(token),access.json(policy),inbox/(downloaded photos),bot.pid(orphan watchdog). -
Access control types (server.ts:89–117):
Accesstype bundlesdmPolicy('pairing' | 'allowlist' | 'disabled'),allowFrom(user IDs),groups(group ID → policy),pending(codes in flight), and UX config (ackReaction,replyToMode,textChunkLimit,chunkMode). -
gate()function (server.ts:227–285): Access control dispatcher. DM path: checks allowlist (deliver), pairing policy (generate 6-char code + expiry), or drop. Group path: requires group entry + member allowlist + mention (unlessrequireMention: false). Returns{ action: 'deliver' | 'pair' | 'drop'; ... }. -
Message mention detection (server.ts:287–311):
isMentioned()parses Telegram entities for@botname, text mentions, implicit replies, and user-supplied regex patterns. -
Pairing approval flow (server.ts:317–339):
checkApprovals()polls~/.claude/channels/telegram/approved/<senderId>files (dropped by/telegram:access skill), sends confirmation DMs, cleans up. Runs every 5s in non-static mode. -
Text chunking (server.ts:344–363):
chunk()splits long text on hard limit (4096) or paragraph/line/space boundaries (ifchunkMode: 'newline'). Strips leading newlines from continuation chunks. -
File safety check (server.ts:135–145):
assertSendable()prevents exfiltration of server state by denying paths insideSTATE_DIRexceptinbox/(where downloaded media is stored). -
MCP server setup (server.ts:369–396): Declares capabilities (
tools,experimental/claude/channel,experimental/claude/channel/permission). Includes 10-line instruction block warning Claude not to invoke/telegram:accessskill or editaccess.jsonfrom chat messages (prompt injection risk). -
Inbound handler (server.ts:894–980):
handleInbound()integrates gate() check, pairing reply, permission-reply intercept, typing indicator, ack reaction, image download, and MCP notification emission. Closes the loop for all 8 message types. -
Shutdown & orphan watchdog (server.ts:636–664):
shutdown()on stdin EOF (MCP connection close), SIGTERM/SIGINT/SIGHUP. Kills stale bot.pid holder on startup. Background interval monitor checks for reparenting (POSIX ppid change) or stdin pipe death. -
Polling loop with backoff (server.ts:993–1032):
bot.start()(Telegram long-poll loop) retries on any error with exponential backoff (up to 15s). 409 Conflict (another poller) gives 8 attempts before exit. Unhandled promise/exception handlers prevent silent process death. -
Commands (server.ts:671–720): DM-only
/start(pairing guide),/help,/status(pair/pending/unpaired).
Process isolation & token safety: Token lives in ~/.claude/channels/telegram/.env (mode 0o600), not env vars or config. Orphan watchdog kills stale Telegram pollers on startup so crashed sessions don't block the next session with 409 Conflict. PID file + background monitor detect reparenting and self-terminate if grandparent dies.
Pairing & allowlist: Pairing mode generates 6-char hex codes (3 random bytes), valid 1h, with rate limit (3 pending max, 2 replies per code). /telegram:access skill drops approval files, server polls and confirms. Seamless UX for one-shot setup; switch to allowlist for production.
No history search: Telegram Bot API exposes no fetch_messages or message search. Photos auto-download on arrival (can't fetch later); docs/voice/audio lazy-load via download_attachment. Inbound metadata includes message_id for threaded replies.
Access granularity: DMs (send directly), groups (group ID + member allowlist + mention requirement). Mention can be explicit (@bot), text mention (reply to bot), or custom regex. Permission relay sends inline buttons to allowlisted DMs only; groups excluded (no security review for group members).
Text splitting & formatting: Telegram hard-caps messages at 4096 chars. Plugin chunks by length or paragraph (user-configurable). MarkdownV2 mode requires caller escape. Files (images/docs/voice/audio/stickers) send as separate messages, optionally threaded under reply_to.
Typing indicator & ack reaction: sendChatAction('typing') fires when inbound arrives, signals processing. Optional ackReaction (fixed emoji) on receipt. Both fire-and-forget (catch/ignore failures).
Permission-reply interception: Text matching (yes|no) [a-km-z]{5} triggers permission relay instead of chat delivery. Callback buttons (perm:allow/deny/more) route via inline query handler. "See more" expands details with pretty-printed JSON preview.
Static mode: TELEGRAM_ACCESS_MODE=static snapshots access.json at boot, disables pairing (downgraded to allowlist with warning), never writes. For immutable deployments.
Error resilience: Unhandled promise rejections and uncaught exceptions logged but don't crash. Polling loop retries with backoff. Grammar's default error handler disabled (bot.catch). Missing token → explicit stderr + exit(1).
- server.ts — main implementation
- .claude-plugin/plugin.json — manifest
- .mcp.json — MCP server launch config
- package.json — dependencies
- README.md — user guide
- ACCESS.md — access control detailed docs