@overpod/mcp-telegram 1.6.0 → 1.8.0

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
@@ -21,6 +21,7 @@ An MCP (Model Context Protocol) server that connects AI assistants like Claude t
21
21
  - **MTProto protocol** -- direct Telegram API access, not the limited Bot API
22
22
  - **Userbot** -- operates as your personal account, not a bot
23
23
  - **Full-featured** -- messaging, reactions, polls, scheduled messages, media, contacts, and more
24
+ - **Forum Topics** -- list topics, read per-topic messages, send to specific topics, per-topic unread counts
24
25
  - **QR code login** -- authenticate by scanning a QR code in the Telegram app
25
26
  - **Session persistence** -- login once, stay connected across restarts
26
27
  - **Human-readable output** -- sender names are resolved, not just numeric IDs
@@ -46,7 +47,9 @@ An MCP (Model Context Protocol) server that connects AI assistants like Claude t
46
47
  TELEGRAM_API_ID=YOUR_ID TELEGRAM_API_HASH=YOUR_HASH npx @overpod/mcp-telegram login
47
48
  ```
48
49
 
49
- A QR code will appear in the terminal. Open Telegram on your phone, go to **Settings > Devices > Link Desktop Device**, and scan the code. The session is saved to `~/.telegram-session` and reused automatically.
50
+ A QR code will appear in the terminal. Open Telegram on your phone, go to **Settings > Devices > Link Desktop Device**, and scan the code. The session is saved to `~/.mcp-telegram/session` and reused automatically.
51
+
52
+ > **Custom session path:** set `TELEGRAM_SESSION_PATH=/path/to/session` to store the session file elsewhere.
50
53
 
51
54
  ### 3. Add to Claude
52
55
 
@@ -59,6 +62,34 @@ claude mcp add telegram -s user \
59
62
 
60
63
  That's it! Ask Claude to run `telegram-status` to verify.
61
64
 
65
+ ### Multiple Accounts
66
+
67
+ Use `TELEGRAM_SESSION_PATH` to run separate Telegram accounts side by side:
68
+
69
+ ```bash
70
+ # Login each account with a unique session path
71
+ TELEGRAM_API_ID=ID1 TELEGRAM_API_HASH=HASH1 TELEGRAM_SESSION_PATH=~/.mcp-telegram/session-work npx @overpod/mcp-telegram login
72
+ TELEGRAM_API_ID=ID2 TELEGRAM_API_HASH=HASH2 TELEGRAM_SESSION_PATH=~/.mcp-telegram/session-personal npx @overpod/mcp-telegram login
73
+ ```
74
+
75
+ Then add each as a separate MCP server:
76
+
77
+ ```bash
78
+ claude mcp add telegram-work -s user \
79
+ -e TELEGRAM_API_ID=ID1 \
80
+ -e TELEGRAM_API_HASH=HASH1 \
81
+ -e TELEGRAM_SESSION_PATH=~/.mcp-telegram/session-work \
82
+ -- npx @overpod/mcp-telegram
83
+
84
+ claude mcp add telegram-personal -s user \
85
+ -e TELEGRAM_API_ID=ID2 \
86
+ -e TELEGRAM_API_HASH=HASH2 \
87
+ -e TELEGRAM_SESSION_PATH=~/.mcp-telegram/session-personal \
88
+ -- npx @overpod/mcp-telegram
89
+ ```
90
+
91
+ Each account gets its own session file — no conflicts.
92
+
62
93
  ## Installation Options
63
94
 
64
95
  ### npx (recommended, zero install)
@@ -173,23 +204,39 @@ const telegramMcp = new MCPClient({
173
204
 
174
205
  | Tool | Description |
175
206
  |------|-------------|
176
- | `telegram-list-chats` | List recent dialogs with unread counts |
207
+ | `telegram-list-chats` | List recent dialogs with unread counts, bot/contact markers |
177
208
  | `telegram-read-messages` | Read recent messages from a chat |
178
209
  | `telegram-search-chats` | Search for chats, users, or channels by name |
179
210
  | `telegram-search-messages` | Search messages in a chat by text |
180
- | `telegram-get-unread` | Get chats with unread messages |
211
+ | `telegram-get-unread` | Get chats with unread messages; forums show per-topic unread breakdown |
212
+ | `telegram-get-contact-requests` | Get incoming messages from non-contacts with preview |
213
+
214
+ ### Forum Topics
215
+
216
+ | Tool | Description |
217
+ |------|-------------|
218
+ | `telegram-list-topics` | List forum topics in a group with unread counts and status |
219
+ | `telegram-read-topic-messages` | Read messages from a specific forum topic |
181
220
 
182
221
  ### Chat Management
183
222
 
184
223
  | Tool | Description |
185
224
  |------|-------------|
186
225
  | `telegram-mark-as-read` | Mark a chat as read |
187
- | `telegram-get-chat-info` | Get detailed info about a chat (name, type, members count, description) |
226
+ | `telegram-get-chat-info` | Get detailed info about a chat (name, type, members, bot/contact status) |
188
227
  | `telegram-get-chat-members` | List members of a group or channel |
189
228
  | `telegram-join-chat` | Join a group or channel by username or invite link |
190
229
  | `telegram-pin-message` | Pin a message in a chat |
191
230
  | `telegram-unpin-message` | Unpin a message in a chat |
192
231
 
232
+ ### Contacts & Moderation
233
+
234
+ | Tool | Description |
235
+ |------|-------------|
236
+ | `telegram-add-contact` | Add a user to your contacts (accept contact request) |
237
+ | `telegram-block-user` | Block a user from sending you messages |
238
+ | `telegram-report-spam` | Report a chat as spam to Telegram |
239
+
193
240
  ### User Info
194
241
 
195
242
  | Tool | Description |
@@ -217,6 +264,23 @@ Most tools accept `chatId` as a string -- either a numeric ID (e.g., `"-10012345
217
264
  | `text` | string | yes | Message text |
218
265
  | `replyTo` | number | no | Message ID to reply to |
219
266
  | `parseMode` | `"md"` / `"html"` | no | Message formatting mode |
267
+ | `topicId` | number | no | Forum topic ID to send into (for groups with Topics) |
268
+
269
+ ### telegram-list-topics
270
+
271
+ | Parameter | Type | Required | Description |
272
+ |-----------|------|----------|-------------|
273
+ | `chatId` | string | yes | Chat ID or @username (group with Topics enabled) |
274
+ | `limit` | number | no | Max topics to return (default: 100) |
275
+
276
+ ### telegram-read-topic-messages
277
+
278
+ | Parameter | Type | Required | Description |
279
+ |-----------|------|----------|-------------|
280
+ | `chatId` | string | yes | Chat ID or @username |
281
+ | `topicId` | number | yes | Topic ID (from `telegram-list-topics`) |
282
+ | `limit` | number | no | Number of messages (default: 20) |
283
+ | `offsetId` | number | no | Message ID for pagination |
220
284
 
221
285
  ### telegram-list-chats
222
286
 
@@ -224,7 +288,7 @@ Most tools accept `chatId` as a string -- either a numeric ID (e.g., `"-10012345
224
288
  |-----------|------|----------|-------------|
225
289
  | `limit` | number | no | Number of chats to return (default: 20) |
226
290
  | `offsetDate` | number | no | Unix timestamp for pagination |
227
- | `filterType` | `"private"` / `"group"` / `"channel"` | no | Filter by chat type |
291
+ | `filterType` | `"private"` / `"group"` / `"channel"` / `"contact_requests"` | no | Filter by chat type |
228
292
 
229
293
  ### telegram-read-messages
230
294
 
@@ -356,6 +420,33 @@ Most tools accept `chatId` as a string -- either a numeric ID (e.g., `"-10012345
356
420
  |-----------|------|----------|-------------|
357
421
  | `limit` | number | no | Number of unread chats (default: 20) |
358
422
 
423
+ ### telegram-get-contact-requests
424
+
425
+ | Parameter | Type | Required | Description |
426
+ |-----------|------|----------|-------------|
427
+ | `limit` | number | no | Number of contact requests (default: 20) |
428
+
429
+ ### telegram-add-contact
430
+
431
+ | Parameter | Type | Required | Description |
432
+ |-----------|------|----------|-------------|
433
+ | `userId` | string | yes | User ID or @username to add |
434
+ | `firstName` | string | yes | First name for the contact |
435
+ | `lastName` | string | no | Last name for the contact |
436
+ | `phone` | string | no | Phone number for the contact |
437
+
438
+ ### telegram-block-user
439
+
440
+ | Parameter | Type | Required | Description |
441
+ |-----------|------|----------|-------------|
442
+ | `userId` | string | yes | User ID or @username to block |
443
+
444
+ ### telegram-report-spam
445
+
446
+ | Parameter | Type | Required | Description |
447
+ |-----------|------|----------|-------------|
448
+ | `chatId` | string | yes | Chat ID or @username to report |
449
+
359
450
  ## Development
360
451
 
361
452
  ```bash
@@ -390,7 +481,8 @@ src/
390
481
  ## Security
391
482
 
392
483
  - API credentials are stored in `.env` (gitignored)
393
- - Session is stored in `.telegram-session` (gitignored)
484
+ - Session is stored in `~/.mcp-telegram/session` with `0600` permissions (owner-only access)
485
+ - Session directory is created with `0700` permissions
394
486
  - Phone number is **not required** -- QR-only authentication
395
487
  - This is a **userbot** (personal account), not a bot -- respect the [Telegram Terms of Service](https://core.telegram.org/api/terms)
396
488
 
package/dist/cli.js CHANGED
File without changes
package/dist/index.js CHANGED
@@ -104,13 +104,15 @@ server.tool("telegram-send-message", "Send a message to a Telegram chat", {
104
104
  text: z.string().describe("Message text"),
105
105
  replyTo: z.number().optional().describe("Message ID to reply to"),
106
106
  parseMode: z.enum(["md", "html"]).optional().describe("Message format: md (Markdown) or html"),
107
- }, async ({ chatId, text, replyTo, parseMode }) => {
107
+ topicId: z.number().optional().describe("Forum topic ID to send message into (for groups with Topics enabled)"),
108
+ }, async ({ chatId, text, replyTo, parseMode, topicId }) => {
108
109
  const err = await requireConnection();
109
110
  if (err)
110
111
  return { content: [{ type: "text", text: err }] };
111
112
  try {
112
- await telegram.sendMessage(chatId, text, replyTo, parseMode);
113
- return { content: [{ type: "text", text: `Message sent to ${chatId}` }] };
113
+ await telegram.sendMessage(chatId, text, replyTo, parseMode, topicId);
114
+ const dest = topicId ? `topic ${topicId} in ${chatId}` : chatId;
115
+ return { content: [{ type: "text", text: `Message sent to ${dest}` }] };
114
116
  }
115
117
  catch (e) {
116
118
  return { content: [{ type: "text", text: `Send error: ${e.message}` }] };
@@ -217,7 +219,13 @@ server.tool("telegram-get-unread", "Get unread Telegram chats", {
217
219
  const prefix = d.type === "group" ? "G" : d.type === "channel" ? "C" : "P";
218
220
  const botTag = d.isBot ? " [bot]" : "";
219
221
  const contactTag = d.type === "private" && d.isContact === false ? " [not in contacts]" : "";
220
- return `${prefix} ${d.name} (${d.id})${botTag}${contactTag} [${d.unreadCount} unread]`;
222
+ const forumTag = d.forum ? " [forum]" : "";
223
+ let line = `${prefix} ${d.name} (${d.id})${botTag}${contactTag}${forumTag} [${d.unreadCount} unread]`;
224
+ if (d.topics && d.topics.length > 0) {
225
+ const topicLines = d.topics.map((t) => ` # ${t.title} [${t.unreadCount} unread]`);
226
+ line += `\n${topicLines.join("\n")}`;
227
+ }
228
+ return line;
221
229
  })
222
230
  .join("\n");
223
231
  return { content: [{ type: "text", text: text || "No unread chats" }] };
@@ -303,6 +311,7 @@ server.tool("telegram-get-chat-info", "Get detailed info about a Telegram chat",
303
311
  `Name: ${info.name}`,
304
312
  `ID: ${info.id}`,
305
313
  `Type: ${info.type}`,
314
+ ...(info.forum ? ["Forum: yes"] : []),
306
315
  ...(info.username ? [`Username: @${info.username}`] : []),
307
316
  ...(info.description ? [`Description: ${info.description}`] : []),
308
317
  ...(info.membersCount != null ? [`Members: ${info.membersCount}`] : []),
@@ -594,6 +603,49 @@ server.tool("telegram-report-spam", "Report a chat as spam to Telegram", {
594
603
  return { content: [{ type: "text", text: `Error: ${e.message}` }] };
595
604
  }
596
605
  });
606
+ server.tool("telegram-list-topics", "List forum topics in a Telegram group with Topics enabled. Shows topic names, unread counts, and status", {
607
+ chatId: z.string().describe("Chat ID or username of a group with Topics enabled"),
608
+ limit: z.number().default(100).describe("Max topics to return"),
609
+ }, async ({ chatId, limit }) => {
610
+ const err = await requireConnection();
611
+ if (err)
612
+ return { content: [{ type: "text", text: err }] };
613
+ try {
614
+ const topics = await telegram.getForumTopics(chatId, limit);
615
+ const text = topics
616
+ .map((t) => {
617
+ const flags = [t.pinned ? "pinned" : "", t.closed ? "closed" : ""].filter(Boolean).join(", ");
618
+ const flagStr = flags ? ` [${flags}]` : "";
619
+ const unread = t.unreadCount > 0 ? ` [${t.unreadCount} unread]` : "";
620
+ return `# ${t.title} (id: ${t.id})${flagStr}${unread}`;
621
+ })
622
+ .join("\n");
623
+ return { content: [{ type: "text", text: text || "No topics found" }] };
624
+ }
625
+ catch (e) {
626
+ return { content: [{ type: "text", text: `Error: ${e.message}` }] };
627
+ }
628
+ });
629
+ server.tool("telegram-read-topic-messages", "Read messages from a specific forum topic in a Telegram group", {
630
+ chatId: z.string().describe("Chat ID or username"),
631
+ topicId: z.number().describe("Topic ID (get from telegram-list-topics)"),
632
+ limit: z.number().default(20).describe("Number of messages to return"),
633
+ offsetId: z.number().optional().describe("Message ID to start from (for pagination)"),
634
+ }, async ({ chatId, topicId, limit, offsetId }) => {
635
+ const err = await requireConnection();
636
+ if (err)
637
+ return { content: [{ type: "text", text: err }] };
638
+ try {
639
+ const messages = await telegram.getTopicMessages(chatId, topicId, limit, offsetId);
640
+ const text = messages
641
+ .map((m) => `[${m.date}] ${m.sender}: ${m.text}${m.media ? ` [${m.media.type}${m.media.fileName ? `: ${m.media.fileName}` : ""}]` : ""}`)
642
+ .join("\n\n");
643
+ return { content: [{ type: "text", text: text || "No messages in this topic" }] };
644
+ }
645
+ catch (e) {
646
+ return { content: [{ type: "text", text: `Error: ${e.message}` }] };
647
+ }
648
+ });
597
649
  // --- Start ---
598
650
  async function main() {
599
651
  // Try to auto-connect with saved session
@@ -4,9 +4,13 @@ export declare class TelegramService {
4
4
  private apiHash;
5
5
  private sessionString;
6
6
  private connected;
7
+ private sessionPath;
7
8
  lastError: string;
8
- constructor(apiId: number, apiHash: string);
9
+ constructor(apiId: number, apiHash: string, options?: {
10
+ sessionPath?: string;
11
+ });
9
12
  loadSession(): Promise<boolean>;
13
+ private isValidSessionString;
10
14
  /** Set session string in memory (for programmatic / hosted use) */
11
15
  setSessionString(session: string): void;
12
16
  /** Get the current session string (for external persistence) */
@@ -32,7 +36,7 @@ export declare class TelegramService {
32
36
  username?: string;
33
37
  firstName?: string;
34
38
  }>;
35
- sendMessage(chatId: string, text: string, replyTo?: number, parseMode?: "md" | "html"): Promise<void>;
39
+ sendMessage(chatId: string, text: string, replyTo?: number, parseMode?: "md" | "html", topicId?: number): Promise<void>;
36
40
  sendFile(chatId: string, filePath: string, caption?: string): Promise<void>;
37
41
  downloadMedia(chatId: string, messageId: number, downloadPath: string): Promise<string>;
38
42
  downloadMediaAsBuffer(chatId: string, messageId: number): Promise<{
@@ -58,6 +62,12 @@ export declare class TelegramService {
58
62
  unreadCount: number;
59
63
  isBot?: boolean;
60
64
  isContact?: boolean;
65
+ forum?: boolean;
66
+ topics?: Array<{
67
+ id: number;
68
+ title: string;
69
+ unreadCount: number;
70
+ }>;
61
71
  }>>;
62
72
  getContactRequests(limit?: number): Promise<Array<{
63
73
  id: string;
@@ -84,6 +94,7 @@ export declare class TelegramService {
84
94
  membersCount?: number;
85
95
  isBot?: boolean;
86
96
  isContact?: boolean;
97
+ forum?: boolean;
87
98
  }>;
88
99
  /** Extract media info from a message */
89
100
  private extractMediaInfo;
@@ -144,6 +155,28 @@ export declare class TelegramService {
144
155
  quiz?: boolean;
145
156
  correctAnswer?: number;
146
157
  }): Promise<number>;
158
+ getForumTopics(chatId: string, limit?: number): Promise<Array<{
159
+ id: number;
160
+ title: string;
161
+ unreadCount: number;
162
+ unreadMentions: number;
163
+ iconColor: number;
164
+ closed: boolean;
165
+ pinned: boolean;
166
+ }>>;
167
+ getTopicMessages(chatId: string, topicId: number, limit?: number, offsetId?: number): Promise<Array<{
168
+ id: number;
169
+ text: string;
170
+ sender: string;
171
+ date: string;
172
+ media?: {
173
+ type: string;
174
+ fileName?: string;
175
+ size?: number;
176
+ };
177
+ }>>;
178
+ /** Check if a chat entity is a forum (has topics enabled) */
179
+ isForum(chatId: string): Promise<boolean>;
147
180
  joinChat(target: string): Promise<{
148
181
  id: string;
149
182
  title: string;
@@ -1,5 +1,6 @@
1
- import { existsSync } from "node:fs";
2
- import { readFile, unlink, writeFile } from "node:fs/promises";
1
+ import { existsSync, mkdirSync } from "node:fs";
2
+ import { chmod, readFile, unlink, writeFile } from "node:fs/promises";
3
+ import { homedir } from "node:os";
3
4
  import { dirname, join } from "node:path";
4
5
  import { fileURLToPath } from "node:url";
5
6
  import bigInt from "big-integer";
@@ -8,25 +9,66 @@ import { TelegramClient } from "telegram";
8
9
  import { StringSession } from "telegram/sessions/index.js";
9
10
  import { Api } from "telegram/tl/index.js";
10
11
  const __dirname = dirname(fileURLToPath(import.meta.url));
11
- const SESSION_FILE = join(__dirname, "..", ".telegram-session");
12
+ const LEGACY_SESSION_FILE = join(__dirname, "..", ".telegram-session");
13
+ const DEFAULT_SESSION_DIR = join(homedir(), ".mcp-telegram");
14
+ const DEFAULT_SESSION_FILE = join(DEFAULT_SESSION_DIR, "session");
15
+ const SESSION_STRING_RE = /^[A-Za-z0-9+/=]+$/;
16
+ const MIN_SESSION_LENGTH = 100;
17
+ function resolveSessionPath(sessionPath) {
18
+ return sessionPath ?? process.env.TELEGRAM_SESSION_PATH ?? DEFAULT_SESSION_FILE;
19
+ }
20
+ function ensureSessionDir(filePath) {
21
+ const dir = dirname(filePath);
22
+ if (!existsSync(dir)) {
23
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
24
+ }
25
+ }
12
26
  export class TelegramService {
13
27
  client = null;
14
28
  apiId;
15
29
  apiHash;
16
30
  sessionString = "";
17
31
  connected = false;
32
+ sessionPath;
18
33
  lastError = "";
19
- constructor(apiId, apiHash) {
34
+ constructor(apiId, apiHash, options) {
20
35
  this.apiId = apiId;
21
36
  this.apiHash = apiHash;
37
+ this.sessionPath = resolveSessionPath(options?.sessionPath);
22
38
  }
23
39
  async loadSession() {
24
- if (existsSync(SESSION_FILE)) {
25
- this.sessionString = (await readFile(SESSION_FILE, "utf-8")).trim();
26
- return true;
40
+ // Try current session path
41
+ if (existsSync(this.sessionPath)) {
42
+ const raw = (await readFile(this.sessionPath, "utf-8")).trim();
43
+ if (this.isValidSessionString(raw)) {
44
+ this.sessionString = raw;
45
+ // Fix permissions on existing files
46
+ try {
47
+ await chmod(this.sessionPath, 0o600);
48
+ }
49
+ catch { }
50
+ return true;
51
+ }
52
+ }
53
+ // Migrate from legacy path (inside node_modules / package root)
54
+ if (this.sessionPath === DEFAULT_SESSION_FILE && existsSync(LEGACY_SESSION_FILE)) {
55
+ const raw = (await readFile(LEGACY_SESSION_FILE, "utf-8")).trim();
56
+ if (this.isValidSessionString(raw)) {
57
+ this.sessionString = raw;
58
+ ensureSessionDir(this.sessionPath);
59
+ await writeFile(this.sessionPath, raw, { encoding: "utf-8", mode: 0o600 });
60
+ try {
61
+ await unlink(LEGACY_SESSION_FILE);
62
+ }
63
+ catch { }
64
+ return true;
65
+ }
27
66
  }
28
67
  return false;
29
68
  }
69
+ isValidSessionString(value) {
70
+ return value.length >= MIN_SESSION_LENGTH && SESSION_STRING_RE.test(value);
71
+ }
30
72
  /** Set session string in memory (for programmatic / hosted use) */
31
73
  setSessionString(session) {
32
74
  this.sessionString = session;
@@ -38,7 +80,8 @@ export class TelegramService {
38
80
  async saveSession(session) {
39
81
  this.sessionString = session;
40
82
  try {
41
- await writeFile(SESSION_FILE, session, "utf-8");
83
+ ensureSessionDir(this.sessionPath);
84
+ await writeFile(this.sessionPath, session, { encoding: "utf-8", mode: 0o600 });
42
85
  }
43
86
  catch {
44
87
  // File write may fail in containerized environments — session string is still in memory
@@ -95,8 +138,8 @@ export class TelegramService {
95
138
  this.connected = false;
96
139
  this.sessionString = "";
97
140
  this.client = null;
98
- if (existsSync(SESSION_FILE)) {
99
- await unlink(SESSION_FILE);
141
+ if (existsSync(this.sessionPath)) {
142
+ await unlink(this.sessionPath);
100
143
  }
101
144
  }
102
145
  /** Ensure connection is active, auto-reconnect if session exists */
@@ -230,14 +273,29 @@ export class TelegramService {
230
273
  firstName: user.firstName ?? undefined,
231
274
  };
232
275
  }
233
- async sendMessage(chatId, text, replyTo, parseMode) {
276
+ async sendMessage(chatId, text, replyTo, parseMode, topicId) {
234
277
  if (!this.client || !this.connected)
235
278
  throw new Error("Not connected");
236
- await this.client.sendMessage(chatId, {
237
- message: text,
238
- ...(replyTo ? { replyTo } : {}),
239
- ...(parseMode ? { parseMode: parseMode === "html" ? "html" : "md" } : {}),
240
- });
279
+ if (topicId) {
280
+ // Forum topics require raw API call with InputReplyToMessage
281
+ const peer = await this.client.getInputEntity(chatId);
282
+ await this.client.invoke(new Api.messages.SendMessage({
283
+ peer,
284
+ message: text,
285
+ randomId: bigInt(Math.floor(Math.random() * 1e15)),
286
+ replyTo: new Api.InputReplyToMessage({
287
+ replyToMsgId: replyTo ?? topicId,
288
+ topMsgId: topicId,
289
+ }),
290
+ }));
291
+ }
292
+ else {
293
+ await this.client.sendMessage(chatId, {
294
+ message: text,
295
+ ...(replyTo ? { replyTo } : {}),
296
+ ...(parseMode ? { parseMode: parseMode === "html" ? "html" : "md" } : {}),
297
+ });
298
+ }
241
299
  }
242
300
  async sendFile(chatId, filePath, caption) {
243
301
  if (!this.client || !this.connected)
@@ -331,12 +389,11 @@ export class TelegramService {
331
389
  if (!this.client || !this.connected)
332
390
  throw new Error("Not connected");
333
391
  const dialogs = await this.client.getDialogs({ limit: limit * 3 });
334
- return dialogs
335
- .filter((d) => d.unreadCount > 0)
336
- .slice(0, limit)
337
- .map((d) => {
392
+ const unread = dialogs.filter((d) => d.unreadCount > 0).slice(0, limit);
393
+ const results = await Promise.all(unread.map(async (d) => {
338
394
  const isUser = d.entity instanceof Api.User;
339
- return {
395
+ const isForum = d.entity instanceof Api.Channel && Boolean(d.entity.forum);
396
+ const base = {
340
397
  id: d.id?.toString() ?? "",
341
398
  name: d.title ?? d.name ?? "Unknown",
342
399
  type: d.isGroup ? "group" : d.isChannel ? "channel" : "private",
@@ -345,7 +402,29 @@ export class TelegramService {
345
402
  ? { isBot: Boolean(d.entity.bot), isContact: Boolean(d.entity.contact) }
346
403
  : {}),
347
404
  };
348
- });
405
+ if (isForum) {
406
+ try {
407
+ const forumTopics = await this.getForumTopics(d.id?.toString() ?? "");
408
+ const unreadTopics = forumTopics
409
+ .filter((t) => t.unreadCount > 0)
410
+ .map((t) => ({ id: t.id, title: t.title, unreadCount: t.unreadCount }));
411
+ const realUnread = unreadTopics.reduce((sum, t) => sum + t.unreadCount, 0);
412
+ if (realUnread === 0)
413
+ return null;
414
+ return {
415
+ ...base,
416
+ unreadCount: realUnread,
417
+ forum: true,
418
+ topics: unreadTopics.length > 0 ? unreadTopics : undefined,
419
+ };
420
+ }
421
+ catch {
422
+ return { ...base, forum: true };
423
+ }
424
+ }
425
+ return base;
426
+ }));
427
+ return results.filter((r) => r !== null);
349
428
  }
350
429
  async getContactRequests(limit = 20) {
351
430
  if (!this.client || !this.connected)
@@ -450,6 +529,7 @@ export class TelegramService {
450
529
  username: entity.username ?? undefined,
451
530
  description,
452
531
  membersCount,
532
+ forum: Boolean(entity.forum) || undefined,
453
533
  };
454
534
  }
455
535
  if (entity instanceof Api.Chat) {
@@ -726,6 +806,75 @@ export class TelegramService {
726
806
  }
727
807
  return 0;
728
808
  }
809
+ async getForumTopics(chatId, limit = 100) {
810
+ if (!this.client || !this.connected)
811
+ throw new Error("Not connected");
812
+ const entity = await this.client.getEntity(chatId);
813
+ if (!(entity instanceof Api.Channel))
814
+ throw new Error("Forum topics are only available in supergroups");
815
+ const result = await this.client.invoke(new Api.channels.GetForumTopics({
816
+ channel: entity,
817
+ limit,
818
+ offsetTopic: 0,
819
+ offsetDate: 0,
820
+ offsetId: 0,
821
+ }));
822
+ const topics = [];
823
+ for (const topic of result.topics) {
824
+ if (topic instanceof Api.ForumTopic) {
825
+ topics.push({
826
+ id: topic.id,
827
+ title: topic.title,
828
+ unreadCount: topic.unreadCount,
829
+ unreadMentions: topic.unreadMentionsCount,
830
+ iconColor: topic.iconColor,
831
+ closed: Boolean(topic.closed),
832
+ pinned: Boolean(topic.pinned),
833
+ });
834
+ }
835
+ }
836
+ return topics;
837
+ }
838
+ async getTopicMessages(chatId, topicId, limit = 20, offsetId) {
839
+ if (!this.client || !this.connected)
840
+ throw new Error("Not connected");
841
+ const peer = await this.client.getInputEntity(chatId);
842
+ const result = await this.client.invoke(new Api.messages.GetReplies({
843
+ peer,
844
+ msgId: topicId,
845
+ limit,
846
+ ...(offsetId ? { offsetId } : {}),
847
+ offsetDate: 0,
848
+ addOffset: 0,
849
+ maxId: 0,
850
+ minId: 0,
851
+ hash: bigInt(0),
852
+ }));
853
+ const messages = "messages" in result ? result.messages : [];
854
+ const results = await Promise.all(messages
855
+ .filter((m) => m instanceof Api.Message)
856
+ .map(async (m) => ({
857
+ id: m.id,
858
+ text: m.message ?? "",
859
+ sender: await this.resolveSenderName(m.senderId),
860
+ date: new Date((m.date ?? 0) * 1000).toISOString(),
861
+ media: this.extractMediaInfo(m.media),
862
+ })));
863
+ return results;
864
+ }
865
+ /** Check if a chat entity is a forum (has topics enabled) */
866
+ async isForum(chatId) {
867
+ if (!this.client || !this.connected)
868
+ throw new Error("Not connected");
869
+ try {
870
+ const entity = await this.client.getEntity(chatId);
871
+ if (entity instanceof Api.Channel) {
872
+ return Boolean(entity.forum);
873
+ }
874
+ }
875
+ catch { }
876
+ return false;
877
+ }
729
878
  async joinChat(target) {
730
879
  if (!this.client)
731
880
  throw new Error("Not connected");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@overpod/mcp-telegram",
3
- "version": "1.6.0",
3
+ "version": "1.8.0",
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",
@@ -56,8 +56,8 @@
56
56
  "zod": "^4.3.6"
57
57
  },
58
58
  "devDependencies": {
59
- "@biomejs/biome": "^2.4.6",
60
- "@types/node": "^25.4.0",
59
+ "@biomejs/biome": "^2.4.7",
60
+ "@types/node": "^25.5.0",
61
61
  "@types/qrcode": "^1.5.6",
62
62
  "tsx": "^4.21.0",
63
63
  "typescript": "^5.9.3"