@overpod/mcp-telegram 1.8.1 → 1.10.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
@@ -183,7 +183,7 @@ server.tool("telegram-search-chats", "Search for Telegram chats/users/channels b
183
183
  try {
184
184
  const results = await telegram.searchChats(query, limit);
185
185
  const text = results
186
- .map((c) => `${c.type === "group" ? "G" : c.type === "channel" ? "C" : "P"} ${c.name}${c.username ? ` (@${c.username})` : ""} (${c.id})`)
186
+ .map((c) => `${c.type === "group" ? "G" : c.type === "channel" ? "C" : "P"} ${c.name}${c.username ? ` (@${c.username})` : ""} (${c.id})${c.membersCount ? ` [${c.membersCount} members]` : ""}${c.description ? ` — ${c.description.split("\n")[0].slice(0, 100)}` : ""}`)
187
187
  .join("\n");
188
188
  return { content: [{ type: "text", text: text || "No results" }] };
189
189
  }
@@ -191,6 +191,26 @@ server.tool("telegram-search-chats", "Search for Telegram chats/users/channels b
191
191
  return { content: [{ type: "text", text: `Error: ${e.message}` }] };
192
192
  }
193
193
  });
194
+ server.tool("telegram-search-global", "Search messages globally across all public Telegram chats and channels", {
195
+ query: z.string().describe("Search text"),
196
+ limit: z.number().default(20).describe("Max results"),
197
+ minDate: z.number().optional().describe("Unix timestamp: only messages after this date"),
198
+ maxDate: z.number().optional().describe("Unix timestamp: only messages before this date"),
199
+ }, async ({ query, limit, minDate, maxDate }) => {
200
+ const err = await requireConnection();
201
+ if (err)
202
+ return { content: [{ type: "text", text: err }] };
203
+ try {
204
+ const messages = await telegram.searchGlobal(query, limit, minDate, maxDate);
205
+ const text = messages
206
+ .map((m) => `[${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}` : ""}]` : ""}`)
207
+ .join("\n\n");
208
+ return { content: [{ type: "text", text: text || "No messages found" }] };
209
+ }
210
+ catch (e) {
211
+ return { content: [{ type: "text", text: `Error: ${e.message}` }] };
212
+ }
213
+ });
194
214
  server.tool("telegram-search-messages", "Search messages in a Telegram chat by text", {
195
215
  chatId: z.string().describe("Chat ID or username"),
196
216
  query: z.string().describe("Search text"),
@@ -424,7 +444,7 @@ server.tool("telegram-get-chat-members", "Get members of a Telegram group or cha
424
444
  return { content: [{ type: "text", text: `Error: ${e.message}` }] };
425
445
  }
426
446
  });
427
- server.tool("telegram-get-profile", "Get detailed profile info of a Telegram user", {
447
+ server.tool("telegram-get-profile", "Get detailed profile info of a Telegram user including bio, birthday, business info and more", {
428
448
  userId: z.string().describe("User ID or username"),
429
449
  }, async ({ userId }) => {
430
450
  const err = await requireConnection();
@@ -439,7 +459,13 @@ server.tool("telegram-get-profile", "Get detailed profile info of a Telegram use
439
459
  ...(profile.phone ? [`Phone: +${profile.phone}`] : []),
440
460
  ...(profile.bio ? [`Bio: ${profile.bio}`] : []),
441
461
  `Photo: ${profile.photo ? "yes" : "no"}`,
462
+ ...(profile.premium ? ["Premium: yes"] : []),
442
463
  ...(profile.lastSeen ? [`Last seen: ${profile.lastSeen}`] : []),
464
+ ...(profile.birthday ? [`Birthday: ${profile.birthday}`] : []),
465
+ ...(profile.commonChatsCount ? [`Common chats: ${profile.commonChatsCount}`] : []),
466
+ ...(profile.personalChannelId ? [`Personal channel ID: ${profile.personalChannelId}`] : []),
467
+ ...(profile.businessLocation ? [`Business location: ${profile.businessLocation}`] : []),
468
+ ...(profile.businessWorkHours ? [`Business hours timezone: ${profile.businessWorkHours}`] : []),
443
469
  ];
444
470
  return { content: [{ type: "text", text: lines.join("\n") }] };
445
471
  }
@@ -447,6 +473,39 @@ server.tool("telegram-get-profile", "Get detailed profile info of a Telegram use
447
473
  return { content: [{ type: "text", text: `Error: ${e.message}` }] };
448
474
  }
449
475
  });
476
+ server.tool("telegram-get-profile-photo", "Download profile photo of a Telegram user, group, or channel. Returns inline image or saves to file", {
477
+ entityId: z.string().describe("User/Chat/Channel ID or username"),
478
+ savePath: z.string().optional().describe("Absolute path to save file. If omitted, returns inline base64 image"),
479
+ size: z
480
+ .enum(["small", "big"])
481
+ .optional()
482
+ .describe("Photo size: 'small' (160x160) or 'big' (640x640). Default: big"),
483
+ }, async ({ entityId, savePath, size }) => {
484
+ const err = await requireConnection();
485
+ if (err)
486
+ return { content: [{ type: "text", text: err }] };
487
+ try {
488
+ const result = await telegram.downloadProfilePhoto(entityId, {
489
+ isBig: size !== "small",
490
+ savePath,
491
+ });
492
+ if (!result) {
493
+ return { content: [{ type: "text", text: "No profile photo found" }] };
494
+ }
495
+ if ("filePath" in result) {
496
+ return { content: [{ type: "text", text: `Downloaded to: ${result.filePath}` }] };
497
+ }
498
+ return {
499
+ content: [
500
+ { type: "image", data: result.buffer.toString("base64"), mimeType: result.mimeType },
501
+ { type: "text", text: `Profile photo (${(result.buffer.length / 1024).toFixed(0)} KB, ${result.mimeType})` },
502
+ ],
503
+ };
504
+ }
505
+ catch (e) {
506
+ return { content: [{ type: "text", text: `Error: ${e.message}` }] };
507
+ }
508
+ });
450
509
  server.tool("telegram-join-chat", "Join a Telegram group or channel by username or invite link", {
451
510
  target: z.string().describe("Username (@group), link (t.me/group), or invite link (t.me/+xxx)"),
452
511
  }, async ({ target }) => {
@@ -116,6 +116,25 @@ export declare class TelegramService {
116
116
  name: string;
117
117
  type: string;
118
118
  username?: string;
119
+ membersCount?: number;
120
+ description?: string;
121
+ }>>;
122
+ searchGlobal(query: string, limit?: number, minDate?: number, maxDate?: number): Promise<Array<{
123
+ id: number;
124
+ text: string;
125
+ sender: string;
126
+ date: string;
127
+ chat: {
128
+ id: string;
129
+ name: string;
130
+ type: string;
131
+ username?: string;
132
+ };
133
+ media?: {
134
+ type: string;
135
+ fileName?: string;
136
+ size?: number;
137
+ };
119
138
  }>>;
120
139
  searchMessages(chatId: string, query: string, limit?: number, minDate?: number, maxDate?: number): Promise<Array<{
121
140
  id: number;
@@ -147,7 +166,24 @@ export declare class TelegramService {
147
166
  bio?: string;
148
167
  photo: boolean;
149
168
  lastSeen?: string;
169
+ premium?: boolean;
170
+ birthday?: string;
171
+ commonChatsCount?: number;
172
+ personalChannelId?: string;
173
+ businessWorkHours?: string;
174
+ businessLocation?: string;
150
175
  }>;
176
+ downloadProfilePhoto(entityId: string, options?: {
177
+ isBig?: boolean;
178
+ savePath?: string;
179
+ }): Promise<{
180
+ buffer: Buffer;
181
+ mimeType: string;
182
+ } | {
183
+ filePath: string;
184
+ } | null>;
185
+ /** Detect MIME type from buffer magic bytes */
186
+ private detectMimeFromBuffer;
151
187
  sendReaction(chatId: string, messageId: number, emoji?: string): Promise<void>;
152
188
  sendScheduledMessage(chatId: string, text: string, scheduleDate: number, replyTo?: number, parseMode?: "md" | "html"): Promise<void>;
153
189
  createPoll(chatId: string, question: string, answers: string[], options?: {
@@ -642,7 +642,12 @@ export class TelegramService {
642
642
  }
643
643
  for (const chat of result.chats) {
644
644
  if (chat instanceof Api.Chat) {
645
- chats.push({ id: chat.id.toString(), name: chat.title, type: "group" });
645
+ chats.push({
646
+ id: chat.id.toString(),
647
+ name: chat.title,
648
+ type: "group",
649
+ membersCount: chat.participantsCount ?? undefined,
650
+ });
646
651
  }
647
652
  else if (chat instanceof Api.Channel) {
648
653
  chats.push({
@@ -650,11 +655,104 @@ export class TelegramService {
650
655
  name: chat.title,
651
656
  type: chat.megagroup ? "group" : "channel",
652
657
  username: chat.username ?? undefined,
658
+ membersCount: chat.participantsCount ?? undefined,
653
659
  });
654
660
  }
655
661
  }
662
+ // Enrich channels/groups with description and accurate members count
663
+ for (const chat of chats) {
664
+ if (chat.type === "private")
665
+ continue;
666
+ try {
667
+ const entity = await this.client.getEntity(chat.id);
668
+ if (entity instanceof Api.Channel) {
669
+ const full = await this.client.invoke(new Api.channels.GetFullChannel({ channel: entity }));
670
+ if (full.fullChat instanceof Api.ChannelFull) {
671
+ chat.description = full.fullChat.about || undefined;
672
+ chat.membersCount = full.fullChat.participantsCount ?? chat.membersCount;
673
+ }
674
+ }
675
+ else if (entity instanceof Api.Chat) {
676
+ const full = await this.client.invoke(new Api.messages.GetFullChat({ chatId: entity.id }));
677
+ if (full.fullChat instanceof Api.ChatFull) {
678
+ chat.description = full.fullChat.about || undefined;
679
+ }
680
+ }
681
+ }
682
+ catch {
683
+ // Skip enrichment on error (private channels, etc.)
684
+ }
685
+ }
656
686
  return chats;
657
687
  }
688
+ async searchGlobal(query, limit = 20, minDate, maxDate) {
689
+ if (!this.client || !this.connected)
690
+ throw new Error("Not connected");
691
+ const result = await this.client.invoke(new Api.messages.SearchGlobal({
692
+ q: query,
693
+ filter: new Api.InputMessagesFilterEmpty(),
694
+ minDate: minDate || 0,
695
+ maxDate: maxDate || 0,
696
+ offsetRate: 0,
697
+ offsetPeer: new Api.InputPeerEmpty(),
698
+ offsetId: 0,
699
+ limit,
700
+ }));
701
+ const chatsMap = new Map();
702
+ if ("chats" in result) {
703
+ for (const chat of result.chats) {
704
+ if (chat instanceof Api.Channel) {
705
+ chatsMap.set(chat.id.toString(), {
706
+ id: chat.id.toString(),
707
+ name: chat.title,
708
+ type: chat.megagroup ? "group" : "channel",
709
+ username: chat.username ?? undefined,
710
+ });
711
+ }
712
+ else if (chat instanceof Api.Chat) {
713
+ chatsMap.set(chat.id.toString(), {
714
+ id: chat.id.toString(),
715
+ name: chat.title,
716
+ type: "group",
717
+ });
718
+ }
719
+ }
720
+ }
721
+ if ("users" in result) {
722
+ for (const user of result.users) {
723
+ if (user instanceof Api.User) {
724
+ const parts = [user.firstName, user.lastName].filter(Boolean);
725
+ chatsMap.set(user.id.toString(), {
726
+ id: user.id.toString(),
727
+ name: parts.join(" ") || "Unknown",
728
+ type: "private",
729
+ username: user.username ?? undefined,
730
+ });
731
+ }
732
+ }
733
+ }
734
+ const rawMessages = "messages" in result ? result.messages : [];
735
+ const messages = rawMessages.filter((m) => m instanceof Api.Message);
736
+ const results = await Promise.all(messages.map(async (m) => {
737
+ const peerId = m.peerId;
738
+ let chatId = "";
739
+ if (peerId instanceof Api.PeerChannel)
740
+ chatId = peerId.channelId.toString();
741
+ else if (peerId instanceof Api.PeerChat)
742
+ chatId = peerId.chatId.toString();
743
+ else if (peerId instanceof Api.PeerUser)
744
+ chatId = peerId.userId.toString();
745
+ return {
746
+ id: m.id,
747
+ text: m.message ?? "",
748
+ sender: await this.resolveSenderName(m.senderId),
749
+ date: new Date((m.date ?? 0) * 1000).toISOString(),
750
+ chat: chatsMap.get(chatId) || { id: chatId, name: "Unknown", type: "unknown" },
751
+ media: this.extractMediaInfo(m.media),
752
+ };
753
+ }));
754
+ return results;
755
+ }
658
756
  async searchMessages(chatId, query, limit = 20, minDate, maxDate) {
659
757
  if (!this.client || !this.connected)
660
758
  throw new Error("Not connected");
@@ -721,7 +819,8 @@ export class TelegramService {
721
819
  throw new Error("Entity is not a user");
722
820
  const inputEntity = await this.client.getInputEntity(userId);
723
821
  const fullResult = await this.client.invoke(new Api.users.GetFullUser({ id: inputEntity }));
724
- const bio = fullResult.fullUser.about ?? undefined;
822
+ const full = fullResult.fullUser;
823
+ const bio = full.about ?? undefined;
725
824
  const parts = [entity.firstName, entity.lastName].filter(Boolean);
726
825
  let lastSeen;
727
826
  if (entity.status instanceof Api.UserStatusOnline) {
@@ -739,6 +838,23 @@ export class TelegramService {
739
838
  else if (entity.status instanceof Api.UserStatusLastMonth) {
740
839
  lastSeen = "last month";
741
840
  }
841
+ let birthday;
842
+ if (full.birthday) {
843
+ const b = full.birthday;
844
+ birthday = b.year
845
+ ? `${b.year}-${String(b.month).padStart(2, "0")}-${String(b.day).padStart(2, "0")}`
846
+ : `${String(b.month).padStart(2, "0")}-${String(b.day).padStart(2, "0")}`;
847
+ }
848
+ let businessWorkHours;
849
+ if (full.businessWorkHours) {
850
+ const wh = full.businessWorkHours;
851
+ businessWorkHours = wh.timezoneId ?? "configured";
852
+ }
853
+ let businessLocation;
854
+ if (full.businessLocation) {
855
+ const loc = full.businessLocation;
856
+ businessLocation = loc.address ?? "configured";
857
+ }
742
858
  return {
743
859
  id: entity.id.toString(),
744
860
  name: parts.join(" ") || "Unknown",
@@ -747,8 +863,42 @@ export class TelegramService {
747
863
  bio,
748
864
  photo: !!entity.photo,
749
865
  lastSeen,
866
+ premium: entity.premium || undefined,
867
+ birthday,
868
+ commonChatsCount: full.commonChatsCount || undefined,
869
+ personalChannelId: full.personalChannelId ? full.personalChannelId.toString() : undefined,
870
+ businessWorkHours,
871
+ businessLocation,
750
872
  };
751
873
  }
874
+ async downloadProfilePhoto(entityId, options) {
875
+ if (!this.client || !this.connected)
876
+ throw new Error("Not connected");
877
+ const entity = await this.client.getEntity(entityId);
878
+ const buffer = (await this.client.downloadProfilePhoto(entity, {
879
+ isBig: options?.isBig !== false,
880
+ }));
881
+ if (!buffer || buffer.length === 0)
882
+ return null;
883
+ const mimeType = this.detectMimeFromBuffer(buffer);
884
+ if (options?.savePath) {
885
+ await writeFile(options.savePath, buffer);
886
+ return { filePath: options.savePath };
887
+ }
888
+ return { buffer, mimeType };
889
+ }
890
+ /** Detect MIME type from buffer magic bytes */
891
+ detectMimeFromBuffer(buffer) {
892
+ if (buffer[0] === 0xff && buffer[1] === 0xd8 && buffer[2] === 0xff)
893
+ return "image/jpeg";
894
+ if (buffer[0] === 0x89 && buffer[1] === 0x50 && buffer[2] === 0x4e && buffer[3] === 0x47)
895
+ return "image/png";
896
+ if (buffer[0] === 0x47 && buffer[1] === 0x49 && buffer[2] === 0x46)
897
+ return "image/gif";
898
+ if (buffer[0] === 0x52 && buffer[1] === 0x49 && buffer[2] === 0x46 && buffer[3] === 0x46)
899
+ return "image/webp";
900
+ return "image/jpeg"; // Telegram profile photos are almost always JPEG
901
+ }
752
902
  async sendReaction(chatId, messageId, emoji) {
753
903
  if (!this.client || !this.connected)
754
904
  throw new Error("Not connected");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@overpod/mcp-telegram",
3
- "version": "1.8.1",
3
+ "version": "1.10.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",