@overpod/mcp-telegram 1.27.0 → 1.28.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/dist/client.d.ts +24 -0
- package/dist/client.js +97 -19
- package/dist/global-lock.d.ts +10 -0
- package/dist/global-lock.js +29 -0
- package/dist/ipc-protocol.d.ts +35 -6
- package/dist/ipc-protocol.js +12 -1
- package/dist/master.d.ts +4 -0
- package/dist/master.js +90 -30
- package/dist/qr-login-cli.js +45 -9
- package/dist/telegram-client.d.ts +1 -1
- package/dist/telegram-client.js +72 -10
- package/dist/telegram-helpers.js +6 -6
- package/package.json +5 -3
package/dist/client.d.ts
CHANGED
|
@@ -1,12 +1,36 @@
|
|
|
1
|
+
import { type Socket } from "node:net";
|
|
2
|
+
import { type IpcLoginDone } from "./ipc-protocol.js";
|
|
3
|
+
export interface IpcClientOptions {
|
|
4
|
+
connectTimeoutMs?: number;
|
|
5
|
+
callTimeoutMs?: number;
|
|
6
|
+
loginTimeoutMs?: number;
|
|
7
|
+
connectFn?: (path: string) => Socket;
|
|
8
|
+
}
|
|
1
9
|
/** Thin IPC proxy: forwards tool calls to the master process over Unix socket */
|
|
2
10
|
export declare class IpcClient {
|
|
3
11
|
private socket;
|
|
4
12
|
private pending;
|
|
13
|
+
private pendingLogins;
|
|
5
14
|
private buf;
|
|
6
15
|
private connected;
|
|
16
|
+
private destroyed;
|
|
17
|
+
private onDisconnect?;
|
|
18
|
+
private readonly connectTimeoutMs;
|
|
19
|
+
private readonly callTimeoutMs;
|
|
20
|
+
private readonly loginTimeoutMs;
|
|
21
|
+
private readonly connectFn;
|
|
22
|
+
constructor(opts?: IpcClientOptions);
|
|
23
|
+
/** Register a callback fired when the peer socket closes unexpectedly.
|
|
24
|
+
* Call this AFTER a successful connect() so aborted connection attempts don't fire it. */
|
|
25
|
+
setOnDisconnect(cb: () => void): void;
|
|
7
26
|
connect(): Promise<boolean>;
|
|
27
|
+
private routeMessage;
|
|
8
28
|
isConnected(): boolean;
|
|
9
29
|
call(tool: string, args: Record<string, unknown>): Promise<unknown>;
|
|
30
|
+
/** Request QR login flow from master. `onQr` fires for each QR URL frame (refreshes ~every 10s).
|
|
31
|
+
* Only one login can run on the master side at a time — a concurrent call gets an immediate
|
|
32
|
+
* `login_done {success:false}` with "Another QR login is already in progress". */
|
|
33
|
+
loginFlow(onQr: (url: string) => void): Promise<IpcLoginDone>;
|
|
10
34
|
destroy(): void;
|
|
11
35
|
}
|
|
12
36
|
export declare function runClient(apiId: number, apiHash: string, version: string): Promise<void>;
|
package/dist/client.js
CHANGED
|
@@ -2,28 +2,47 @@ import { randomUUID } from "node:crypto";
|
|
|
2
2
|
import { connect } from "node:net";
|
|
3
3
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
4
4
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5
|
-
import { encodeMessage, parseMessages } from "./ipc-protocol.js";
|
|
5
|
+
import { encodeMessage, parseMessages, } from "./ipc-protocol.js";
|
|
6
6
|
import { socketPath } from "./lock.js";
|
|
7
7
|
import { TelegramService } from "./telegram-client.js";
|
|
8
8
|
import { registerTools } from "./tools/index.js";
|
|
9
9
|
const CONNECT_TIMEOUT_MS = 5_000;
|
|
10
10
|
const IPC_CALL_TIMEOUT_MS = 30_000;
|
|
11
|
+
const LOGIN_FLOW_TIMEOUT_MS = 360_000; // 6 min — QR has ~5 min server-side window
|
|
11
12
|
const MAX_RECONNECT_ATTEMPTS = 5;
|
|
12
13
|
/** Thin IPC proxy: forwards tool calls to the master process over Unix socket */
|
|
13
14
|
export class IpcClient {
|
|
14
15
|
socket = null;
|
|
15
16
|
pending = new Map();
|
|
17
|
+
pendingLogins = new Map();
|
|
16
18
|
buf = "";
|
|
17
19
|
connected = false;
|
|
20
|
+
destroyed = false;
|
|
21
|
+
onDisconnect;
|
|
22
|
+
connectTimeoutMs;
|
|
23
|
+
callTimeoutMs;
|
|
24
|
+
loginTimeoutMs;
|
|
25
|
+
connectFn;
|
|
26
|
+
constructor(opts = {}) {
|
|
27
|
+
this.connectTimeoutMs = opts.connectTimeoutMs ?? CONNECT_TIMEOUT_MS;
|
|
28
|
+
this.callTimeoutMs = opts.callTimeoutMs ?? IPC_CALL_TIMEOUT_MS;
|
|
29
|
+
this.loginTimeoutMs = opts.loginTimeoutMs ?? LOGIN_FLOW_TIMEOUT_MS;
|
|
30
|
+
this.connectFn = opts.connectFn ?? connect;
|
|
31
|
+
}
|
|
32
|
+
/** Register a callback fired when the peer socket closes unexpectedly.
|
|
33
|
+
* Call this AFTER a successful connect() so aborted connection attempts don't fire it. */
|
|
34
|
+
setOnDisconnect(cb) {
|
|
35
|
+
this.onDisconnect = cb;
|
|
36
|
+
}
|
|
18
37
|
async connect() {
|
|
19
38
|
return new Promise((resolve) => {
|
|
20
39
|
const sock = socketPath();
|
|
21
|
-
const s =
|
|
40
|
+
const s = this.connectFn(sock);
|
|
22
41
|
// One-shot connect timeout — cleared immediately on connect (HIGH-3)
|
|
23
42
|
const connectTimer = setTimeout(() => {
|
|
24
43
|
s.destroy();
|
|
25
44
|
resolve(false);
|
|
26
|
-
},
|
|
45
|
+
}, this.connectTimeoutMs);
|
|
27
46
|
const onConnect = () => {
|
|
28
47
|
clearTimeout(connectTimer);
|
|
29
48
|
this.socket = s;
|
|
@@ -33,20 +52,8 @@ export class IpcClient {
|
|
|
33
52
|
this.buf += chunk.toString("utf-8");
|
|
34
53
|
const { messages, remaining } = parseMessages(this.buf);
|
|
35
54
|
this.buf = remaining;
|
|
36
|
-
for (const msg of messages)
|
|
37
|
-
|
|
38
|
-
const pending = this.pending.get(res.id);
|
|
39
|
-
if (!pending)
|
|
40
|
-
continue;
|
|
41
|
-
clearTimeout(pending.timer);
|
|
42
|
-
this.pending.delete(res.id);
|
|
43
|
-
if (res.error) {
|
|
44
|
-
pending.reject(new Error(res.error));
|
|
45
|
-
}
|
|
46
|
-
else {
|
|
47
|
-
pending.resolve(res.result);
|
|
48
|
-
}
|
|
49
|
-
}
|
|
55
|
+
for (const msg of messages)
|
|
56
|
+
this.routeMessage(msg);
|
|
50
57
|
});
|
|
51
58
|
s.on("close", () => {
|
|
52
59
|
this.connected = false;
|
|
@@ -55,7 +62,17 @@ export class IpcClient {
|
|
|
55
62
|
p.reject(new Error("IPC connection closed"));
|
|
56
63
|
}
|
|
57
64
|
this.pending.clear();
|
|
65
|
+
for (const [, l] of this.pendingLogins) {
|
|
66
|
+
clearTimeout(l.timer);
|
|
67
|
+
l.reject(new Error("IPC connection closed"));
|
|
68
|
+
}
|
|
69
|
+
this.pendingLogins.clear();
|
|
70
|
+
if (!this.destroyed)
|
|
71
|
+
this.onDisconnect?.();
|
|
58
72
|
});
|
|
73
|
+
// Post-connect errors (EPIPE, ECONNRESET) land here. Silent drop keeps the
|
|
74
|
+
// process alive for the "close" handler above to clean up pending calls.
|
|
75
|
+
// Node requires an error listener on sockets — absence crashes the process.
|
|
59
76
|
s.on("error", () => { });
|
|
60
77
|
resolve(true);
|
|
61
78
|
};
|
|
@@ -67,6 +84,35 @@ export class IpcClient {
|
|
|
67
84
|
s.once("error", onError);
|
|
68
85
|
});
|
|
69
86
|
}
|
|
87
|
+
routeMessage(msg) {
|
|
88
|
+
if (msg.type === "tool_response") {
|
|
89
|
+
const pending = this.pending.get(msg.id);
|
|
90
|
+
if (!pending)
|
|
91
|
+
return;
|
|
92
|
+
clearTimeout(pending.timer);
|
|
93
|
+
this.pending.delete(msg.id);
|
|
94
|
+
if (msg.error)
|
|
95
|
+
pending.reject(new Error(msg.error));
|
|
96
|
+
else
|
|
97
|
+
pending.resolve(msg.result);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
if (msg.type === "login_qr") {
|
|
101
|
+
const login = this.pendingLogins.get(msg.id);
|
|
102
|
+
login?.onQr(msg.url);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
if (msg.type === "login_done") {
|
|
106
|
+
const login = this.pendingLogins.get(msg.id);
|
|
107
|
+
if (!login)
|
|
108
|
+
return;
|
|
109
|
+
clearTimeout(login.timer);
|
|
110
|
+
this.pendingLogins.delete(msg.id);
|
|
111
|
+
login.resolve(msg);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
// tool / login_start are client→master only; ignored if received
|
|
115
|
+
}
|
|
70
116
|
isConnected() {
|
|
71
117
|
return this.connected;
|
|
72
118
|
}
|
|
@@ -80,17 +126,41 @@ export class IpcClient {
|
|
|
80
126
|
const timer = setTimeout(() => {
|
|
81
127
|
this.pending.delete(id);
|
|
82
128
|
reject(new Error(`IPC call timeout: ${tool}`));
|
|
83
|
-
},
|
|
129
|
+
}, this.callTimeoutMs);
|
|
84
130
|
this.pending.set(id, { resolve, reject, timer });
|
|
85
|
-
socket.write(encodeMessage({ id, tool, args }));
|
|
131
|
+
socket.write(encodeMessage({ type: "tool", id, tool, args }));
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
/** Request QR login flow from master. `onQr` fires for each QR URL frame (refreshes ~every 10s).
|
|
135
|
+
* Only one login can run on the master side at a time — a concurrent call gets an immediate
|
|
136
|
+
* `login_done {success:false}` with "Another QR login is already in progress". */
|
|
137
|
+
async loginFlow(onQr) {
|
|
138
|
+
if (!this.socket || !this.connected) {
|
|
139
|
+
throw new Error("IPC client not connected");
|
|
140
|
+
}
|
|
141
|
+
const id = randomUUID();
|
|
142
|
+
const socket = this.socket;
|
|
143
|
+
return new Promise((resolve, reject) => {
|
|
144
|
+
const timer = setTimeout(() => {
|
|
145
|
+
this.pendingLogins.delete(id);
|
|
146
|
+
reject(new Error("Login flow timeout"));
|
|
147
|
+
}, this.loginTimeoutMs);
|
|
148
|
+
this.pendingLogins.set(id, { onQr, resolve, reject, timer });
|
|
149
|
+
socket.write(encodeMessage({ type: "login_start", id }));
|
|
86
150
|
});
|
|
87
151
|
}
|
|
88
152
|
destroy() {
|
|
153
|
+
this.destroyed = true;
|
|
89
154
|
for (const [, p] of this.pending) {
|
|
90
155
|
clearTimeout(p.timer);
|
|
91
156
|
p.reject(new Error("IPC client destroyed"));
|
|
92
157
|
}
|
|
93
158
|
this.pending.clear();
|
|
159
|
+
for (const [, l] of this.pendingLogins) {
|
|
160
|
+
clearTimeout(l.timer);
|
|
161
|
+
l.reject(new Error("IPC client destroyed"));
|
|
162
|
+
}
|
|
163
|
+
this.pendingLogins.clear();
|
|
94
164
|
this.socket?.destroy();
|
|
95
165
|
this.socket = null;
|
|
96
166
|
this.connected = false;
|
|
@@ -124,12 +194,20 @@ export async function runClient(apiId, apiHash, version) {
|
|
|
124
194
|
process.exit(1);
|
|
125
195
|
}
|
|
126
196
|
console.error(`[mcp-telegram] Client mode — proxying to master via ${socketPath()}`);
|
|
197
|
+
// Master died → socket closes → nothing to proxy. Exit so parent respawns us against a fresh master.
|
|
198
|
+
// Wire only AFTER successful connect, so retry attempts inside the loop above can't trip it.
|
|
199
|
+
ipc.setOnDisconnect(() => {
|
|
200
|
+
console.error("[mcp-telegram] IPC connection to master closed, exiting");
|
|
201
|
+
process.exit(1);
|
|
202
|
+
});
|
|
127
203
|
// Register all tools for MCP schema; dummy telegram instance is never used for actual calls
|
|
128
204
|
const telegram = new TelegramService(apiId, apiHash);
|
|
129
205
|
const server = new McpServer({ name: "mcp-telegram", version });
|
|
130
206
|
registerTools(server, telegram);
|
|
131
207
|
// Replace all handlers with IPC-forwarding versions
|
|
132
208
|
wireIpcProxies(server, ipc);
|
|
209
|
+
// Parent closed stdio → exit so parent can spawn a fresh instance cleanly
|
|
210
|
+
process.stdin.on("end", () => process.exit(0));
|
|
133
211
|
const transport = new StdioServerTransport();
|
|
134
212
|
await server.connect(transport);
|
|
135
213
|
console.error("[mcp-telegram] MCP server running on stdio (client)");
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** FIFO mutex — ensures only one critical section runs at a time.
|
|
2
|
+
* Used in master to serialize tool calls with QR login flow:
|
|
3
|
+
* login holds the lock for up to minutes; tool calls queue behind it. */
|
|
4
|
+
export declare class GlobalLock {
|
|
5
|
+
private locked;
|
|
6
|
+
private waiters;
|
|
7
|
+
acquire(): Promise<() => void>;
|
|
8
|
+
isLocked(): boolean;
|
|
9
|
+
waitingCount(): number;
|
|
10
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/** FIFO mutex — ensures only one critical section runs at a time.
|
|
2
|
+
* Used in master to serialize tool calls with QR login flow:
|
|
3
|
+
* login holds the lock for up to minutes; tool calls queue behind it. */
|
|
4
|
+
export class GlobalLock {
|
|
5
|
+
locked = false;
|
|
6
|
+
waiters = [];
|
|
7
|
+
async acquire() {
|
|
8
|
+
if (this.locked) {
|
|
9
|
+
await new Promise((resolve) => this.waiters.push(resolve));
|
|
10
|
+
}
|
|
11
|
+
this.locked = true;
|
|
12
|
+
let released = false;
|
|
13
|
+
return () => {
|
|
14
|
+
if (released)
|
|
15
|
+
return;
|
|
16
|
+
released = true;
|
|
17
|
+
this.locked = false;
|
|
18
|
+
const next = this.waiters.shift();
|
|
19
|
+
if (next)
|
|
20
|
+
next();
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
isLocked() {
|
|
24
|
+
return this.locked;
|
|
25
|
+
}
|
|
26
|
+
waitingCount() {
|
|
27
|
+
return this.waiters.length;
|
|
28
|
+
}
|
|
29
|
+
}
|
package/dist/ipc-protocol.d.ts
CHANGED
|
@@ -1,19 +1,48 @@
|
|
|
1
|
-
/**
|
|
2
|
-
export
|
|
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
|
+
/** Client → Master: invoke MCP tool */
|
|
9
|
+
export interface IpcToolRequest {
|
|
10
|
+
type: "tool";
|
|
3
11
|
id: string;
|
|
4
12
|
tool: string;
|
|
5
13
|
args: Record<string, unknown>;
|
|
6
14
|
}
|
|
7
|
-
/**
|
|
8
|
-
export interface
|
|
15
|
+
/** Master → Client: tool result */
|
|
16
|
+
export interface IpcToolResponse {
|
|
17
|
+
type: "tool_response";
|
|
9
18
|
id: string;
|
|
10
19
|
result?: unknown;
|
|
11
20
|
error?: string;
|
|
12
21
|
}
|
|
22
|
+
/** Client → Master: begin QR login flow */
|
|
23
|
+
export interface IpcLoginStart {
|
|
24
|
+
type: "login_start";
|
|
25
|
+
id: string;
|
|
26
|
+
}
|
|
27
|
+
/** Master → Client: QR code URL to display (may fire multiple times as URL refreshes) */
|
|
28
|
+
export interface IpcLoginQr {
|
|
29
|
+
type: "login_qr";
|
|
30
|
+
id: string;
|
|
31
|
+
url: string;
|
|
32
|
+
}
|
|
33
|
+
/** Master → Client: QR login finished */
|
|
34
|
+
export interface IpcLoginDone {
|
|
35
|
+
type: "login_done";
|
|
36
|
+
id: string;
|
|
37
|
+
success: boolean;
|
|
38
|
+
username?: string;
|
|
39
|
+
error?: string;
|
|
40
|
+
}
|
|
41
|
+
export type IpcMessage = IpcToolRequest | IpcToolResponse | IpcLoginStart | IpcLoginQr | IpcLoginDone;
|
|
13
42
|
/** Encode a message as newline-delimited JSON */
|
|
14
|
-
export declare function encodeMessage(msg:
|
|
43
|
+
export declare function encodeMessage(msg: IpcMessage): string;
|
|
15
44
|
/** Parse newline-delimited JSON messages from a buffer, returns parsed messages + leftover */
|
|
16
45
|
export declare function parseMessages(buf: string): {
|
|
17
|
-
messages:
|
|
46
|
+
messages: IpcMessage[];
|
|
18
47
|
remaining: string;
|
|
19
48
|
};
|
package/dist/ipc-protocol.js
CHANGED
|
@@ -12,7 +12,9 @@ export function parseMessages(buf) {
|
|
|
12
12
|
if (!trimmed)
|
|
13
13
|
continue;
|
|
14
14
|
try {
|
|
15
|
-
|
|
15
|
+
const parsed = JSON.parse(trimmed);
|
|
16
|
+
if (isIpcMessage(parsed))
|
|
17
|
+
messages.push(parsed);
|
|
16
18
|
}
|
|
17
19
|
catch {
|
|
18
20
|
// Skip malformed lines
|
|
@@ -20,3 +22,12 @@ export function parseMessages(buf) {
|
|
|
20
22
|
}
|
|
21
23
|
return { messages, remaining };
|
|
22
24
|
}
|
|
25
|
+
function isIpcMessage(m) {
|
|
26
|
+
if (!m || typeof m !== "object" || typeof m.type !== "string" || typeof m.id !== "string")
|
|
27
|
+
return false;
|
|
28
|
+
return (m.type === "tool" ||
|
|
29
|
+
m.type === "tool_response" ||
|
|
30
|
+
m.type === "login_start" ||
|
|
31
|
+
m.type === "login_qr" ||
|
|
32
|
+
m.type === "login_done");
|
|
33
|
+
}
|
package/dist/master.d.ts
CHANGED
|
@@ -1 +1,5 @@
|
|
|
1
|
+
import { type Socket } from "node:net";
|
|
2
|
+
import { type McpServerInternal } from "./ipc-protocol.js";
|
|
3
|
+
import { TelegramService } from "./telegram-client.js";
|
|
4
|
+
export declare function handleClient(socket: Socket, mcpServer: McpServerInternal, telegram: TelegramService): void;
|
|
1
5
|
export declare function runMaster(apiId: number, apiHash: string, version: string): Promise<void>;
|
package/dist/master.js
CHANGED
|
@@ -1,52 +1,51 @@
|
|
|
1
1
|
import { createServer } from "node:net";
|
|
2
2
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
3
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
-
import {
|
|
4
|
+
import { GlobalLock } from "./global-lock.js";
|
|
5
|
+
import { encodeMessage, parseMessages, } from "./ipc-protocol.js";
|
|
5
6
|
import { releaseLock, releaseSocket, socketPath } from "./lock.js";
|
|
6
7
|
import { TelegramService } from "./telegram-client.js";
|
|
7
8
|
import { registerTools } from "./tools/index.js";
|
|
8
|
-
let socketServer = null;
|
|
9
9
|
let cleanedUp = false;
|
|
10
10
|
function cleanup() {
|
|
11
11
|
if (cleanedUp)
|
|
12
12
|
return;
|
|
13
13
|
cleanedUp = true;
|
|
14
|
+
// Sync unlink only — process.exit handlers cannot await async server.close(),
|
|
15
|
+
// and unlinking the socket file is sufficient to release the listening address.
|
|
14
16
|
releaseLock();
|
|
15
17
|
releaseSocket();
|
|
16
|
-
socketServer?.close();
|
|
17
18
|
}
|
|
18
19
|
process.on("exit", cleanup);
|
|
19
20
|
process.on("SIGINT", () => process.exit(0));
|
|
20
21
|
process.on("SIGTERM", () => process.exit(0));
|
|
21
|
-
|
|
22
|
+
// Serializes tool calls with QR login — login holds the lock for up to minutes,
|
|
23
|
+
// tool calls queue behind it. Prevents tool calls from running against a stale
|
|
24
|
+
// Telegram client mid-relogin.
|
|
25
|
+
const globalLock = new GlobalLock();
|
|
26
|
+
let activeLogin = null;
|
|
27
|
+
export function handleClient(socket, mcpServer, telegram) {
|
|
22
28
|
let buf = "";
|
|
23
|
-
// Processing queue — ensures sequential handling even when handler awaits
|
|
24
29
|
let processing = false;
|
|
25
30
|
const queue = [];
|
|
31
|
+
// Per-socket FIFO: messages from one client execute in arrival order.
|
|
32
|
+
// Parallelism across DIFFERENT clients is enforced by globalLock (acquired per handler),
|
|
33
|
+
// not here — a tool call from client A can proceed while client B is in login flow.
|
|
26
34
|
async function drainQueue() {
|
|
27
35
|
if (processing)
|
|
28
36
|
return;
|
|
29
37
|
processing = true;
|
|
30
38
|
while (queue.length > 0) {
|
|
31
|
-
const
|
|
32
|
-
if (!
|
|
39
|
+
const msg = queue.shift();
|
|
40
|
+
if (!msg)
|
|
33
41
|
break;
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
if (!tool) {
|
|
37
|
-
response.error = `Unknown tool: ${req.tool}`;
|
|
42
|
+
if (msg.type === "tool") {
|
|
43
|
+
await handleToolRequest(socket, msg, mcpServer);
|
|
38
44
|
}
|
|
39
|
-
else {
|
|
40
|
-
|
|
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));
|
|
45
|
+
else if (msg.type === "login_start") {
|
|
46
|
+
await handleLoginStart(socket, msg, telegram);
|
|
49
47
|
}
|
|
48
|
+
// Responses and QR frames are master→client only — client-side messages ignored here
|
|
50
49
|
}
|
|
51
50
|
processing = false;
|
|
52
51
|
}
|
|
@@ -54,15 +53,73 @@ function handleClient(socket, mcpServer) {
|
|
|
54
53
|
buf += chunk.toString("utf-8");
|
|
55
54
|
const { messages, remaining } = parseMessages(buf);
|
|
56
55
|
buf = remaining;
|
|
57
|
-
for (const msg of messages)
|
|
58
|
-
|
|
59
|
-
if (!req.id || !req.tool)
|
|
60
|
-
continue;
|
|
61
|
-
queue.push(req);
|
|
62
|
-
}
|
|
56
|
+
for (const msg of messages)
|
|
57
|
+
queue.push(msg);
|
|
63
58
|
drainQueue();
|
|
64
59
|
});
|
|
65
|
-
socket
|
|
60
|
+
// If this socket owned an in-progress QR login, abort it so globalLock
|
|
61
|
+
// releases and tool calls from other clients aren't blocked for minutes.
|
|
62
|
+
socket.on("close", () => {
|
|
63
|
+
if (activeLogin && activeLogin.socket === socket) {
|
|
64
|
+
activeLogin.abort.abort();
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
// Node requires an error listener on sockets. EPIPE/ECONNRESET happen when the peer
|
|
68
|
+
// disappears mid-write; log for diagnostics but don't crash the master.
|
|
69
|
+
socket.on("error", (err) => {
|
|
70
|
+
console.error("[mcp-telegram] IPC socket error:", err.message);
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
function send(socket, msg) {
|
|
74
|
+
if (!socket.destroyed)
|
|
75
|
+
socket.write(encodeMessage(msg));
|
|
76
|
+
}
|
|
77
|
+
async function handleToolRequest(socket, req, mcpServer) {
|
|
78
|
+
const tool = mcpServer._registeredTools[req.tool];
|
|
79
|
+
const response = { type: "tool_response", id: req.id };
|
|
80
|
+
if (!tool) {
|
|
81
|
+
response.error = `Unknown tool: ${req.tool}`;
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
const unlock = await globalLock.acquire();
|
|
85
|
+
try {
|
|
86
|
+
response.result = await tool.handler(req.args ?? {}, {});
|
|
87
|
+
}
|
|
88
|
+
catch (err) {
|
|
89
|
+
response.error = err instanceof Error ? err.message : String(err);
|
|
90
|
+
}
|
|
91
|
+
finally {
|
|
92
|
+
unlock();
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
send(socket, response);
|
|
96
|
+
}
|
|
97
|
+
async function handleLoginStart(socket, req, telegram) {
|
|
98
|
+
const fail = (error) => send(socket, { type: "login_done", id: req.id, success: false, error });
|
|
99
|
+
if (activeLogin) {
|
|
100
|
+
fail("Another QR login is already in progress");
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
const abort = new AbortController();
|
|
104
|
+
activeLogin = { socket, abort };
|
|
105
|
+
const unlock = await globalLock.acquire();
|
|
106
|
+
try {
|
|
107
|
+
const result = await telegram.startQrLogin(() => { }, (url) => send(socket, { type: "login_qr", id: req.id, url }), abort.signal);
|
|
108
|
+
if (result.success) {
|
|
109
|
+
const me = await telegram.getMe();
|
|
110
|
+
send(socket, { type: "login_done", id: req.id, success: true, username: me.username ?? undefined });
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
fail(result.message);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
catch (err) {
|
|
117
|
+
fail(err instanceof Error ? err.message : String(err));
|
|
118
|
+
}
|
|
119
|
+
finally {
|
|
120
|
+
activeLogin = null;
|
|
121
|
+
unlock();
|
|
122
|
+
}
|
|
66
123
|
}
|
|
67
124
|
export async function runMaster(apiId, apiHash, version) {
|
|
68
125
|
const telegram = new TelegramService(apiId, apiHash);
|
|
@@ -72,8 +129,7 @@ export async function runMaster(apiId, apiHash, version) {
|
|
|
72
129
|
// Remove stale socket file from previous crash before attempting to listen (HIGH-2)
|
|
73
130
|
releaseSocket();
|
|
74
131
|
const sock = socketPath();
|
|
75
|
-
const srv = createServer((socket) => handleClient(socket, mcpServer));
|
|
76
|
-
socketServer = srv;
|
|
132
|
+
const srv = createServer((socket) => handleClient(socket, mcpServer, telegram));
|
|
77
133
|
await new Promise((resolve, reject) => {
|
|
78
134
|
srv.listen(sock, resolve);
|
|
79
135
|
srv.once("error", reject);
|
|
@@ -84,6 +140,10 @@ export async function runMaster(apiId, apiHash, version) {
|
|
|
84
140
|
}
|
|
85
141
|
catch { }
|
|
86
142
|
console.error(`[mcp-telegram] Master mode — IPC socket ready: ${sock}`);
|
|
143
|
+
// Parent (Claude Code / MCP client) can close stdio without sending a signal.
|
|
144
|
+
// Without this, the process keeps running as an orphan with a live Telegram connection,
|
|
145
|
+
// blocking auth_key from being reused — causes AUTH_KEY_DUPLICATED on next start.
|
|
146
|
+
process.stdin.on("end", () => process.exit(0));
|
|
87
147
|
const transport = new StdioServerTransport();
|
|
88
148
|
await server.connect(transport);
|
|
89
149
|
console.error("[mcp-telegram] MCP server running on stdio (master)");
|
package/dist/qr-login-cli.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import "dotenv/config";
|
|
3
3
|
import QRCode from "qrcode";
|
|
4
|
+
import { IpcClient } from "./client.js";
|
|
4
5
|
import { TelegramService } from "./telegram-client.js";
|
|
5
6
|
const API_ID = Number(process.env.TELEGRAM_API_ID);
|
|
6
7
|
const API_HASH = process.env.TELEGRAM_API_HASH;
|
|
@@ -8,9 +9,43 @@ if (!API_ID || !API_HASH) {
|
|
|
8
9
|
console.error("Set TELEGRAM_API_ID and TELEGRAM_API_HASH in .env file");
|
|
9
10
|
process.exit(1);
|
|
10
11
|
}
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
async function printQr(url) {
|
|
13
|
+
const terminalQr = await QRCode.toString(url, { type: "terminal", small: true });
|
|
14
|
+
console.log(terminalQr);
|
|
15
|
+
console.log("Waiting for scan...\n");
|
|
16
|
+
}
|
|
17
|
+
function printLoginHeader(viaDaemon) {
|
|
18
|
+
console.log(`\nStarting Telegram QR login${viaDaemon ? " (via running daemon)" : ""}...\n`);
|
|
19
|
+
console.log("Scan the QR code in Telegram app:");
|
|
20
|
+
console.log(" Settings > Devices > Link Desktop Device\n");
|
|
21
|
+
}
|
|
22
|
+
async function ipcLogin() {
|
|
23
|
+
const ipc = new IpcClient();
|
|
24
|
+
const connected = await ipc.connect();
|
|
25
|
+
if (!connected)
|
|
26
|
+
return false;
|
|
27
|
+
printLoginHeader(true);
|
|
28
|
+
try {
|
|
29
|
+
const result = await ipc.loginFlow((url) => {
|
|
30
|
+
printQr(url).catch((err) => {
|
|
31
|
+
console.error(`[mcp-telegram] Failed to render QR: ${err instanceof Error ? err.message : String(err)}`);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
if (result.success) {
|
|
35
|
+
console.log(`Login successful!`);
|
|
36
|
+
console.log(` Account: @${result.username ?? "unknown"}\n`);
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
console.log(`Error: ${result.error ?? "unknown"}\n`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
finally {
|
|
43
|
+
ipc.destroy();
|
|
44
|
+
}
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
async function standaloneLogin() {
|
|
48
|
+
const telegram = new TelegramService(API_ID, API_HASH);
|
|
14
49
|
await telegram.loadSession();
|
|
15
50
|
if (await telegram.connect()) {
|
|
16
51
|
const me = await telegram.getMe();
|
|
@@ -18,13 +53,9 @@ async function main() {
|
|
|
18
53
|
await telegram.disconnect();
|
|
19
54
|
return;
|
|
20
55
|
}
|
|
21
|
-
|
|
22
|
-
console.log("Scan the QR code in Telegram app:");
|
|
23
|
-
console.log(" Settings > Devices > Link Desktop Device\n");
|
|
56
|
+
printLoginHeader(false);
|
|
24
57
|
const result = await telegram.startQrLogin(() => { }, async (url) => {
|
|
25
|
-
|
|
26
|
-
console.log(terminalQr);
|
|
27
|
-
console.log("Waiting for scan...\n");
|
|
58
|
+
await printQr(url);
|
|
28
59
|
});
|
|
29
60
|
if (result.success) {
|
|
30
61
|
console.log("Login successful!");
|
|
@@ -36,4 +67,9 @@ async function main() {
|
|
|
36
67
|
}
|
|
37
68
|
await telegram.disconnect();
|
|
38
69
|
}
|
|
70
|
+
async function main() {
|
|
71
|
+
const viaIpc = await ipcLogin();
|
|
72
|
+
if (!viaIpc)
|
|
73
|
+
await standaloneLogin();
|
|
74
|
+
}
|
|
39
75
|
main().catch(console.error);
|
|
@@ -38,7 +38,7 @@ export declare class TelegramService {
|
|
|
38
38
|
*/
|
|
39
39
|
logOut(): Promise<boolean>;
|
|
40
40
|
isConnected(): boolean;
|
|
41
|
-
startQrLogin(onQrDataUrl: (dataUrl: string) => void, onQrUrl?: (url: string) => void): Promise<{
|
|
41
|
+
startQrLogin(onQrDataUrl: (dataUrl: string) => void, onQrUrl?: (url: string) => void, signal?: AbortSignal): Promise<{
|
|
42
42
|
success: boolean;
|
|
43
43
|
message: string;
|
|
44
44
|
}>;
|
package/dist/telegram-client.js
CHANGED
|
@@ -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
|
}
|
|
@@ -217,7 +218,10 @@ export class TelegramService {
|
|
|
217
218
|
isConnected() {
|
|
218
219
|
return this.connected;
|
|
219
220
|
}
|
|
220
|
-
async startQrLogin(onQrDataUrl, onQrUrl) {
|
|
221
|
+
async startQrLogin(onQrDataUrl, onQrUrl, signal) {
|
|
222
|
+
// Early exit if already aborted — avoids creating a Telegram connection we'd immediately tear down.
|
|
223
|
+
if (signal?.aborted)
|
|
224
|
+
return { success: false, message: "QR login aborted" };
|
|
221
225
|
const session = new StringSession("");
|
|
222
226
|
const proxy = resolveProxy();
|
|
223
227
|
const client = new TelegramClient(session, this.apiId, this.apiHash, {
|
|
@@ -226,6 +230,13 @@ export class TelegramService {
|
|
|
226
230
|
});
|
|
227
231
|
try {
|
|
228
232
|
await client.connect();
|
|
233
|
+
if (signal?.aborted) {
|
|
234
|
+
try {
|
|
235
|
+
await client.destroy();
|
|
236
|
+
}
|
|
237
|
+
catch { }
|
|
238
|
+
return { success: false, message: "QR login aborted" };
|
|
239
|
+
}
|
|
229
240
|
let loginAccepted = false;
|
|
230
241
|
let resolved = false;
|
|
231
242
|
let lastQrUrl = "";
|
|
@@ -236,6 +247,8 @@ export class TelegramService {
|
|
|
236
247
|
});
|
|
237
248
|
const maxAttempts = 30; // 5 minutes
|
|
238
249
|
for (let i = 0; i < maxAttempts && !resolved; i++) {
|
|
250
|
+
if (signal?.aborted)
|
|
251
|
+
break;
|
|
239
252
|
try {
|
|
240
253
|
const result = await client.invoke(new Api.auth.ExportLoginToken({
|
|
241
254
|
apiId: this.apiId,
|
|
@@ -276,17 +289,45 @@ export class TelegramService {
|
|
|
276
289
|
}
|
|
277
290
|
}
|
|
278
291
|
if (!resolved) {
|
|
279
|
-
|
|
292
|
+
// Abortable sleep — wakes immediately when caller cancels
|
|
293
|
+
const waitMs = loginAccepted ? 1500 : 10000;
|
|
294
|
+
await new Promise((resolve) => {
|
|
295
|
+
const timer = setTimeout(() => {
|
|
296
|
+
signal?.removeEventListener("abort", onAbort);
|
|
297
|
+
resolve();
|
|
298
|
+
}, waitMs);
|
|
299
|
+
const onAbort = () => {
|
|
300
|
+
clearTimeout(timer);
|
|
301
|
+
resolve();
|
|
302
|
+
};
|
|
303
|
+
if (signal?.aborted)
|
|
304
|
+
onAbort();
|
|
305
|
+
else
|
|
306
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
307
|
+
});
|
|
280
308
|
}
|
|
281
309
|
}
|
|
310
|
+
if (signal?.aborted && !resolved) {
|
|
311
|
+
try {
|
|
312
|
+
await client.destroy();
|
|
313
|
+
}
|
|
314
|
+
catch { }
|
|
315
|
+
return { success: false, message: "QR login aborted" };
|
|
316
|
+
}
|
|
282
317
|
if (resolved) {
|
|
283
318
|
const newSession = client.session.save();
|
|
284
|
-
//
|
|
285
|
-
//
|
|
319
|
+
// Persist FIRST, adopt SECOND — so if file write fails, in-memory state still
|
|
320
|
+
// matches whatever's on disk; saveSession is try/catch-safe for Docker etc.
|
|
321
|
+
await this.saveSession(newSession);
|
|
322
|
+
// Destroy the previous in-memory client to free its auth_key / socket.
|
|
323
|
+
// Previously left dangling → accumulated orphan Telegram connections per relogin.
|
|
324
|
+
const oldClient = this.client;
|
|
286
325
|
this.client = client;
|
|
287
|
-
this.sessionString = newSession;
|
|
288
326
|
this.connected = true;
|
|
289
|
-
|
|
327
|
+
this.entityCache.clear();
|
|
328
|
+
if (oldClient) {
|
|
329
|
+
oldClient.destroy().catch(() => { });
|
|
330
|
+
}
|
|
290
331
|
return { success: true, message: "Telegram login successful" };
|
|
291
332
|
}
|
|
292
333
|
await client.destroy();
|
|
@@ -300,6 +341,7 @@ export class TelegramService {
|
|
|
300
341
|
return { success: false, message: `Login failed: ${err.message}` };
|
|
301
342
|
}
|
|
302
343
|
}
|
|
344
|
+
// ─── Messages ──────────────────────────────────────────────────────────────
|
|
303
345
|
async getMe() {
|
|
304
346
|
if (!this.client || !this.connected)
|
|
305
347
|
throw new Error(NOT_CONNECTED_ERROR);
|
|
@@ -410,6 +452,7 @@ export class TelegramService {
|
|
|
410
452
|
const resolved = await this.resolvePeer(chatId);
|
|
411
453
|
await this.client.unpinMessage(resolved, messageId);
|
|
412
454
|
}
|
|
455
|
+
// ─── Dialogs ───────────────────────────────────────────────────────────────
|
|
413
456
|
async getDialogs(limit = 20, offsetDate, filterType) {
|
|
414
457
|
if (!this.client || !this.connected)
|
|
415
458
|
throw new Error(NOT_CONNECTED_ERROR);
|
|
@@ -499,6 +542,7 @@ export class TelegramService {
|
|
|
499
542
|
};
|
|
500
543
|
});
|
|
501
544
|
}
|
|
545
|
+
// ─── Contacts ──────────────────────────────────────────────────────────────
|
|
502
546
|
async addContact(userId, firstName, lastName, phone) {
|
|
503
547
|
if (!this.client || !this.connected)
|
|
504
548
|
throw new Error(NOT_CONNECTED_ERROR);
|
|
@@ -522,6 +566,7 @@ export class TelegramService {
|
|
|
522
566
|
const peer = await this.client.getInputEntity(chatId);
|
|
523
567
|
await this.client.invoke(new Api.messages.ReportSpam({ peer }));
|
|
524
568
|
}
|
|
569
|
+
// ─── Read state ────────────────────────────────────────────────────────────
|
|
525
570
|
async markAsRead(chatId) {
|
|
526
571
|
if (!this.client || !this.connected)
|
|
527
572
|
throw new Error(NOT_CONNECTED_ERROR);
|
|
@@ -786,6 +831,7 @@ export class TelegramService {
|
|
|
786
831
|
}
|
|
787
832
|
}, `sendTyping in ${chatId}`);
|
|
788
833
|
}
|
|
834
|
+
// ─── Chat lookup & info ────────────────────────────────────────────────────
|
|
789
835
|
/**
|
|
790
836
|
* Resolve a chat by ID, username, or display name.
|
|
791
837
|
* Falls back to searching user's dialogs if getEntity() fails.
|
|
@@ -1125,6 +1171,7 @@ export class TelegramService {
|
|
|
1125
1171
|
})));
|
|
1126
1172
|
return results;
|
|
1127
1173
|
}
|
|
1174
|
+
// ─── Search ────────────────────────────────────────────────────────────────
|
|
1128
1175
|
async getContacts(limit = 50) {
|
|
1129
1176
|
if (!this.client || !this.connected)
|
|
1130
1177
|
throw new Error(NOT_CONNECTED_ERROR);
|
|
@@ -1302,6 +1349,7 @@ export class TelegramService {
|
|
|
1302
1349
|
businessLocation,
|
|
1303
1350
|
};
|
|
1304
1351
|
}
|
|
1352
|
+
// ─── Profiles & Media ──────────────────────────────────────────────────────
|
|
1305
1353
|
async downloadProfilePhoto(entityId, options) {
|
|
1306
1354
|
if (!this.client || !this.connected)
|
|
1307
1355
|
throw new Error(NOT_CONNECTED_ERROR);
|
|
@@ -1360,6 +1408,7 @@ export class TelegramService {
|
|
|
1360
1408
|
}
|
|
1361
1409
|
return items.length > 0 ? items : undefined;
|
|
1362
1410
|
}
|
|
1411
|
+
// ─── Reactions ─────────────────────────────────────────────────────────────
|
|
1363
1412
|
async sendReaction(chatId, messageId, emoji, addToExisting = false) {
|
|
1364
1413
|
if (!this.client || !this.connected)
|
|
1365
1414
|
throw new Error(NOT_CONNECTED_ERROR);
|
|
@@ -1496,6 +1545,7 @@ export class TelegramService {
|
|
|
1496
1545
|
return out;
|
|
1497
1546
|
}, "getRecentReactions");
|
|
1498
1547
|
}
|
|
1548
|
+
// ─── Scheduled & Polls ─────────────────────────────────────────────────────
|
|
1499
1549
|
async sendScheduledMessage(chatId, text, scheduleDate, replyTo, parseMode) {
|
|
1500
1550
|
if (!this.client || !this.connected)
|
|
1501
1551
|
throw new Error(NOT_CONNECTED_ERROR);
|
|
@@ -1614,6 +1664,7 @@ export class TelegramService {
|
|
|
1614
1664
|
catch { }
|
|
1615
1665
|
return false;
|
|
1616
1666
|
}
|
|
1667
|
+
// ─── Chat membership & management ──────────────────────────────────────────
|
|
1617
1668
|
async joinChat(target) {
|
|
1618
1669
|
if (!this.client)
|
|
1619
1670
|
throw new Error(NOT_CONNECTED_ERROR);
|
|
@@ -1895,7 +1946,7 @@ export class TelegramService {
|
|
|
1895
1946
|
rank: "",
|
|
1896
1947
|
}));
|
|
1897
1948
|
}
|
|
1898
|
-
//
|
|
1949
|
+
// ─── Chat settings & moderation ────────────────────────────────────────────
|
|
1899
1950
|
async unblockUser(userId) {
|
|
1900
1951
|
if (!this.client || !this.connected)
|
|
1901
1952
|
throw new Error(NOT_CONNECTED_ERROR);
|
|
@@ -2133,6 +2184,7 @@ export class TelegramService {
|
|
|
2133
2184
|
await this.client?.invoke(new Api.messages.HideChatJoinRequest({ peer: entity, userId: inputUser, approved }));
|
|
2134
2185
|
}, `approveChatJoinRequest ${chatId}/${userId}`);
|
|
2135
2186
|
}
|
|
2187
|
+
// ─── Inline bots & buttons ─────────────────────────────────────────────────
|
|
2136
2188
|
async getInlineBotResults(bot, chatId, query, offset) {
|
|
2137
2189
|
if (!this.client || !this.connected)
|
|
2138
2190
|
throw new Error(NOT_CONNECTED_ERROR);
|
|
@@ -2305,7 +2357,7 @@ export class TelegramService {
|
|
|
2305
2357
|
result = response;
|
|
2306
2358
|
}
|
|
2307
2359
|
catch (e) {
|
|
2308
|
-
const msg = e.message
|
|
2360
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
2309
2361
|
if (/CHAT_ADMIN_REQUIRED|ADMIN_RANK_INVALID/i.test(msg)) {
|
|
2310
2362
|
throw new Error("Access denied: channel stats require admin rights (and may require Telegram Premium)");
|
|
2311
2363
|
}
|
|
@@ -2337,7 +2389,7 @@ export class TelegramService {
|
|
|
2337
2389
|
result = response;
|
|
2338
2390
|
}
|
|
2339
2391
|
catch (e) {
|
|
2340
|
-
const msg = e.message
|
|
2392
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
2341
2393
|
if (/CHAT_ADMIN_REQUIRED|ADMIN_RANK_INVALID/i.test(msg)) {
|
|
2342
2394
|
throw new Error("Access denied: supergroup stats require admin rights");
|
|
2343
2395
|
}
|
|
@@ -2349,6 +2401,7 @@ export class TelegramService {
|
|
|
2349
2401
|
return summarizeMegagroupStats(result, options?.includeGraphs === true);
|
|
2350
2402
|
}, `getMegagroupStats ${chatId}`, { throwOnFloodWait: true });
|
|
2351
2403
|
}
|
|
2404
|
+
// ─── Stats & updates ───────────────────────────────────────────────────────
|
|
2352
2405
|
async getUpdatesState() {
|
|
2353
2406
|
if (!this.client || !this.connected)
|
|
2354
2407
|
throw new Error(NOT_CONNECTED_ERROR);
|
|
@@ -2404,6 +2457,7 @@ export class TelegramService {
|
|
|
2404
2457
|
return summarizeChannelDifference(diff, entity.id.toString(), cursor.pts);
|
|
2405
2458
|
}, `getChannelUpdates ${chatId}`);
|
|
2406
2459
|
}
|
|
2460
|
+
// ─── Forum topics ──────────────────────────────────────────────────────────
|
|
2407
2461
|
async createForumTopic(chatId, title, iconColor, iconEmojiId) {
|
|
2408
2462
|
if (!this.client || !this.connected)
|
|
2409
2463
|
throw new Error(NOT_CONNECTED_ERROR);
|
|
@@ -2477,6 +2531,7 @@ export class TelegramService {
|
|
|
2477
2531
|
}));
|
|
2478
2532
|
}, `deleteForumTopic ${chatId}/${topicId}`);
|
|
2479
2533
|
}
|
|
2534
|
+
// ─── Invite links & folders ────────────────────────────────────────────────
|
|
2480
2535
|
async exportInviteLink(chatId, options) {
|
|
2481
2536
|
if (!this.client || !this.connected)
|
|
2482
2537
|
throw new Error(NOT_CONNECTED_ERROR);
|
|
@@ -2552,6 +2607,7 @@ export class TelegramService {
|
|
|
2552
2607
|
const peer = await this.client.getInputEntity(resolved);
|
|
2553
2608
|
await this.client.invoke(new Api.messages.SetHistoryTTL({ peer, period }));
|
|
2554
2609
|
}
|
|
2610
|
+
// ─── Account & privacy ─────────────────────────────────────────────────────
|
|
2555
2611
|
async getActiveSessions() {
|
|
2556
2612
|
if (!this.client || !this.connected)
|
|
2557
2613
|
throw new Error(NOT_CONNECTED_ERROR);
|
|
@@ -2655,7 +2711,7 @@ export class TelegramService {
|
|
|
2655
2711
|
throw new Error(NOT_CONNECTED_ERROR);
|
|
2656
2712
|
await this.client.invoke(new Api.account.UpdateUsername({ username }));
|
|
2657
2713
|
}
|
|
2658
|
-
// ─── Stickers
|
|
2714
|
+
// ─── Stickers ──────────────────────────────────────────────────────────────
|
|
2659
2715
|
async getStickerSet(shortName) {
|
|
2660
2716
|
if (!this.client || !this.connected)
|
|
2661
2717
|
throw new Error(NOT_CONNECTED_ERROR);
|
|
@@ -2748,6 +2804,7 @@ export class TelegramService {
|
|
|
2748
2804
|
});
|
|
2749
2805
|
}, `sendSticker to ${chatId}`);
|
|
2750
2806
|
}
|
|
2807
|
+
// ─── Drafts & saved dialogs ────────────────────────────────────────────────
|
|
2751
2808
|
async saveDraft(chatId, text, replyTo) {
|
|
2752
2809
|
if (!this.client || !this.connected)
|
|
2753
2810
|
throw new Error(NOT_CONNECTED_ERROR);
|
|
@@ -2943,6 +3000,7 @@ export class TelegramService {
|
|
|
2943
3000
|
emoji: emojiMap.get(doc.id.toString()) || "",
|
|
2944
3001
|
}));
|
|
2945
3002
|
}
|
|
3003
|
+
// ─── Stories ───────────────────────────────────────────────────────────────
|
|
2946
3004
|
async getAllStories(options) {
|
|
2947
3005
|
if (!this.client || !this.connected)
|
|
2948
3006
|
throw new Error(NOT_CONNECTED_ERROR);
|
|
@@ -2999,6 +3057,7 @@ export class TelegramService {
|
|
|
2999
3057
|
return summarizeStoryViewsList(response);
|
|
3000
3058
|
}, `getStoryViewsList ${chatId}/${options.id}`);
|
|
3001
3059
|
}
|
|
3060
|
+
// ─── Boosts ────────────────────────────────────────────────────────────────
|
|
3002
3061
|
async getMyBoosts() {
|
|
3003
3062
|
if (!this.client || !this.connected)
|
|
3004
3063
|
throw new Error(NOT_CONNECTED_ERROR);
|
|
@@ -3046,6 +3105,7 @@ export class TelegramService {
|
|
|
3046
3105
|
return summarizeBusinessChatLinks(response);
|
|
3047
3106
|
}, "getBusinessChatLinks");
|
|
3048
3107
|
}
|
|
3108
|
+
// ─── Group calls ───────────────────────────────────────────────────────────
|
|
3049
3109
|
async getGroupCall(chatId, options = {}) {
|
|
3050
3110
|
if (!this.client || !this.connected)
|
|
3051
3111
|
throw new Error(NOT_CONNECTED_ERROR);
|
|
@@ -3078,6 +3138,7 @@ export class TelegramService {
|
|
|
3078
3138
|
return summarizeGroupCallParticipants(response);
|
|
3079
3139
|
}, `getGroupCallParticipants ${chatId}`);
|
|
3080
3140
|
}
|
|
3141
|
+
// ─── Stars ─────────────────────────────────────────────────────────────────
|
|
3081
3142
|
async getStarsStatus(chatId) {
|
|
3082
3143
|
if (!this.client || !this.connected)
|
|
3083
3144
|
throw new Error(NOT_CONNECTED_ERROR);
|
|
@@ -3108,6 +3169,7 @@ export class TelegramService {
|
|
|
3108
3169
|
return summarizeStarsStatus(response);
|
|
3109
3170
|
}, `getStarsTransactions ${chatId}`);
|
|
3110
3171
|
}
|
|
3172
|
+
// ─── Quick replies ─────────────────────────────────────────────────────────
|
|
3111
3173
|
async getQuickReplies(hash) {
|
|
3112
3174
|
if (!this.client || !this.connected)
|
|
3113
3175
|
throw new Error(NOT_CONNECTED_ERROR);
|
package/dist/telegram-helpers.js
CHANGED
|
@@ -109,12 +109,12 @@ function compactGraph(g) {
|
|
|
109
109
|
}
|
|
110
110
|
return { type: "data", data: parsed, zoomToken: g.zoomToken };
|
|
111
111
|
}
|
|
112
|
-
const
|
|
113
|
-
if (typeof
|
|
114
|
-
return { type: "async", token:
|
|
115
|
-
if (typeof
|
|
116
|
-
return { type: "error", error:
|
|
117
|
-
return { type: "data", data:
|
|
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.
|
|
3
|
+
"version": "1.28.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",
|
|
@@ -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"
|