@overpod/mcp-telegram 1.38.2 → 1.39.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,20 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [1.39.1](https://github.com/mcp-telegram/mcp-telegram/compare/v1.39.0...v1.39.1) (2026-06-21)
9
+
10
+
11
+ ### Fixed
12
+
13
+ * **resolve:** recover bare numeric peer IDs that lack access_hash ([#62](https://github.com/mcp-telegram/mcp-telegram/issues/62)) ([678077a](https://github.com/mcp-telegram/mcp-telegram/commit/678077a78ce3a5852e07d5d831f664c3e08a81e3))
14
+
15
+ ## [1.39.0](https://github.com/mcp-telegram/mcp-telegram/compare/v1.38.2...v1.39.0) (2026-06-21)
16
+
17
+
18
+ ### Added
19
+
20
+ * complete QR login for accounts with 2FA ([#59](https://github.com/mcp-telegram/mcp-telegram/issues/59)) ([8b57965](https://github.com/mcp-telegram/mcp-telegram/commit/8b5796584edcc974d31b410db15fe1ee31ca84ca))
21
+
8
22
  ## [1.38.2](https://github.com/mcp-telegram/mcp-telegram/compare/v1.38.1...v1.38.2) (2026-06-19)
9
23
 
10
24
 
package/README.md CHANGED
@@ -67,6 +67,14 @@ A QR code will appear in the terminal. Open Telegram on your phone, go to **Sett
67
67
 
68
68
  > **Custom session path:** set `TELEGRAM_SESSION_PATH=/path/to/session` to store the session file elsewhere.
69
69
 
70
+ > **Two-step verification (2FA):** if your account has a cloud password enabled, scanning the QR code is not enough — Telegram also requires the password. Provide it via `TELEGRAM_2FA_PASSWORD` so the login can complete:
71
+ >
72
+ > ```bash
73
+ > TELEGRAM_API_ID=YOUR_ID TELEGRAM_API_HASH=YOUR_HASH TELEGRAM_2FA_PASSWORD=YOUR_PASSWORD npx @overpod/mcp-telegram login
74
+ > ```
75
+ >
76
+ > The password is only used locally to answer Telegram's SRP challenge and is never persisted.
77
+
70
78
  ### 3. Add to Claude
71
79
 
72
80
  ```bash
@@ -3,6 +3,30 @@ import { Api } from "telegram/tl/index.js";
3
3
  import type { AllStoriesSummary, BoostsListSummary, BoostsStatusSummary, BroadcastStatsSummary, BusinessChatLinksSummary, ChannelDifferenceSummary, ChatPermissions, DiscussionMessageSummary, EmojiStatusSummary, GroupCallParticipantsSummary, GroupCallSummary, GroupsForDiscussionSummary, MegagroupStatsSummary, MessageButtonDescriptor, MyBoostsSummary, PeerStoriesSummary, PollSummary, QuickRepliesSummary, QuickReplyMessagesSummary, ReadParticipantsSummary, ReportResultSummary, ResolvedBusinessChatLinkSummary, StarsStatusSummary, StoriesByIdSummary, StoryPrivacy, StoryViewsListSummary, UpdatesDifferenceSummary } from "./telegram-helpers.js";
4
4
  export type { AllStoriesSummary, BoostSummary, BoostsListSummary, BoostsStatusSummary, BroadcastStatsSummary, BusinessChatLinkSummary, BusinessChatLinksSummary, ChannelDifferenceSummary, ChatPermissions, CompactPeer, CompactStatsGraph, DiscussionMessageSummary, EmojiStatusSummary, GroupCallInfoSummary, GroupCallParticipantSummary, GroupCallParticipantsSummary, GroupCallSummary, GroupsForDiscussionSummary, MegagroupStatsSummary, MessageButtonDescriptor, MyBoostSummary, MyBoostsSummary, PeerStoriesSummary, PeerSummary, PollSummary, PrepaidGiveawaySummary, QuickRepliesSummary, QuickReplyMessageSummary, QuickReplyMessagesSummary, QuickReplySummary, ReadParticipantsSummary, ReportResultSummary, ResolvedBusinessChatLinkSummary, StarsAmountSummary, StarsStatusSummary, StarsSubscriptionPricingSummary, StarsSubscriptionSummary, StarsTransactionPeerSummary, StarsTransactionSummary, StatsValue, StoriesByIdSummary, StoryItemSummary, StoryPrivacy, StoryViewSummary, StoryViewsListSummary, UpdatesDifferenceSummary, UpdatesMessageSummary, } from "./telegram-helpers.js";
5
5
  export { buildStoryPrivacyRules, describeAdminLogAction, describeAdminLogDetails, describeKeyboardButton, detectMediaType, extractPeerId, extractPollMediaFromUpdates, extractStoryIdFromUpdates, mergeBannedRights, peerToCompact, reactionToEmoji, summarizeAllStories, summarizeBoost, summarizeBoostsList, summarizeBoostsStatus, summarizeBroadcastStats, summarizeBusinessChatLink, summarizeBusinessChatLinks, summarizeChannelDifference, summarizeDiscussionMessage, summarizeEmojiStatus, summarizeGroupCall, summarizeGroupCallInfo, summarizeGroupCallParticipant, summarizeGroupCallParticipants, summarizeGroupsForDiscussion, summarizeMegagroupStats, summarizeMyBoost, summarizeMyBoosts, summarizePeer, summarizePeerStories, summarizePoll, summarizePrepaidGiveaway, summarizeQuickReplies, summarizeQuickReply, summarizeQuickReplyMessage, summarizeQuickReplyMessages, summarizeReadParticipants, summarizeReportResult, summarizeStarsAmount, summarizeStarsStatus, summarizeStarsSubscription, summarizeStarsTransaction, summarizeStarsTransactionPeer, summarizeStoriesById, summarizeStoryItem, summarizeStoryView, summarizeStoryViewsList, summarizeUpdatesDifference, } from "./telegram-helpers.js";
6
+ /** Minimal client surface the 2FA SRP step needs — lets us unit-test the
7
+ * branch logic with a stub instead of a live TelegramClient. */
8
+ interface SrpClient {
9
+ invoke(request: unknown): Promise<unknown>;
10
+ }
11
+ /** The SRP digest function (GetPassword response + plaintext → InputCheckPasswordSRP).
12
+ * Injectable so tests can exercise the orchestration without GramJS's real crypto. */
13
+ type ComputeCheckFn = (request: Api.account.Password, password: string) => Promise<Api.TypeInputCheckPasswordSRP>;
14
+ /**
15
+ * Complete a QR login that Telegram answered with SESSION_PASSWORD_NEEDED by
16
+ * running the SRP cloud-password check: GetPassword → computeCheck → CheckPassword.
17
+ *
18
+ * Returns a discriminated outcome rather than throwing so the caller owns
19
+ * connection teardown. The password is only used to answer the SRP challenge
20
+ * and is never logged or persisted.
21
+ *
22
+ * `compute` is injectable for tests; production always uses GramJS `computeCheck`.
23
+ */
24
+ export declare function completeTwoFactorLogin(client: SrpClient, password: string | undefined, compute?: ComputeCheckFn): Promise<{
25
+ ok: true;
26
+ } | {
27
+ ok: false;
28
+ message: string;
29
+ }>;
6
30
  export type ChatEntity = Api.User | Api.Chat | Api.Channel | Api.TypeUser | Api.TypeChat;
7
31
  export declare class TelegramService {
8
32
  private client;
@@ -252,6 +276,13 @@ export declare class TelegramService {
252
276
  * Handles display names by searching dialogs.
253
277
  */
254
278
  private resolvePeer;
279
+ /**
280
+ * Resolve a bare numeric ID to a cached/dialog entity so GramJS can build a
281
+ * valid InputPeer. Falls back to the raw ID string if no dialog matches —
282
+ * GramJS may still resolve it (e.g. a contact or a peer it has messaged),
283
+ * and we must not regress that path.
284
+ */
285
+ private resolveNumericPeer;
255
286
  getChatInfo(chatId: string): Promise<{
256
287
  id: string;
257
288
  name: string;
@@ -7,6 +7,7 @@ import bigInt from "big-integer";
7
7
  import QRCode from "qrcode";
8
8
  import { TelegramClient } from "telegram";
9
9
  import { CustomFile } from "telegram/client/uploads.js";
10
+ import { computeCheck } from "telegram/Password.js";
10
11
  import { StringSession } from "telegram/sessions/index.js";
11
12
  import { Api } from "telegram/tl/index.js";
12
13
  import { RateLimiter } from "./rate-limiter.js";
@@ -22,6 +23,46 @@ const NOT_CONNECTED_ERROR = "Not connected. Run telegram-status to check or tele
22
23
  function resolveSessionPath(sessionPath) {
23
24
  return sessionPath ?? process.env.TELEGRAM_SESSION_PATH ?? DEFAULT_SESSION_FILE;
24
25
  }
26
+ // Cloud password (2FA) for accounts that have two-step verification enabled.
27
+ // QR login alone cannot complete such logins — Telegram answers the imported
28
+ // login token with SESSION_PASSWORD_NEEDED, after which an SRP password check
29
+ // is required. Supplied via env so it works across all login entry points
30
+ // (standalone CLI, daemon IPC, and the telegram-login MCP tool), none of which
31
+ // can reliably prompt interactively mid-flow.
32
+ function resolveTwoFactorPassword() {
33
+ return process.env.TELEGRAM_2FA_PASSWORD || undefined;
34
+ }
35
+ /**
36
+ * Complete a QR login that Telegram answered with SESSION_PASSWORD_NEEDED by
37
+ * running the SRP cloud-password check: GetPassword → computeCheck → CheckPassword.
38
+ *
39
+ * Returns a discriminated outcome rather than throwing so the caller owns
40
+ * connection teardown. The password is only used to answer the SRP challenge
41
+ * and is never logged or persisted.
42
+ *
43
+ * `compute` is injectable for tests; production always uses GramJS `computeCheck`.
44
+ */
45
+ export async function completeTwoFactorLogin(client, password, compute = computeCheck) {
46
+ if (!password) {
47
+ return {
48
+ ok: false,
49
+ message: "2FA is enabled on this account. Set TELEGRAM_2FA_PASSWORD to your cloud password and run login again.",
50
+ };
51
+ }
52
+ try {
53
+ const passwordInfo = (await client.invoke(new Api.account.GetPassword()));
54
+ const check = await compute(passwordInfo, password);
55
+ await client.invoke(new Api.auth.CheckPassword({ password: check }));
56
+ return { ok: true };
57
+ }
58
+ catch (pwErr) {
59
+ const reason = pwErr instanceof Error ? pwErr.message : String(pwErr);
60
+ return {
61
+ ok: false,
62
+ message: `2FA password check failed: ${reason}. Verify TELEGRAM_2FA_PASSWORD is correct.`,
63
+ };
64
+ }
65
+ }
25
66
  function resolveProxy() {
26
67
  const ip = process.env.TELEGRAM_PROXY_IP;
27
68
  const port = process.env.TELEGRAM_PROXY_PORT;
@@ -318,8 +359,22 @@ export class TelegramService {
318
359
  catch (err) {
319
360
  const error = err;
320
361
  if (error.errorMessage === "SESSION_PASSWORD_NEEDED") {
321
- await client.disconnect();
322
- return { success: false, message: "2FA enabled QR login not supported with 2FA" };
362
+ // The QR was scanned, but the account has two-step verification.
363
+ // Complete the login with an SRP password check if we have the
364
+ // cloud password; otherwise tell the user how to provide it.
365
+ const outcome = await completeTwoFactorLogin(client, resolveTwoFactorPassword());
366
+ if (outcome.ok) {
367
+ resolved = true;
368
+ break;
369
+ }
370
+ // destroy() (not disconnect()) to free the auth_key/socket, matching
371
+ // every other failure exit in this method — important in daemon mode
372
+ // where the process lives on across logins.
373
+ try {
374
+ await client.destroy();
375
+ }
376
+ catch { }
377
+ return { success: false, message: outcome.message };
323
378
  }
324
379
  }
325
380
  if (!resolved) {
@@ -1136,12 +1191,78 @@ export class TelegramService {
1136
1191
  // Normalize '@me' — GramJS only intercepts the plain 'me' string as InputPeerSelf
1137
1192
  if (chatId === "@me")
1138
1193
  return "me";
1139
- // Numeric IDs and @usernames work directly
1140
- if (/^-?\d+$/.test(chatId) || chatId.startsWith("@"))
1194
+ // @usernames resolve directly via contacts.ResolveUsername
1195
+ if (chatId.startsWith("@"))
1141
1196
  return chatId;
1142
- // Everything else resolve via dialogs
1197
+ // Bare numeric IDs need an entity with access_hash. GramJS can build an
1198
+ // InputPeer from a raw number only if it's already cached or the account
1199
+ // is a contact / has messaged us — otherwise getInputEntity throws
1200
+ // "Could not find the input entity". A bare positive number is also
1201
+ // ambiguous (GramJS assumes PeerUser, so channel IDs fail outright).
1202
+ // Recover by looking the ID up among dialogs, which yields a full entity
1203
+ // (with access_hash) for both users and channels.
1204
+ if (/^-?\d+$/.test(chatId))
1205
+ return this.resolveNumericPeer(chatId);
1206
+ // Everything else — resolve display name via dialogs
1143
1207
  return this.resolveChat(chatId);
1144
1208
  }
1209
+ /**
1210
+ * Resolve a bare numeric ID to a cached/dialog entity so GramJS can build a
1211
+ * valid InputPeer. Falls back to the raw ID string if no dialog matches —
1212
+ * GramJS may still resolve it (e.g. a contact or a peer it has messaged),
1213
+ * and we must not regress that path.
1214
+ */
1215
+ async resolveNumericPeer(chatId) {
1216
+ if (!this.client)
1217
+ throw new Error(NOT_CONNECTED_ERROR);
1218
+ const cached = this.entityCache.get(chatId);
1219
+ if (cached)
1220
+ return cached;
1221
+ // Direct resolve first — succeeds when GramJS already knows the peer.
1222
+ try {
1223
+ const entity = await this.client.getEntity(chatId);
1224
+ this.entityCache.set(chatId, entity);
1225
+ return entity;
1226
+ }
1227
+ catch {
1228
+ // Fall through to dialog scan.
1229
+ }
1230
+ // Scan dialogs for a matching entity. IDs reach us in two shapes:
1231
+ // • bare positive (e.g. 1004294063929 for a channel, 8959122940 for a
1232
+ // user) — this is what list-chats/search emit and what GramJS can't
1233
+ // disambiguate; match it against any entity's bare id.
1234
+ // • marked (-100<id> for channels, -<id> for basic groups) — the
1235
+ // sign/-100 prefix carries the type, so require the entity to match that
1236
+ // exact marked form, otherwise a group "-123" could wrongly match a user
1237
+ // with bare id 123.
1238
+ const isMarked = chatId.startsWith("-");
1239
+ try {
1240
+ const dialogs = await this.client.getDialogs({ limit: 100 });
1241
+ const match = dialogs.find((d) => {
1242
+ const entity = d.entity;
1243
+ if (!entity?.id)
1244
+ return false;
1245
+ const bare = entity.id.toString();
1246
+ if (!isMarked)
1247
+ return chatId === bare;
1248
+ // Marked input must match the entity's marked form.
1249
+ if (entity instanceof Api.Channel)
1250
+ return chatId === `-100${bare}`;
1251
+ if (entity instanceof Api.Chat)
1252
+ return chatId === `-${bare}`;
1253
+ return false;
1254
+ });
1255
+ if (match?.entity) {
1256
+ this.entityCache.set(chatId, match.entity);
1257
+ return match.entity;
1258
+ }
1259
+ }
1260
+ catch {
1261
+ // Dialog fetch failed — fall back to the raw ID below.
1262
+ }
1263
+ // Last resort: hand the raw ID to GramJS and let it try GetUsers/GetChannels.
1264
+ return chatId;
1265
+ }
1145
1266
  async getChatInfo(chatId) {
1146
1267
  if (!this.client || !this.connected)
1147
1268
  throw new Error(NOT_CONNECTED_ERROR);
@@ -50,6 +50,8 @@ export function registerAuthTools(server, telegram) {
50
50
  `If the QR image is not visible, it's also saved to: ${qrFilePath}`,
51
51
  "",
52
52
  "After scanning, run **telegram-status** to verify the connection.",
53
+ "",
54
+ "If the account has two-step verification (2FA), set the `TELEGRAM_2FA_PASSWORD` environment variable to the cloud password and log in again — scanning alone cannot complete a 2FA login.",
53
55
  ].join("\n");
54
56
  return {
55
57
  content: [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@overpod/mcp-telegram",
3
- "version": "1.38.2",
3
+ "version": "1.39.1",
4
4
  "description": "MCP server for Telegram userbot — messages, media, reactions, polls & more. Built on GramJS/MTProto.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -35,7 +35,6 @@
35
35
  "test:watch": "tsx --test --watch 'src/**/*.test.ts'",
36
36
  "test:coverage": "c8 --all --src src --exclude 'src/**/*.test.ts' --reporter=text tsx --test 'src/**/*.test.ts'",
37
37
  "gen:changelog": "tsx scripts/gen-changelog-docs.ts",
38
- "gen:changelog:check": "tsx scripts/gen-changelog-docs.ts --check",
39
38
  "predocs:dev": "npm run gen:changelog",
40
39
  "docs:dev": "vitepress dev docs",
41
40
  "predocs:build": "npm run gen:changelog",
@@ -73,7 +72,7 @@
73
72
  },
74
73
  "devDependencies": {
75
74
  "@biomejs/biome": "^2.5.0",
76
- "@types/node": "^25.9.3",
75
+ "@types/node": "^26.0.0",
77
76
  "@types/qrcode": "^1.5.6",
78
77
  "c8": "^11.0.0",
79
78
  "tsx": "^4.22.4",