@overpod/mcp-telegram 1.18.0 → 1.19.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 +56 -1
- package/dist/__tests__/tools/shared.test.d.ts +1 -0
- package/dist/__tests__/tools/shared.test.js +110 -0
- package/dist/index.js +10 -9
- package/dist/telegram-client.d.ts +1 -0
- package/dist/telegram-client.js +3 -0
- package/dist/tools/auth.js +7 -11
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -138,6 +138,34 @@ cd mcp-telegram
|
|
|
138
138
|
npm install && npm run build
|
|
139
139
|
```
|
|
140
140
|
|
|
141
|
+
### Docker
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
docker build -t mcp-telegram https://github.com/overpod/mcp-telegram.git
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Login (interactive terminal required):
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
docker run -it --rm \
|
|
151
|
+
-e TELEGRAM_API_ID=YOUR_ID \
|
|
152
|
+
-e TELEGRAM_API_HASH=YOUR_HASH \
|
|
153
|
+
-v ~/.mcp-telegram:/root/.mcp-telegram \
|
|
154
|
+
--entrypoint node mcp-telegram dist/qr-login-cli.js
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Run the MCP server:
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
docker run -i --rm \
|
|
161
|
+
-e TELEGRAM_API_ID=YOUR_ID \
|
|
162
|
+
-e TELEGRAM_API_HASH=YOUR_HASH \
|
|
163
|
+
-v ~/.mcp-telegram:/root/.mcp-telegram \
|
|
164
|
+
mcp-telegram
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
> **Note**: Login must be done once via terminal. After that, the session is persisted in `~/.mcp-telegram` and reused automatically.
|
|
168
|
+
|
|
141
169
|
## Usage with MCP Clients
|
|
142
170
|
|
|
143
171
|
### Claude Code (CLI)
|
|
@@ -174,12 +202,37 @@ claude mcp add telegram -s user \
|
|
|
174
202
|
|
|
175
203
|
3. Restart Claude Desktop.
|
|
176
204
|
|
|
177
|
-
4. Ask Claude: **"Run telegram-login"** -- a QR code will appear. If the image is not visible,
|
|
205
|
+
4. Ask Claude: **"Run telegram-login"** -- a QR code will appear. If the image is not visible, it's also saved to `~/.mcp-telegram/qr-login.png`. Scan it in Telegram (**Settings > Devices > Link Desktop Device**).
|
|
178
206
|
|
|
179
207
|
5. Ask Claude: **"Run telegram-status"** to verify the connection.
|
|
180
208
|
|
|
181
209
|
> **Note**: No terminal required! Login works entirely through Claude Desktop.
|
|
182
210
|
|
|
211
|
+
### Claude Desktop (Docker)
|
|
212
|
+
|
|
213
|
+
1. Login via terminal first (see [Docker](#docker) section above).
|
|
214
|
+
|
|
215
|
+
2. Add to your config file:
|
|
216
|
+
|
|
217
|
+
```json
|
|
218
|
+
{
|
|
219
|
+
"mcpServers": {
|
|
220
|
+
"telegram": {
|
|
221
|
+
"command": "docker",
|
|
222
|
+
"args": [
|
|
223
|
+
"run", "-i", "--rm",
|
|
224
|
+
"-e", "TELEGRAM_API_ID=YOUR_ID",
|
|
225
|
+
"-e", "TELEGRAM_API_HASH=YOUR_HASH",
|
|
226
|
+
"-v", "~/.mcp-telegram:/root/.mcp-telegram",
|
|
227
|
+
"mcp-telegram"
|
|
228
|
+
]
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
3. Restart Claude Desktop. Ask Claude: **"Run telegram-status"** to verify.
|
|
235
|
+
|
|
183
236
|
### Cursor / VS Code
|
|
184
237
|
|
|
185
238
|
Add the same JSON config above to your MCP settings (Cursor Settings > MCP, or VS Code MCP config).
|
|
@@ -276,6 +329,8 @@ Then set `TELEGRAM_SESSION_PATH` in each environment's MCP config accordingly.
|
|
|
276
329
|
- Session is stored in `~/.mcp-telegram/session` with `0600` permissions (owner-only access)
|
|
277
330
|
- Session directory is created with `0700` permissions
|
|
278
331
|
- Phone number is **not required** -- QR-only authentication
|
|
332
|
+
- No data is sent to third-party services -- all communication goes directly to Telegram servers via MTProto
|
|
333
|
+
- QR login codes are generated locally and never leave your machine
|
|
279
334
|
- **One session per process** -- using the same session in multiple processes simultaneously causes `AUTH_KEY_DUPLICATED` errors (see [Troubleshooting](#troubleshooting))
|
|
280
335
|
- This is a **userbot** (personal account), not a bot -- respect the [Telegram Terms of Service](https://core.telegram.org/api/terms)
|
|
281
336
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import assert from "node:assert";
|
|
2
|
+
import { describe, it } from "node:test";
|
|
3
|
+
import { DESTRUCTIVE, fail, formatReactions, ok, READ_ONLY, sanitize, WRITE } from "../../tools/shared.js";
|
|
4
|
+
describe("shared utilities", () => {
|
|
5
|
+
describe("ok()", () => {
|
|
6
|
+
it("should return success response with text content", () => {
|
|
7
|
+
const result = ok("Operation successful");
|
|
8
|
+
assert.deepStrictEqual(result, {
|
|
9
|
+
content: [{ type: "text", text: "Operation successful" }],
|
|
10
|
+
});
|
|
11
|
+
});
|
|
12
|
+
it("should handle empty string", () => {
|
|
13
|
+
const result = ok("");
|
|
14
|
+
assert.deepStrictEqual(result, {
|
|
15
|
+
content: [{ type: "text", text: "" }],
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
describe("fail()", () => {
|
|
20
|
+
it("should return error response with isError flag", () => {
|
|
21
|
+
const error = new Error("Something went wrong");
|
|
22
|
+
const result = fail(error);
|
|
23
|
+
assert.deepStrictEqual(result, {
|
|
24
|
+
content: [{ type: "text", text: "Error: Something went wrong" }],
|
|
25
|
+
isError: true,
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
it("should handle non-Error objects", () => {
|
|
29
|
+
const result = fail({ message: "Custom error" });
|
|
30
|
+
assert.ok(result.content[0].text.includes("Error:"));
|
|
31
|
+
assert.strictEqual(result.isError, true);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
describe("sanitize()", () => {
|
|
35
|
+
it("should remove unpaired high surrogates", () => {
|
|
36
|
+
const input = "Hello\uD800World";
|
|
37
|
+
const result = sanitize(input);
|
|
38
|
+
assert.strictEqual(result, "Hello\uFFFDWorld");
|
|
39
|
+
});
|
|
40
|
+
it("should remove unpaired low surrogates", () => {
|
|
41
|
+
const input = "Hello\uDC00World";
|
|
42
|
+
const result = sanitize(input);
|
|
43
|
+
assert.strictEqual(result, "Hello\uFFFDWorld");
|
|
44
|
+
});
|
|
45
|
+
it("should preserve valid surrogate pairs", () => {
|
|
46
|
+
const input = "Hello\uD83D\uDE00World"; // 😀 emoji
|
|
47
|
+
const result = sanitize(input);
|
|
48
|
+
assert.strictEqual(result, "Hello\uD83D\uDE00World");
|
|
49
|
+
});
|
|
50
|
+
it("should handle normal text without surrogates", () => {
|
|
51
|
+
const input = "Hello World";
|
|
52
|
+
const result = sanitize(input);
|
|
53
|
+
assert.strictEqual(result, "Hello World");
|
|
54
|
+
});
|
|
55
|
+
it("should handle empty string", () => {
|
|
56
|
+
const result = sanitize("");
|
|
57
|
+
assert.strictEqual(result, "");
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
describe("formatReactions()", () => {
|
|
61
|
+
it("should format reactions with counts", () => {
|
|
62
|
+
const reactions = [
|
|
63
|
+
{ emoji: "👍", count: 5, me: false },
|
|
64
|
+
{ emoji: "❤️", count: 3, me: true },
|
|
65
|
+
{ emoji: "🔥", count: 1, me: false },
|
|
66
|
+
];
|
|
67
|
+
const result = formatReactions(reactions);
|
|
68
|
+
assert.strictEqual(result, " [👍×5 ❤️×3(me) 🔥×1]");
|
|
69
|
+
});
|
|
70
|
+
it("should mark reactions from current user", () => {
|
|
71
|
+
const reactions = [{ emoji: "👍", count: 2, me: true }];
|
|
72
|
+
const result = formatReactions(reactions);
|
|
73
|
+
assert.strictEqual(result, " [👍×2(me)]");
|
|
74
|
+
});
|
|
75
|
+
it("should return empty string for undefined reactions", () => {
|
|
76
|
+
const result = formatReactions(undefined);
|
|
77
|
+
assert.strictEqual(result, "");
|
|
78
|
+
});
|
|
79
|
+
it("should return empty string for empty reactions array", () => {
|
|
80
|
+
const result = formatReactions([]);
|
|
81
|
+
assert.strictEqual(result, "");
|
|
82
|
+
});
|
|
83
|
+
it("should handle single reaction", () => {
|
|
84
|
+
const reactions = [{ emoji: "🎉", count: 1, me: false }];
|
|
85
|
+
const result = formatReactions(reactions);
|
|
86
|
+
assert.strictEqual(result, " [🎉×1]");
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
describe("MCP tool annotations", () => {
|
|
90
|
+
it("should define READ_ONLY preset", () => {
|
|
91
|
+
assert.deepStrictEqual(READ_ONLY, {
|
|
92
|
+
readOnlyHint: true,
|
|
93
|
+
openWorldHint: true,
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
it("should define WRITE preset", () => {
|
|
97
|
+
assert.deepStrictEqual(WRITE, {
|
|
98
|
+
readOnlyHint: false,
|
|
99
|
+
openWorldHint: true,
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
it("should define DESTRUCTIVE preset", () => {
|
|
103
|
+
assert.deepStrictEqual(DESTRUCTIVE, {
|
|
104
|
+
readOnlyHint: false,
|
|
105
|
+
destructiveHint: true,
|
|
106
|
+
openWorldHint: true,
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
});
|
package/dist/index.js
CHANGED
|
@@ -24,18 +24,19 @@ const server = new McpServer({
|
|
|
24
24
|
});
|
|
25
25
|
registerTools(server, telegram);
|
|
26
26
|
async function main() {
|
|
27
|
-
// Try to auto-connect with saved session
|
|
28
|
-
await telegram.loadSession();
|
|
29
|
-
if (await telegram.connect()) {
|
|
30
|
-
const me = await telegram.getMe();
|
|
31
|
-
console.error(`[mcp-telegram] Auto-connected as @${me.username}`);
|
|
32
|
-
}
|
|
33
|
-
else if (telegram.lastError) {
|
|
34
|
-
console.error(`[mcp-telegram] ${telegram.lastError}`);
|
|
35
|
-
}
|
|
36
27
|
const transport = new StdioServerTransport();
|
|
37
28
|
await server.connect(transport);
|
|
38
29
|
console.error("[mcp-telegram] MCP server running on stdio");
|
|
30
|
+
// Auto-connect with saved session after MCP is ready (non-blocking)
|
|
31
|
+
telegram.loadSession().then(async () => {
|
|
32
|
+
if (await telegram.connect()) {
|
|
33
|
+
const me = await telegram.getMe();
|
|
34
|
+
console.error(`[mcp-telegram] Auto-connected as @${me.username}`);
|
|
35
|
+
}
|
|
36
|
+
else if (telegram.lastError) {
|
|
37
|
+
console.error(`[mcp-telegram] ${telegram.lastError}`);
|
|
38
|
+
}
|
|
39
|
+
});
|
|
39
40
|
}
|
|
40
41
|
main().catch((err) => {
|
|
41
42
|
console.error("[mcp-telegram] Fatal:", err);
|
package/dist/telegram-client.js
CHANGED
package/dist/tools/auth.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { writeFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
1
3
|
import { fail, ok, READ_ONLY, WRITE } from "./shared.js";
|
|
2
4
|
export function registerAuthTools(server, telegram) {
|
|
3
5
|
server.registerTool("telegram-status", { description: "Check Telegram connection status", annotations: READ_ONLY }, async () => {
|
|
@@ -18,11 +20,8 @@ export function registerAuthTools(server, telegram) {
|
|
|
18
20
|
annotations: WRITE,
|
|
19
21
|
}, async () => {
|
|
20
22
|
let qrDataUrl = "";
|
|
21
|
-
let qrRawUrl = "";
|
|
22
23
|
const loginPromise = telegram.startQrLogin((dataUrl) => {
|
|
23
24
|
qrDataUrl = dataUrl;
|
|
24
|
-
}, (url) => {
|
|
25
|
-
qrRawUrl = url;
|
|
26
25
|
});
|
|
27
26
|
// Wait for first QR to be generated
|
|
28
27
|
const startTime = Date.now();
|
|
@@ -41,20 +40,17 @@ export function registerAuthTools(server, telegram) {
|
|
|
41
40
|
console.error(`[mcp-telegram] Login failed: ${result.message}`);
|
|
42
41
|
}
|
|
43
42
|
});
|
|
44
|
-
//
|
|
43
|
+
// Save QR to file as fallback (no data sent to third-party services)
|
|
45
44
|
const base64 = qrDataUrl.replace(/^data:image\/png;base64,/, "");
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
: "";
|
|
45
|
+
const qrFilePath = join(telegram.sessionDir, "qr-login.png");
|
|
46
|
+
await writeFile(qrFilePath, Buffer.from(base64, "base64")).catch(() => { });
|
|
49
47
|
const instructions = [
|
|
50
48
|
"Scan this QR code in Telegram: **Settings → Devices → Link Desktop Device**.",
|
|
51
49
|
"",
|
|
52
|
-
|
|
50
|
+
`If the QR image is not visible, it's also saved to: ${qrFilePath}`,
|
|
53
51
|
"",
|
|
54
52
|
"After scanning, run **telegram-status** to verify the connection.",
|
|
55
|
-
]
|
|
56
|
-
.filter(Boolean)
|
|
57
|
-
.join("\n");
|
|
53
|
+
].join("\n");
|
|
58
54
|
return {
|
|
59
55
|
content: [
|
|
60
56
|
{
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@overpod/mcp-telegram",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.19.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",
|
|
@@ -25,7 +25,9 @@
|
|
|
25
25
|
"prepublishOnly": "npm run build",
|
|
26
26
|
"lint": "biome check src/",
|
|
27
27
|
"lint:fix": "biome check --fix src/",
|
|
28
|
-
"format": "biome format --write src/"
|
|
28
|
+
"format": "biome format --write src/",
|
|
29
|
+
"test": "tsx --test src/**/*.test.ts",
|
|
30
|
+
"test:watch": "tsx --test --watch src/**/*.test.ts"
|
|
29
31
|
},
|
|
30
32
|
"keywords": [
|
|
31
33
|
"mcp",
|