@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 +14 -0
- package/README.md +8 -0
- package/dist/telegram-client.d.ts +31 -0
- package/dist/telegram-client.js +126 -5
- package/dist/tools/auth.js +2 -0
- package/package.json +2 -3
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;
|
package/dist/telegram-client.js
CHANGED
|
@@ -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
|
-
|
|
322
|
-
|
|
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
|
-
//
|
|
1140
|
-
if (
|
|
1194
|
+
// @usernames resolve directly via contacts.ResolveUsername
|
|
1195
|
+
if (chatId.startsWith("@"))
|
|
1141
1196
|
return chatId;
|
|
1142
|
-
//
|
|
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);
|
package/dist/tools/auth.js
CHANGED
|
@@ -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.
|
|
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": "^
|
|
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",
|