Skip to content

Commit f360d80

Browse files
committed
docs: tighten account-check and reconnect wording
1 parent 0cf01d1 commit f360d80

5 files changed

Lines changed: 12 additions & 60 deletions

File tree

‎.env.example‎

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,10 +51,7 @@ TELEGRAM_SESSION_NAME=telegram_session
5151
# TELEGRAM_APP_VERSION=1.0
5252

5353
# --- Expected account check (optional) ---
54-
# Refuse to start if the session is logged in to a different Telegram account
55-
# than expected (compared with get_me().username, case-insensitive, '@'
56-
# optional). Per-account values use the same _<LABEL> suffix as the session
57-
# variables and override the unsuffixed one. Unset = no check.
54+
# Refuse to start if the session belongs to another account. _<LABEL> overrides.
5855
# TELEGRAM_EXPECTED_USERNAME=my_username
5956
# TELEGRAM_EXPECTED_USERNAME_WORK=my_work_username
6057

‎README.md‎

Lines changed: 3 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -570,30 +570,7 @@ every connect), so set them in the same place as your other credentials.
570570

571571
## Expected Account Check
572572

573-
Optionally pin each session to the Telegram account it is supposed to belong to.
574-
When set, the server calls `get_me()` right after connecting and compares the
575-
account's username (case-insensitive, leading `@` optional, active collectible
576-
usernames included) with the configured value. On a mismatch, or if the account
577-
has no username, it disconnects and refuses to start, so a swapped or
578-
mislabelled session string cannot silently act as the wrong person:
579-
580-
```env
581-
TELEGRAM_EXPECTED_USERNAME=my_username
582-
```
583-
584-
In multi-account setups use the same `_<LABEL>` suffix as the session variables;
585-
a suffixed value overrides the unsuffixed one for that account:
586-
587-
```env
588-
TELEGRAM_SESSION_STRING_WORK=session_string_for_work
589-
TELEGRAM_SESSION_STRING_PERSONAL=session_string_for_personal
590-
TELEGRAM_EXPECTED_USERNAME_WORK=my_work_username
591-
TELEGRAM_EXPECTED_USERNAME_PERSONAL=my_personal_username
592-
```
593-
594-
An unsuffixed `TELEGRAM_EXPECTED_USERNAME` applies to every account that has no
595-
suffixed value. When neither is set (the default) no check is made and no extra
596-
request is sent.
573+
Set `TELEGRAM_EXPECTED_USERNAME` (or `TELEGRAM_EXPECTED_USERNAME_<LABEL>` per account) and the server refuses to start when the session belongs to a different account. The comparison is case-insensitive and ignores a leading `@`. Unset means no check.
597574

598575
## Proxy Support
599576

@@ -867,10 +844,8 @@ Telegram messages, display names, chat titles, and button labels are untrusted c
867844
the MCP server when you can scan from an existing Telegram app, or
868845
`uv run session_string_generator.py --phone` when you need phone-code login.
869846
Then set `TELEGRAM_SESSION_STRING` in `.env`. The MCP server does not perform
870-
interactive phone-code login over stdio. This also applies when a session is
871-
terminated (for example from **Settings > Devices** on another device) while the
872-
server is running: the next reconnect fails with "not authorized after reconnect"
873-
instead of prompting for a phone number. Re-authorize as above and restart the server.
847+
interactive phone-code login over stdio, including after a reconnect: a session
848+
terminated while the server runs fails with "not authorized after reconnect".
874849
- **Invalid API credentials:** verify `TELEGRAM_API_ID` and `TELEGRAM_API_HASH` at [my.telegram.org/apps](https://my.telegram.org/apps).
875850
- **Database is locked:** make sure no other process is using the same file session.
876851
- **`AuthKeyDuplicatedError` / "Another telegram-mcp process is already connected with this session":** two processes tried to connect the same Telegram session at once (e.g. an MCP client restarted the connector before the old process exited), which Telegram rejects and can invalidate the session for both. The server now takes an exclusive lock per session before connecting; a second concurrent launch waits briefly (default 20s, override with `TELEGRAM_LOCK_GRACE_SECONDS`) for the first to release it and otherwise exits without ever calling `connect()`, instead of racing into a duplicate connection. Retry once only one instance is running — the refusal names the PID holding the lock. If several instances on this host are meant to share one session (all reaching Telegram from the same IP), set `TELEGRAM_SESSION_LOCK=shared`; see [Sharing one session from one host](#sharing-one-session-from-one-host).

‎telegram_mcp/runner.py‎

Lines changed: 3 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -68,31 +68,21 @@ def _normalize_username(value) -> str:
6868

6969

7070
def _expected_username(label: str) -> str:
71-
"""``TELEGRAM_EXPECTED_USERNAME[_<LABEL>]``: the account a session must belong to.
72-
73-
The per-account ``_<LABEL>`` variable overrides the unsuffixed one, like the
74-
``TELEGRAM_PROXY_*`` variables. Returns ``""`` (no check) when neither is set.
75-
"""
71+
"""TELEGRAM_EXPECTED_USERNAME_<LABEL>, else TELEGRAM_EXPECTED_USERNAME; "" if unset."""
7672
raw = os.getenv(f"TELEGRAM_EXPECTED_USERNAME_{label.upper()}") or os.getenv(
7773
"TELEGRAM_EXPECTED_USERNAME"
7874
)
7975
return _normalize_username(raw)
8076

8177

8278
async def _verify_expected_username(label: str, client) -> None:
83-
"""Refuse to serve a session that is logged in to a different account.
84-
85-
Guards against a swapped or mislabelled session string (e.g. the personal
86-
account's session configured under the work label), which would otherwise
87-
let MCP clients read and send messages as the wrong person.
88-
"""
79+
"""Refuse to serve a session logged in to a different account."""
8980
expected = _expected_username(label)
9081
if not expected:
9182
return
9283

9384
me = await client.get_me()
9485
usernames = {_normalize_username(getattr(me, "username", None))}
95-
# Accounts with collectible usernames list the extra ones in `usernames`.
9686
for entry in getattr(me, "usernames", None) or []:
9787
if getattr(entry, "active", False):
9888
usernames.add(_normalize_username(getattr(entry, "username", None)))
@@ -107,9 +97,7 @@ async def _verify_expected_username(label: str, client) -> None:
10797
pass
10898
raise RuntimeError(
10999
f"Telegram client '{label}' is logged in to a different account than "
110-
f"TELEGRAM_EXPECTED_USERNAME_{label.upper()} / TELEGRAM_EXPECTED_USERNAME "
111-
"expects. Refusing to start; check which session string or session file is "
112-
"configured for this account."
100+
f"TELEGRAM_EXPECTED_USERNAME_{label.upper()} / TELEGRAM_EXPECTED_USERNAME expects."
113101
)
114102

115103

‎telegram_mcp/runtime.py‎

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -813,23 +813,15 @@ async def _force_reconnect(cl: TelegramClient):
813813
f"Reconnecting to Telegram timed out after {_RECONNECT_TIMEOUT:.0f}s."
814814
) from exc
815815
if not await cl.is_user_authorized():
816-
# The session was revoked or logged out while the server was running.
817-
# Never fall back to cl.start(): its phone/code prompts call input()
818-
# synchronously, which blocks the event loop (asyncio.wait_for cannot
819-
# cancel it) and, over the stdio transport, reads MCP protocol frames
820-
# as a phone number. Startup already refuses interactive login for the
821-
# same reason (see runner._connect_authorized_client).
816+
# cl.start() would prompt via blocking input(): it stalls the event loop
817+
# and, over stdio, reads protocol frames as a phone number.
822818
try:
823819
await cl.disconnect()
824820
except Exception:
825821
pass
826822
raise RuntimeError(
827-
"Telegram session is not authorized after reconnect (it may have been "
828-
"terminated from another device or logged out). Interactive login is "
829-
"disabled inside the MCP server. Re-authorize outside the server with "
830-
"`uv run session_string_generator.py`, update TELEGRAM_SESSION_STRING "
831-
"or TELEGRAM_SESSION_STRING_<LABEL> (or log the file session in again), "
832-
"then restart the server."
823+
"Telegram session is not authorized after reconnect. Re-authorize it "
824+
"outside the server (session_string_generator.py) and restart."
833825
)
834826
_last_conn_verified[id(cl)] = time.time()
835827
reconnect_logger.warning("Forced reconnect successful")

‎tests/test_runtime.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -575,7 +575,7 @@ async def test_ensure_connected_surfaces_revoked_session(monkeypatch):
575575
client = _ConnectivityClient(connected=False, authorized=False)
576576
monkeypatch.setattr(runtime, "_last_conn_verified", {})
577577

578-
with pytest.raises(RuntimeError, match="Interactive login is disabled"):
578+
with pytest.raises(RuntimeError, match="not authorized after reconnect"):
579579
await runtime.ensure_connected(client)
580580

581581
assert "start" not in client.calls

0 commit comments

Comments
 (0)