@overpod/mcp-telegram 1.1.1 → 1.4.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 +19 -3
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +0 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +25 -0
- package/dist/qr-login-cli.d.ts +2 -0
- package/dist/qr-login-cli.js +1 -20
- package/dist/telegram-client.d.ts +127 -0
- package/dist/telegram-client.js +139 -10
- package/package.json +6 -1
package/README.md
CHANGED
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
# MCP Telegram
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/@overpod/mcp-telegram)
|
|
4
|
-
[](https://www.npmjs.com/package/@overpod/mcp-telegram)
|
|
5
|
+
[](https://nodejs.org/)
|
|
6
|
+
[](https://www.typescriptlang.org/)
|
|
5
7
|
[](https://modelcontextprotocol.io/)
|
|
6
8
|
[](LICENSE)
|
|
9
|
+
[](https://glama.ai/mcp/servers/overpod/mcp-telegram)
|
|
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.
|
|
12
|
+
|
|
13
|
+
<p align="center">
|
|
14
|
+
<img src="assets/demo.gif" alt="MCP Telegram demo — connect and summarize chats in Claude" width="700">
|
|
15
|
+
</p>
|
|
7
16
|
|
|
8
17
|
An MCP (Model Context Protocol) server that connects AI assistants like Claude to Telegram via the MTProto protocol. Unlike bots, this runs as a **userbot** -- it operates under your personal Telegram account using [GramJS](https://github.com/nicedoc/gramjs), giving full access to your chats, contacts, and message history.
|
|
9
18
|
|
|
@@ -11,7 +20,7 @@ An MCP (Model Context Protocol) server that connects AI assistants like Claude t
|
|
|
11
20
|
|
|
12
21
|
- **MTProto protocol** -- direct Telegram API access, not the limited Bot API
|
|
13
22
|
- **Userbot** -- operates as your personal account, not a bot
|
|
14
|
-
- **
|
|
23
|
+
- **21 tools** -- messaging, chat management, media, contacts, and more
|
|
15
24
|
- **QR code login** -- authenticate by scanning a QR code in the Telegram app
|
|
16
25
|
- **Session persistence** -- login once, stay connected across restarts
|
|
17
26
|
- **Human-readable output** -- sender names are resolved, not just numeric IDs
|
|
@@ -174,6 +183,7 @@ const telegramMcp = new MCPClient({
|
|
|
174
183
|
| `telegram-mark-as-read` | Mark a chat as read |
|
|
175
184
|
| `telegram-get-chat-info` | Get detailed info about a chat (name, type, members count, description) |
|
|
176
185
|
| `telegram-get-chat-members` | List members of a group or channel |
|
|
186
|
+
| `telegram-join-chat` | Join a group or channel by username or invite link |
|
|
177
187
|
| `telegram-pin-message` | Pin a message in a chat |
|
|
178
188
|
| `telegram-unpin-message` | Unpin a message in a chat |
|
|
179
189
|
|
|
@@ -268,6 +278,12 @@ Most tools accept `chatId` as a string -- either a numeric ID (e.g., `"-10012345
|
|
|
268
278
|
| `messageId` | number | yes | Message ID to pin |
|
|
269
279
|
| `silent` | boolean | no | Pin without notification (default: false) |
|
|
270
280
|
|
|
281
|
+
### telegram-join-chat
|
|
282
|
+
|
|
283
|
+
| Parameter | Type | Required | Description |
|
|
284
|
+
|-----------|------|----------|-------------|
|
|
285
|
+
| `target` | string | yes | Username (@group), link (t.me/group), or invite link (t.me/+xxx) |
|
|
286
|
+
|
|
271
287
|
### telegram-search-messages
|
|
272
288
|
|
|
273
289
|
| Parameter | Type | Required | Description |
|
|
@@ -324,7 +340,7 @@ npm run format # Format code with Biome
|
|
|
324
340
|
|
|
325
341
|
```
|
|
326
342
|
src/
|
|
327
|
-
index.ts -- MCP server entry point,
|
|
343
|
+
index.ts -- MCP server entry point, 21 tool definitions
|
|
328
344
|
telegram-client.ts -- TelegramService class (GramJS wrapper)
|
|
329
345
|
qr-login-cli.ts -- CLI utility for QR code login
|
|
330
346
|
```
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
CHANGED
|
File without changes
|
package/dist/index.d.ts
ADDED
package/dist/index.js
CHANGED
|
@@ -418,6 +418,31 @@ server.tool("telegram-get-profile", "Get detailed profile info of a Telegram use
|
|
|
418
418
|
return { content: [{ type: "text", text: `Error: ${e.message}` }] };
|
|
419
419
|
}
|
|
420
420
|
});
|
|
421
|
+
server.tool("telegram-join-chat", "Join a Telegram group or channel by username or invite link", {
|
|
422
|
+
target: z
|
|
423
|
+
.string()
|
|
424
|
+
.describe("Username (@group), link (t.me/group), or invite link (t.me/+xxx)"),
|
|
425
|
+
}, async ({ target }) => {
|
|
426
|
+
const err = await requireConnection();
|
|
427
|
+
if (err)
|
|
428
|
+
return { content: [{ type: "text", text: err }] };
|
|
429
|
+
try {
|
|
430
|
+
const result = await telegram.joinChat(target);
|
|
431
|
+
return {
|
|
432
|
+
content: [
|
|
433
|
+
{
|
|
434
|
+
type: "text",
|
|
435
|
+
text: `Joined ${result.type}: ${result.title} (ID: ${result.id})`,
|
|
436
|
+
},
|
|
437
|
+
],
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
catch (e) {
|
|
441
|
+
return {
|
|
442
|
+
content: [{ type: "text", text: `Error: ${e.message}` }],
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
});
|
|
421
446
|
// --- Start ---
|
|
422
447
|
async function main() {
|
|
423
448
|
// Try to auto-connect with saved session
|
package/dist/qr-login-cli.js
CHANGED
|
@@ -1,13 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import "dotenv/config";
|
|
3
|
-
import { exec } from "node:child_process";
|
|
4
|
-
import { writeFile } from "node:fs/promises";
|
|
5
|
-
import { dirname, join } from "node:path";
|
|
6
|
-
import { fileURLToPath } from "node:url";
|
|
7
3
|
import QRCode from "qrcode";
|
|
8
4
|
import { TelegramService } from "./telegram-client.js";
|
|
9
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
10
|
-
const QR_IMAGE_PATH = join(__dirname, "..", "qr-login.png");
|
|
11
5
|
const API_ID = Number(process.env.TELEGRAM_API_ID);
|
|
12
6
|
const API_HASH = process.env.TELEGRAM_API_HASH;
|
|
13
7
|
if (!API_ID || !API_HASH) {
|
|
@@ -15,10 +9,6 @@ if (!API_ID || !API_HASH) {
|
|
|
15
9
|
process.exit(1);
|
|
16
10
|
}
|
|
17
11
|
const telegram = new TelegramService(API_ID, API_HASH);
|
|
18
|
-
function openFile(path) {
|
|
19
|
-
const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
20
|
-
exec(`${cmd} "${path}"`);
|
|
21
|
-
}
|
|
22
12
|
async function main() {
|
|
23
13
|
// Check if already connected
|
|
24
14
|
await telegram.loadSession();
|
|
@@ -31,16 +21,7 @@ async function main() {
|
|
|
31
21
|
console.log("\nStarting Telegram QR login...\n");
|
|
32
22
|
console.log("Scan the QR code in Telegram app:");
|
|
33
23
|
console.log(" Settings > Devices > Link Desktop Device\n");
|
|
34
|
-
const result = await telegram.startQrLogin(
|
|
35
|
-
// onQrDataUrl — save as PNG and open
|
|
36
|
-
async (dataUrl) => {
|
|
37
|
-
const base64 = dataUrl.replace(/^data:image\/png;base64,/, "");
|
|
38
|
-
await writeFile(QR_IMAGE_PATH, Buffer.from(base64, "base64"));
|
|
39
|
-
console.log(`QR saved: ${QR_IMAGE_PATH}`);
|
|
40
|
-
openFile(QR_IMAGE_PATH);
|
|
41
|
-
},
|
|
42
|
-
// onQrUrl — also show in terminal
|
|
43
|
-
async (url) => {
|
|
24
|
+
const result = await telegram.startQrLogin(() => { }, async (url) => {
|
|
44
25
|
const terminalQr = await QRCode.toString(url, { type: "terminal", small: true });
|
|
45
26
|
console.log(terminalQr);
|
|
46
27
|
console.log("Waiting for scan...\n");
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
export declare class TelegramService {
|
|
2
|
+
private client;
|
|
3
|
+
private apiId;
|
|
4
|
+
private apiHash;
|
|
5
|
+
private sessionString;
|
|
6
|
+
private connected;
|
|
7
|
+
lastError: string;
|
|
8
|
+
constructor(apiId: number, apiHash: string);
|
|
9
|
+
loadSession(): Promise<boolean>;
|
|
10
|
+
/** Set session string in memory (for programmatic / hosted use) */
|
|
11
|
+
setSessionString(session: string): void;
|
|
12
|
+
/** Get the current session string (for external persistence) */
|
|
13
|
+
getSessionString(): string;
|
|
14
|
+
private saveSession;
|
|
15
|
+
connect(): Promise<boolean>;
|
|
16
|
+
clearSession(): Promise<void>;
|
|
17
|
+
/** Ensure connection is active, auto-reconnect if session exists */
|
|
18
|
+
ensureConnected(): Promise<boolean>;
|
|
19
|
+
disconnect(): Promise<void>;
|
|
20
|
+
/**
|
|
21
|
+
* Log out from Telegram completely — terminates the session on Telegram servers.
|
|
22
|
+
* After this, the session string becomes invalid and won't appear in "Active Sessions".
|
|
23
|
+
*/
|
|
24
|
+
logOut(): Promise<boolean>;
|
|
25
|
+
isConnected(): boolean;
|
|
26
|
+
startQrLogin(onQrDataUrl: (dataUrl: string) => void, onQrUrl?: (url: string) => void): Promise<{
|
|
27
|
+
success: boolean;
|
|
28
|
+
message: string;
|
|
29
|
+
}>;
|
|
30
|
+
getMe(): Promise<{
|
|
31
|
+
id: string;
|
|
32
|
+
username?: string;
|
|
33
|
+
firstName?: string;
|
|
34
|
+
}>;
|
|
35
|
+
sendMessage(chatId: string, text: string, replyTo?: number, parseMode?: "md" | "html"): Promise<void>;
|
|
36
|
+
sendFile(chatId: string, filePath: string, caption?: string): Promise<void>;
|
|
37
|
+
downloadMedia(chatId: string, messageId: number, downloadPath: string): Promise<string>;
|
|
38
|
+
downloadMediaAsBuffer(chatId: string, messageId: number): Promise<{
|
|
39
|
+
buffer: Buffer;
|
|
40
|
+
mimeType: string;
|
|
41
|
+
}>;
|
|
42
|
+
/** Detect MIME type from buffer magic bytes, falling back to media metadata */
|
|
43
|
+
private detectMimeType;
|
|
44
|
+
pinMessage(chatId: string, messageId: number, silent?: boolean): Promise<void>;
|
|
45
|
+
unpinMessage(chatId: string, messageId: number): Promise<void>;
|
|
46
|
+
getDialogs(limit?: number, offsetDate?: number, filterType?: "private" | "group" | "channel"): Promise<Array<{
|
|
47
|
+
id: string;
|
|
48
|
+
name: string;
|
|
49
|
+
type: string;
|
|
50
|
+
unreadCount: number;
|
|
51
|
+
}>>;
|
|
52
|
+
getUnreadDialogs(limit?: number): Promise<Array<{
|
|
53
|
+
id: string;
|
|
54
|
+
name: string;
|
|
55
|
+
type: string;
|
|
56
|
+
unreadCount: number;
|
|
57
|
+
}>>;
|
|
58
|
+
markAsRead(chatId: string): Promise<void>;
|
|
59
|
+
forwardMessage(fromChatId: string, toChatId: string, messageIds: number[]): Promise<void>;
|
|
60
|
+
editMessage(chatId: string, messageId: number, newText: string): Promise<void>;
|
|
61
|
+
deleteMessages(chatId: string, messageIds: number[]): Promise<void>;
|
|
62
|
+
getChatInfo(chatId: string): Promise<{
|
|
63
|
+
id: string;
|
|
64
|
+
name: string;
|
|
65
|
+
type: string;
|
|
66
|
+
username?: string;
|
|
67
|
+
description?: string;
|
|
68
|
+
membersCount?: number;
|
|
69
|
+
}>;
|
|
70
|
+
/** Extract media info from a message */
|
|
71
|
+
private extractMediaInfo;
|
|
72
|
+
/** Resolve sender ID to a display name */
|
|
73
|
+
private resolveSenderName;
|
|
74
|
+
getMessages(chatId: string, limit?: number, offsetId?: number, minDate?: number, maxDate?: number): Promise<Array<{
|
|
75
|
+
id: number;
|
|
76
|
+
text: string;
|
|
77
|
+
sender: string;
|
|
78
|
+
date: string;
|
|
79
|
+
media?: {
|
|
80
|
+
type: string;
|
|
81
|
+
fileName?: string;
|
|
82
|
+
size?: number;
|
|
83
|
+
};
|
|
84
|
+
}>>;
|
|
85
|
+
searchChats(query: string, limit?: number): Promise<Array<{
|
|
86
|
+
id: string;
|
|
87
|
+
name: string;
|
|
88
|
+
type: string;
|
|
89
|
+
username?: string;
|
|
90
|
+
}>>;
|
|
91
|
+
searchMessages(chatId: string, query: string, limit?: number, minDate?: number, maxDate?: number): Promise<Array<{
|
|
92
|
+
id: number;
|
|
93
|
+
text: string;
|
|
94
|
+
sender: string;
|
|
95
|
+
date: string;
|
|
96
|
+
media?: {
|
|
97
|
+
type: string;
|
|
98
|
+
fileName?: string;
|
|
99
|
+
size?: number;
|
|
100
|
+
};
|
|
101
|
+
}>>;
|
|
102
|
+
getContacts(limit?: number): Promise<Array<{
|
|
103
|
+
id: string;
|
|
104
|
+
name: string;
|
|
105
|
+
username?: string;
|
|
106
|
+
phone?: string;
|
|
107
|
+
}>>;
|
|
108
|
+
getChatMembers(chatId: string, limit?: number): Promise<Array<{
|
|
109
|
+
id: string;
|
|
110
|
+
name: string;
|
|
111
|
+
username?: string;
|
|
112
|
+
}>>;
|
|
113
|
+
getProfile(userId: string): Promise<{
|
|
114
|
+
id: string;
|
|
115
|
+
name: string;
|
|
116
|
+
username?: string;
|
|
117
|
+
phone?: string;
|
|
118
|
+
bio?: string;
|
|
119
|
+
photo: boolean;
|
|
120
|
+
lastSeen?: string;
|
|
121
|
+
}>;
|
|
122
|
+
joinChat(target: string): Promise<{
|
|
123
|
+
id: string;
|
|
124
|
+
title: string;
|
|
125
|
+
type: string;
|
|
126
|
+
}>;
|
|
127
|
+
}
|
package/dist/telegram-client.js
CHANGED
|
@@ -27,9 +27,22 @@ export class TelegramService {
|
|
|
27
27
|
}
|
|
28
28
|
return false;
|
|
29
29
|
}
|
|
30
|
+
/** Set session string in memory (for programmatic / hosted use) */
|
|
31
|
+
setSessionString(session) {
|
|
32
|
+
this.sessionString = session;
|
|
33
|
+
}
|
|
34
|
+
/** Get the current session string (for external persistence) */
|
|
35
|
+
getSessionString() {
|
|
36
|
+
return this.sessionString;
|
|
37
|
+
}
|
|
30
38
|
async saveSession(session) {
|
|
31
39
|
this.sessionString = session;
|
|
32
|
-
|
|
40
|
+
try {
|
|
41
|
+
await writeFile(SESSION_FILE, session, "utf-8");
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
// File write may fail in containerized environments — session string is still in memory
|
|
45
|
+
}
|
|
33
46
|
}
|
|
34
47
|
async connect() {
|
|
35
48
|
if (this.connected && this.client)
|
|
@@ -96,11 +109,32 @@ export class TelegramService {
|
|
|
96
109
|
}
|
|
97
110
|
async disconnect() {
|
|
98
111
|
if (this.client && this.connected) {
|
|
99
|
-
await this.client.
|
|
112
|
+
await this.client.destroy();
|
|
100
113
|
this.connected = false;
|
|
101
114
|
this.client = null;
|
|
102
115
|
}
|
|
103
116
|
}
|
|
117
|
+
/**
|
|
118
|
+
* Log out from Telegram completely — terminates the session on Telegram servers.
|
|
119
|
+
* After this, the session string becomes invalid and won't appear in "Active Sessions".
|
|
120
|
+
*/
|
|
121
|
+
async logOut() {
|
|
122
|
+
if (!this.client || !this.connected)
|
|
123
|
+
return false;
|
|
124
|
+
try {
|
|
125
|
+
await this.client.invoke(new Api.auth.LogOut());
|
|
126
|
+
await this.client.destroy();
|
|
127
|
+
this.connected = false;
|
|
128
|
+
this.sessionString = "";
|
|
129
|
+
this.client = null;
|
|
130
|
+
return true;
|
|
131
|
+
}
|
|
132
|
+
catch (error) {
|
|
133
|
+
console.error("[telegram] logOut error:", error);
|
|
134
|
+
await this.disconnect();
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
104
138
|
isConnected() {
|
|
105
139
|
return this.connected;
|
|
106
140
|
}
|
|
@@ -166,19 +200,20 @@ export class TelegramService {
|
|
|
166
200
|
}
|
|
167
201
|
if (resolved) {
|
|
168
202
|
const newSession = client.session.save();
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
203
|
+
// Adopt the QR login client directly instead of destroy+reconnect
|
|
204
|
+
// This avoids creating a second Telegram session from DC migration auth keys
|
|
205
|
+
this.client = client;
|
|
172
206
|
this.sessionString = newSession;
|
|
173
|
-
|
|
207
|
+
this.connected = true;
|
|
208
|
+
await this.saveSession(newSession);
|
|
174
209
|
return { success: true, message: "Telegram login successful" };
|
|
175
210
|
}
|
|
176
|
-
await client.
|
|
211
|
+
await client.destroy();
|
|
177
212
|
return { success: false, message: "QR login timeout" };
|
|
178
213
|
}
|
|
179
214
|
catch (err) {
|
|
180
215
|
try {
|
|
181
|
-
await client.
|
|
216
|
+
await client.destroy();
|
|
182
217
|
}
|
|
183
218
|
catch { }
|
|
184
219
|
return { success: false, message: `Login failed: ${err.message}` };
|
|
@@ -224,6 +259,41 @@ export class TelegramService {
|
|
|
224
259
|
await writeFile(downloadPath, buffer);
|
|
225
260
|
return downloadPath;
|
|
226
261
|
}
|
|
262
|
+
async downloadMediaAsBuffer(chatId, messageId) {
|
|
263
|
+
if (!this.client || !this.connected)
|
|
264
|
+
throw new Error("Not connected");
|
|
265
|
+
const messages = await this.client.getMessages(chatId, { ids: [messageId] });
|
|
266
|
+
const message = messages[0];
|
|
267
|
+
if (!message)
|
|
268
|
+
throw new Error(`Message ${messageId} not found`);
|
|
269
|
+
if (!message.media)
|
|
270
|
+
throw new Error(`Message ${messageId} has no media`);
|
|
271
|
+
const buffer = (await this.client.downloadMedia(message));
|
|
272
|
+
if (!buffer)
|
|
273
|
+
throw new Error("Failed to download media");
|
|
274
|
+
const mimeType = this.detectMimeType(buffer, message.media);
|
|
275
|
+
return { buffer, mimeType };
|
|
276
|
+
}
|
|
277
|
+
/** Detect MIME type from buffer magic bytes, falling back to media metadata */
|
|
278
|
+
detectMimeType(buffer, media) {
|
|
279
|
+
// Check magic bytes first
|
|
280
|
+
if (buffer[0] === 0xff && buffer[1] === 0xd8 && buffer[2] === 0xff)
|
|
281
|
+
return "image/jpeg";
|
|
282
|
+
if (buffer[0] === 0x89 && buffer[1] === 0x50 && buffer[2] === 0x4e && buffer[3] === 0x47)
|
|
283
|
+
return "image/png";
|
|
284
|
+
if (buffer[0] === 0x47 && buffer[1] === 0x49 && buffer[2] === 0x46)
|
|
285
|
+
return "image/gif";
|
|
286
|
+
if (buffer[0] === 0x52 && buffer[1] === 0x49 && buffer[2] === 0x46 && buffer[3] === 0x46)
|
|
287
|
+
return "image/webp";
|
|
288
|
+
// Fall back to document mimeType
|
|
289
|
+
const m = media;
|
|
290
|
+
const doc = m.document;
|
|
291
|
+
if (doc?.mimeType)
|
|
292
|
+
return doc.mimeType;
|
|
293
|
+
if (m.photo)
|
|
294
|
+
return "image/jpeg";
|
|
295
|
+
return "application/octet-stream";
|
|
296
|
+
}
|
|
227
297
|
async pinMessage(chatId, messageId, silent = false) {
|
|
228
298
|
if (!this.client || !this.connected)
|
|
229
299
|
throw new Error("Not connected");
|
|
@@ -295,20 +365,48 @@ export class TelegramService {
|
|
|
295
365
|
};
|
|
296
366
|
}
|
|
297
367
|
if (entity instanceof Api.Channel) {
|
|
368
|
+
let membersCount = entity.participantsCount ?? undefined;
|
|
369
|
+
let description;
|
|
370
|
+
try {
|
|
371
|
+
const full = await this.client.invoke(new Api.channels.GetFullChannel({ channel: entity }));
|
|
372
|
+
if (full.fullChat instanceof Api.ChannelFull) {
|
|
373
|
+
membersCount = membersCount ?? full.fullChat.participantsCount ?? undefined;
|
|
374
|
+
description = full.fullChat.about || undefined;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
catch {
|
|
378
|
+
// May fail for some channels — fall back to basic info
|
|
379
|
+
}
|
|
298
380
|
return {
|
|
299
381
|
id: entity.id.toString(),
|
|
300
382
|
name: entity.title,
|
|
301
383
|
type: entity.megagroup ? "group" : "channel",
|
|
302
384
|
username: entity.username ?? undefined,
|
|
303
|
-
|
|
385
|
+
description,
|
|
386
|
+
membersCount,
|
|
304
387
|
};
|
|
305
388
|
}
|
|
306
389
|
if (entity instanceof Api.Chat) {
|
|
390
|
+
let membersCount = entity.participantsCount ?? undefined;
|
|
391
|
+
let description;
|
|
392
|
+
try {
|
|
393
|
+
const full = await this.client.invoke(new Api.messages.GetFullChat({ chatId: entity.id }));
|
|
394
|
+
if (full.fullChat instanceof Api.ChatFull) {
|
|
395
|
+
if (!membersCount && full.fullChat.participants instanceof Api.ChatParticipants) {
|
|
396
|
+
membersCount = full.fullChat.participants.participants.length;
|
|
397
|
+
}
|
|
398
|
+
description = full.fullChat.about || undefined;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
catch {
|
|
402
|
+
// Fall back to basic info
|
|
403
|
+
}
|
|
307
404
|
return {
|
|
308
405
|
id: entity.id.toString(),
|
|
309
406
|
name: entity.title,
|
|
310
407
|
type: "group",
|
|
311
|
-
|
|
408
|
+
description,
|
|
409
|
+
membersCount,
|
|
312
410
|
};
|
|
313
411
|
}
|
|
314
412
|
return { id: chatId, name: "Unknown", type: "unknown" };
|
|
@@ -505,4 +603,35 @@ export class TelegramService {
|
|
|
505
603
|
lastSeen,
|
|
506
604
|
};
|
|
507
605
|
}
|
|
606
|
+
async joinChat(target) {
|
|
607
|
+
if (!this.client)
|
|
608
|
+
throw new Error("Not connected");
|
|
609
|
+
// Extract invite hash from various link formats
|
|
610
|
+
const inviteMatch = target.match(/(?:t\.me\/\+|t\.me\/joinchat\/|tg:\/\/join\?invite=)([a-zA-Z0-9_-]+)/);
|
|
611
|
+
if (inviteMatch) {
|
|
612
|
+
const result = await this.client.invoke(new Api.messages.ImportChatInvite({ hash: inviteMatch[1] }));
|
|
613
|
+
const chat = result.chats?.[0];
|
|
614
|
+
if (!chat)
|
|
615
|
+
throw new Error("Failed to join via invite link");
|
|
616
|
+
return {
|
|
617
|
+
id: chat.id.toString(),
|
|
618
|
+
title: chat.title ?? "Unknown",
|
|
619
|
+
type: chat.className === "Channel" ? "channel" : "group",
|
|
620
|
+
};
|
|
621
|
+
}
|
|
622
|
+
// Public channel/group by username
|
|
623
|
+
const username = target.replace(/^@/, "").replace(/^https?:\/\/t\.me\//, "");
|
|
624
|
+
const entity = await this.client.getEntity(username);
|
|
625
|
+
if (entity instanceof Api.Channel || entity instanceof Api.Chat) {
|
|
626
|
+
await this.client.invoke(new Api.channels.JoinChannel({
|
|
627
|
+
channel: entity,
|
|
628
|
+
}));
|
|
629
|
+
return {
|
|
630
|
+
id: entity.id.toString(),
|
|
631
|
+
title: entity.title ?? "Unknown",
|
|
632
|
+
type: entity.className === "Channel" ? "channel" : "group",
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
throw new Error("Target is not a group or channel. Use username, @username, or invite link.");
|
|
636
|
+
}
|
|
508
637
|
}
|
package/package.json
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@overpod/mcp-telegram",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "MCP server for Telegram userbot — 20 tools for messages, media, contacts & more. Built on GramJS/MTProto.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./dist/index.js",
|
|
9
|
+
"./service": "./dist/telegram-client.js"
|
|
10
|
+
},
|
|
7
11
|
"bin": {
|
|
8
12
|
"mcp-telegram": "dist/cli.js"
|
|
9
13
|
},
|
|
@@ -17,6 +21,7 @@
|
|
|
17
21
|
"start": "node dist/index.js",
|
|
18
22
|
"login": "node dist/qr-login-cli.js",
|
|
19
23
|
"build": "tsc",
|
|
24
|
+
"typecheck": "tsc --noEmit",
|
|
20
25
|
"prepublishOnly": "npm run build",
|
|
21
26
|
"lint": "biome check src/",
|
|
22
27
|
"lint:fix": "biome check --fix src/",
|