@overpod/mcp-telegram 1.1.0 → 1.3.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/README.md CHANGED
@@ -83,9 +83,13 @@ claude mcp add telegram -s user \
83
83
  -- npx @overpod/mcp-telegram
84
84
  ```
85
85
 
86
- ### Claude Desktop / Cursor / VS Code
86
+ ### Claude Desktop
87
87
 
88
- Add to your MCP configuration file:
88
+ 1. Open your config file:
89
+ - **macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json`
90
+ - **Windows**: `%APPDATA%\Claude\claude_desktop_config.json`
91
+
92
+ 2. Add the Telegram server:
89
93
 
90
94
  ```json
91
95
  {
@@ -102,6 +106,18 @@ Add to your MCP configuration file:
102
106
  }
103
107
  ```
104
108
 
109
+ 3. Restart Claude Desktop.
110
+
111
+ 4. Ask Claude: **"Run telegram-login"** -- a QR code will appear. If the image is not visible, Claude will provide a browser link to view the QR code. Scan it in Telegram (**Settings > Devices > Link Desktop Device**).
112
+
113
+ 5. Ask Claude: **"Run telegram-status"** to verify the connection.
114
+
115
+ > **Note**: No terminal required! Login works entirely through Claude Desktop.
116
+
117
+ ### Cursor / VS Code
118
+
119
+ Add the same JSON config above to your MCP settings (Cursor Settings > MCP, or VS Code MCP config).
120
+
105
121
  ### Mastra
106
122
 
107
123
  ```typescript
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js CHANGED
File without changes
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import "dotenv/config";
package/dist/index.js CHANGED
@@ -48,8 +48,11 @@ server.tool("telegram-status", "Check Telegram connection status", {}, async ()
48
48
  });
49
49
  server.tool("telegram-login", "Login to Telegram via QR code. Returns QR image. IMPORTANT: pass the entire result to user without modifications.", {}, async () => {
50
50
  let qrDataUrl = "";
51
+ let qrRawUrl = "";
51
52
  const loginPromise = telegram.startQrLogin((dataUrl) => {
52
53
  qrDataUrl = dataUrl;
54
+ }, (url) => {
55
+ qrRawUrl = url;
53
56
  });
54
57
  // Wait for first QR to be generated
55
58
  const startTime = Date.now();
@@ -68,8 +71,20 @@ server.tool("telegram-login", "Login to Telegram via QR code. Returns QR image.
68
71
  console.error(`[mcp-telegram] Login failed: ${result.message}`);
69
72
  }
70
73
  });
71
- // Return as MCP image content + markdown image as fallback
74
+ // Return as MCP image content + text with fallback options
72
75
  const base64 = qrDataUrl.replace(/^data:image\/png;base64,/, "");
76
+ const qrApiUrl = qrRawUrl
77
+ ? `https://api.qrserver.com/v1/create-qr-code/?size=256x256&data=${encodeURIComponent(qrRawUrl)}`
78
+ : "";
79
+ const instructions = [
80
+ "Scan this QR code in Telegram: **Settings → Devices → Link Desktop Device**.",
81
+ "",
82
+ qrApiUrl ? `If the QR image is not visible, open this link in your browser:\n${qrApiUrl}` : "",
83
+ "",
84
+ "After scanning, run **telegram-status** to verify the connection.",
85
+ ]
86
+ .filter(Boolean)
87
+ .join("\n");
73
88
  return {
74
89
  content: [
75
90
  {
@@ -79,7 +94,7 @@ server.tool("telegram-login", "Login to Telegram via QR code. Returns QR image.
79
94
  },
80
95
  {
81
96
  type: "text",
82
- text: `Scan QR in Telegram: Settings → Devices → Link Desktop Device.\n\nIf image not visible: ![QR](${qrDataUrl})\n\nAfter scanning, check with telegram-status.`,
97
+ text: instructions,
83
98
  },
84
99
  ],
85
100
  };
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import "dotenv/config";
@@ -0,0 +1,122 @@
1
+ export declare class TelegramService {
2
+ private client;
3
+ private apiId;
4
+ private apiHash;
5
+ private sessionString;
6
+ private connected;
7
+ lastError: string;
8
+ constructor(apiId: number, apiHash: string);
9
+ loadSession(): Promise<boolean>;
10
+ /** Set session string in memory (for programmatic / hosted use) */
11
+ setSessionString(session: string): void;
12
+ /** Get the current session string (for external persistence) */
13
+ getSessionString(): string;
14
+ private saveSession;
15
+ connect(): Promise<boolean>;
16
+ clearSession(): Promise<void>;
17
+ /** Ensure connection is active, auto-reconnect if session exists */
18
+ ensureConnected(): Promise<boolean>;
19
+ disconnect(): Promise<void>;
20
+ /**
21
+ * Log out from Telegram completely — terminates the session on Telegram servers.
22
+ * After this, the session string becomes invalid and won't appear in "Active Sessions".
23
+ */
24
+ logOut(): Promise<boolean>;
25
+ isConnected(): boolean;
26
+ startQrLogin(onQrDataUrl: (dataUrl: string) => void, onQrUrl?: (url: string) => void): Promise<{
27
+ success: boolean;
28
+ message: string;
29
+ }>;
30
+ getMe(): Promise<{
31
+ id: string;
32
+ username?: string;
33
+ firstName?: string;
34
+ }>;
35
+ sendMessage(chatId: string, text: string, replyTo?: number, parseMode?: "md" | "html"): Promise<void>;
36
+ sendFile(chatId: string, filePath: string, caption?: string): Promise<void>;
37
+ downloadMedia(chatId: string, messageId: number, downloadPath: string): Promise<string>;
38
+ downloadMediaAsBuffer(chatId: string, messageId: number): Promise<{
39
+ buffer: Buffer;
40
+ mimeType: string;
41
+ }>;
42
+ /** Detect MIME type from buffer magic bytes, falling back to media metadata */
43
+ private detectMimeType;
44
+ pinMessage(chatId: string, messageId: number, silent?: boolean): Promise<void>;
45
+ unpinMessage(chatId: string, messageId: number): Promise<void>;
46
+ getDialogs(limit?: number, offsetDate?: number, filterType?: "private" | "group" | "channel"): Promise<Array<{
47
+ id: string;
48
+ name: string;
49
+ type: string;
50
+ unreadCount: number;
51
+ }>>;
52
+ getUnreadDialogs(limit?: number): Promise<Array<{
53
+ id: string;
54
+ name: string;
55
+ type: string;
56
+ unreadCount: number;
57
+ }>>;
58
+ markAsRead(chatId: string): Promise<void>;
59
+ forwardMessage(fromChatId: string, toChatId: string, messageIds: number[]): Promise<void>;
60
+ editMessage(chatId: string, messageId: number, newText: string): Promise<void>;
61
+ deleteMessages(chatId: string, messageIds: number[]): Promise<void>;
62
+ getChatInfo(chatId: string): Promise<{
63
+ id: string;
64
+ name: string;
65
+ type: string;
66
+ username?: string;
67
+ description?: string;
68
+ membersCount?: number;
69
+ }>;
70
+ /** Extract media info from a message */
71
+ private extractMediaInfo;
72
+ /** Resolve sender ID to a display name */
73
+ private resolveSenderName;
74
+ getMessages(chatId: string, limit?: number, offsetId?: number, minDate?: number, maxDate?: number): Promise<Array<{
75
+ id: number;
76
+ text: string;
77
+ sender: string;
78
+ date: string;
79
+ media?: {
80
+ type: string;
81
+ fileName?: string;
82
+ size?: number;
83
+ };
84
+ }>>;
85
+ searchChats(query: string, limit?: number): Promise<Array<{
86
+ id: string;
87
+ name: string;
88
+ type: string;
89
+ username?: string;
90
+ }>>;
91
+ searchMessages(chatId: string, query: string, limit?: number, minDate?: number, maxDate?: number): Promise<Array<{
92
+ id: number;
93
+ text: string;
94
+ sender: string;
95
+ date: string;
96
+ media?: {
97
+ type: string;
98
+ fileName?: string;
99
+ size?: number;
100
+ };
101
+ }>>;
102
+ getContacts(limit?: number): Promise<Array<{
103
+ id: string;
104
+ name: string;
105
+ username?: string;
106
+ phone?: string;
107
+ }>>;
108
+ getChatMembers(chatId: string, limit?: number): Promise<Array<{
109
+ id: string;
110
+ name: string;
111
+ username?: string;
112
+ }>>;
113
+ getProfile(userId: string): Promise<{
114
+ id: string;
115
+ name: string;
116
+ username?: string;
117
+ phone?: string;
118
+ bio?: string;
119
+ photo: boolean;
120
+ lastSeen?: string;
121
+ }>;
122
+ }
@@ -27,9 +27,22 @@ export class TelegramService {
27
27
  }
28
28
  return false;
29
29
  }
30
+ /** Set session string in memory (for programmatic / hosted use) */
31
+ setSessionString(session) {
32
+ this.sessionString = session;
33
+ }
34
+ /** Get the current session string (for external persistence) */
35
+ getSessionString() {
36
+ return this.sessionString;
37
+ }
30
38
  async saveSession(session) {
31
39
  this.sessionString = session;
32
- await writeFile(SESSION_FILE, session, "utf-8");
40
+ try {
41
+ await writeFile(SESSION_FILE, session, "utf-8");
42
+ }
43
+ catch {
44
+ // File write may fail in containerized environments — session string is still in memory
45
+ }
33
46
  }
34
47
  async connect() {
35
48
  if (this.connected && this.client)
@@ -96,9 +109,29 @@ export class TelegramService {
96
109
  }
97
110
  async disconnect() {
98
111
  if (this.client && this.connected) {
99
- await this.client.disconnect();
112
+ await this.client.destroy();
113
+ this.connected = false;
114
+ this.client = null;
115
+ }
116
+ }
117
+ /**
118
+ * Log out from Telegram completely — terminates the session on Telegram servers.
119
+ * After this, the session string becomes invalid and won't appear in "Active Sessions".
120
+ */
121
+ async logOut() {
122
+ if (!this.client || !this.connected)
123
+ return false;
124
+ try {
125
+ await this.client.invoke(new Api.auth.LogOut());
100
126
  this.connected = false;
127
+ this.sessionString = "";
101
128
  this.client = null;
129
+ return true;
130
+ }
131
+ catch (error) {
132
+ console.error("[telegram] logOut error:", error);
133
+ await this.disconnect();
134
+ return false;
102
135
  }
103
136
  }
104
137
  isConnected() {
@@ -224,6 +257,41 @@ export class TelegramService {
224
257
  await writeFile(downloadPath, buffer);
225
258
  return downloadPath;
226
259
  }
260
+ async downloadMediaAsBuffer(chatId, messageId) {
261
+ if (!this.client || !this.connected)
262
+ throw new Error("Not connected");
263
+ const messages = await this.client.getMessages(chatId, { ids: [messageId] });
264
+ const message = messages[0];
265
+ if (!message)
266
+ throw new Error(`Message ${messageId} not found`);
267
+ if (!message.media)
268
+ throw new Error(`Message ${messageId} has no media`);
269
+ const buffer = await this.client.downloadMedia(message);
270
+ if (!buffer)
271
+ throw new Error("Failed to download media");
272
+ const mimeType = this.detectMimeType(buffer, message.media);
273
+ return { buffer, mimeType };
274
+ }
275
+ /** Detect MIME type from buffer magic bytes, falling back to media metadata */
276
+ detectMimeType(buffer, media) {
277
+ // Check magic bytes first
278
+ if (buffer[0] === 0xFF && buffer[1] === 0xD8 && buffer[2] === 0xFF)
279
+ return "image/jpeg";
280
+ if (buffer[0] === 0x89 && buffer[1] === 0x50 && buffer[2] === 0x4E && buffer[3] === 0x47)
281
+ return "image/png";
282
+ if (buffer[0] === 0x47 && buffer[1] === 0x49 && buffer[2] === 0x46)
283
+ return "image/gif";
284
+ if (buffer[0] === 0x52 && buffer[1] === 0x49 && buffer[2] === 0x46 && buffer[3] === 0x46)
285
+ return "image/webp";
286
+ // Fall back to document mimeType
287
+ const m = media;
288
+ const doc = m.document;
289
+ if (doc?.mimeType)
290
+ return doc.mimeType;
291
+ if (m.photo)
292
+ return "image/jpeg";
293
+ return "application/octet-stream";
294
+ }
227
295
  async pinMessage(chatId, messageId, silent = false) {
228
296
  if (!this.client || !this.connected)
229
297
  throw new Error("Not connected");
@@ -295,20 +363,48 @@ export class TelegramService {
295
363
  };
296
364
  }
297
365
  if (entity instanceof Api.Channel) {
366
+ let membersCount = entity.participantsCount ?? undefined;
367
+ let description;
368
+ try {
369
+ const full = await this.client.invoke(new Api.channels.GetFullChannel({ channel: entity }));
370
+ if (full.fullChat instanceof Api.ChannelFull) {
371
+ membersCount = membersCount ?? full.fullChat.participantsCount ?? undefined;
372
+ description = full.fullChat.about || undefined;
373
+ }
374
+ }
375
+ catch {
376
+ // May fail for some channels — fall back to basic info
377
+ }
298
378
  return {
299
379
  id: entity.id.toString(),
300
380
  name: entity.title,
301
381
  type: entity.megagroup ? "group" : "channel",
302
382
  username: entity.username ?? undefined,
303
- membersCount: entity.participantsCount ?? undefined,
383
+ description,
384
+ membersCount,
304
385
  };
305
386
  }
306
387
  if (entity instanceof Api.Chat) {
388
+ let membersCount = entity.participantsCount ?? undefined;
389
+ let description;
390
+ try {
391
+ const full = await this.client.invoke(new Api.messages.GetFullChat({ chatId: entity.id }));
392
+ if (full.fullChat instanceof Api.ChatFull) {
393
+ if (!membersCount && full.fullChat.participants instanceof Api.ChatParticipants) {
394
+ membersCount = full.fullChat.participants.participants.length;
395
+ }
396
+ description = full.fullChat.about || undefined;
397
+ }
398
+ }
399
+ catch {
400
+ // Fall back to basic info
401
+ }
307
402
  return {
308
403
  id: entity.id.toString(),
309
404
  name: entity.title,
310
405
  type: "group",
311
- membersCount: entity.participantsCount ?? undefined,
406
+ description,
407
+ membersCount,
312
408
  };
313
409
  }
314
410
  return { id: chatId, name: "Unknown", type: "unknown" };
package/package.json CHANGED
@@ -1,9 +1,13 @@
1
1
  {
2
2
  "name": "@overpod/mcp-telegram",
3
- "version": "1.1.0",
3
+ "version": "1.3.1",
4
4
  "description": "MCP server for Telegram userbot — 20 tools for messages, media, contacts & more. Built on GramJS/MTProto.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
+ "exports": {
8
+ ".": "./dist/index.js",
9
+ "./service": "./dist/telegram-client.js"
10
+ },
7
11
  "bin": {
8
12
  "mcp-telegram": "dist/cli.js"
9
13
  },