Skip to content

Commit 4527001

Browse files
committed
feat: clarify installation instructions and warn against using incorrect PyPI package & add install_guard.py
1 parent bee2085 commit 4527001

7 files changed

Lines changed: 402 additions & 13 deletions

File tree

README.md

Lines changed: 61 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -58,10 +58,16 @@ All tool results that include Telegram user-controlled content are sanitized and
5858
- Telegram API credentials from [my.telegram.org/apps](https://my.telegram.org/apps)
5959
- A Telegram session string or file-based session
6060
- An MCP client such as Claude Desktop, Cursor, or another MCP-compatible host
61-
- Optional: [uv](https://docs.astral.sh/uv/) for local development and `uvx` usage
61+
- Optional: [uv](https://docs.astral.sh/uv/) for local development
6262

6363
## Quick Start
6464

65+
> Do not install this server with `uvx telegram-mcp`, `uvx --from telegram-mcp`,
66+
> or `pip install telegram-mcp`. The `telegram-mcp` name on PyPI is currently
67+
> owned by a different project and does not install this repository. Passing
68+
> `TELEGRAM_API_ID`, `TELEGRAM_API_HASH`, or `TELEGRAM_SESSION_STRING` to that
69+
> package can expose Telegram account credentials to unrelated third-party code.
70+
6571
### 1. Clone and Install
6672

6773
```bash
@@ -102,7 +108,8 @@ uv run main.py
102108

103109
## MCP Client Configuration
104110

105-
For Claude Desktop or Cursor, point the MCP server at this project:
111+
For Claude Desktop or Cursor, point the MCP server at a cloned checkout of
112+
this project:
106113

107114
```json
108115
{
@@ -114,20 +121,33 @@ For Claude Desktop or Cursor, point the MCP server at this project:
114121
"/full/path/to/telegram-mcp",
115122
"run",
116123
"main.py"
117-
]
124+
],
125+
"env": {
126+
"TELEGRAM_API_ID": "your_api_id_here",
127+
"TELEGRAM_API_HASH": "your_api_hash_here",
128+
"TELEGRAM_SESSION_STRING": "your_session_string_here"
129+
}
118130
}
119131
}
120132
}
121133
```
122134

123-
You can also run from PyPI with `uvx`:
135+
Alternatively, install this repository directly from GitHub into a virtual
136+
environment using a specific release tag or commit:
137+
138+
```bash
139+
python -m venv .venv
140+
. .venv/bin/activate
141+
pip install "git+https://github.com/chigwell/telegram-mcp.git@<tag-or-commit>"
142+
```
143+
144+
Then configure your MCP client to run the installed console script:
124145

125146
```json
126147
{
127148
"mcpServers": {
128149
"telegram-mcp": {
129-
"command": "uvx",
130-
"args": ["telegram-mcp"],
150+
"command": "/full/path/to/.venv/bin/telegram-mcp",
131151
"env": {
132152
"TELEGRAM_API_ID": "your_api_id_here",
133153
"TELEGRAM_API_HASH": "your_api_hash_here",
@@ -138,10 +158,11 @@ You can also run from PyPI with `uvx`:
138158
}
139159
```
140160

141-
Generate a session string without cloning the repo:
161+
Generate a session string without cloning the repo by sourcing this repository
162+
from GitHub explicitly:
142163

143164
```bash
144-
uvx --from telegram-mcp telegram-mcp-generate-session
165+
uvx --from "git+https://github.com/chigwell/telegram-mcp.git@<pinned-release-tag-or-commit>" telegram-mcp-generate-session
145166
```
146167

147168
## Multi-Account Setup
@@ -244,10 +265,29 @@ Run with allowed roots:
244265
uv run main.py /data/telegram /tmp/telegram-mcp
245266
```
246267

247-
Or with `uvx`:
268+
From an MCP client configuration, pass the same roots after `main.py`:
248269

249-
```bash
250-
uvx telegram-mcp /data/telegram /tmp/telegram-mcp
270+
```json
271+
{
272+
"mcpServers": {
273+
"telegram-mcp": {
274+
"command": "uv",
275+
"args": [
276+
"--directory",
277+
"/full/path/to/telegram-mcp",
278+
"run",
279+
"main.py",
280+
"/data/telegram",
281+
"/tmp/telegram-mcp"
282+
],
283+
"env": {
284+
"TELEGRAM_API_ID": "your_api_id_here",
285+
"TELEGRAM_API_HASH": "your_api_hash_here",
286+
"TELEGRAM_SESSION_STRING": "your_session_string_here"
287+
}
288+
}
289+
}
290+
}
251291
```
252292

253293
## Docker
@@ -314,8 +354,17 @@ uv run flake8 .
314354

315355
- Never commit `.env`, session strings, or `.session` files.
316356
- A Telegram session string grants access to the account it belongs to.
357+
- The `telegram-mcp` package name on PyPI is not controlled by this project.
358+
Avoid PyPI-based `telegram-mcp` install commands unless ownership changes and
359+
the package is verified.
360+
- This repository includes a best-effort startup guard that refuses installed
361+
`telegram-mcp` distributions without a source checkout or direct git/file
362+
install record. That guard cannot run when the unrelated PyPI package itself
363+
is launched, so use clone-based or explicit git installs.
317364
- Prefer session strings over file sessions when running multiple server instances.
318-
- All Telegram API calls go directly from your machine/container to Telegram.
365+
- By default, Telegram API calls go directly from your machine/container to Telegram.
366+
If `TELEGRAM_PROXY_*` is configured, Telegram traffic is routed through the
367+
configured SOCKS/HTTP/MTProxy proxy instead.
319368
- User-generated Telegram content is sanitized before being returned to MCP clients.
320369

321370
### Prompt Injection Protection

main.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,13 @@
44
historic `main` import path and console script target working.
55
"""
66

7+
from telegram_mcp.install_guard import UnsafeInstallationError, assert_safe_distribution
8+
9+
try:
10+
assert_safe_distribution()
11+
except UnsafeInstallationError as exc:
12+
raise SystemExit(str(exc)) from None
13+
714
from telegram_mcp import runtime as _runtime
815
from telegram_mcp.runtime import *
916
from telegram_mcp.runner import _main, main

session_string_generator.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,19 @@
2828
from telethon import errors
2929
from telethon.sessions import StringSession
3030
from telethon.sync import TelegramClient
31+
from telegram_mcp.install_guard import UnsafeInstallationError, assert_safe_distribution
3132

3233
load_dotenv()
3334

3435

36+
def _check_installation() -> None:
37+
try:
38+
assert_safe_distribution()
39+
except UnsafeInstallationError as exc:
40+
print(str(exc), file=sys.stderr)
41+
sys.exit(1)
42+
43+
3544
def _qr_login(client: TelegramClient) -> None:
3645
import qrcode
3746

@@ -90,6 +99,8 @@ def _phone_login(client: TelegramClient) -> None:
9099

91100

92101
def main() -> None:
102+
_check_installation()
103+
93104
API_ID = os.getenv("TELEGRAM_API_ID")
94105
API_HASH = os.getenv("TELEGRAM_API_HASH")
95106

telegram_mcp/__init__.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
11
"""Telegram MCP server package."""
22

3-
from telegram_mcp.runtime import mcp
3+
4+
def __getattr__(name: str):
5+
if name == "mcp":
6+
from telegram_mcp.install_guard import assert_safe_distribution
7+
8+
assert_safe_distribution()
9+
from telegram_mcp.runtime import mcp
10+
11+
return mcp
12+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
13+
414

515
__all__ = ["mcp"]

telegram_mcp/install_guard.py

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
"""Installation provenance checks for the Telegram MCP server.
2+
3+
The PyPI distribution name ``telegram-mcp`` is currently occupied by an
4+
unrelated project. This guard can only protect executions that reach this
5+
repository's code; it cannot run when a user launches the third-party package.
6+
"""
7+
8+
from __future__ import annotations
9+
10+
import json
11+
from dataclasses import dataclass
12+
from importlib import metadata
13+
from pathlib import Path
14+
from urllib.parse import unquote, urlparse
15+
16+
DISTRIBUTION_NAME = "telegram-mcp"
17+
18+
19+
class UnsafeInstallationError(RuntimeError):
20+
"""Raised when installed package metadata points at the wrong project."""
21+
22+
23+
@dataclass(frozen=True)
24+
class DistributionIdentity:
25+
"""Small, testable view of Python package metadata."""
26+
27+
name: str
28+
version: str
29+
authors: tuple[str, ...] = ()
30+
maintainers: tuple[str, ...] = ()
31+
urls: tuple[str, ...] = ()
32+
summary: str = ""
33+
direct_url: str = ""
34+
35+
@classmethod
36+
def from_distribution(cls, dist: metadata.Distribution) -> "DistributionIdentity":
37+
package_metadata = dist.metadata
38+
urls = tuple(package_metadata.get_all("Project-URL") or ())
39+
homepage = package_metadata.get("Home-page")
40+
if homepage:
41+
urls += (homepage,)
42+
43+
authors = tuple(
44+
value
45+
for value in (
46+
package_metadata.get("Author"),
47+
package_metadata.get("Author-email"),
48+
)
49+
if value
50+
)
51+
maintainers = tuple(
52+
value
53+
for value in (
54+
package_metadata.get("Maintainer"),
55+
package_metadata.get("Maintainer-email"),
56+
)
57+
if value
58+
)
59+
60+
read_text = getattr(dist, "read_text", None)
61+
direct_url = read_text("direct_url.json") if callable(read_text) else ""
62+
63+
return cls(
64+
name=package_metadata.get("Name") or getattr(dist, "name", DISTRIBUTION_NAME),
65+
version=package_metadata.get("Version") or dist.version,
66+
authors=authors,
67+
maintainers=maintainers,
68+
urls=urls,
69+
summary=package_metadata.get("Summary", ""),
70+
direct_url=direct_url,
71+
)
72+
73+
74+
def _project_root_declares_distribution_name(path: Path) -> bool:
75+
pyproject_path = path / "pyproject.toml"
76+
if not pyproject_path.is_file():
77+
return False
78+
79+
try:
80+
pyproject_text = pyproject_path.read_text(encoding="utf-8")
81+
except OSError:
82+
return False
83+
84+
return f'name = "{DISTRIBUTION_NAME}"' in pyproject_text
85+
86+
87+
def _direct_url_json(direct_url: str) -> dict:
88+
if not direct_url:
89+
return {}
90+
91+
try:
92+
direct_url_data = json.loads(direct_url)
93+
except json.JSONDecodeError:
94+
return {}
95+
96+
return direct_url_data if isinstance(direct_url_data, dict) else {}
97+
98+
99+
def _direct_url_is_explicit_source_install(direct_url: str) -> bool:
100+
direct_url_data = _direct_url_json(direct_url)
101+
if not direct_url_data:
102+
return False
103+
104+
raw_url = str(direct_url_data.get("url", "")).strip()
105+
if not raw_url:
106+
return False
107+
108+
parsed_url = urlparse(raw_url)
109+
110+
if parsed_url.scheme == "file":
111+
source_path = Path(unquote(parsed_url.path)).resolve()
112+
return _project_root_declares_distribution_name(source_path)
113+
114+
vcs_info = direct_url_data.get("vcs_info")
115+
return isinstance(vcs_info, dict) and bool(vcs_info.get("vcs"))
116+
117+
118+
def _looks_like_explicit_source_install(identity: DistributionIdentity) -> bool:
119+
return _direct_url_is_explicit_source_install(identity.direct_url)
120+
121+
122+
def _format_unsafe_installation_message(identity: DistributionIdentity) -> str:
123+
authors = ", ".join(identity.authors) or "unknown"
124+
maintainers = ", ".join(identity.maintainers) or "unknown"
125+
urls = "; ".join(identity.urls) or "unknown"
126+
127+
return (
128+
"Refusing to start: the installed 'telegram-mcp' distribution was not "
129+
"installed from an explicit source checkout.\n"
130+
f"Detected distribution: name={identity.name!r}, version={identity.version!r}, "
131+
f"authors={authors!r}, maintainers={maintainers!r}, urls={urls!r}.\n"
132+
"The 'telegram-mcp' name on PyPI is currently owned by a different project. "
133+
"This guard requires a source checkout or installer-recorded direct "
134+
"git/file URL. Run this server from a cloned checkout or install a "
135+
"trusted repository explicitly with: "
136+
'pip install "git+https://github.com/chigwell/telegram-mcp.git"'
137+
)
138+
139+
140+
def assert_safe_distribution(distribution_name: str = DISTRIBUTION_NAME) -> None:
141+
"""Abort when the installed distribution metadata is not this project.
142+
143+
Source checkouts that have not been installed as a distribution are allowed:
144+
``uv --directory /path/to/telegram-mcp run main.py`` can run from source
145+
without package metadata. If metadata for ``telegram-mcp`` is present, it
146+
must come from an explicit git or file install recorded by the installer.
147+
"""
148+
149+
try:
150+
dist = metadata.distribution(distribution_name)
151+
except metadata.PackageNotFoundError:
152+
return
153+
154+
identity = DistributionIdentity.from_distribution(dist)
155+
if _looks_like_explicit_source_install(identity):
156+
return
157+
158+
raise UnsafeInstallationError(_format_unsafe_installation_message(identity))

telegram_mcp/runner.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
"""Application entrypoints for the Telegram MCP server."""
22

3+
from telegram_mcp.install_guard import UnsafeInstallationError, assert_safe_distribution
4+
5+
try:
6+
assert_safe_distribution()
7+
except UnsafeInstallationError as exc:
8+
raise SystemExit(str(exc)) from None
9+
310
from telegram_mcp.runtime import *
411
import telegram_mcp.tools # noqa: F401 - registers MCP tools via decorators
512

0 commit comments

Comments
 (0)