@overpod/mcp-telegram 1.39.0 → 1.40.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/CHANGELOG.md +14 -0
- package/dist/telegram-client.d.ts +29 -1
- package/dist/telegram-client.js +99 -7
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,20 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [1.40.0](https://github.com/mcp-telegram/mcp-telegram/compare/v1.39.1...v1.40.0) (2026-06-23)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
* **media:** add thumbnail option to downloadMediaAsBuffer ([#64](https://github.com/mcp-telegram/mcp-telegram/issues/64)) ([d9f302c](https://github.com/mcp-telegram/mcp-telegram/commit/d9f302c8efde73a8d86467b23d05b8a12650662f))
|
|
14
|
+
|
|
15
|
+
## [1.39.1](https://github.com/mcp-telegram/mcp-telegram/compare/v1.39.0...v1.39.1) (2026-06-21)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
|
|
20
|
+
* **resolve:** recover bare numeric peer IDs that lack access_hash ([#62](https://github.com/mcp-telegram/mcp-telegram/issues/62)) ([678077a](https://github.com/mcp-telegram/mcp-telegram/commit/678077a78ce3a5852e07d5d831f664c3e08a81e3))
|
|
21
|
+
|
|
8
22
|
## [1.39.0](https://github.com/mcp-telegram/mcp-telegram/compare/v1.38.2...v1.39.0) (2026-06-21)
|
|
9
23
|
|
|
10
24
|
|
|
@@ -142,9 +142,30 @@ export declare class TelegramService {
|
|
|
142
142
|
ids: number[];
|
|
143
143
|
}>;
|
|
144
144
|
downloadMedia(chatId: string, messageId: number, downloadPath: string): Promise<string>;
|
|
145
|
-
|
|
145
|
+
/**
|
|
146
|
+
* Download a message's media into memory.
|
|
147
|
+
*
|
|
148
|
+
* When `options.thumb` is given, GramJS fetches that thumbnail size instead of
|
|
149
|
+
* the full file (`0` = smallest, larger indexes = bigger previews). This keeps
|
|
150
|
+
* payloads tiny — useful for callers that inline the bytes and pay per byte
|
|
151
|
+
* (e.g. base64 in an LLM context). If the message has no thumbnail at that
|
|
152
|
+
* index, GramJS returns an empty/`undefined` buffer; we transparently fall
|
|
153
|
+
* back to the full file so the caller always gets bytes. `isThumb` reports
|
|
154
|
+
* which one was served.
|
|
155
|
+
*
|
|
156
|
+
* Note: GramJS signals "no bytes" inconsistently — `undefined` for a missing
|
|
157
|
+
* thumbnail, but `Buffer.alloc(0)` for undownloadable media (geo/poll/dice,
|
|
158
|
+
* documents without a file, and several failure paths). An empty Buffer is
|
|
159
|
+
* truthy, so we guard on `.length`, not truthiness, in both the fallback and
|
|
160
|
+
* the final failure check — otherwise a 0-byte buffer would be returned as a
|
|
161
|
+
* "successful" (but garbage) download.
|
|
162
|
+
*/
|
|
163
|
+
downloadMediaAsBuffer(chatId: string, messageId: number, options?: {
|
|
164
|
+
thumb?: number;
|
|
165
|
+
}): Promise<{
|
|
146
166
|
buffer: Buffer;
|
|
147
167
|
mimeType: string;
|
|
168
|
+
isThumb: boolean;
|
|
148
169
|
}>;
|
|
149
170
|
/** Detect MIME type from buffer magic bytes, falling back to media metadata */
|
|
150
171
|
private detectMimeType;
|
|
@@ -276,6 +297,13 @@ export declare class TelegramService {
|
|
|
276
297
|
* Handles display names by searching dialogs.
|
|
277
298
|
*/
|
|
278
299
|
private resolvePeer;
|
|
300
|
+
/**
|
|
301
|
+
* Resolve a bare numeric ID to a cached/dialog entity so GramJS can build a
|
|
302
|
+
* valid InputPeer. Falls back to the raw ID string if no dialog matches —
|
|
303
|
+
* GramJS may still resolve it (e.g. a contact or a peer it has messaged),
|
|
304
|
+
* and we must not regress that path.
|
|
305
|
+
*/
|
|
306
|
+
private resolveNumericPeer;
|
|
279
307
|
getChatInfo(chatId: string): Promise<{
|
|
280
308
|
id: string;
|
|
281
309
|
name: string;
|
package/dist/telegram-client.js
CHANGED
|
@@ -708,7 +708,25 @@ export class TelegramService {
|
|
|
708
708
|
await writeFile(downloadPath, buffer);
|
|
709
709
|
return downloadPath;
|
|
710
710
|
}
|
|
711
|
-
|
|
711
|
+
/**
|
|
712
|
+
* Download a message's media into memory.
|
|
713
|
+
*
|
|
714
|
+
* When `options.thumb` is given, GramJS fetches that thumbnail size instead of
|
|
715
|
+
* the full file (`0` = smallest, larger indexes = bigger previews). This keeps
|
|
716
|
+
* payloads tiny — useful for callers that inline the bytes and pay per byte
|
|
717
|
+
* (e.g. base64 in an LLM context). If the message has no thumbnail at that
|
|
718
|
+
* index, GramJS returns an empty/`undefined` buffer; we transparently fall
|
|
719
|
+
* back to the full file so the caller always gets bytes. `isThumb` reports
|
|
720
|
+
* which one was served.
|
|
721
|
+
*
|
|
722
|
+
* Note: GramJS signals "no bytes" inconsistently — `undefined` for a missing
|
|
723
|
+
* thumbnail, but `Buffer.alloc(0)` for undownloadable media (geo/poll/dice,
|
|
724
|
+
* documents without a file, and several failure paths). An empty Buffer is
|
|
725
|
+
* truthy, so we guard on `.length`, not truthiness, in both the fallback and
|
|
726
|
+
* the final failure check — otherwise a 0-byte buffer would be returned as a
|
|
727
|
+
* "successful" (but garbage) download.
|
|
728
|
+
*/
|
|
729
|
+
async downloadMediaAsBuffer(chatId, messageId, options) {
|
|
712
730
|
if (!this.client || !this.connected)
|
|
713
731
|
throw new Error(NOT_CONNECTED_ERROR);
|
|
714
732
|
const resolved = await this.resolvePeer(chatId);
|
|
@@ -718,11 +736,19 @@ export class TelegramService {
|
|
|
718
736
|
throw new Error(`Message ${messageId} not found`);
|
|
719
737
|
if (!message.media)
|
|
720
738
|
throw new Error(`Message ${messageId} has no media`);
|
|
721
|
-
|
|
722
|
-
|
|
739
|
+
let isThumb = false;
|
|
740
|
+
let buffer;
|
|
741
|
+
if (options?.thumb !== undefined) {
|
|
742
|
+
buffer = (await this.client.downloadMedia(message, { thumb: options.thumb }));
|
|
743
|
+
isThumb = !!buffer?.length;
|
|
744
|
+
}
|
|
745
|
+
// No thumb requested, or this media has no thumbnail at that size → full file.
|
|
746
|
+
if (!buffer?.length)
|
|
747
|
+
buffer = (await this.client.downloadMedia(message));
|
|
748
|
+
if (!buffer?.length)
|
|
723
749
|
throw new Error("Failed to download media");
|
|
724
750
|
const mimeType = this.detectMimeType(buffer, message.media);
|
|
725
|
-
return { buffer, mimeType };
|
|
751
|
+
return { buffer, mimeType, isThumb };
|
|
726
752
|
}
|
|
727
753
|
/** Detect MIME type from buffer magic bytes, falling back to media metadata */
|
|
728
754
|
detectMimeType(buffer, media) {
|
|
@@ -1191,12 +1217,78 @@ export class TelegramService {
|
|
|
1191
1217
|
// Normalize '@me' — GramJS only intercepts the plain 'me' string as InputPeerSelf
|
|
1192
1218
|
if (chatId === "@me")
|
|
1193
1219
|
return "me";
|
|
1194
|
-
//
|
|
1195
|
-
if (
|
|
1220
|
+
// @usernames resolve directly via contacts.ResolveUsername
|
|
1221
|
+
if (chatId.startsWith("@"))
|
|
1196
1222
|
return chatId;
|
|
1197
|
-
//
|
|
1223
|
+
// Bare numeric IDs need an entity with access_hash. GramJS can build an
|
|
1224
|
+
// InputPeer from a raw number only if it's already cached or the account
|
|
1225
|
+
// is a contact / has messaged us — otherwise getInputEntity throws
|
|
1226
|
+
// "Could not find the input entity". A bare positive number is also
|
|
1227
|
+
// ambiguous (GramJS assumes PeerUser, so channel IDs fail outright).
|
|
1228
|
+
// Recover by looking the ID up among dialogs, which yields a full entity
|
|
1229
|
+
// (with access_hash) for both users and channels.
|
|
1230
|
+
if (/^-?\d+$/.test(chatId))
|
|
1231
|
+
return this.resolveNumericPeer(chatId);
|
|
1232
|
+
// Everything else — resolve display name via dialogs
|
|
1198
1233
|
return this.resolveChat(chatId);
|
|
1199
1234
|
}
|
|
1235
|
+
/**
|
|
1236
|
+
* Resolve a bare numeric ID to a cached/dialog entity so GramJS can build a
|
|
1237
|
+
* valid InputPeer. Falls back to the raw ID string if no dialog matches —
|
|
1238
|
+
* GramJS may still resolve it (e.g. a contact or a peer it has messaged),
|
|
1239
|
+
* and we must not regress that path.
|
|
1240
|
+
*/
|
|
1241
|
+
async resolveNumericPeer(chatId) {
|
|
1242
|
+
if (!this.client)
|
|
1243
|
+
throw new Error(NOT_CONNECTED_ERROR);
|
|
1244
|
+
const cached = this.entityCache.get(chatId);
|
|
1245
|
+
if (cached)
|
|
1246
|
+
return cached;
|
|
1247
|
+
// Direct resolve first — succeeds when GramJS already knows the peer.
|
|
1248
|
+
try {
|
|
1249
|
+
const entity = await this.client.getEntity(chatId);
|
|
1250
|
+
this.entityCache.set(chatId, entity);
|
|
1251
|
+
return entity;
|
|
1252
|
+
}
|
|
1253
|
+
catch {
|
|
1254
|
+
// Fall through to dialog scan.
|
|
1255
|
+
}
|
|
1256
|
+
// Scan dialogs for a matching entity. IDs reach us in two shapes:
|
|
1257
|
+
// • bare positive (e.g. 1004294063929 for a channel, 8959122940 for a
|
|
1258
|
+
// user) — this is what list-chats/search emit and what GramJS can't
|
|
1259
|
+
// disambiguate; match it against any entity's bare id.
|
|
1260
|
+
// • marked (-100<id> for channels, -<id> for basic groups) — the
|
|
1261
|
+
// sign/-100 prefix carries the type, so require the entity to match that
|
|
1262
|
+
// exact marked form, otherwise a group "-123" could wrongly match a user
|
|
1263
|
+
// with bare id 123.
|
|
1264
|
+
const isMarked = chatId.startsWith("-");
|
|
1265
|
+
try {
|
|
1266
|
+
const dialogs = await this.client.getDialogs({ limit: 100 });
|
|
1267
|
+
const match = dialogs.find((d) => {
|
|
1268
|
+
const entity = d.entity;
|
|
1269
|
+
if (!entity?.id)
|
|
1270
|
+
return false;
|
|
1271
|
+
const bare = entity.id.toString();
|
|
1272
|
+
if (!isMarked)
|
|
1273
|
+
return chatId === bare;
|
|
1274
|
+
// Marked input must match the entity's marked form.
|
|
1275
|
+
if (entity instanceof Api.Channel)
|
|
1276
|
+
return chatId === `-100${bare}`;
|
|
1277
|
+
if (entity instanceof Api.Chat)
|
|
1278
|
+
return chatId === `-${bare}`;
|
|
1279
|
+
return false;
|
|
1280
|
+
});
|
|
1281
|
+
if (match?.entity) {
|
|
1282
|
+
this.entityCache.set(chatId, match.entity);
|
|
1283
|
+
return match.entity;
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
catch {
|
|
1287
|
+
// Dialog fetch failed — fall back to the raw ID below.
|
|
1288
|
+
}
|
|
1289
|
+
// Last resort: hand the raw ID to GramJS and let it try GetUsers/GetChannels.
|
|
1290
|
+
return chatId;
|
|
1291
|
+
}
|
|
1200
1292
|
async getChatInfo(chatId) {
|
|
1201
1293
|
if (!this.client || !this.connected)
|
|
1202
1294
|
throw new Error(NOT_CONNECTED_ERROR);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@overpod/mcp-telegram",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.40.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",
|
|
@@ -72,7 +72,7 @@
|
|
|
72
72
|
},
|
|
73
73
|
"devDependencies": {
|
|
74
74
|
"@biomejs/biome": "^2.5.0",
|
|
75
|
-
"@types/node": "^
|
|
75
|
+
"@types/node": "^26.0.0",
|
|
76
76
|
"@types/qrcode": "^1.5.6",
|
|
77
77
|
"c8": "^11.0.0",
|
|
78
78
|
"tsx": "^4.22.4",
|