#
tokens: 12090/50000 15/15 files
lines: on (toggle) GitHub
raw markdown copy reset
# Directory Structure

```
├── .DS_Store
├── .gitattributes
├── .gitignore
├── LICENSE.txt
├── pyproject.toml
├── README.md
├── src
│   ├── .DS_Store
│   ├── mcp_telegram
│   │   ├── __init__.py
│   │   ├── .env.example
│   │   ├── convostyle.txt
│   │   ├── main.py
│   │   └── telethon_auth.py
│   └── telegram_mcp.egg-info
│       ├── dependency_links.txt
│       ├── entry_points.txt
│       ├── PKG-INFO
│       ├── requires.txt
│       ├── SOURCES.txt
│       └── top_level.txt
└── uv.lock
```

# Files

--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------

```
1 | # Auto detect text files and perform LF normalization
2 | * text=auto
3 | 
```

--------------------------------------------------------------------------------
/src/mcp_telegram/.env.example:
--------------------------------------------------------------------------------

```
 1 | # Telegram MCP Configuration
 2 | # Copy this file to .env and fill in your values
 3 | 
 4 | # Required: Get these from https://my.telegram.org/apps
 5 | TELEGRAM_API_ID=
 6 | TELEGRAM_API_HASH=
 7 | 
 8 | # Optional: Will be filled automatically during authentication
 9 | TELEGRAM_PHONE=
10 | TELEGRAM_2FA_PASSWORD=
11 | TELEGRAM_SESSION_STRING=
```

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
  1 | # Byte-compiled / optimized / DLL files
  2 | __pycache__/
  3 | *.py[cod]
  4 | *$py.class
  5 | 
  6 | # C extensions
  7 | *.so
  8 | 
  9 | # Distribution / packaging
 10 | .Python
 11 | build/
 12 | develop-eggs/
 13 | dist/
 14 | downloads/
 15 | eggs/
 16 | .eggs/
 17 | lib/
 18 | lib64/
 19 | parts/
 20 | sdist/
 21 | var/
 22 | wheels/
 23 | share/python-wheels/
 24 | *.egg-info/
 25 | .installed.cfg
 26 | *.egg
 27 | MANIFEST
 28 | 
 29 | # PyInstaller
 30 | #  Usually these files are written by a python script from a template
 31 | #  before PyInstaller builds the exe, so as to inject date/other infos into it.
 32 | *.manifest
 33 | *.spec
 34 | 
 35 | # Installer logs
 36 | pip-log.txt
 37 | pip-delete-this-directory.txt
 38 | 
 39 | # Unit test / coverage reports
 40 | htmlcov/
 41 | .tox/
 42 | .nox/
 43 | .coverage
 44 | .coverage.*
 45 | .cache
 46 | nosetests.xml
 47 | coverage.xml
 48 | *.cover
 49 | *.py,cover
 50 | .hypothesis/
 51 | .pytest_cache/
 52 | cover/
 53 | 
 54 | # Translations
 55 | *.mo
 56 | *.pot
 57 | 
 58 | # Django stuff:
 59 | *.log
 60 | local_settings.py
 61 | db.sqlite3
 62 | db.sqlite3-journal
 63 | 
 64 | # Flask stuff:
 65 | instance/
 66 | .webassets-cache
 67 | 
 68 | # Scrapy stuff:
 69 | .scrapy
 70 | 
 71 | # Sphinx documentation
 72 | docs/_build/
 73 | 
 74 | # PyBuilder
 75 | .pybuilder/
 76 | target/
 77 | 
 78 | # Jupyter Notebook
 79 | .ipynb_checkpoints
 80 | 
 81 | # IPython
 82 | profile_default/
 83 | ipython_config.py
 84 | 
 85 | # pyenv
 86 | #   For a library or package, you might want to ignore these files since the code is
 87 | #   intended to run in multiple environments; otherwise, check them in:
 88 | # .python-version
 89 | 
 90 | # pipenv
 91 | #   According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
 92 | #   However, in case of collaboration, if having platform-specific dependencies or dependencies
 93 | #   having no cross-platform support, pipenv may install dependencies that don't work, or not
 94 | #   install all needed dependencies.
 95 | #Pipfile.lock
 96 | 
 97 | # UV
 98 | #   Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
 99 | #   This is especially recommended for binary packages to ensure reproducibility, and is more
100 | #   commonly ignored for libraries.
101 | #uv.lock
102 | 
103 | # poetry
104 | #   Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
105 | #   This is especially recommended for binary packages to ensure reproducibility, and is more
106 | #   commonly ignored for libraries.
107 | #   https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
108 | #poetry.lock
109 | 
110 | # pdm
111 | #   Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
112 | #pdm.lock
113 | #   pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
114 | #   in version control.
115 | #   https://pdm.fming.dev/latest/usage/project/#working-with-version-control
116 | .pdm.toml
117 | .pdm-python
118 | .pdm-build/
119 | 
120 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
121 | __pypackages__/
122 | 
123 | # Celery stuff
124 | celerybeat-schedule
125 | celerybeat.pid
126 | 
127 | # SageMath parsed files
128 | *.sage.py
129 | 
130 | # Environments
131 | .env
132 | .venv
133 | env/
134 | venv/
135 | ENV/
136 | env.bak/
137 | venv.bak/
138 | 
139 | # Spyder project settings
140 | .spyderproject
141 | .spyproject
142 | 
143 | # Rope project settings
144 | .ropeproject
145 | 
146 | # mkdocs documentation
147 | /site
148 | 
149 | # mypy
150 | .mypy_cache/
151 | .dmypy.json
152 | dmypy.json
153 | 
154 | # Pyre type checker
155 | .pyre/
156 | 
157 | # pytype static type analyzer
158 | .pytype/
159 | 
160 | # Cython debug symbols
161 | cython_debug/
162 | 
163 | # Ruff stuff:
164 | .ruff_cache/
165 | 
166 | # PyPI configuration file
167 | .pypirc
168 | 
169 | # Environment variables
170 | .env
```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
  1 | # Telegram MCP Server
  2 | 
  3 | Connect Claude to your Telegram account to read and send messages.
  4 | 
  5 | ## Features
  6 | 
  7 | ### Available Tools
  8 | 
  9 | 1. **get_chats** - List your Telegram chats
 10 |    - Returns paginated list with chat names, IDs, and unread counts
 11 |    - For page 1, just provide page number
 12 |    - For subsequent pages, use the pagination parameters from the previous response
 13 | 
 14 | 2. **get_messages** - Read messages from a specific chat
 15 |    - Fetches paginated message history
 16 |    - Automatically marks messages as read
 17 | 
 18 | 3. **mark_messages_read** - Mark all unread messages in a chat as read
 19 | 
 20 | 4. **send_message** - Send messages to any chat
 21 |    - Supports replying to specific messages
 22 | 
 23 | 5. **get_conversation_context** - Analyze chat style for natural responses
 24 |    - Reads your conversation style guide from `convostyle.txt`
 25 |    - Helps Claude match your texting patterns
 26 | 
 27 | ## Setup Guide
 28 | 
 29 | ### Step 1: Get Telegram API Credentials
 30 | 
 31 | 1. Go to [https://my.telegram.org/apps](https://my.telegram.org/apps)
 32 | 2. Log in and create an application
 33 | 3. Save your **API ID** and **API Hash**
 34 | 
 35 | ### Step 2: Install
 36 | 
 37 | ```bash
 38 | # Clone the repository
 39 | git clone https://github.com/alexandertsai/mcp-telegram
 40 | cd mcp-telegram
 41 | 
 42 | # Set up Python environment
 43 | pip install uv
 44 | uv venv
 45 | source .venv/bin/activate  # On Windows: .venv\Scripts\activate
 46 | uv sync
 47 | ```
 48 | 
 49 | ### Step 3: Configure
 50 | 
 51 | ```bash
 52 | # Copy the example file
 53 | cp .env.example .env
 54 | 
 55 | # Edit .env and add your API credentials:
 56 | # TELEGRAM_API_ID=your_api_id_here
 57 | # TELEGRAM_API_HASH=your_api_hash_here
 58 | ```
 59 | 
 60 | ### Step 4: Authenticate
 61 | 
 62 | ```bash
 63 | cd src/mcp_telegram
 64 | python telethon_auth.py
 65 | ```
 66 | 
 67 | Follow the prompts:
 68 | - Enter your phone number (with country code, e.g., +1234567890)
 69 | - Enter the code sent to your Telegram
 70 | - Enter your 2FA password if you have one
 71 | 
 72 | ### Step 5: Add to Claude Desktop
 73 | 
 74 | Find your Claude Desktop config file:
 75 | - **macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json`  
 76 | - **Windows**: `%APPDATA%\Claude\claude_desktop_config.json`
 77 | 
 78 | Add this configuration:
 79 | 
 80 | ```json
 81 | {
 82 |   "mcpServers": {
 83 |     "telegram": {
 84 |       "command": "/path/to/python",
 85 |       "args": ["/path/to/mcp-telegram/src/mcp_telegram/main.py"]
 86 |     }
 87 |   }
 88 | }
 89 | ```
 90 | 
 91 | **To find paths:**
 92 | - Python: Run `which python` (Mac) or `where.exe python` (Windows)
 93 | - main.py: Right-click the file and select "Copy Path"
 94 | 
 95 | Restart Claude Desktop.
 96 | 
 97 | ## Usage
 98 | 
 99 | After setup, you can ask Claude to:
100 | - "Check my Telegram messages"
101 | - "Send a message to [contact name]"
102 | - "What are my unread chats?"
103 | - "Reply to the last message from [contact name]"
104 | 
105 | ## Style Guide (Optional)
106 | 
107 | Create `src/mcp_telegram/convostyle.txt` to help Claude match your texting style:
108 | 
109 | ```
110 | I text casually with friends, formally with work contacts.
111 | I use emojis sparingly and prefer short messages.
112 | ```
113 | 
114 | ## Troubleshooting
115 | 
116 | ### Authentication Issues
117 | 
118 | If authentication fails:
119 | 1. Check your API credentials in `.env`
120 | 2. Remove the TELEGRAM_SESSION_STRING line from `.env`
121 | 3. Run `python telethon_auth.py` again
122 | 
123 | ### Common Errors
124 | 
125 | - **"Please set TELEGRAM_API_ID and TELEGRAM_API_HASH"**: Missing `.env` file or credentials
126 | - **"Session string is invalid or expired"**: Re-run authentication
127 | - **2FA password not showing**: This is normal - keep typing
128 | 
129 | ## Requirements
130 | 
131 | - Python 3.10+
132 | - Claude Desktop
133 | - Telegram account
134 | 
135 | ## License
136 | 
137 | Apache 2.0
```

--------------------------------------------------------------------------------
/src/telegram_mcp.egg-info/dependency_links.txt:
--------------------------------------------------------------------------------

```
1 | 
2 | 
```

--------------------------------------------------------------------------------
/src/telegram_mcp.egg-info/top_level.txt:
--------------------------------------------------------------------------------

```
1 | mcp_telegram
2 | 
```

--------------------------------------------------------------------------------
/src/mcp_telegram/__init__.py:
--------------------------------------------------------------------------------

```python
1 | """Telegram MCP server for reading and writing messages."""
2 | 
3 | __version__ = "0.1.0"
4 | 
```

--------------------------------------------------------------------------------
/src/telegram_mcp.egg-info/requires.txt:
--------------------------------------------------------------------------------

```
1 | httpx>=0.24.0
2 | mcp>=1.4.1
3 | nest-asyncio>=1.5.6
4 | python-dotenv>=1.0.0
5 | telethon>=1.28.0
6 | 
```

--------------------------------------------------------------------------------
/src/telegram_mcp.egg-info/entry_points.txt:
--------------------------------------------------------------------------------

```
1 | [console_scripts]
2 | telegram-auth = mcp_telegram.telethon_auth:main
3 | telegram-mcp = mcp_telegram.main:main
4 | 
```

--------------------------------------------------------------------------------
/src/telegram_mcp.egg-info/SOURCES.txt:
--------------------------------------------------------------------------------

```
 1 | LICENSE.txt
 2 | README.md
 3 | pyproject.toml
 4 | src/mcp_telegram/__init__.py
 5 | src/mcp_telegram/main.py
 6 | src/mcp_telegram/telethon_auth.py
 7 | src/telegram_mcp.egg-info/PKG-INFO
 8 | src/telegram_mcp.egg-info/SOURCES.txt
 9 | src/telegram_mcp.egg-info/dependency_links.txt
10 | src/telegram_mcp.egg-info/entry_points.txt
11 | src/telegram_mcp.egg-info/requires.txt
12 | src/telegram_mcp.egg-info/top_level.txt
```

--------------------------------------------------------------------------------
/src/mcp_telegram/convostyle.txt:
--------------------------------------------------------------------------------

```
 1 | EXAMPLE: I type concisely in proper english. I do not use many emojis (except 😭 occasionally) but I do sometimes use text emotes. However, I do keep my tone friendly.
 2 | 
 3 | Here are some examples of how I'd respond to messages:
 4 | SENDER: "How was your day?"
 5 | ME: "Good! What about yours :)"
 6 | 
 7 | SENDER: "Can you get this done?"
 8 | ME: "Okays"
 9 | 
10 | SENDER: "What do you think of this idea?"
11 | ME: "Honestly it sounds good, but I'm a bit worried it might be too static."
12 | 
13 | SENDER: "I'm really tired and my test went so badly..."
14 | ME: "Oh dear what happened?"
```

--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------

```toml
 1 | [project]
 2 | name = "telegram-mcp"
 3 | version = "0.1.0"
 4 | description = "A Telegram MCP server for reading and writing messages."
 5 | readme = "README.md"
 6 | requires-python = ">=3.10"
 7 | license = "Apache-2.0"
 8 | authors = [
 9 |     {name = "Alexander Tsai"}
10 | ]
11 | keywords = ["telegram", "mcp", "claude", "ai", "llm"]
12 | dependencies = [
13 |     "httpx>=0.24.0",
14 |     "mcp>=1.4.1",
15 |     "python-dotenv>=1.0.0",
16 |     "telethon>=1.28.0",
17 | ]
18 | 
19 | [project.scripts]
20 | telegram-mcp = "mcp_telegram.main:main"
21 | telegram-auth = "mcp_telegram.telethon_auth:main"
22 | 
23 | [project.urls]
24 | "Homepage" = "https://github.com/alexandertsai/telegram-mcp"
25 | "Bug Tracker" = "https://github.com/alexandertsai/telegram-mcp/issues"
26 | 
27 | [build-system]
28 | requires = ["setuptools>=61.0"]
29 | build-backend = "setuptools.build_meta"
30 | 
31 | [tool.setuptools]
32 | package-dir = {"" = "src"}
33 | packages = ["mcp_telegram"]
34 | 
```

--------------------------------------------------------------------------------
/src/mcp_telegram/telethon_auth.py:
--------------------------------------------------------------------------------

```python
  1 | #!/usr/bin/env python3
  2 | # telethon_auth.py
  3 | 
  4 | import asyncio
  5 | import os
  6 | import sys
  7 | import logging
  8 | import getpass
  9 | from telethon import TelegramClient
 10 | from telethon.sessions import StringSession
 11 | from telethon.errors import SessionPasswordNeededError
 12 | from dotenv import load_dotenv, set_key
 13 | 
 14 | # Configure logging
 15 | logging.basicConfig(
 16 |     level=logging.INFO,
 17 |     format='[%(asctime)s] %(levelname)-8s %(message)s',
 18 |     datefmt='%m/%d/%y %H:%M:%S',
 19 | )
 20 | logger = logging.getLogger(__name__)
 21 | 
 22 | async def authenticate():
 23 |     # Load existing .env file
 24 |     load_dotenv()
 25 |     
 26 |     # Get API credentials from .env
 27 |     api_id = os.getenv('TELEGRAM_API_ID')
 28 |     api_hash = os.getenv('TELEGRAM_API_HASH')
 29 |     phone = os.getenv('TELEGRAM_PHONE')
 30 |     password = os.getenv('TELEGRAM_2FA_PASSWORD')  # Optional, for 2FA
 31 |     session_string = os.getenv('TELEGRAM_SESSION_STRING')
 32 |     
 33 |     # Check if we already have a valid session
 34 |     if session_string:
 35 |         logger.info("Found existing session string in .env file. Verifying...")
 36 |         client = TelegramClient(StringSession(session_string), api_id, api_hash)
 37 |         try:
 38 |             await client.connect()
 39 |             if await client.is_user_authorized():
 40 |                 logger.info("✓ Existing session is valid! You're already authenticated.")
 41 |                 await client.disconnect()
 42 |                 return True
 43 |             else:
 44 |                 logger.warning("Existing session is invalid or expired. Need to re-authenticate.")
 45 |         except Exception as e:
 46 |             logger.warning(f"Error with existing session: {str(e)}")
 47 |         finally:
 48 |             await client.disconnect()
 49 |     
 50 |     # Check required credentials
 51 |     if not api_id or not api_hash:
 52 |         logger.error("Missing required credentials in .env file!")
 53 |         logger.error("Please add the following to your .env file:")
 54 |         logger.error("TELEGRAM_API_ID=your_api_id")
 55 |         logger.error("TELEGRAM_API_HASH=your_api_hash")
 56 |         logger.error("Get these from https://my.telegram.org/apps")
 57 |         return False
 58 |     
 59 |     # Get phone number if not in .env
 60 |     if not phone:
 61 |         phone = input("Enter your phone number (with country code, e.g. +12345678900): ")
 62 |         # Save phone to .env for future use
 63 |         env_path = os.path.join(os.getcwd(), '.env')
 64 |         set_key(env_path, 'TELEGRAM_PHONE', phone)
 65 |         logger.info(f"Phone number saved to .env file")
 66 |     else:
 67 |         logger.info(f"Using phone number from .env: {phone}")
 68 |         use_saved = input(f"Use saved phone number {phone}? (y/n): ").lower()
 69 |         if use_saved != 'y':
 70 |             phone = input("Enter your phone number (with country code, e.g. +12345678900): ")
 71 |             env_path = os.path.join(os.getcwd(), '.env')
 72 |             set_key(env_path, 'TELEGRAM_PHONE', phone)
 73 |     
 74 |     # Create new session
 75 |     client = TelegramClient(StringSession(), api_id, api_hash)
 76 |     
 77 |     try:
 78 |         await client.connect()
 79 |         
 80 |         # Send code request
 81 |         logger.info(f"Sending authentication code to {phone}...")
 82 |         await client.send_code_request(phone)
 83 |         
 84 |         # Get the code from the user
 85 |         code = input("Enter the code you received: ")
 86 |         
 87 |         # Initialize save_pwd to avoid reference error
 88 |         save_pwd = 'n'
 89 |         
 90 |         try:
 91 |             # Try to sign in with the code
 92 |             await client.sign_in(phone, code)
 93 |         except SessionPasswordNeededError:
 94 |             # 2FA is enabled
 95 |             logger.info("Two-factor authentication is enabled.")
 96 |             
 97 |             # Try to use saved password first
 98 |             if password:
 99 |                 logger.info("Using 2FA password from .env file...")
100 |                 try:
101 |                     await client.sign_in(password=password)
102 |                     logger.info("✓ Successfully authenticated with saved password!")
103 |                 except Exception as e:
104 |                     logger.warning("Saved password didn't work, please enter manually.")
105 |                     password = None
106 |             
107 |             # If no saved password or it didn't work, ask user
108 |             if not password:
109 |                 password = getpass.getpass("Enter your 2FA password: ")
110 |                 await client.sign_in(password=password)
111 |                 
112 |                 # Ask if user wants to save password
113 |                 save_pwd = input("Save 2FA password to .env file for future use? (y/n): ").lower()
114 |                 if save_pwd == 'y':
115 |                     env_path = os.path.join(os.getcwd(), '.env')
116 |                     set_key(env_path, 'TELEGRAM_2FA_PASSWORD', password)
117 |                     logger.info("2FA password saved to .env file")
118 |         
119 |         if await client.is_user_authorized():
120 |             # Save the string session to .env
121 |             session_string = client.session.save()
122 |             env_path = os.path.join(os.getcwd(), '.env')
123 |             set_key(env_path, 'TELEGRAM_SESSION_STRING', session_string)
124 |             
125 |             logger.info("✓ Authentication successful!")
126 |             logger.info("✓ Session string saved to .env file")
127 |             logger.info("")
128 |             logger.info("You can now start the MCP server with: python main.py")
129 |             logger.info("")
130 |             logger.info("Your .env file has been updated with:")
131 |             logger.info("- TELEGRAM_SESSION_STRING (required)")
132 |             logger.info("- TELEGRAM_PHONE (for convenience)")
133 |             if save_pwd == 'y':
134 |                 logger.info("- TELEGRAM_2FA_PASSWORD (optional, for automatic re-auth)")
135 |             
136 |             return True
137 |         else:
138 |             logger.error("Authentication failed.")
139 |             return False
140 |             
141 |     except Exception as e:
142 |         logger.error(f"Authentication error: {str(e)}")
143 |         return False
144 |     finally:
145 |         await client.disconnect()
146 | 
147 | def main():
148 |     logger.info("Telegram MCP Authentication Setup")
149 |     logger.info("-" * 40)
150 |     
151 |     # Check if .env file exists
152 |     env_path = os.path.join(os.getcwd(), '.env')
153 |     if not os.path.exists(env_path):
154 |         logger.info("No .env file found. Creating one...")
155 |         with open(env_path, 'w') as f:
156 |             f.write("# Telegram MCP Configuration\n")
157 |             f.write("# Get API credentials from https://my.telegram.org/apps\n")
158 |             f.write("TELEGRAM_API_ID=\n")
159 |             f.write("TELEGRAM_API_HASH=\n")
160 |             f.write("TELEGRAM_PHONE=\n")
161 |             f.write("TELEGRAM_2FA_PASSWORD=\n")
162 |             f.write("TELEGRAM_SESSION_STRING=\n")
163 |         logger.info(f"Created .env file at: {env_path}")
164 |         logger.info("Please edit it and add your TELEGRAM_API_ID and TELEGRAM_API_HASH")
165 |         logger.info("Then run this script again.")
166 |         sys.exit(1)
167 |     
168 |     success = asyncio.run(authenticate())
169 |     sys.exit(0 if success else 1)
170 | 
171 | if __name__ == "__main__":
172 |     main()
```

--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------

```
  1 |                                  Apache License
  2 |                            Version 2.0, January 2004
  3 |                         http://www.apache.org/licenses/
  4 | 
  5 |    TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
  6 | 
  7 |    1. Definitions.
  8 | 
  9 |       "License" shall mean the terms and conditions for use, reproduction,
 10 |       and distribution as defined by Sections 1 through 9 of this document.
 11 | 
 12 |       "Licensor" shall mean the copyright owner or entity authorized by
 13 |       the copyright owner that is granting the License.
 14 | 
 15 |       "Legal Entity" shall mean the union of the acting entity and all
 16 |       other entities that control, are controlled by, or are under common
 17 |       control with that entity. For the purposes of this definition,
 18 |       "control" means (i) the power, direct or indirect, to cause the
 19 |       direction or management of such entity, whether by contract or
 20 |       otherwise, or (ii) ownership of fifty percent (50%) or more of the
 21 |       outstanding shares, or (iii) beneficial ownership of such entity.
 22 | 
 23 |       "You" (or "Your") shall mean an individual or Legal Entity
 24 |       exercising permissions granted by this License.
 25 | 
 26 |       "Source" form shall mean the preferred form for making modifications,
 27 |       including but not limited to software source code, documentation
 28 |       source, and configuration files.
 29 | 
 30 |       "Object" form shall mean any form resulting from mechanical
 31 |       transformation or translation of a Source form, including but
 32 |       not limited to compiled object code, generated documentation,
 33 |       and conversions to other media types.
 34 | 
 35 |       "Work" shall mean the work of authorship, whether in Source or
 36 |       Object form, made available under the License, as indicated by a
 37 |       copyright notice that is included in or attached to the work
 38 |       (an example is provided in the Appendix below).
 39 | 
 40 |       "Derivative Works" shall mean any work, whether in Source or Object
 41 |       form, that is based on (or derived from) the Work and for which the
 42 |       editorial revisions, annotations, elaborations, or other modifications
 43 |       represent, as a whole, an original work of authorship. For the purposes
 44 |       of this License, Derivative Works shall not include works that remain
 45 |       separable from, or merely link (or bind by name) to the interfaces of,
 46 |       the Work and Derivative Works thereof.
 47 | 
 48 |       "Contribution" shall mean any work of authorship, including
 49 |       the original version of the Work and any modifications or additions
 50 |       to that Work or Derivative Works thereof, that is intentionally
 51 |       submitted to Licensor for inclusion in the Work by the copyright owner
 52 |       or by an individual or Legal Entity authorized to submit on behalf of
 53 |       the copyright owner. For the purposes of this definition, "submitted"
 54 |       means any form of electronic, verbal, or written communication sent
 55 |       to the Licensor or its representatives, including but not limited to
 56 |       communication on electronic mailing lists, source code control systems,
 57 |       and issue tracking systems that are managed by, or on behalf of, the
 58 |       Licensor for the purpose of discussing and improving the Work, but
 59 |       excluding communication that is conspicuously marked or otherwise
 60 |       designated in writing by the copyright owner as "Not a Contribution."
 61 | 
 62 |       "Contributor" shall mean Licensor and any individual or Legal Entity
 63 |       on behalf of whom a Contribution has been received by Licensor and
 64 |       subsequently incorporated within the Work.
 65 | 
 66 |    2. Grant of Copyright License. Subject to the terms and conditions of
 67 |       this License, each Contributor hereby grants to You a perpetual,
 68 |       worldwide, non-exclusive, no-charge, royalty-free, irrevocable
 69 |       copyright license to reproduce, prepare Derivative Works of,
 70 |       publicly display, publicly perform, sublicense, and distribute the
 71 |       Work and such Derivative Works in Source or Object form.
 72 | 
 73 |    3. Grant of Patent License. Subject to the terms and conditions of
 74 |       this License, each Contributor hereby grants to You a perpetual,
 75 |       worldwide, non-exclusive, no-charge, royalty-free, irrevocable
 76 |       (except as stated in this section) patent license to make, have made,
 77 |       use, offer to sell, sell, import, and otherwise transfer the Work,
 78 |       where such license applies only to those patent claims licensable
 79 |       by such Contributor that are necessarily infringed by their
 80 |       Contribution(s) alone or by combination of their Contribution(s)
 81 |       with the Work to which such Contribution(s) was submitted. If You
 82 |       institute patent litigation against any entity (including a
 83 |       cross-claim or counterclaim in a lawsuit) alleging that the Work
 84 |       or a Contribution incorporated within the Work constitutes direct
 85 |       or contributory patent infringement, then any patent licenses
 86 |       granted to You under this License for that Work shall terminate
 87 |       as of the date such litigation is filed.
 88 | 
 89 |    4. Redistribution. You may reproduce and distribute copies of the
 90 |       Work or Derivative Works thereof in any medium, with or without
 91 |       modifications, and in Source or Object form, provided that You
 92 |       meet the following conditions:
 93 | 
 94 |       (a) You must give any other recipients of the Work or
 95 |           Derivative Works a copy of this License; and
 96 | 
 97 |       (b) You must cause any modified files to carry prominent notices
 98 |           stating that You changed the files; and
 99 | 
100 |       (c) You must retain, in the Source form of any Derivative Works
101 |           that You distribute, all copyright, patent, trademark, and
102 |           attribution notices from the Source form of the Work,
103 |           excluding those notices that do not pertain to any part of
104 |           the Derivative Works; and
105 | 
106 |       (d) If the Work includes a "NOTICE" text file as part of its
107 |           distribution, then any Derivative Works that You distribute must
108 |           include a readable copy of the attribution notices contained
109 |           within such NOTICE file, excluding those notices that do not
110 |           pertain to any part of the Derivative Works, in at least one
111 |           of the following places: within a NOTICE text file distributed
112 |           as part of the Derivative Works; within the Source form or
113 |           documentation, if provided along with the Derivative Works; or,
114 |           within a display generated by the Derivative Works, if and
115 |           wherever such third-party notices normally appear. The contents
116 |           of the NOTICE file are for informational purposes only and
117 |           do not modify the License. You may add Your own attribution
118 |           notices within Derivative Works that You distribute, alongside
119 |           or as an addendum to the NOTICE text from the Work, provided
120 |           that such additional attribution notices cannot be construed
121 |           as modifying the License.
122 | 
123 |       You may add Your own copyright statement to Your modifications and
124 |       may provide additional or different license terms and conditions
125 |       for use, reproduction, or distribution of Your modifications, or
126 |       for any such Derivative Works as a whole, provided Your use,
127 |       reproduction, and distribution of the Work otherwise complies with
128 |       the conditions stated in this License.
129 | 
130 |    5. Submission of Contributions. Unless You explicitly state otherwise,
131 |       any Contribution intentionally submitted for inclusion in the Work
132 |       by You to the Licensor shall be under the terms and conditions of
133 |       this License, without any additional terms or conditions.
134 |       Notwithstanding the above, nothing herein shall supersede or modify
135 |       the terms of any separate license agreement you may have executed
136 |       with Licensor regarding such Contributions.
137 | 
138 |    6. Trademarks. This License does not grant permission to use the trade
139 |       names, trademarks, service marks, or product names of the Licensor,
140 |       except as required for reasonable and customary use in describing the
141 |       origin of the Work and reproducing the content of the NOTICE file.
142 | 
143 |    7. Disclaimer of Warranty. Unless required by applicable law or
144 |       agreed to in writing, Licensor provides the Work (and each
145 |       Contributor provides its Contributions) on an "AS IS" BASIS,
146 |       WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 |       implied, including, without limitation, any warranties or conditions
148 |       of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 |       PARTICULAR PURPOSE. You are solely responsible for determining the
150 |       appropriateness of using or redistributing the Work and assume any
151 |       risks associated with Your exercise of permissions under this License.
152 | 
153 |    8. Limitation of Liability. In no event and under no legal theory,
154 |       whether in tort (including negligence), contract, or otherwise,
155 |       unless required by applicable law (such as deliberate and grossly
156 |       negligent acts) or agreed to in writing, shall any Contributor be
157 |       liable to You for damages, including any direct, indirect, special,
158 |       incidental, or consequential damages of any character arising as a
159 |       result of this License or out of the use or inability to use the
160 |       Work (including but not limited to damages for loss of goodwill,
161 |       work stoppage, computer failure or malfunction, or any and all
162 |       other commercial damages or losses), even if such Contributor
163 |       has been advised of the possibility of such damages.
164 | 
165 |    9. Accepting Warranty or Additional Liability. While redistributing
166 |       the Work or Derivative Works thereof, You may choose to offer,
167 |       and charge a fee for, acceptance of support, warranty, indemnity,
168 |       or other liability obligations and/or rights consistent with this
169 |       License. However, in accepting such obligations, You may act only
170 |       on Your own behalf and on Your sole responsibility, not on behalf
171 |       of any other Contributor, and only if You agree to indemnify,
172 |       defend, and hold each Contributor harmless for any liability
173 |       incurred by, or claims asserted against, such Contributor by reason
174 |       of your accepting any such warranty or additional liability.
175 | 
176 |    END OF TERMS AND CONDITIONS
177 | 
178 |    Copyright [2025] [Alexander Tsai]
179 | 
180 |    Licensed under the Apache License, Version 2.0 (the "License");
181 |    you may not use this file except in compliance with the License.
182 |    You may obtain a copy of the License at
183 | 
184 |        http://www.apache.org/licenses/LICENSE-2.0
185 | 
186 |    Unless required by applicable law or agreed to in writing, software
187 |    distributed under the License is distributed on an "AS IS" BASIS,
188 |    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
189 |    See the License for the specific language governing permissions and
190 |    limitations under the License.
191 | 
```

--------------------------------------------------------------------------------
/src/mcp_telegram/main.py:
--------------------------------------------------------------------------------

```python
  1 | #!/usr/bin/env python3
  2 | 
  3 | import os
  4 | import sys
  5 | import json
  6 | from telethon import TelegramClient
  7 | from telethon.sessions import StringSession
  8 | from mcp.server.fastmcp import FastMCP
  9 | from typing import Optional
 10 | from dotenv import load_dotenv
 11 | 
 12 | # Client will be initialized when needed
 13 | client: Optional[TelegramClient] = None
 14 | 
 15 | class TelegramServer:
 16 |     def __init__(self):
 17 |         self.app = FastMCP("telegram")
 18 |         self.client = None
 19 |         self.register_tools()
 20 |     
 21 |     async def initialize_client(self):
 22 |         """Initialize Telegram client if not already initialized"""
 23 |         if self.client and self.client.is_connected():
 24 |             return
 25 |         
 26 |         api_id = os.getenv('TELEGRAM_API_ID')
 27 |         api_hash = os.getenv('TELEGRAM_API_HASH')
 28 |         session_string = os.getenv('TELEGRAM_SESSION_STRING')
 29 |         
 30 |         if not api_id or not api_hash or not session_string:
 31 |             raise ValueError("Missing Telegram credentials in .env file")
 32 |         
 33 |         self.client = TelegramClient(StringSession(session_string), api_id, api_hash)
 34 |         await self.client.connect()
 35 |         
 36 |         if not await self.client.is_user_authorized():
 37 |             raise ValueError("Session string is invalid or expired")
 38 |     
 39 |     def register_tools(self):
 40 |         """Register Telegram-related tools with the MCP server"""
 41 |         
 42 |         @self.app.tool()
 43 |         async def get_chats(page: int, page_size: int = 20, offset_id: int = 0, offset_date: str = None, offset_peer_id: int = None) -> str:
 44 |             """
 45 |             Used when checking messages. Gets a paginated list of chats from Telegram. 
 46 |             
 47 |             Args:
 48 |                 page: Page number (1-indexed).
 49 |                 page_size: Number of chats per page.
 50 |                 offset_id: Message ID to use as offset for pagination (from previous page's last_message_id).
 51 |                 offset_date: Date to use as offset for pagination (from previous page's last_message_date).
 52 |                 offset_peer_id: Peer ID to use as offset for pagination (from previous page's last_peer_id).
 53 |             
 54 |             After using this tool, use get_messages to read the actual messages from chats
 55 |   with unread_count > 0
 56 |             """
 57 |             try:
 58 |                 await self.initialize_client()
 59 |                 # For page 1, start from the beginning
 60 |                 if page == 1 or (offset_id == 0 and offset_date is None and offset_peer_id is None):
 61 |                     dialogs = await self.client.get_dialogs(limit=page_size, archived=False)
 62 |                 else:
 63 |                     # For subsequent pages, use the provided offset parameters
 64 |                     offset_peer = None
 65 |                     if offset_peer_id:
 66 |                         # Get the peer entity from the ID
 67 |                         offset_peer = await self.client.get_entity(offset_peer_id)
 68 |                     
 69 |                     # Parse the offset date if provided
 70 |                     from datetime import datetime
 71 |                     offset_date_obj = None
 72 |                     if offset_date:
 73 |                         try:
 74 |                             offset_date_obj = datetime.fromisoformat(offset_date.replace('Z', '+00:00'))
 75 |                         except:
 76 |                             offset_date_obj = None
 77 |                     
 78 |                     dialogs = await self.client.get_dialogs(
 79 |                         limit=page_size,
 80 |                         offset_date=offset_date_obj,
 81 |                         offset_id=offset_id,
 82 |                         offset_peer=offset_peer,
 83 |                         archived=False
 84 |                     )
 85 |                 
 86 |                 result = []
 87 |                 pagination_info = None
 88 |                 
 89 |                 for dialog in dialogs:
 90 |                     chat = {
 91 |                         "id": dialog.id,
 92 |                         "name": dialog.name,
 93 |                         "unread_count": dialog.unread_count,
 94 |                         "type": str(dialog.entity.__class__.__name__)
 95 |                     }
 96 |                     result.append(chat)
 97 |                     
 98 |                     # Store pagination info from the last dialog
 99 |                     if dialog == dialogs[-1] and dialog.message:
100 |                         pagination_info = {
101 |                             "last_message_id": dialog.message.id,
102 |                             "last_message_date": dialog.date.isoformat() if dialog.date else None,
103 |                             "last_peer_id": dialog.id
104 |                         }
105 |                 
106 |                 # Return both results and pagination info
107 |                 response = {
108 |                     "chats": result,
109 |                     "page": page,
110 |                     "page_size": page_size,
111 |                     "has_more": len(result) == page_size
112 |                 }
113 |                 
114 |                 if pagination_info:
115 |                     response["next_page_params"] = {
116 |                         "page": page + 1,
117 |                         "page_size": page_size,
118 |                         "offset_id": pagination_info["last_message_id"],
119 |                         "offset_date": pagination_info["last_message_date"],
120 |                         "offset_peer_id": pagination_info["last_peer_id"]
121 |                     }
122 |                 
123 |                 return json.dumps(response, indent=2)
124 |             except Exception as e:
125 |                 return json.dumps({"error": str(e)})
126 |                 
127 |         @self.app.tool()
128 |         async def get_messages(chat_id: int, page: int, page_size: int = 20) -> str:
129 |             """
130 |             Get paginated messages from a specific chat from Telegram.
131 |             
132 |             Args:
133 |                 chat_id: The ID of the chat.
134 |                 page: Page number (1-indexed).
135 |                 page_size: Number of messages per page.
136 |             """
137 |             offset = (page - 1) * page_size
138 |             limit = page_size
139 |             
140 |             try:
141 |                 await self.initialize_client()
142 |                 messages = await self.client.get_messages(chat_id, limit=limit, offset_id=0, offset_date=None, add_offset=offset)
143 |                 await self.client.send_read_acknowledge(entity=chat_id)
144 |                 
145 |                 result = []
146 |                 for message in messages:
147 |                     msg = {
148 |                         "id": message.id,
149 |                         "date": message.date.isoformat(),
150 |                         "text": message.text,
151 |                         "sender_id": message.sender_id,
152 |                         "reply_to_msg_id": message.reply_to_msg_id
153 |                     }
154 |                     result.append(msg)
155 |                 
156 |                 return json.dumps(result, indent=2)
157 |             except Exception as e:
158 |                 return json.dumps({"error": str(e)})
159 |             
160 |         @self.app.tool()
161 |         async def mark_messages_read(chat_id: int) -> str:
162 |             """
163 |             Mark all unread messages in a specific Telegram chat as read.
164 |             
165 |             Args:
166 |                 chat_id: The ID of the chat whose messages should be marked as read.
167 |             """
168 |             try:
169 |                 await self.initialize_client()
170 |                 # The read_history method marks messages as read
171 |                 result = await self.client.send_read_acknowledge(entity=chat_id)
172 |                 return json.dumps({
173 |                     "success": True, 
174 |                     "message": f"Successfully marked messages as read in chat {chat_id}"
175 |                 })
176 |             except Exception as e:
177 |                 return json.dumps({"success": False, "error": str(e)})
178 |                 
179 |         @self.app.tool()
180 |         async def send_message(chat_id: int, message: str, reply_to_msg_id: int = None) -> str:
181 |             """
182 |             Send a message to a specific chat in Telegram.
183 |             
184 |             Args:
185 |                 chat_id: The ID of the chat.
186 |                 message: The message content to send.
187 |                 reply_to_msg_id: Optional ID of a message to reply to. If provided, this message will be a reply to that specific message.
188 |             """
189 |             try:
190 |                 await self.initialize_client()
191 |                 result = await self.client.send_message(
192 |                     entity=chat_id, 
193 |                     message=message,
194 |                     reply_to=reply_to_msg_id
195 |                 )
196 |                 return json.dumps({
197 |                     "success": True, 
198 |                     "message_id": result.id,
199 |                     "is_reply": reply_to_msg_id is not None,
200 |                     "replied_to_message_id": reply_to_msg_id
201 |                 })
202 |             except Exception as e:
203 |                 return json.dumps({
204 |                     "success": False, 
205 |                     "error": str(e),
206 |                     "is_reply": reply_to_msg_id is not None,
207 |                     "replied_to_message_id": reply_to_msg_id
208 |                 })
209 |         
210 |         @self.app.tool()
211 |         async def get_conversation_context(chat_id: int, message_count: int = 30) -> str:
212 |             """
213 |             This function retrieves recent messages from a specific chat to help
214 |             understand the conversational style and tone, allowing it to
215 |             generate responses that match the existing conversation pattern.
216 |             The function also reads a user-defined style guide from convostyle.txt
217 |             to further refine the response style.
218 |             
219 |             Args:
220 |                 chat_id: The ID of the chat to analyze.
221 |                 message_count: Number of recent messages to retrieve (default: 30).
222 |             """
223 |             try:
224 |                 await self.initialize_client()
225 |                 # Get messages from the chat
226 |                 messages = await self.client.get_messages(chat_id, limit=message_count)
227 |                 
228 |                 # Process and organize the conversation
229 |                 conversation = []
230 |                 sender_info = {}
231 |                 
232 |                 # First pass: collect unique senders and their information
233 |                 for msg in messages:
234 |                     if msg.sender_id and msg.sender_id not in sender_info:
235 |                         try:
236 |                             # Get sender information
237 |                             entity = await self.client.get_entity(msg.sender_id)
238 |                             sender_name = getattr(entity, 'first_name', '') or getattr(entity, 'title', '') or str(msg.sender_id)
239 |                             sender_info[msg.sender_id] = {
240 |                                 'id': msg.sender_id,
241 |                                 'name': sender_name,
242 |                                 'is_self': msg.out
243 |                             }
244 |                         except Exception as e:
245 |                             # If we can't get entity info, use minimal information
246 |                             sender_info[msg.sender_id] = {
247 |                                 'id': msg.sender_id,
248 |                                 'name': f"User {msg.sender_id}",
249 |                                 'is_self': msg.out
250 |                             }
251 |                 
252 |                 # Second pass: organize messages into conversation format
253 |                 # Start with newest messages first in the API response, so we reverse to get chronological order
254 |                 for msg in reversed(messages):
255 |                     if not msg.text:  # Skip non-text messages
256 |                         continue
257 |                     
258 |                     sender = sender_info.get(msg.sender_id, {'name': 'Unknown', 'is_self': False})
259 |                     
260 |                     conversation.append({
261 |                         'timestamp': msg.date.isoformat(),
262 |                         'sender_name': sender['name'],
263 |                         'is_self': sender['is_self'],
264 |                         'text': msg.text,
265 |                         'message_id': msg.id
266 |                     })
267 |                 
268 |                 # Read the user's conversation style guide
269 |                 style_guide = ""
270 |                 style_guide_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "convostyle.txt")
271 |                 
272 |                 try:
273 |                     with open(style_guide_path, 'r', encoding='utf-8') as file:
274 |                         style_guide = file.read().strip()
275 |                 except Exception as e:
276 |                     style_guide = "Style guide file not available. Focus only on conversation history."
277 |                 
278 |                 # Add analysis information to help Claude
279 |                 result = {
280 |                     'conversation': conversation,
281 |                     'user_style_guide': style_guide,
282 |                     'analysis_instructions': """
283 |                         You're helping generate messages that match the user's texting style. 
284 |                         To do this effectively:
285 |                         
286 |                         1. FIRST, carefully read the user's own description of their texting style in the 'user_style_guide' field.
287 |                            This is the primary source of information about how they want to come across in messages.
288 |                         
289 |                         2. SECOND, analyze the conversation history to understand both:
290 |                            - The overall conversation context (topic, relationship between participants)
291 |                            - Specific examples of the user's actual writing style in practice, paying attention to:
292 |                              * Tone (formal, casual, friendly, professional)
293 |                              * Typical message length
294 |                              * Use of emoji, slang, abbreviations, or special formatting
295 |                              * Common greeting/closing patterns
296 |                              * Sentence structure and vocabulary level
297 |                         
298 |                         3. SYNTHESIS: Blend the explicit style guide with observed patterns in their messages,
299 |                            but when there's any conflict, the explicit style guide takes precedence.
300 |                         
301 |                         Generate a response that feels authentic to both how they say they write
302 |                         and how they actually write, while being appropriate to the current conversation.
303 |                     """
304 |                 }
305 |                 
306 |                 return json.dumps(result, indent=2, ensure_ascii=False)
307 |                 
308 |             except Exception as e:
309 |                 return json.dumps({
310 |                     "error": str(e),
311 |                     "message": "Failed to retrieve conversation context"
312 |                 })
313 |     
314 |     def run(self):
315 |         """Run the MCP server"""
316 |         self.app.run(transport='stdio')
317 | 
318 | def main():
319 |     # Load .env file
320 |     load_dotenv()
321 |     
322 |     # Get Telegram API credentials from .env file
323 |     api_id = os.getenv('TELEGRAM_API_ID')
324 |     api_hash = os.getenv('TELEGRAM_API_HASH')
325 |     session_string = os.getenv('TELEGRAM_SESSION_STRING')
326 |     
327 |     if not api_id or not api_hash:
328 |         print("Please set TELEGRAM_API_ID and TELEGRAM_API_HASH in your .env file", file=sys.stderr)
329 |         print("Get these from https://my.telegram.org/apps", file=sys.stderr)
330 |         sys.exit(1)
331 |     
332 |     if not session_string:
333 |         print("Please set TELEGRAM_SESSION_STRING in your .env file", file=sys.stderr)
334 |         print("Run: python telethon_auth.py to generate a session string", file=sys.stderr)
335 |         sys.exit(1)
336 |     
337 |     try:
338 |         # Create and run server
339 |         server = TelegramServer()
340 |         server.run()
341 |         
342 |     except Exception as e:
343 |         print(f"Error: {str(e)}", file=sys.stderr)
344 |         sys.exit(1)
345 | 
346 | if __name__ == "__main__":
347 |     main()
```