@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 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, Claude will provide a browser link to view the QR code. Scan it in Telegram (**Settings > Devices > Link Desktop Device**).
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);
@@ -6,6 +6,7 @@ export declare class TelegramService {
6
6
  private connected;
7
7
  private sessionPath;
8
8
  lastError: string;
9
+ get sessionDir(): string;
9
10
  constructor(apiId: number, apiHash: string, options?: {
10
11
  sessionPath?: string;
11
12
  });
@@ -50,6 +50,9 @@ export class TelegramService {
50
50
  connected = false;
51
51
  sessionPath;
52
52
  lastError = "";
53
+ get sessionDir() {
54
+ return dirname(this.sessionPath);
55
+ }
53
56
  constructor(apiId, apiHash, options) {
54
57
  this.apiId = apiId;
55
58
  this.apiHash = apiHash;
@@ -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
- // Return as MCP image content + text with fallback options
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 qrApiUrl = qrRawUrl
47
- ? `https://api.qrserver.com/v1/create-qr-code/?size=256x256&data=${encodeURIComponent(qrRawUrl)}`
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
- qrApiUrl ? `If the QR image is not visible, open this link in your browser:\n${qrApiUrl}` : "",
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.18.0",
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",