@overpod/mcp-telegram 1.10.1 → 1.11.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/dist/index.js CHANGED
@@ -18,6 +18,13 @@ if (!API_ID || !API_HASH) {
18
18
  process.exit(1);
19
19
  }
20
20
  const telegram = new TelegramService(API_ID, API_HASH);
21
+ /** Format reactions array into compact text like: [👍×5 ❤️×3(me) 🔥×1] */
22
+ function formatReactions(reactions) {
23
+ if (!reactions?.length)
24
+ return "";
25
+ const parts = reactions.map((r) => `${r.emoji}×${r.count}${r.me ? "(me)" : ""}`);
26
+ return ` [${parts.join(" ")}]`;
27
+ }
21
28
  const server = new McpServer({
22
29
  name: "mcp-telegram",
23
30
  version: "1.0.0",
@@ -165,7 +172,7 @@ server.tool("telegram-read-messages", "Read recent messages from a Telegram chat
165
172
  try {
166
173
  const messages = await telegram.getMessages(chatId, limit, offsetId, minDate, maxDate);
167
174
  const text = messages
168
- .map((m) => `[#${m.id}] [${m.date}] ${m.sender}: ${m.text}${m.media ? ` [${m.media.type}${m.media.fileName ? `: ${m.media.fileName}` : ""}]` : ""}`)
175
+ .map((m) => `[#${m.id}] [${m.date}] ${m.sender}: ${m.text}${m.media ? ` [${m.media.type}${m.media.fileName ? `: ${m.media.fileName}` : ""}]` : ""}${formatReactions(m.reactions)}`)
169
176
  .join("\n\n");
170
177
  return { content: [{ type: "text", text: text || "No messages" }] };
171
178
  }
@@ -203,7 +210,7 @@ server.tool("telegram-search-global", "Search messages globally across all publi
203
210
  try {
204
211
  const messages = await telegram.searchGlobal(query, limit, minDate, maxDate);
205
212
  const text = messages
206
- .map((m) => `[#${m.id}] [${m.date}] [${m.chat.type === "channel" ? "C" : m.chat.type === "group" ? "G" : "P"} ${m.chat.name}${m.chat.username ? ` @${m.chat.username}` : ""}] ${m.sender}: ${m.text}${m.media ? ` [${m.media.type}${m.media.fileName ? `: ${m.media.fileName}` : ""}]` : ""}`)
213
+ .map((m) => `[#${m.id}] [${m.date}] [${m.chat.type === "channel" ? "C" : m.chat.type === "group" ? "G" : "P"} ${m.chat.name}${m.chat.username ? ` @${m.chat.username}` : ""}] ${m.sender}: ${m.text}${m.media ? ` [${m.media.type}${m.media.fileName ? `: ${m.media.fileName}` : ""}]` : ""}${formatReactions(m.reactions)}`)
207
214
  .join("\n\n");
208
215
  return { content: [{ type: "text", text: text || "No messages found" }] };
209
216
  }
@@ -224,7 +231,7 @@ server.tool("telegram-search-messages", "Search messages in a Telegram chat by t
224
231
  try {
225
232
  const messages = await telegram.searchMessages(chatId, query, limit, minDate, maxDate);
226
233
  const text = messages
227
- .map((m) => `[#${m.id}] [${m.date}] ${m.sender}: ${m.text}${m.media ? ` [${m.media.type}${m.media.fileName ? `: ${m.media.fileName}` : ""}]` : ""}`)
234
+ .map((m) => `[#${m.id}] [${m.date}] ${m.sender}: ${m.text}${m.media ? ` [${m.media.type}${m.media.fileName ? `: ${m.media.fileName}` : ""}]` : ""}${formatReactions(m.reactions)}`)
228
235
  .join("\n\n");
229
236
  return { content: [{ type: "text", text: text || "No messages found" }] };
230
237
  }
@@ -529,23 +536,57 @@ server.tool("telegram-join-chat", "Join a Telegram group or channel by username
529
536
  };
530
537
  }
531
538
  });
532
- server.tool("telegram-send-reaction", "Send an emoji reaction to a message. Pass emoji to react, omit to remove reaction", {
539
+ server.tool("telegram-send-reaction", "Send emoji reaction(s) to a message. Supports multiple reactions and adding to existing ones. Omit emoji to remove all reactions", {
533
540
  chatId: z.string().describe("Chat ID or username"),
534
541
  messageId: z.number().describe("Message ID to react to"),
535
- emoji: z.string().optional().describe("Reaction emoji (e.g. 👍❤️🔥😂🎉). Omit to remove reaction"),
536
- }, async ({ chatId, messageId, emoji }) => {
542
+ emoji: z
543
+ .union([z.string(), z.array(z.string())])
544
+ .optional()
545
+ .describe("Reaction emoji(s): single '👍' or array ['👍','🔥']. Omit to remove all reactions"),
546
+ addToExisting: z
547
+ .boolean()
548
+ .default(false)
549
+ .describe("If true, add reaction(s) to existing ones instead of replacing"),
550
+ }, async ({ chatId, messageId, emoji, addToExisting }) => {
537
551
  const err = await requireConnection();
538
552
  if (err)
539
553
  return { content: [{ type: "text", text: err }] };
540
554
  try {
541
- await telegram.sendReaction(chatId, messageId, emoji);
542
- const action = emoji ? `Reacted ${emoji} to` : "Removed reaction from";
543
- return { content: [{ type: "text", text: `${action} message ${messageId} in ${chatId}` }] };
555
+ const updated = await telegram.sendReaction(chatId, messageId, emoji, addToExisting);
556
+ const emojiStr = Array.isArray(emoji) ? emoji.join("") : emoji;
557
+ const action = emoji ? `Reacted ${emojiStr} to` : "Removed reactions from";
558
+ const reactionsInfo = updated
559
+ ? ` | Reactions: ${updated.map((r) => `${r.emoji}×${r.count}${r.me ? "(me)" : ""}`).join(" ")}`
560
+ : "";
561
+ return { content: [{ type: "text", text: `${action} message ${messageId} in ${chatId}${reactionsInfo}` }] };
544
562
  }
545
563
  catch (e) {
546
564
  return { content: [{ type: "text", text: `Reaction error: ${e.message}` }] };
547
565
  }
548
566
  });
567
+ server.tool("telegram-get-reactions", "Get detailed reaction info for a message: which reactions, counts, and who reacted (when visible)", {
568
+ chatId: z.string().describe("Chat ID or username"),
569
+ messageId: z.number().describe("Message ID to get reactions for"),
570
+ }, async ({ chatId, messageId }) => {
571
+ const err = await requireConnection();
572
+ if (err)
573
+ return { content: [{ type: "text", text: err }] };
574
+ try {
575
+ const result = await telegram.getMessageReactions(chatId, messageId);
576
+ if (result.reactions.length === 0) {
577
+ return { content: [{ type: "text", text: `No reactions on message ${messageId}` }] };
578
+ }
579
+ const lines = result.reactions.map((r) => {
580
+ const usersStr = r.users.length > 0 ? `: ${r.users.map((u) => u.name).join(", ")}` : "";
581
+ return `${r.emoji} × ${r.count}${usersStr}`;
582
+ });
583
+ lines.push(`\nTotal: ${result.total} reactions`);
584
+ return { content: [{ type: "text", text: lines.join("\n") }] };
585
+ }
586
+ catch (e) {
587
+ return { content: [{ type: "text", text: `Error: ${e.message}` }] };
588
+ }
589
+ });
549
590
  server.tool("telegram-send-scheduled", "Send a scheduled message to a Telegram chat. The message will be delivered at the specified time by Telegram servers", {
550
591
  chatId: z.string().describe("Chat ID or username (use 'me' or 'self' for Saved Messages)"),
551
592
  text: z.string().describe("Message text"),
@@ -703,7 +744,7 @@ server.tool("telegram-read-topic-messages", "Read messages from a specific forum
703
744
  try {
704
745
  const messages = await telegram.getTopicMessages(chatId, topicId, limit, offsetId);
705
746
  const text = messages
706
- .map((m) => `[#${m.id}] [${m.date}] ${m.sender}: ${m.text}${m.media ? ` [${m.media.type}${m.media.fileName ? `: ${m.media.fileName}` : ""}]` : ""}`)
747
+ .map((m) => `[#${m.id}] [${m.date}] ${m.sender}: ${m.text}${m.media ? ` [${m.media.type}${m.media.fileName ? `: ${m.media.fileName}` : ""}]` : ""}${formatReactions(m.reactions)}`)
707
748
  .join("\n\n");
708
749
  return { content: [{ type: "text", text: text || "No messages in this topic" }] };
709
750
  }
@@ -110,6 +110,11 @@ export declare class TelegramService {
110
110
  fileName?: string;
111
111
  size?: number;
112
112
  };
113
+ reactions?: {
114
+ emoji: string;
115
+ count: number;
116
+ me: boolean;
117
+ }[];
113
118
  }>>;
114
119
  searchChats(query: string, limit?: number): Promise<Array<{
115
120
  id: string;
@@ -135,6 +140,11 @@ export declare class TelegramService {
135
140
  fileName?: string;
136
141
  size?: number;
137
142
  };
143
+ reactions?: {
144
+ emoji: string;
145
+ count: number;
146
+ me: boolean;
147
+ }[];
138
148
  }>>;
139
149
  searchMessages(chatId: string, query: string, limit?: number, minDate?: number, maxDate?: number): Promise<Array<{
140
150
  id: number;
@@ -146,6 +156,11 @@ export declare class TelegramService {
146
156
  fileName?: string;
147
157
  size?: number;
148
158
  };
159
+ reactions?: {
160
+ emoji: string;
161
+ count: number;
162
+ me: boolean;
163
+ }[];
149
164
  }>>;
150
165
  getContacts(limit?: number): Promise<Array<{
151
166
  id: string;
@@ -184,7 +199,24 @@ export declare class TelegramService {
184
199
  } | null>;
185
200
  /** Detect MIME type from buffer magic bytes */
186
201
  private detectMimeFromBuffer;
187
- sendReaction(chatId: string, messageId: number, emoji?: string): Promise<void>;
202
+ /** Extract reactions from a message into a simple format */
203
+ private extractReactions;
204
+ sendReaction(chatId: string, messageId: number, emoji?: string | string[], addToExisting?: boolean): Promise<{
205
+ emoji: string;
206
+ count: number;
207
+ me: boolean;
208
+ }[] | undefined>;
209
+ getMessageReactions(chatId: string, messageId: number): Promise<{
210
+ reactions: {
211
+ emoji: string;
212
+ count: number;
213
+ users: {
214
+ id: string;
215
+ name: string;
216
+ }[];
217
+ }[];
218
+ total: number;
219
+ }>;
188
220
  sendScheduledMessage(chatId: string, text: string, scheduleDate: number, replyTo?: number, parseMode?: "md" | "html"): Promise<void>;
189
221
  createPoll(chatId: string, question: string, answers: string[], options?: {
190
222
  multipleChoice?: boolean;
@@ -210,6 +242,11 @@ export declare class TelegramService {
210
242
  fileName?: string;
211
243
  size?: number;
212
244
  };
245
+ reactions?: {
246
+ emoji: string;
247
+ count: number;
248
+ me: boolean;
249
+ }[];
213
250
  }>>;
214
251
  /** Check if a chat entity is a forum (has topics enabled) */
215
252
  isForum(chatId: string): Promise<boolean>;
@@ -621,6 +621,7 @@ export class TelegramService {
621
621
  sender: await this.resolveSenderName(m.senderId),
622
622
  date: new Date((m.date ?? 0) * 1000).toISOString(),
623
623
  media: this.extractMediaInfo(m.media),
624
+ reactions: this.extractReactions(m.reactions),
624
625
  })));
625
626
  return results;
626
627
  }
@@ -749,6 +750,7 @@ export class TelegramService {
749
750
  date: new Date((m.date ?? 0) * 1000).toISOString(),
750
751
  chat: chatsMap.get(chatId) || { id: chatId, name: "Unknown", type: "unknown" },
751
752
  media: this.extractMediaInfo(m.media),
753
+ reactions: this.extractReactions(m.reactions),
752
754
  };
753
755
  }));
754
756
  return results;
@@ -771,6 +773,7 @@ export class TelegramService {
771
773
  sender: await this.resolveSenderName(m.senderId),
772
774
  date: new Date((m.date ?? 0) * 1000).toISOString(),
773
775
  media: this.extractMediaInfo(m.media),
776
+ reactions: this.extractReactions(m.reactions),
774
777
  })));
775
778
  return results;
776
779
  }
@@ -899,16 +902,121 @@ export class TelegramService {
899
902
  return "image/webp";
900
903
  return "image/jpeg"; // Telegram profile photos are almost always JPEG
901
904
  }
902
- async sendReaction(chatId, messageId, emoji) {
905
+ /** Extract reactions from a message into a simple format */
906
+ extractReactions(reactions) {
907
+ if (!reactions?.results?.length)
908
+ return undefined;
909
+ const items = [];
910
+ for (const r of reactions.results) {
911
+ let emoji;
912
+ if (r.reaction instanceof Api.ReactionEmoji) {
913
+ emoji = r.reaction.emoticon;
914
+ }
915
+ else if (r.reaction instanceof Api.ReactionCustomEmoji) {
916
+ emoji = `custom:${r.reaction.documentId}`;
917
+ }
918
+ else if (r.reaction instanceof Api.ReactionPaid) {
919
+ emoji = "⭐";
920
+ }
921
+ else {
922
+ continue;
923
+ }
924
+ items.push({ emoji, count: r.count, me: r.chosenOrder != null });
925
+ }
926
+ return items.length > 0 ? items : undefined;
927
+ }
928
+ async sendReaction(chatId, messageId, emoji, addToExisting = false) {
903
929
  if (!this.client || !this.connected)
904
930
  throw new Error("Not connected");
905
931
  const peer = await this.client.getInputEntity(chatId);
906
- const reaction = emoji ? [new Api.ReactionEmoji({ emoticon: emoji })] : []; // empty = remove reaction
907
- await this.client.invoke(new Api.messages.SendReaction({
932
+ const reactionList = [];
933
+ if (emoji) {
934
+ const emojis = Array.isArray(emoji) ? emoji : [emoji];
935
+ if (addToExisting) {
936
+ // Fetch current reactions to preserve them
937
+ const msgs = await this.client.getMessages(chatId, { ids: [messageId] });
938
+ const msg = msgs[0];
939
+ if (msg?.reactions?.results) {
940
+ for (const r of msg.reactions.results) {
941
+ if (r.chosenOrder != null) {
942
+ reactionList.push(r.reaction);
943
+ }
944
+ }
945
+ }
946
+ }
947
+ for (const e of emojis) {
948
+ reactionList.push(new Api.ReactionEmoji({ emoticon: e }));
949
+ }
950
+ }
951
+ // empty array = remove all reactions
952
+ const result = await this.client.invoke(new Api.messages.SendReaction({
908
953
  peer,
909
954
  msgId: messageId,
910
- reaction,
955
+ reaction: reactionList,
911
956
  }));
957
+ // Extract updated reactions from the response
958
+ if ("updates" in result) {
959
+ for (const upd of result.updates) {
960
+ if (upd instanceof Api.UpdateMessageReactions) {
961
+ return this.extractReactions(upd.reactions);
962
+ }
963
+ }
964
+ }
965
+ return undefined;
966
+ }
967
+ async getMessageReactions(chatId, messageId) {
968
+ if (!this.client || !this.connected)
969
+ throw new Error("Not connected");
970
+ const peer = await this.client.getInputEntity(chatId);
971
+ // First get the message to know which reactions exist
972
+ const msgs = await this.client.getMessages(chatId, { ids: [messageId] });
973
+ const msg = msgs[0];
974
+ if (!msg?.reactions?.results?.length) {
975
+ return { reactions: [], total: 0 };
976
+ }
977
+ const reactionsOut = [];
978
+ for (const rc of msg.reactions.results) {
979
+ let emoji;
980
+ if (rc.reaction instanceof Api.ReactionEmoji) {
981
+ emoji = rc.reaction.emoticon;
982
+ }
983
+ else if (rc.reaction instanceof Api.ReactionCustomEmoji) {
984
+ emoji = `custom:${rc.reaction.documentId}`;
985
+ }
986
+ else if (rc.reaction instanceof Api.ReactionPaid) {
987
+ emoji = "⭐";
988
+ }
989
+ else {
990
+ continue;
991
+ }
992
+ const users = [];
993
+ // Try to get the list of users who reacted (may fail if canSeeList is false)
994
+ if (msg.reactions.canSeeList) {
995
+ try {
996
+ const list = await this.client.invoke(new Api.messages.GetMessageReactionsList({
997
+ peer,
998
+ id: messageId,
999
+ reaction: rc.reaction,
1000
+ limit: 50,
1001
+ }));
1002
+ if (list instanceof Api.messages.MessageReactionsList) {
1003
+ for (const r of list.reactions) {
1004
+ const userId = r.peerId instanceof Api.PeerUser ? r.peerId.userId.toString() : "";
1005
+ if (userId) {
1006
+ const name = await this.resolveSenderName(bigInt(Number.parseInt(userId)));
1007
+ users.push({ id: userId, name });
1008
+ }
1009
+ }
1010
+ }
1011
+ }
1012
+ catch {
1013
+ // canSeeList may be false or request may fail for channels
1014
+ }
1015
+ }
1016
+ reactionsOut.push({ emoji, count: rc.count, users });
1017
+ }
1018
+ const total = reactionsOut.reduce((sum, r) => sum + r.count, 0);
1019
+ return { reactions: reactionsOut, total };
912
1020
  }
913
1021
  async sendScheduledMessage(chatId, text, scheduleDate, replyTo, parseMode) {
914
1022
  if (!this.client || !this.connected)
@@ -1009,6 +1117,7 @@ export class TelegramService {
1009
1117
  sender: await this.resolveSenderName(m.senderId),
1010
1118
  date: new Date((m.date ?? 0) * 1000).toISOString(),
1011
1119
  media: this.extractMediaInfo(m.media),
1120
+ reactions: this.extractReactions(m.reactions),
1012
1121
  })));
1013
1122
  return results;
1014
1123
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@overpod/mcp-telegram",
3
- "version": "1.10.1",
3
+ "version": "1.11.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",