@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.
- package/dist/client.d.ts +22 -0
- package/dist/client.js +144 -0
- package/dist/index.js +12 -24
- package/dist/ipc-protocol.d.ts +26 -0
- package/dist/ipc-protocol.js +22 -0
- package/dist/lock.d.ts +12 -0
- package/dist/lock.js +83 -0
- package/dist/master.d.ts +4 -0
- package/dist/master.js +105 -0
- package/dist/telegram-client.js +26 -4
- package/dist/telegram-helpers.js +6 -6
- package/package.json +5 -3
package/dist/client.d.ts
ADDED
|
@@ -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 {
|
|
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
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
|
|
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
|
+
}
|
package/dist/master.d.ts
ADDED
|
@@ -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
|
+
}
|
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
|
}
|
|
@@ -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
|
-
//
|
|
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
|
|
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
|
|
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);
|
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.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"
|