Skip to content

Commit 746ddf8

Browse files
authored
Merge pull request chigwell#113 from VladimirGutuev/feat/proxy-support
feat: add proxy support (SOCKS5/SOCKS4/HTTP/MTProxy)
2 parents 531993d + 35fa5a1 commit 746ddf8

6 files changed

Lines changed: 360 additions & 9 deletions

File tree

‎.env.example‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,22 @@ TELEGRAM_SESSION_STRING=1231231232erfdfdffd
1313
# Shared API_ID and API_HASH are used for all accounts.
1414
# TELEGRAM_SESSION_STRING_WORK=<session string for work account>
1515
# TELEGRAM_SESSION_STRING_PERSONAL=<session string for personal account>
16+
17+
# --- Proxy (optional) ---
18+
# Route Telegram traffic through a proxy. Set TELEGRAM_PROXY_TYPE to enable.
19+
# Supported types: socks5, socks4, http, mtproxy.
20+
# SOCKS/HTTP proxies require the 'python-socks' package (install via
21+
# `uv sync --extra proxy` or `pip install python-socks`).
22+
# Per-account overrides use the same _<LABEL> suffix as session variables.
23+
# TELEGRAM_PROXY_TYPE=socks5
24+
# TELEGRAM_PROXY_HOST=127.0.0.1
25+
# TELEGRAM_PROXY_PORT=1080
26+
# TELEGRAM_PROXY_USERNAME=<optional>
27+
# TELEGRAM_PROXY_PASSWORD=<optional>
28+
# TELEGRAM_PROXY_RDNS=true
29+
# For MTProxy, set TELEGRAM_PROXY_TYPE=mtproxy and provide the secret:
30+
# TELEGRAM_PROXY_SECRET=<hex secret>
31+
# Per-account override example:
32+
# TELEGRAM_PROXY_TYPE_WORK=http
33+
# TELEGRAM_PROXY_HOST_WORK=proxy.work.example
34+
# TELEGRAM_PROXY_PORT_WORK=3128

‎README.md‎

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ Message sent successfully:
3030
- [Quick Start](#quick-start)
3131
- [MCP Client Configuration](#mcp-client-configuration)
3232
- [Multi-Account Setup](#multi-account-setup)
33+
- [Proxy Support](#proxy-support)
3334
- [File Path Security](#file-path-security)
3435
- [Docker](#docker)
3536
- [Development](#development)
@@ -166,6 +167,58 @@ Example prompts:
166167
- "Show unread messages from all accounts"
167168
- "Send this from my work account to @example"
168169

170+
## Proxy Support
171+
172+
Route Telegram traffic through a proxy by setting the `TELEGRAM_PROXY_*`
173+
environment variables. Supported types are `socks5`, `socks4`, `http`, and
174+
`mtproxy`.
175+
176+
SOCKS and HTTP proxies require the optional `python-socks` package:
177+
178+
```bash
179+
uv sync --extra proxy
180+
# or
181+
pip install python-socks
182+
```
183+
184+
Single-account configuration:
185+
186+
```env
187+
TELEGRAM_PROXY_TYPE=socks5
188+
TELEGRAM_PROXY_HOST=127.0.0.1
189+
TELEGRAM_PROXY_PORT=1080
190+
TELEGRAM_PROXY_USERNAME=optional_user
191+
TELEGRAM_PROXY_PASSWORD=optional_pass
192+
TELEGRAM_PROXY_RDNS=true
193+
```
194+
195+
MTProxy:
196+
197+
```env
198+
TELEGRAM_PROXY_TYPE=mtproxy
199+
TELEGRAM_PROXY_HOST=mtproxy.example
200+
TELEGRAM_PROXY_PORT=443
201+
TELEGRAM_PROXY_SECRET=ee0123456789abcdef...
202+
```
203+
204+
Per-account overrides use the same `_<LABEL>` suffix as session variables and
205+
take precedence over the unsuffixed defaults:
206+
207+
```env
208+
TELEGRAM_PROXY_TYPE=socks5
209+
TELEGRAM_PROXY_HOST=127.0.0.1
210+
TELEGRAM_PROXY_PORT=1080
211+
212+
TELEGRAM_PROXY_TYPE_WORK=http
213+
TELEGRAM_PROXY_HOST_WORK=proxy.work.example
214+
TELEGRAM_PROXY_PORT_WORK=3128
215+
```
216+
217+
Misconfigured proxy settings (unknown type, missing host/port, invalid port,
218+
missing MTProxy secret, or a missing `python-socks` package) cause the server
219+
to fail fast at startup with a clear error message instead of silently
220+
bypassing the proxy.
221+
169222
## File Path Security
170223

171224
File-path tools are disabled until allowed roots are configured. This affects tools such as `send_file`, `download_media`, `upload_file`, `send_voice`, `send_sticker`, `set_profile_photo`, and `edit_chat_photo`.

‎pyproject.toml‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,12 @@ dependencies = [
3333
"telethon>=1.42.0",
3434
]
3535

36+
[project.optional-dependencies]
37+
# Required only when TELEGRAM_PROXY_TYPE is set to socks5/socks4/http.
38+
proxy = [
39+
"python-socks>=2.4.3",
40+
]
41+
3642
[project.urls]
3743
"Homepage" = "https://github.com/chigwell/telegram-mcp"
3844
"Bug Tracker" = "https://github.com/chigwell/telegram-mcp/issues"

‎telegram_mcp/runtime.py‎

Lines changed: 109 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -133,13 +133,118 @@ async def annotated_handler(req):
133133
# ---------------------------------------------------------------------------
134134

135135

136+
_PROXY_TYPES_SOCKS_HTTP = {"socks5", "socks4", "http"}
137+
_PROXY_TYPES_ALL = _PROXY_TYPES_SOCKS_HTTP | {"mtproxy"}
138+
139+
140+
def _get_proxy_env(name: str, label: str) -> Optional[str]:
141+
"""Resolve a TELEGRAM_PROXY_* env var with optional ``_<LABEL>`` suffix.
142+
143+
Per-account values override the unsuffixed defaults so a global proxy can
144+
coexist with per-label overrides.
145+
"""
146+
suffixed = os.getenv(f"TELEGRAM_PROXY_{name}_{label.upper()}")
147+
if suffixed:
148+
return suffixed
149+
return os.getenv(f"TELEGRAM_PROXY_{name}") or None
150+
151+
152+
def _parse_bool_env(value: Optional[str], default: bool) -> bool:
153+
if value is None:
154+
return default
155+
return value.strip().lower() in {"1", "true", "yes", "on"}
156+
157+
158+
def _build_proxy_for_label(label: str) -> tuple[Optional[Any], Optional[Any]]:
159+
"""Return ``(proxy, connection)`` kwargs for ``TelegramClient`` for a label.
160+
161+
Reads ``TELEGRAM_PROXY_*`` env vars (with optional ``_<LABEL>`` suffix).
162+
Returns ``(None, None)`` when no proxy is configured. Raises
163+
:class:`ValidationError` for malformed configuration so the server fails
164+
fast instead of silently bypassing the proxy.
165+
"""
166+
proxy_type = _get_proxy_env("TYPE", label)
167+
if not proxy_type:
168+
return None, None
169+
170+
proxy_type = proxy_type.strip().lower()
171+
if proxy_type not in _PROXY_TYPES_ALL:
172+
raise ValidationError(
173+
f"Invalid TELEGRAM_PROXY_TYPE '{proxy_type}'. "
174+
f"Expected one of: {', '.join(sorted(_PROXY_TYPES_ALL))}."
175+
)
176+
177+
host = _get_proxy_env("HOST", label)
178+
port_raw = _get_proxy_env("PORT", label)
179+
if not host or not port_raw:
180+
raise ValidationError(
181+
"TELEGRAM_PROXY_HOST and TELEGRAM_PROXY_PORT are required when "
182+
"TELEGRAM_PROXY_TYPE is set."
183+
)
184+
try:
185+
port = int(port_raw)
186+
except ValueError as exc:
187+
raise ValidationError(
188+
f"TELEGRAM_PROXY_PORT must be an integer, got '{port_raw}'."
189+
) from exc
190+
191+
if proxy_type == "mtproxy":
192+
secret = _get_proxy_env("SECRET", label)
193+
if not secret:
194+
raise ValidationError("TELEGRAM_PROXY_SECRET is required for mtproxy.")
195+
try:
196+
from telethon.network import ConnectionTcpMTProxyRandomizedIntermediate
197+
except ImportError as exc: # pragma: no cover - defensive guard
198+
raise ValidationError(
199+
"Telethon MTProxy connection class is unavailable; upgrade telethon."
200+
) from exc
201+
return (host, port, secret), ConnectionTcpMTProxyRandomizedIntermediate
202+
203+
# SOCKS4/SOCKS5/HTTP via python-socks (Telethon's optional dependency).
204+
try:
205+
import python_socks # noqa: F401
206+
except ImportError as exc:
207+
raise ValidationError(
208+
f"Proxy type '{proxy_type}' requires the 'python-socks' package. "
209+
"Install it with `pip install python-socks` or `uv sync --extra proxy`."
210+
) from exc
211+
212+
proxy: dict[str, Any] = {
213+
"proxy_type": proxy_type,
214+
"addr": host,
215+
"port": port,
216+
"rdns": _parse_bool_env(_get_proxy_env("RDNS", label), default=True),
217+
}
218+
username = _get_proxy_env("USERNAME", label)
219+
password = _get_proxy_env("PASSWORD", label)
220+
if username:
221+
proxy["username"] = username
222+
if password:
223+
proxy["password"] = password
224+
return proxy, None
225+
226+
227+
def _build_client(session: Any, label: str) -> TelegramClient:
228+
"""Construct a ``TelegramClient`` honoring per-label proxy configuration."""
229+
proxy, connection = _build_proxy_for_label(label)
230+
kwargs: dict[str, Any] = {}
231+
if proxy is not None:
232+
kwargs["proxy"] = proxy
233+
if connection is not None:
234+
kwargs["connection"] = connection
235+
return TelegramClient(session, TELEGRAM_API_ID, TELEGRAM_API_HASH, **kwargs)
236+
237+
136238
def _discover_accounts() -> dict[str, TelegramClient]:
137239
"""Scan env vars to build account label -> TelegramClient mapping.
138240
139241
Detection rules:
140242
- TELEGRAM_SESSION_STRING_<LABEL> / TELEGRAM_SESSION_NAME_<LABEL> -> multi-mode
141243
- Unsuffixed TELEGRAM_SESSION_STRING / TELEGRAM_SESSION_NAME -> label "default"
142244
- If both suffixed and unsuffixed exist -> unsuffixed becomes "default"
245+
246+
Each client is constructed via :func:`_build_client`, which applies any
247+
matching ``TELEGRAM_PROXY_*`` configuration (optionally per-label).
143248
"""
144249
accounts: dict[str, TelegramClient] = {}
145250

@@ -149,23 +254,19 @@ def _discover_accounts() -> dict[str, TelegramClient]:
149254
for key, value in os.environ.items():
150255
if key.startswith(prefix_str) and value:
151256
label = key[len(prefix_str) :].lower()
152-
accounts[label] = TelegramClient(
153-
StringSession(value), TELEGRAM_API_ID, TELEGRAM_API_HASH
154-
)
257+
accounts[label] = _build_client(StringSession(value), label)
155258
elif key.startswith(prefix_name) and value:
156259
label = key[len(prefix_name) :].lower()
157-
accounts[label] = TelegramClient(value, TELEGRAM_API_ID, TELEGRAM_API_HASH)
260+
accounts[label] = _build_client(value, label)
158261

159262
# Backward-compatible unsuffixed variables
160263
session_string = os.getenv("TELEGRAM_SESSION_STRING")
161264
session_name = os.getenv("TELEGRAM_SESSION_NAME")
162265

163266
if session_string and "default" not in accounts:
164-
accounts["default"] = TelegramClient(
165-
StringSession(session_string), TELEGRAM_API_ID, TELEGRAM_API_HASH
166-
)
267+
accounts["default"] = _build_client(StringSession(session_string), "default")
167268
elif session_name and "default" not in accounts:
168-
accounts["default"] = TelegramClient(session_name, TELEGRAM_API_ID, TELEGRAM_API_HASH)
269+
accounts["default"] = _build_client(session_name, "default")
169270

170271
if not accounts:
171272
print(

0 commit comments

Comments
 (0)