@overpod/mcp-telegram 1.26.1 → 1.27.1

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.
@@ -0,0 +1,22 @@
1
+ import { type Socket } from "node:net";
2
+ export interface IpcClientOptions {
3
+ connectTimeoutMs?: number;
4
+ callTimeoutMs?: number;
5
+ connectFn?: (path: string) => Socket;
6
+ }
7
+ /** Thin IPC proxy: forwards tool calls to the master process over Unix socket */
8
+ export declare class IpcClient {
9
+ private socket;
10
+ private pending;
11
+ private buf;
12
+ private connected;
13
+ private readonly connectTimeoutMs;
14
+ private readonly callTimeoutMs;
15
+ private readonly connectFn;
16
+ constructor(opts?: IpcClientOptions);
17
+ connect(): Promise<boolean>;
18
+ isConnected(): boolean;
19
+ call(tool: string, args: Record<string, unknown>): Promise<unknown>;
20
+ destroy(): void;
21
+ }
22
+ export declare function runClient(apiId: number, apiHash: string, version: string): Promise<void>;
package/dist/client.js ADDED
@@ -0,0 +1,144 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { connect } from "node:net";
3
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
4
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5
+ import { encodeMessage, parseMessages } from "./ipc-protocol.js";
6
+ import { socketPath } from "./lock.js";
7
+ import { TelegramService } from "./telegram-client.js";
8
+ import { registerTools } from "./tools/index.js";
9
+ const CONNECT_TIMEOUT_MS = 5_000;
10
+ const IPC_CALL_TIMEOUT_MS = 30_000;
11
+ const MAX_RECONNECT_ATTEMPTS = 5;
12
+ /** Thin IPC proxy: forwards tool calls to the master process over Unix socket */
13
+ export class IpcClient {
14
+ socket = null;
15
+ pending = new Map();
16
+ buf = "";
17
+ connected = false;
18
+ connectTimeoutMs;
19
+ callTimeoutMs;
20
+ connectFn;
21
+ constructor(opts = {}) {
22
+ this.connectTimeoutMs = opts.connectTimeoutMs ?? CONNECT_TIMEOUT_MS;
23
+ this.callTimeoutMs = opts.callTimeoutMs ?? IPC_CALL_TIMEOUT_MS;
24
+ this.connectFn = opts.connectFn ?? connect;
25
+ }
26
+ async connect() {
27
+ return new Promise((resolve) => {
28
+ const sock = socketPath();
29
+ const s = this.connectFn(sock);
30
+ // One-shot connect timeout — cleared immediately on connect (HIGH-3)
31
+ const connectTimer = setTimeout(() => {
32
+ s.destroy();
33
+ resolve(false);
34
+ }, this.connectTimeoutMs);
35
+ const onConnect = () => {
36
+ clearTimeout(connectTimer);
37
+ this.socket = s;
38
+ this.connected = true;
39
+ s.removeListener("error", onError);
40
+ s.on("data", (chunk) => {
41
+ this.buf += chunk.toString("utf-8");
42
+ const { messages, remaining } = parseMessages(this.buf);
43
+ this.buf = remaining;
44
+ for (const msg of messages) {
45
+ const res = msg;
46
+ const pending = this.pending.get(res.id);
47
+ if (!pending)
48
+ continue;
49
+ clearTimeout(pending.timer);
50
+ this.pending.delete(res.id);
51
+ if (res.error) {
52
+ pending.reject(new Error(res.error));
53
+ }
54
+ else {
55
+ pending.resolve(res.result);
56
+ }
57
+ }
58
+ });
59
+ s.on("close", () => {
60
+ this.connected = false;
61
+ for (const [, p] of this.pending) {
62
+ clearTimeout(p.timer);
63
+ p.reject(new Error("IPC connection closed"));
64
+ }
65
+ this.pending.clear();
66
+ });
67
+ s.on("error", () => { });
68
+ resolve(true);
69
+ };
70
+ const onError = () => {
71
+ clearTimeout(connectTimer);
72
+ resolve(false);
73
+ };
74
+ s.once("connect", onConnect);
75
+ s.once("error", onError);
76
+ });
77
+ }
78
+ isConnected() {
79
+ return this.connected;
80
+ }
81
+ async call(tool, args) {
82
+ if (!this.socket || !this.connected) {
83
+ throw new Error("IPC client not connected");
84
+ }
85
+ const id = randomUUID();
86
+ const socket = this.socket;
87
+ return new Promise((resolve, reject) => {
88
+ const timer = setTimeout(() => {
89
+ this.pending.delete(id);
90
+ reject(new Error(`IPC call timeout: ${tool}`));
91
+ }, this.callTimeoutMs);
92
+ this.pending.set(id, { resolve, reject, timer });
93
+ socket.write(encodeMessage({ id, tool, args }));
94
+ });
95
+ }
96
+ destroy() {
97
+ for (const [, p] of this.pending) {
98
+ clearTimeout(p.timer);
99
+ p.reject(new Error("IPC client destroyed"));
100
+ }
101
+ this.pending.clear();
102
+ this.socket?.destroy();
103
+ this.socket = null;
104
+ this.connected = false;
105
+ }
106
+ }
107
+ function wireIpcProxies(server, ipc) {
108
+ const s = server;
109
+ for (const [name, tool] of Object.entries(s._registeredTools)) {
110
+ Object.assign(tool, {
111
+ handler: (args) => ipc.call(name, args),
112
+ });
113
+ }
114
+ }
115
+ export async function runClient(apiId, apiHash, version) {
116
+ // Try to connect to master with retries — master may still be initializing its socket
117
+ let ipc = null;
118
+ for (let attempt = 0; attempt < MAX_RECONNECT_ATTEMPTS; attempt++) {
119
+ const candidate = new IpcClient();
120
+ if (await candidate.connect()) {
121
+ ipc = candidate;
122
+ break;
123
+ }
124
+ if (attempt < MAX_RECONNECT_ATTEMPTS - 1) {
125
+ await new Promise((r) => setTimeout(r, 150 * 2 ** attempt)); // 150ms, 300ms, 600ms, 1200ms
126
+ }
127
+ }
128
+ if (!ipc) {
129
+ // Master acquired lock but socket not ready — this process should not become master
130
+ // (it lost the lock race). Exit with clear message instead of creating two masters (CRITICAL-2)
131
+ console.error("[mcp-telegram] Cannot connect to master process. Try again in a moment.");
132
+ process.exit(1);
133
+ }
134
+ console.error(`[mcp-telegram] Client mode — proxying to master via ${socketPath()}`);
135
+ // Register all tools for MCP schema; dummy telegram instance is never used for actual calls
136
+ const telegram = new TelegramService(apiId, apiHash);
137
+ const server = new McpServer({ name: "mcp-telegram", version });
138
+ registerTools(server, telegram);
139
+ // Replace all handlers with IPC-forwarding versions
140
+ wireIpcProxies(server, ipc);
141
+ const transport = new StdioServerTransport();
142
+ await server.connect(transport);
143
+ console.error("[mcp-telegram] MCP server running on stdio (client)");
144
+ }
package/dist/index.js CHANGED
@@ -1,16 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
  // Redirect console.log to stderr BEFORE any imports.
3
3
  // GramJS Logger uses console.log (stdout) which corrupts MCP JSON-RPC stream.
4
- const _origLog = console.log;
5
4
  console.log = (...args) => {
6
5
  console.error(...args);
7
6
  };
8
7
  import "dotenv/config";
9
8
  import { createRequire } from "node:module";
10
- import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
11
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
12
- import { TelegramService } from "./telegram-client.js";
13
- import { registerTools } from "./tools/index.js";
9
+ import { tryAcquireLock } from "./lock.js";
14
10
  const require = createRequire(import.meta.url);
15
11
  const { version } = require("../package.json");
16
12
  // Telegram API credentials from env
@@ -22,26 +18,18 @@ if (!API_ID || !API_HASH) {
22
18
  console.error("Set them in .env or export as environment variables");
23
19
  process.exit(1);
24
20
  }
25
- const telegram = new TelegramService(API_ID, API_HASH);
26
- const server = new McpServer({
27
- name: "mcp-telegram",
28
- version,
29
- });
30
- registerTools(server, telegram);
31
21
  async function main() {
32
- const transport = new StdioServerTransport();
33
- await server.connect(transport);
34
- console.error("[mcp-telegram] MCP server running on stdio");
35
- // Auto-connect with saved session after MCP is ready (non-blocking)
36
- telegram.loadSession().then(async () => {
37
- if (await telegram.connect()) {
38
- const me = await telegram.getMe();
39
- console.error(`[mcp-telegram] Auto-connected as @${me.username}`);
40
- }
41
- else if (telegram.lastError) {
42
- console.error(`[mcp-telegram] ${telegram.lastError}`);
43
- }
44
- });
22
+ const isMaster = tryAcquireLock();
23
+ if (isMaster) {
24
+ console.error("[mcp-telegram] Starting as master process");
25
+ const { runMaster } = await import("./master.js");
26
+ await runMaster(API_ID, API_HASH, version);
27
+ }
28
+ else {
29
+ console.error("[mcp-telegram] Starting as client process (master already running)");
30
+ const { runClient } = await import("./client.js");
31
+ await runClient(API_ID, API_HASH, version);
32
+ }
45
33
  }
46
34
  main().catch((err) => {
47
35
  console.error("[mcp-telegram] Fatal:", err);
@@ -0,0 +1,26 @@
1
+ /** MCP SDK internal tool registry — field name "handler" confirmed in SDK v1.29.0 */
2
+ export type McpRegisteredTool = {
3
+ handler: (args: Record<string, unknown>, extra: Record<string, unknown>) => Promise<unknown>;
4
+ };
5
+ export interface McpServerInternal {
6
+ _registeredTools: Record<string, McpRegisteredTool>;
7
+ }
8
+ /** Request from Client → Master */
9
+ export interface IpcRequest {
10
+ id: string;
11
+ tool: string;
12
+ args: Record<string, unknown>;
13
+ }
14
+ /** Response from Master → Client */
15
+ export interface IpcResponse {
16
+ id: string;
17
+ result?: unknown;
18
+ error?: string;
19
+ }
20
+ /** Encode a message as newline-delimited JSON */
21
+ export declare function encodeMessage(msg: IpcRequest | IpcResponse): string;
22
+ /** Parse newline-delimited JSON messages from a buffer, returns parsed messages + leftover */
23
+ export declare function parseMessages(buf: string): {
24
+ messages: (IpcRequest | IpcResponse)[];
25
+ remaining: string;
26
+ };
@@ -0,0 +1,22 @@
1
+ /** Encode a message as newline-delimited JSON */
2
+ export function encodeMessage(msg) {
3
+ return `${JSON.stringify(msg)}\n`;
4
+ }
5
+ /** Parse newline-delimited JSON messages from a buffer, returns parsed messages + leftover */
6
+ export function parseMessages(buf) {
7
+ const lines = buf.split("\n");
8
+ const remaining = lines.pop() ?? "";
9
+ const messages = [];
10
+ for (const line of lines) {
11
+ const trimmed = line.trim();
12
+ if (!trimmed)
13
+ continue;
14
+ try {
15
+ messages.push(JSON.parse(trimmed));
16
+ }
17
+ catch {
18
+ // Skip malformed lines
19
+ }
20
+ }
21
+ return { messages, remaining };
22
+ }
package/dist/lock.d.ts ADDED
@@ -0,0 +1,12 @@
1
+ export declare function lockPath(): string;
2
+ export declare function socketPath(): string;
3
+ /**
4
+ * Try to acquire the master lock.
5
+ * Returns true if this process is now the master.
6
+ * Returns false if another live master process holds the lock.
7
+ *
8
+ * Uses PID file + kill -0 to detect stale locks after crashes.
9
+ */
10
+ export declare function tryAcquireLock(): boolean;
11
+ export declare function releaseLock(): void;
12
+ export declare function releaseSocket(): void;
package/dist/lock.js ADDED
@@ -0,0 +1,83 @@
1
+ import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { dirname, join } from "node:path";
4
+ const DEFAULT_SESSION_DIR = join(homedir(), ".mcp-telegram");
5
+ function resolveSessionDir() {
6
+ const sessionPath = process.env.TELEGRAM_SESSION_PATH;
7
+ if (sessionPath)
8
+ return dirname(sessionPath);
9
+ return DEFAULT_SESSION_DIR;
10
+ }
11
+ export function lockPath() {
12
+ return join(resolveSessionDir(), "daemon.lock");
13
+ }
14
+ export function socketPath() {
15
+ return join(resolveSessionDir(), "daemon.sock");
16
+ }
17
+ /**
18
+ * Try to acquire the master lock.
19
+ * Returns true if this process is now the master.
20
+ * Returns false if another live master process holds the lock.
21
+ *
22
+ * Uses PID file + kill -0 to detect stale locks after crashes.
23
+ */
24
+ export function tryAcquireLock() {
25
+ const lock = lockPath();
26
+ const dir = dirname(lock);
27
+ if (!existsSync(dir)) {
28
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
29
+ }
30
+ if (existsSync(lock)) {
31
+ try {
32
+ const pid = Number.parseInt(readFileSync(lock, "utf-8").trim(), 10);
33
+ if (!Number.isNaN(pid) && pid > 0) {
34
+ try {
35
+ // kill -0: check if process is alive without sending a signal
36
+ process.kill(pid, 0);
37
+ // Process is alive — another master owns the lock
38
+ return false;
39
+ }
40
+ catch {
41
+ // ESRCH: process not found — stale lock, take over
42
+ unlinkSync(lock);
43
+ }
44
+ }
45
+ }
46
+ catch {
47
+ // Unreadable lock — remove and take over
48
+ try {
49
+ unlinkSync(lock);
50
+ }
51
+ catch { }
52
+ }
53
+ }
54
+ try {
55
+ // O_EXCL flag: atomic exclusive create — prevents TOCTOU race between two simultaneous starts
56
+ writeFileSync(lock, String(process.pid), { flag: "wx", mode: 0o600 });
57
+ return true;
58
+ }
59
+ catch {
60
+ // EEXIST: another process just created the lock — we lost the race, become client
61
+ return false;
62
+ }
63
+ }
64
+ export function releaseLock() {
65
+ try {
66
+ const lock = lockPath();
67
+ if (existsSync(lock)) {
68
+ const pid = Number.parseInt(readFileSync(lock, "utf-8").trim(), 10);
69
+ // Only remove our own lock
70
+ if (pid === process.pid)
71
+ unlinkSync(lock);
72
+ }
73
+ }
74
+ catch { }
75
+ }
76
+ export function releaseSocket() {
77
+ try {
78
+ const sock = socketPath();
79
+ if (existsSync(sock))
80
+ unlinkSync(sock);
81
+ }
82
+ catch { }
83
+ }
@@ -0,0 +1,4 @@
1
+ import { type Socket } from "node:net";
2
+ import { type McpServerInternal } from "./ipc-protocol.js";
3
+ export declare function handleClient(socket: Socket, mcpServer: McpServerInternal): void;
4
+ export declare function runMaster(apiId: number, apiHash: string, version: string): Promise<void>;
package/dist/master.js ADDED
@@ -0,0 +1,105 @@
1
+ import { createServer } from "node:net";
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { encodeMessage, parseMessages, } from "./ipc-protocol.js";
5
+ import { releaseLock, releaseSocket, socketPath } from "./lock.js";
6
+ import { TelegramService } from "./telegram-client.js";
7
+ import { registerTools } from "./tools/index.js";
8
+ let socketServer = null;
9
+ let cleanedUp = false;
10
+ function cleanup() {
11
+ if (cleanedUp)
12
+ return;
13
+ cleanedUp = true;
14
+ releaseLock();
15
+ releaseSocket();
16
+ socketServer?.close();
17
+ }
18
+ process.on("exit", cleanup);
19
+ process.on("SIGINT", () => process.exit(0));
20
+ process.on("SIGTERM", () => process.exit(0));
21
+ export function handleClient(socket, mcpServer) {
22
+ let buf = "";
23
+ // Processing queue — ensures sequential handling even when handler awaits
24
+ let processing = false;
25
+ const queue = [];
26
+ async function drainQueue() {
27
+ if (processing)
28
+ return;
29
+ processing = true;
30
+ while (queue.length > 0) {
31
+ const req = queue.shift();
32
+ if (!req)
33
+ break;
34
+ const tool = mcpServer._registeredTools[req.tool];
35
+ const response = { id: req.id };
36
+ if (!tool) {
37
+ response.error = `Unknown tool: ${req.tool}`;
38
+ }
39
+ else {
40
+ try {
41
+ response.result = await tool.handler(req.args ?? {}, {});
42
+ }
43
+ catch (err) {
44
+ response.error = err instanceof Error ? err.message : String(err);
45
+ }
46
+ }
47
+ if (!socket.destroyed) {
48
+ socket.write(encodeMessage(response));
49
+ }
50
+ }
51
+ processing = false;
52
+ }
53
+ socket.on("data", (chunk) => {
54
+ buf += chunk.toString("utf-8");
55
+ const { messages, remaining } = parseMessages(buf);
56
+ buf = remaining;
57
+ for (const msg of messages) {
58
+ const req = msg;
59
+ if (!req.id || !req.tool)
60
+ continue;
61
+ queue.push(req);
62
+ }
63
+ drainQueue();
64
+ });
65
+ socket.on("error", () => { });
66
+ }
67
+ export async function runMaster(apiId, apiHash, version) {
68
+ const telegram = new TelegramService(apiId, apiHash);
69
+ const server = new McpServer({ name: "mcp-telegram", version });
70
+ registerTools(server, telegram);
71
+ const mcpServer = server;
72
+ // Remove stale socket file from previous crash before attempting to listen (HIGH-2)
73
+ releaseSocket();
74
+ const sock = socketPath();
75
+ const srv = createServer((socket) => handleClient(socket, mcpServer));
76
+ socketServer = srv;
77
+ await new Promise((resolve, reject) => {
78
+ srv.listen(sock, resolve);
79
+ srv.once("error", reject);
80
+ });
81
+ const { chmod } = await import("node:fs/promises");
82
+ try {
83
+ await chmod(sock, 0o600);
84
+ }
85
+ catch { }
86
+ console.error(`[mcp-telegram] Master mode — IPC socket ready: ${sock}`);
87
+ const transport = new StdioServerTransport();
88
+ await server.connect(transport);
89
+ console.error("[mcp-telegram] MCP server running on stdio (master)");
90
+ // Auto-connect with saved session — catch to avoid unhandled rejection (MEDIUM-2)
91
+ telegram
92
+ .loadSession()
93
+ .then(async () => {
94
+ if (await telegram.connect()) {
95
+ const me = await telegram.getMe();
96
+ console.error(`[mcp-telegram] Auto-connected as @${me.username}`);
97
+ }
98
+ else if (telegram.lastError) {
99
+ console.error(`[mcp-telegram] ${telegram.lastError}`);
100
+ }
101
+ })
102
+ .catch((err) => {
103
+ console.error("[mcp-telegram] Auto-connect failed:", err);
104
+ });
105
+ }
@@ -60,6 +60,7 @@ export class TelegramService {
60
60
  get sessionDir() {
61
61
  return dirname(this.sessionPath);
62
62
  }
63
+ // ─── Session & Auth ────────────────────────────────────────────────────────
63
64
  getClient() {
64
65
  return this.client;
65
66
  }
@@ -300,6 +301,7 @@ export class TelegramService {
300
301
  return { success: false, message: `Login failed: ${err.message}` };
301
302
  }
302
303
  }
304
+ // ─── Messages ──────────────────────────────────────────────────────────────
303
305
  async getMe() {
304
306
  if (!this.client || !this.connected)
305
307
  throw new Error(NOT_CONNECTED_ERROR);
@@ -410,6 +412,7 @@ export class TelegramService {
410
412
  const resolved = await this.resolvePeer(chatId);
411
413
  await this.client.unpinMessage(resolved, messageId);
412
414
  }
415
+ // ─── Dialogs ───────────────────────────────────────────────────────────────
413
416
  async getDialogs(limit = 20, offsetDate, filterType) {
414
417
  if (!this.client || !this.connected)
415
418
  throw new Error(NOT_CONNECTED_ERROR);
@@ -499,6 +502,7 @@ export class TelegramService {
499
502
  };
500
503
  });
501
504
  }
505
+ // ─── Contacts ──────────────────────────────────────────────────────────────
502
506
  async addContact(userId, firstName, lastName, phone) {
503
507
  if (!this.client || !this.connected)
504
508
  throw new Error(NOT_CONNECTED_ERROR);
@@ -522,6 +526,7 @@ export class TelegramService {
522
526
  const peer = await this.client.getInputEntity(chatId);
523
527
  await this.client.invoke(new Api.messages.ReportSpam({ peer }));
524
528
  }
529
+ // ─── Read state ────────────────────────────────────────────────────────────
525
530
  async markAsRead(chatId) {
526
531
  if (!this.client || !this.connected)
527
532
  throw new Error(NOT_CONNECTED_ERROR);
@@ -786,6 +791,7 @@ export class TelegramService {
786
791
  }
787
792
  }, `sendTyping in ${chatId}`);
788
793
  }
794
+ // ─── Chat lookup & info ────────────────────────────────────────────────────
789
795
  /**
790
796
  * Resolve a chat by ID, username, or display name.
791
797
  * Falls back to searching user's dialogs if getEntity() fails.
@@ -1125,6 +1131,7 @@ export class TelegramService {
1125
1131
  })));
1126
1132
  return results;
1127
1133
  }
1134
+ // ─── Search ────────────────────────────────────────────────────────────────
1128
1135
  async getContacts(limit = 50) {
1129
1136
  if (!this.client || !this.connected)
1130
1137
  throw new Error(NOT_CONNECTED_ERROR);
@@ -1302,6 +1309,7 @@ export class TelegramService {
1302
1309
  businessLocation,
1303
1310
  };
1304
1311
  }
1312
+ // ─── Profiles & Media ──────────────────────────────────────────────────────
1305
1313
  async downloadProfilePhoto(entityId, options) {
1306
1314
  if (!this.client || !this.connected)
1307
1315
  throw new Error(NOT_CONNECTED_ERROR);
@@ -1360,6 +1368,7 @@ export class TelegramService {
1360
1368
  }
1361
1369
  return items.length > 0 ? items : undefined;
1362
1370
  }
1371
+ // ─── Reactions ─────────────────────────────────────────────────────────────
1363
1372
  async sendReaction(chatId, messageId, emoji, addToExisting = false) {
1364
1373
  if (!this.client || !this.connected)
1365
1374
  throw new Error(NOT_CONNECTED_ERROR);
@@ -1496,6 +1505,7 @@ export class TelegramService {
1496
1505
  return out;
1497
1506
  }, "getRecentReactions");
1498
1507
  }
1508
+ // ─── Scheduled & Polls ─────────────────────────────────────────────────────
1499
1509
  async sendScheduledMessage(chatId, text, scheduleDate, replyTo, parseMode) {
1500
1510
  if (!this.client || !this.connected)
1501
1511
  throw new Error(NOT_CONNECTED_ERROR);
@@ -1614,6 +1624,7 @@ export class TelegramService {
1614
1624
  catch { }
1615
1625
  return false;
1616
1626
  }
1627
+ // ─── Chat membership & management ──────────────────────────────────────────
1617
1628
  async joinChat(target) {
1618
1629
  if (!this.client)
1619
1630
  throw new Error(NOT_CONNECTED_ERROR);
@@ -1895,7 +1906,7 @@ export class TelegramService {
1895
1906
  rank: "",
1896
1907
  }));
1897
1908
  }
1898
- // ── New tools: feature parity ──────────────────────────────────────
1909
+ // ─── Chat settings & moderation ────────────────────────────────────────────
1899
1910
  async unblockUser(userId) {
1900
1911
  if (!this.client || !this.connected)
1901
1912
  throw new Error(NOT_CONNECTED_ERROR);
@@ -2133,6 +2144,7 @@ export class TelegramService {
2133
2144
  await this.client?.invoke(new Api.messages.HideChatJoinRequest({ peer: entity, userId: inputUser, approved }));
2134
2145
  }, `approveChatJoinRequest ${chatId}/${userId}`);
2135
2146
  }
2147
+ // ─── Inline bots & buttons ─────────────────────────────────────────────────
2136
2148
  async getInlineBotResults(bot, chatId, query, offset) {
2137
2149
  if (!this.client || !this.connected)
2138
2150
  throw new Error(NOT_CONNECTED_ERROR);
@@ -2305,7 +2317,7 @@ export class TelegramService {
2305
2317
  result = response;
2306
2318
  }
2307
2319
  catch (e) {
2308
- const msg = e.message ?? String(e);
2320
+ const msg = e instanceof Error ? e.message : String(e);
2309
2321
  if (/CHAT_ADMIN_REQUIRED|ADMIN_RANK_INVALID/i.test(msg)) {
2310
2322
  throw new Error("Access denied: channel stats require admin rights (and may require Telegram Premium)");
2311
2323
  }
@@ -2337,7 +2349,7 @@ export class TelegramService {
2337
2349
  result = response;
2338
2350
  }
2339
2351
  catch (e) {
2340
- const msg = e.message ?? String(e);
2352
+ const msg = e instanceof Error ? e.message : String(e);
2341
2353
  if (/CHAT_ADMIN_REQUIRED|ADMIN_RANK_INVALID/i.test(msg)) {
2342
2354
  throw new Error("Access denied: supergroup stats require admin rights");
2343
2355
  }
@@ -2349,6 +2361,7 @@ export class TelegramService {
2349
2361
  return summarizeMegagroupStats(result, options?.includeGraphs === true);
2350
2362
  }, `getMegagroupStats ${chatId}`, { throwOnFloodWait: true });
2351
2363
  }
2364
+ // ─── Stats & updates ───────────────────────────────────────────────────────
2352
2365
  async getUpdatesState() {
2353
2366
  if (!this.client || !this.connected)
2354
2367
  throw new Error(NOT_CONNECTED_ERROR);
@@ -2404,6 +2417,7 @@ export class TelegramService {
2404
2417
  return summarizeChannelDifference(diff, entity.id.toString(), cursor.pts);
2405
2418
  }, `getChannelUpdates ${chatId}`);
2406
2419
  }
2420
+ // ─── Forum topics ──────────────────────────────────────────────────────────
2407
2421
  async createForumTopic(chatId, title, iconColor, iconEmojiId) {
2408
2422
  if (!this.client || !this.connected)
2409
2423
  throw new Error(NOT_CONNECTED_ERROR);
@@ -2477,6 +2491,7 @@ export class TelegramService {
2477
2491
  }));
2478
2492
  }, `deleteForumTopic ${chatId}/${topicId}`);
2479
2493
  }
2494
+ // ─── Invite links & folders ────────────────────────────────────────────────
2480
2495
  async exportInviteLink(chatId, options) {
2481
2496
  if (!this.client || !this.connected)
2482
2497
  throw new Error(NOT_CONNECTED_ERROR);
@@ -2552,6 +2567,7 @@ export class TelegramService {
2552
2567
  const peer = await this.client.getInputEntity(resolved);
2553
2568
  await this.client.invoke(new Api.messages.SetHistoryTTL({ peer, period }));
2554
2569
  }
2570
+ // ─── Account & privacy ─────────────────────────────────────────────────────
2555
2571
  async getActiveSessions() {
2556
2572
  if (!this.client || !this.connected)
2557
2573
  throw new Error(NOT_CONNECTED_ERROR);
@@ -2655,7 +2671,7 @@ export class TelegramService {
2655
2671
  throw new Error(NOT_CONNECTED_ERROR);
2656
2672
  await this.client.invoke(new Api.account.UpdateUsername({ username }));
2657
2673
  }
2658
- // ─── Stickers ──────────────────────────────────────────────
2674
+ // ─── Stickers ──────────────────────────────────────────────────────────────
2659
2675
  async getStickerSet(shortName) {
2660
2676
  if (!this.client || !this.connected)
2661
2677
  throw new Error(NOT_CONNECTED_ERROR);
@@ -2748,6 +2764,7 @@ export class TelegramService {
2748
2764
  });
2749
2765
  }, `sendSticker to ${chatId}`);
2750
2766
  }
2767
+ // ─── Drafts & saved dialogs ────────────────────────────────────────────────
2751
2768
  async saveDraft(chatId, text, replyTo) {
2752
2769
  if (!this.client || !this.connected)
2753
2770
  throw new Error(NOT_CONNECTED_ERROR);
@@ -2943,6 +2960,7 @@ export class TelegramService {
2943
2960
  emoji: emojiMap.get(doc.id.toString()) || "",
2944
2961
  }));
2945
2962
  }
2963
+ // ─── Stories ───────────────────────────────────────────────────────────────
2946
2964
  async getAllStories(options) {
2947
2965
  if (!this.client || !this.connected)
2948
2966
  throw new Error(NOT_CONNECTED_ERROR);
@@ -2999,6 +3017,7 @@ export class TelegramService {
2999
3017
  return summarizeStoryViewsList(response);
3000
3018
  }, `getStoryViewsList ${chatId}/${options.id}`);
3001
3019
  }
3020
+ // ─── Boosts ────────────────────────────────────────────────────────────────
3002
3021
  async getMyBoosts() {
3003
3022
  if (!this.client || !this.connected)
3004
3023
  throw new Error(NOT_CONNECTED_ERROR);
@@ -3046,6 +3065,7 @@ export class TelegramService {
3046
3065
  return summarizeBusinessChatLinks(response);
3047
3066
  }, "getBusinessChatLinks");
3048
3067
  }
3068
+ // ─── Group calls ───────────────────────────────────────────────────────────
3049
3069
  async getGroupCall(chatId, options = {}) {
3050
3070
  if (!this.client || !this.connected)
3051
3071
  throw new Error(NOT_CONNECTED_ERROR);
@@ -3078,6 +3098,7 @@ export class TelegramService {
3078
3098
  return summarizeGroupCallParticipants(response);
3079
3099
  }, `getGroupCallParticipants ${chatId}`);
3080
3100
  }
3101
+ // ─── Stars ─────────────────────────────────────────────────────────────────
3081
3102
  async getStarsStatus(chatId) {
3082
3103
  if (!this.client || !this.connected)
3083
3104
  throw new Error(NOT_CONNECTED_ERROR);
@@ -3108,6 +3129,7 @@ export class TelegramService {
3108
3129
  return summarizeStarsStatus(response);
3109
3130
  }, `getStarsTransactions ${chatId}`);
3110
3131
  }
3132
+ // ─── Quick replies ─────────────────────────────────────────────────────────
3111
3133
  async getQuickReplies(hash) {
3112
3134
  if (!this.client || !this.connected)
3113
3135
  throw new Error(NOT_CONNECTED_ERROR);
@@ -109,12 +109,12 @@ function compactGraph(g) {
109
109
  }
110
110
  return { type: "data", data: parsed, zoomToken: g.zoomToken };
111
111
  }
112
- const any = g;
113
- if (typeof any.token === "string")
114
- return { type: "async", token: any.token };
115
- if (typeof any.error === "string")
116
- return { type: "error", error: any.error };
117
- return { type: "data", data: any.json?.data, zoomToken: any.zoomToken };
112
+ const graph = g;
113
+ if (typeof graph.token === "string")
114
+ return { type: "async", token: graph.token };
115
+ if (typeof graph.error === "string")
116
+ return { type: "error", error: graph.error };
117
+ return { type: "data", data: graph.json?.data, zoomToken: graph.zoomToken };
118
118
  }
119
119
  export function summarizeMegagroupStats(stats, includeGraphs) {
120
120
  const summary = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@overpod/mcp-telegram",
3
- "version": "1.26.1",
3
+ "version": "1.27.1",
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",
@@ -27,8 +27,9 @@
27
27
  "lint": "biome check src/",
28
28
  "lint:fix": "biome check --fix src/",
29
29
  "format": "biome format --write src/",
30
- "test": "tsx --test src/**/*.test.ts",
31
- "test:watch": "tsx --test --watch src/**/*.test.ts",
30
+ "test": "tsx --test 'src/**/*.test.ts'",
31
+ "test:watch": "tsx --test --watch 'src/**/*.test.ts'",
32
+ "test:coverage": "c8 --all --src src --exclude 'src/**/*.test.ts' --reporter=text tsx --test 'src/**/*.test.ts'",
32
33
  "docs:dev": "vitepress dev docs",
33
34
  "docs:build": "vitepress build docs",
34
35
  "docs:preview": "vitepress preview docs"
@@ -65,6 +66,7 @@
65
66
  "@biomejs/biome": "^2.4.12",
66
67
  "@types/node": "^25.6.0",
67
68
  "@types/qrcode": "^1.5.6",
69
+ "c8": "^11.0.0",
68
70
  "tsx": "^4.21.0",
69
71
  "typescript": "^6.0.3",
70
72
  "vitepress": "^1.6.4"