@overpod/mcp-telegram 1.5.0 → 1.6.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
@@ -8,7 +8,7 @@
8
8
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
9
9
  [![mcp-telegram MCP server](https://glama.ai/mcp/servers/overpod/mcp-telegram/badges/score.svg)](https://glama.ai/mcp/servers/overpod/mcp-telegram)
10
10
 
11
- > **Hosted version available!** Don't want to self-host? Use [mcp-telegram.com](https://mcp-telegram.com) -- connect Telegram to Claude.ai in 30 seconds with QR code. No API keys needed.
11
+ > **Hosted version available!** Don't want to self-host? Use [mcp-telegram.com](https://mcp-telegram.com) -- connect Telegram to Claude.ai or ChatGPT in 30 seconds with QR code. No API keys needed.
12
12
 
13
13
  <p align="center">
14
14
  <img src="assets/demo.gif" alt="MCP Telegram demo — connect and summarize chats in Claude" width="700">
@@ -20,11 +20,11 @@ An MCP (Model Context Protocol) server that connects AI assistants like Claude t
20
20
 
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
- - **21 tools** -- messaging, chat management, media, contacts, and more
23
+ - **Full-featured** -- messaging, reactions, polls, scheduled messages, media, contacts, and more
24
24
  - **QR code login** -- authenticate by scanning a QR code in the Telegram app
25
25
  - **Session persistence** -- login once, stay connected across restarts
26
26
  - **Human-readable output** -- sender names are resolved, not just numeric IDs
27
- - **Works with any MCP client** -- Claude Code, Claude Desktop, Cursor, VS Code, Mastra, etc.
27
+ - **Works with any MCP client** -- Claude Code, Claude Desktop, ChatGPT, Cursor, VS Code, Mastra, etc.
28
28
 
29
29
  ## Prerequisites
30
30
 
@@ -162,6 +162,9 @@ const telegramMcp = new MCPClient({
162
162
  |------|-------------|
163
163
  | `telegram-send-message` | Send a text message to a chat |
164
164
  | `telegram-send-file` | Send a file (photo, document, video, etc.) to a chat |
165
+ | `telegram-send-reaction` | Send or remove an emoji reaction on a message |
166
+ | `telegram-send-scheduled` | Schedule a message for future delivery |
167
+ | `telegram-create-poll` | Create a poll (multiple choice or quiz mode) |
165
168
  | `telegram-edit-message` | Edit a previously sent message |
166
169
  | `telegram-delete-message` | Delete messages in a chat |
167
170
  | `telegram-forward-message` | Forward messages between chats |
@@ -284,6 +287,35 @@ Most tools accept `chatId` as a string -- either a numeric ID (e.g., `"-10012345
284
287
  |-----------|------|----------|-------------|
285
288
  | `target` | string | yes | Username (@group), link (t.me/group), or invite link (t.me/+xxx) |
286
289
 
290
+ ### telegram-send-reaction
291
+
292
+ | Parameter | Type | Required | Description |
293
+ |-----------|------|----------|-------------|
294
+ | `chatId` | string | yes | Chat ID or @username |
295
+ | `messageId` | number | yes | Message ID to react to |
296
+ | `emoji` | string | no | Reaction emoji (e.g. 👍❤️🔥😂🎉). Omit to remove reaction |
297
+
298
+ ### telegram-send-scheduled
299
+
300
+ | Parameter | Type | Required | Description |
301
+ |-----------|------|----------|-------------|
302
+ | `chatId` | string | yes | Chat ID or @username (use `"me"` or `"self"` for Saved Messages) |
303
+ | `text` | string | yes | Message text |
304
+ | `scheduleDate` | number | yes | Unix timestamp when to send (must be in the future) |
305
+ | `replyTo` | number | no | Message ID to reply to |
306
+ | `parseMode` | `"md"` / `"html"` | no | Message formatting mode |
307
+
308
+ ### telegram-create-poll
309
+
310
+ | Parameter | Type | Required | Description |
311
+ |-----------|------|----------|-------------|
312
+ | `chatId` | string | yes | Chat ID or @username |
313
+ | `question` | string | yes | Poll question |
314
+ | `answers` | string[] | yes | Answer options (2-10 items) |
315
+ | `multipleChoice` | boolean | no | Allow multiple answers (default: false) |
316
+ | `quiz` | boolean | no | Quiz mode with one correct answer (default: false) |
317
+ | `correctAnswer` | number | no | Index of correct answer, 0-based (required for quiz mode) |
318
+
287
319
  ### telegram-search-messages
288
320
 
289
321
  | Parameter | Type | Required | Description |
@@ -340,7 +372,7 @@ npm run format # Format code with Biome
340
372
 
341
373
  ```
342
374
  src/
343
- index.ts -- MCP server entry point, 21 tool definitions
375
+ index.ts -- MCP server entry point, tool definitions
344
376
  telegram-client.ts -- TelegramService class (GramJS wrapper)
345
377
  qr-login-cli.ts -- CLI utility for QR code login
346
378
  ```
package/dist/index.js CHANGED
@@ -119,7 +119,10 @@ server.tool("telegram-send-message", "Send a message to a Telegram chat", {
119
119
  server.tool("telegram-list-chats", "List Telegram chats", {
120
120
  limit: z.number().default(20).describe("Number of chats to return"),
121
121
  offsetDate: z.number().optional().describe("Unix timestamp offset for pagination"),
122
- filterType: z.enum(["private", "group", "channel"]).optional().describe("Filter by chat type"),
122
+ filterType: z
123
+ .enum(["private", "group", "channel", "contact_requests"])
124
+ .optional()
125
+ .describe("Filter by chat type. 'contact_requests' shows only private chats from non-contacts"),
123
126
  }, async ({ limit, offsetDate, filterType }) => {
124
127
  const err = await requireConnection();
125
128
  if (err)
@@ -127,7 +130,13 @@ server.tool("telegram-list-chats", "List Telegram chats", {
127
130
  try {
128
131
  const dialogs = await telegram.getDialogs(limit, offsetDate, filterType);
129
132
  const text = dialogs
130
- .map((d) => `${d.type === "group" ? "G" : d.type === "channel" ? "C" : "P"} ${d.name} (${d.id}) ${d.unreadCount > 0 ? `[${d.unreadCount} unread]` : ""}`)
133
+ .map((d) => {
134
+ const prefix = d.type === "group" ? "G" : d.type === "channel" ? "C" : "P";
135
+ const botTag = d.isBot ? " [bot]" : "";
136
+ const contactTag = d.type === "private" && d.isContact === false ? " [not in contacts]" : "";
137
+ const unread = d.unreadCount > 0 ? ` [${d.unreadCount} unread]` : "";
138
+ return `${prefix} ${d.name} (${d.id})${botTag}${contactTag}${unread}`;
139
+ })
131
140
  .join("\n");
132
141
  return { content: [{ type: "text", text: text || "No chats" }] };
133
142
  }
@@ -204,7 +213,12 @@ server.tool("telegram-get-unread", "Get unread Telegram chats", {
204
213
  try {
205
214
  const dialogs = await telegram.getUnreadDialogs(limit);
206
215
  const text = dialogs
207
- .map((d) => `${d.type === "group" ? "G" : d.type === "channel" ? "C" : "P"} ${d.name} (${d.id}) [${d.unreadCount} unread]`)
216
+ .map((d) => {
217
+ const prefix = d.type === "group" ? "G" : d.type === "channel" ? "C" : "P";
218
+ const botTag = d.isBot ? " [bot]" : "";
219
+ const contactTag = d.type === "private" && d.isContact === false ? " [not in contacts]" : "";
220
+ return `${prefix} ${d.name} (${d.id})${botTag}${contactTag} [${d.unreadCount} unread]`;
221
+ })
208
222
  .join("\n");
209
223
  return { content: [{ type: "text", text: text || "No unread chats" }] };
210
224
  }
@@ -507,6 +521,79 @@ server.tool("telegram-create-poll", "Create a poll in a Telegram chat", {
507
521
  return { content: [{ type: "text", text: `Poll error: ${e.message}` }] };
508
522
  }
509
523
  });
524
+ server.tool("telegram-get-contact-requests", "Get incoming messages from non-contacts (contact requests). Shows who messaged you without being in your contacts, with message preview", {
525
+ limit: z.number().default(20).describe("Number of contact requests to return"),
526
+ }, async ({ limit }) => {
527
+ const err = await requireConnection();
528
+ if (err)
529
+ return { content: [{ type: "text", text: err }] };
530
+ try {
531
+ const requests = await telegram.getContactRequests(limit);
532
+ if (requests.length === 0) {
533
+ return { content: [{ type: "text", text: "No contact requests" }] };
534
+ }
535
+ const text = requests
536
+ .map((r) => {
537
+ const tag = r.isBot ? "[bot]" : "[user]";
538
+ const username = r.username ? ` @${r.username}` : "";
539
+ const unread = r.unreadCount > 0 ? ` [${r.unreadCount} unread]` : "";
540
+ const preview = r.lastMessage ? `\n > ${r.lastMessage.slice(0, 100)}` : "";
541
+ return `${tag} ${r.name}${username} (${r.id})${unread}${preview}`;
542
+ })
543
+ .join("\n");
544
+ return { content: [{ type: "text", text: text }] };
545
+ }
546
+ catch (e) {
547
+ return { content: [{ type: "text", text: `Error: ${e.message}` }] };
548
+ }
549
+ });
550
+ server.tool("telegram-add-contact", "Add a user to your Telegram contacts. Use this to accept contact requests from non-contacts", {
551
+ userId: z.string().describe("User ID or username to add"),
552
+ firstName: z.string().describe("First name for the contact"),
553
+ lastName: z.string().optional().describe("Last name for the contact"),
554
+ phone: z.string().optional().describe("Phone number for the contact"),
555
+ }, async ({ userId, firstName, lastName, phone }) => {
556
+ const err = await requireConnection();
557
+ if (err)
558
+ return { content: [{ type: "text", text: err }] };
559
+ try {
560
+ await telegram.addContact(userId, firstName, lastName, phone);
561
+ return {
562
+ content: [{ type: "text", text: `Contact added: ${firstName}${lastName ? ` ${lastName}` : ""} (${userId})` }],
563
+ };
564
+ }
565
+ catch (e) {
566
+ return { content: [{ type: "text", text: `Error: ${e.message}` }] };
567
+ }
568
+ });
569
+ server.tool("telegram-block-user", "Block a Telegram user. Blocked users cannot send you messages", {
570
+ userId: z.string().describe("User ID or username to block"),
571
+ }, async ({ userId }) => {
572
+ const err = await requireConnection();
573
+ if (err)
574
+ return { content: [{ type: "text", text: err }] };
575
+ try {
576
+ await telegram.blockUser(userId);
577
+ return { content: [{ type: "text", text: `User blocked: ${userId}` }] };
578
+ }
579
+ catch (e) {
580
+ return { content: [{ type: "text", text: `Error: ${e.message}` }] };
581
+ }
582
+ });
583
+ server.tool("telegram-report-spam", "Report a chat as spam to Telegram", {
584
+ chatId: z.string().describe("Chat ID or username to report"),
585
+ }, async ({ chatId }) => {
586
+ const err = await requireConnection();
587
+ if (err)
588
+ return { content: [{ type: "text", text: err }] };
589
+ try {
590
+ await telegram.reportSpam(chatId);
591
+ return { content: [{ type: "text", text: `Reported as spam: ${chatId}` }] };
592
+ }
593
+ catch (e) {
594
+ return { content: [{ type: "text", text: `Error: ${e.message}` }] };
595
+ }
596
+ });
510
597
  // --- Start ---
511
598
  async function main() {
512
599
  // Try to auto-connect with saved session
@@ -43,18 +43,34 @@ export declare class TelegramService {
43
43
  private detectMimeType;
44
44
  pinMessage(chatId: string, messageId: number, silent?: boolean): Promise<void>;
45
45
  unpinMessage(chatId: string, messageId: number): Promise<void>;
46
- getDialogs(limit?: number, offsetDate?: number, filterType?: "private" | "group" | "channel"): Promise<Array<{
46
+ getDialogs(limit?: number, offsetDate?: number, filterType?: "private" | "group" | "channel" | "contact_requests"): Promise<Array<{
47
47
  id: string;
48
48
  name: string;
49
49
  type: string;
50
50
  unreadCount: number;
51
+ isBot?: boolean;
52
+ isContact?: boolean;
51
53
  }>>;
52
54
  getUnreadDialogs(limit?: number): Promise<Array<{
53
55
  id: string;
54
56
  name: string;
55
57
  type: string;
56
58
  unreadCount: number;
59
+ isBot?: boolean;
60
+ isContact?: boolean;
57
61
  }>>;
62
+ getContactRequests(limit?: number): Promise<Array<{
63
+ id: string;
64
+ name: string;
65
+ username?: string;
66
+ isBot: boolean;
67
+ unreadCount: number;
68
+ lastMessage?: string;
69
+ lastMessageDate?: number;
70
+ }>>;
71
+ addContact(userId: string, firstName: string, lastName?: string, phone?: string): Promise<void>;
72
+ blockUser(userId: string): Promise<void>;
73
+ reportSpam(chatId: string): Promise<void>;
58
74
  markAsRead(chatId: string): Promise<void>;
59
75
  forwardMessage(fromChatId: string, toChatId: string, messageIds: number[]): Promise<void>;
60
76
  editMessage(chatId: string, messageId: number, newText: string): Promise<void>;
@@ -66,6 +82,8 @@ export declare class TelegramService {
66
82
  username?: string;
67
83
  description?: string;
68
84
  membersCount?: number;
85
+ isBot?: boolean;
86
+ isContact?: boolean;
69
87
  }>;
70
88
  /** Extract media info from a message */
71
89
  private extractMediaInfo;
@@ -309,12 +309,22 @@ export class TelegramService {
309
309
  throw new Error("Not connected");
310
310
  const fetchLimit = filterType ? limit * 3 : limit;
311
311
  const dialogs = await this.client.getDialogs({ limit: fetchLimit, ...(offsetDate ? { offsetDate } : {}) });
312
- const mapped = dialogs.map((d) => ({
313
- id: d.id?.toString() ?? "",
314
- name: d.title ?? d.name ?? "Unknown",
315
- type: d.isGroup ? "group" : d.isChannel ? "channel" : "private",
316
- unreadCount: d.unreadCount,
317
- }));
312
+ const mapped = dialogs.map((d) => {
313
+ const type = d.isGroup ? "group" : d.isChannel ? "channel" : "private";
314
+ const isUser = d.entity instanceof Api.User;
315
+ return {
316
+ id: d.id?.toString() ?? "",
317
+ name: d.title ?? d.name ?? "Unknown",
318
+ type,
319
+ unreadCount: d.unreadCount,
320
+ ...(isUser
321
+ ? { isBot: Boolean(d.entity.bot), isContact: Boolean(d.entity.contact) }
322
+ : {}),
323
+ };
324
+ });
325
+ if (filterType === "contact_requests") {
326
+ return mapped.filter((d) => d.type === "private" && d.isContact === false).slice(0, limit);
327
+ }
318
328
  return filterType ? mapped.filter((d) => d.type === filterType).slice(0, limit) : mapped;
319
329
  }
320
330
  async getUnreadDialogs(limit = 20) {
@@ -324,13 +334,67 @@ export class TelegramService {
324
334
  return dialogs
325
335
  .filter((d) => d.unreadCount > 0)
326
336
  .slice(0, limit)
327
- .map((d) => ({
328
- id: d.id?.toString() ?? "",
329
- name: d.title ?? d.name ?? "Unknown",
330
- type: d.isGroup ? "group" : d.isChannel ? "channel" : "private",
331
- unreadCount: d.unreadCount,
337
+ .map((d) => {
338
+ const isUser = d.entity instanceof Api.User;
339
+ return {
340
+ id: d.id?.toString() ?? "",
341
+ name: d.title ?? d.name ?? "Unknown",
342
+ type: d.isGroup ? "group" : d.isChannel ? "channel" : "private",
343
+ unreadCount: d.unreadCount,
344
+ ...(isUser
345
+ ? { isBot: Boolean(d.entity.bot), isContact: Boolean(d.entity.contact) }
346
+ : {}),
347
+ };
348
+ });
349
+ }
350
+ async getContactRequests(limit = 20) {
351
+ if (!this.client || !this.connected)
352
+ throw new Error("Not connected");
353
+ const dialogs = await this.client.getDialogs({ limit: limit * 5 });
354
+ return dialogs
355
+ .filter((d) => {
356
+ if (d.isGroup || d.isChannel)
357
+ return false;
358
+ return d.entity instanceof Api.User && !d.entity.contact;
359
+ })
360
+ .slice(0, limit)
361
+ .map((d) => {
362
+ const user = d.entity;
363
+ const msg = d.message;
364
+ return {
365
+ id: d.id?.toString() ?? "",
366
+ name: [user.firstName, user.lastName].filter(Boolean).join(" ") || "Unknown",
367
+ username: user.username ?? undefined,
368
+ isBot: Boolean(user.bot),
369
+ unreadCount: d.unreadCount,
370
+ lastMessage: msg?.message ?? undefined,
371
+ lastMessageDate: msg?.date ?? undefined,
372
+ };
373
+ });
374
+ }
375
+ async addContact(userId, firstName, lastName, phone) {
376
+ if (!this.client || !this.connected)
377
+ throw new Error("Not connected");
378
+ const entity = await this.client.getInputEntity(userId);
379
+ await this.client.invoke(new Api.contacts.AddContact({
380
+ id: entity,
381
+ firstName,
382
+ lastName: lastName ?? "",
383
+ phone: phone ?? "",
332
384
  }));
333
385
  }
386
+ async blockUser(userId) {
387
+ if (!this.client || !this.connected)
388
+ throw new Error("Not connected");
389
+ const entity = await this.client.getInputEntity(userId);
390
+ await this.client.invoke(new Api.contacts.Block({ id: entity }));
391
+ }
392
+ async reportSpam(chatId) {
393
+ if (!this.client || !this.connected)
394
+ throw new Error("Not connected");
395
+ const peer = await this.client.getInputEntity(chatId);
396
+ await this.client.invoke(new Api.messages.ReportSpam({ peer }));
397
+ }
334
398
  async markAsRead(chatId) {
335
399
  if (!this.client || !this.connected)
336
400
  throw new Error("Not connected");
@@ -362,6 +426,8 @@ export class TelegramService {
362
426
  name: parts.join(" ") || "Unknown",
363
427
  type: "private",
364
428
  username: entity.username ?? undefined,
429
+ isBot: Boolean(entity.bot),
430
+ isContact: Boolean(entity.contact),
365
431
  };
366
432
  }
367
433
  if (entity instanceof Api.Channel) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@overpod/mcp-telegram",
3
- "version": "1.5.0",
4
- "description": "MCP server for Telegram userbot — 24 tools for messages, media, contacts & more. Built on GramJS/MTProto.",
3
+ "version": "1.6.0",
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",
7
7
  "exports": {