Build a Telegram MCP Server for Claude Code: Complete Guide
I have an AI agent that sits in Telegram 24/7, reads my chats, replies to messages, and manages contacts. All of this runs through an MCP Server that wraps Telethon into a set of tools for Claude Code. Here's how it works and how to build one yourself.
Why Telegram as an MCP server
MCP (Model Context Protocol) is the standard for connecting AI models to external tools. Claude Code supports MCP natively — you describe a server, and the model gets access to its functions as regular tools.
Telegram via MCP gives Claude Code the ability to:
- Read message history from any chat
- Send messages and replies
- Full-text search across chats
- Get information about contacts and groups
- React to messages, forward, delete
This isn't the Bot API. This is a userbot via Telethon — an MTProto client that operates as a real user account. The Bot API is too limited: it can't see other people's message history, can't search across chats, and has no access to private groups where the bot isn't added.
Architecture: Telethon + FastMCP
The stack is straightforward:
- Telethon — Python library for MTProto. Connects to Telegram as a regular client
- FastMCP — framework for building MCP servers in Python. Minimal boilerplate
- nest_asyncio — critically important. Without it, async code inside MCP calls crashes with "event loop already running"
Basic structure:
from mcp.server.fastmcp import FastMCP
from telethon import TelegramClient
import nest_asyncio
import asyncio
nest_asyncio.apply()
mcp = FastMCP("telegram")
client = TelegramClient("session", api_id, api_hash)
@mcp.tool()
async def get_messages(chat_id: int, limit: int = 10):
"""Get recent messages from a chat."""
messages = []
async for msg in client.iter_messages(chat_id, limit=limit):
messages.append({
"id": msg.id,
"text": msg.message, # NOT msg.text!
"date": str(msg.date),
"sender": msg.sender_id
})
return messagesCritical detail: use msg.message, not msg.text. This is non-obvious and cost me several hours of debugging. msg.text returns None in certain contexts (specifically when working through GetRepliesRequest), while msg.message always contains the actual text content.
Essential operations
My production Telegram MCP server implements 92 methods. But to get started, you only need 5-7 core operations:
# Core operations
get_messages(chat_id, limit) # Fetch recent N messages
send_message(chat_id, text) # Send a message
search_messages(chat_id, query) # Full-text search
list_chats(chat_type) # List chats (groups, channels, users)
get_message_context(chat_id, msg_id) # Context around a specific message
execute_code(code) # Run arbitrary Python with client accessThat last one is the most powerful. It's essentially an escape hatch: if you need an operation that you haven't implemented as a dedicated tool, Claude Code can write and execute Python code directly inside the MCP server, with full access to the Telethon client. This is how I handle edge cases without constantly expanding the tool list.
The async trap: nest_asyncio is mandatory
The biggest pitfall is the event loop. Telethon uses asyncio. The MCP server also runs in an asyncio event loop. When Claude Code calls an MCP tool, you're already inside a running loop — and asyncio.run() or client.loop.run_until_complete() will crash.
The reliable pattern:
import nest_asyncio
nest_asyncio.apply()
import asyncio
async def main():
msgs = []
async for msg in client.iter_messages(chat_id, limit=50):
msgs.append(msg.message or '[media]')
return msgs
result = asyncio.get_event_loop().run_until_complete(main())nest_asyncio.apply() at the top + asyncio.get_event_loop().run_until_complete() for calls. Don't try await at the top level or client.loop.run_until_complete() — both will break.
Also: async for at the module level gives you a SyntaxError. Always wrap in an async function first.
Common pitfalls (from months of production use)
ChannelPrivateError. Occurs when trying to send a message to a group where the account doesn't have access. The confusing part: you can read from a channel via GetRepliesRequest, but you can't write. These are different permission levels in the Telegram API. In my setup, the agent can read channel discussion replies but cannot post in them because it's not a group member.
sender = None. In group chats where the account isn't a member, sender profiles are inaccessible. msg.sender_id can be None. Handle this gracefully — don't retry, it won't resolve. I use a fallback: msg.message or ('[media]' if msg.media else '[action]').
execute_code timeouts. If you run code through execute_code, set hard limits. Any loop with more than 10 API calls inside will likely time out (the 60-second limit is real). My rule: maximum 10 operations per execute_code call, split into separate calls for larger batches.
FloodWait. Telegram rate-limits aggressively. Send 20 messages in rapid succession and you'll get a FloodWaitError with delays up to several minutes. Add asyncio.sleep(1) between sends.
Session string vs session file. For server deployments, use a session string (StringSession). Session files are bound to the filesystem and don't transfer between containers. Generate the string once, store it as an environment variable.
My use case: always-on server agent
I use Telegram MCP as the primary interface for an AI agent running 24/7 on a VPS. The architecture: Claude Code runs in a Docker container, connects to Telegram through MCP, and processes incoming messages.
What the agent does through Telegram MCP:
- Answers my DMs — as a personal assistant
- Monitors channels — parses posts, collects data
- Replies in my channel's discussion — in my voice, using a digital twin prompt
- Runs cron tasks — weekly reports, automated checks
Telegram MCP turned out to be more convenient than the Bot API or webhooks: full access to message history, search, chat management — everything a regular Telegram client can do, but controlled by an AI agent.
Connecting to Claude Code
In your project's .mcp.json (or globally at ~/.claude/mcp.json):
{
"mcpServers": {
"telegram": {
"command": "python",
"args": ["/path/to/telegram_mcp_server.py"],
"env": {
"TELEGRAM_API_ID": "your_api_id",
"TELEGRAM_API_HASH": "your_api_hash",
"TELEGRAM_SESSION_STRING": "your_session_string"
}
}
}
}You'll need Telegram API credentials — get them from my.telegram.org. Generate a session string by running a Telethon script locally, authenticating with your phone number, and calling client.session.save().
After launch, Claude Code will see Telegram tools in its available tool list and can call them like regular functions.
Open source implementations exist — telegram-mcp on GitHub is a good starting point. My version is based on Telethon with an extended set of 92 methods covering everything from basic messaging to contact management and code execution.
A Telegram MCP server is probably the most useful MCP server you can build. Telegram is already the communication center for many people, and connecting it to Claude Code gives you an AI agent with full access to your messenger. The setup takes an afternoon; the productivity gain is permanent.
Follow me on Twitter/X @danokhlopkov for more on AI agents and automation.