@overpod/mcp-telegram 1.37.1 → 1.38.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/CHANGELOG.md +19 -0
- package/README.md +16 -0
- package/dist/index.js +24 -7
- package/dist/lock.js +30 -4
- package/dist/master.d.ts +16 -1
- package/dist/master.js +48 -18
- package/dist/serve.d.ts +6 -0
- package/dist/serve.js +22 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,25 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [1.38.1](https://github.com/mcp-telegram/mcp-telegram/compare/v1.38.0...v1.38.1) (2026-06-10)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
* **deps:** bump transitive hono 4.12.18 → 4.12.25 (security) ([f90b32c](https://github.com/mcp-telegram/mcp-telegram/commit/f90b32c61588c92e4f83d938d6b68c83d4deb82f))
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
### Documentation
|
|
17
|
+
|
|
18
|
+
* **daemon:** fix dead link to packaging/ — point at GitHub, not a relative path ([0574c6a](https://github.com/mcp-telegram/mcp-telegram/commit/0574c6a369b9c0d30311abf374c59f80462b289e))
|
|
19
|
+
|
|
20
|
+
## [1.38.0](https://github.com/mcp-telegram/mcp-telegram/compare/v1.37.1...v1.38.0) (2026-06-10)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
|
|
25
|
+
* shared daemon (serve mode) for concurrent MCP clients ([#49](https://github.com/mcp-telegram/mcp-telegram/issues/49)) ([7568927](https://github.com/mcp-telegram/mcp-telegram/commit/75689270ffb21ca4f14e9e7673a36af25f36cde5))
|
|
26
|
+
|
|
8
27
|
## [1.37.1](https://github.com/mcp-telegram/mcp-telegram/compare/v1.37.0...v1.37.1) (2026-06-06)
|
|
9
28
|
|
|
10
29
|
|
package/README.md
CHANGED
|
@@ -102,6 +102,22 @@ claude mcp add telegram-personal -s user \
|
|
|
102
102
|
|
|
103
103
|
Each account gets its own session file — no conflicts.
|
|
104
104
|
|
|
105
|
+
### Multiple agents / concurrent clients (shared daemon)
|
|
106
|
+
|
|
107
|
+
The opposite of multiple accounts: **one** account driven by **many** clients at once — several Claude Code windows, parallel sub-agents, or multiple IDEs. Normally each process opens the same session and they evict one another with `AUTH_KEY_DUPLICATED`. Serve mode fixes this.
|
|
108
|
+
|
|
109
|
+
Run a single persistent **daemon** that owns the one Telegram connection. Every other process auto-detects the daemon (via a PID lock) and becomes a thin client that proxies tool calls to it over a local Unix socket:
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
# On the host, once: start the daemon (owns the connection, no stdio)
|
|
113
|
+
TELEGRAM_API_ID=YOUR_ID TELEGRAM_API_HASH=YOUR_HASH mcp-telegram serve
|
|
114
|
+
# (or set MCP_TELEGRAM_DAEMON=1 instead of the `serve` argument)
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Then point each MCP client at the same install with the same `TELEGRAM_SESSION_PATH` — no `serve` argument. They connect to the daemon automatically; closing any client never drops the shared connection. Credentials are only required by the daemon (the owner), so client commands can omit `TELEGRAM_API_ID`/`TELEGRAM_API_HASH` and keep them where the daemon runs.
|
|
118
|
+
|
|
119
|
+
See the **[shared daemon guide](docs/guides/shared-daemon.md)** for a systemd unit and SSH usage.
|
|
120
|
+
|
|
105
121
|
### Proxy Support
|
|
106
122
|
|
|
107
123
|
If Telegram is blocked or you're running in a containerized environment (Docker, K3s), use a SOCKS5 or MTProxy:
|
package/dist/index.js
CHANGED
|
@@ -12,23 +12,40 @@ const { version } = require("../package.json");
|
|
|
12
12
|
// Telegram API credentials from env
|
|
13
13
|
const API_ID = Number(process.env.TELEGRAM_API_ID);
|
|
14
14
|
const API_HASH = process.env.TELEGRAM_API_HASH;
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
15
|
+
// Credentials are required only on the OWNER paths (serve/master) that open the real
|
|
16
|
+
// Telegram connection. A client proxies every call over IPC and never connects, so it
|
|
17
|
+
// runs without them — letting credentials stay only where the daemon runs.
|
|
18
|
+
function requireCreds() {
|
|
19
|
+
if (!API_ID || !API_HASH) {
|
|
20
|
+
console.error("[mcp-telegram] Missing TELEGRAM_API_ID and TELEGRAM_API_HASH (required to own the Telegram connection)");
|
|
21
|
+
console.error("Get your credentials at https://my.telegram.org/apps (API development tools)");
|
|
22
|
+
console.error("Set them in .env or export as environment variables");
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
20
25
|
}
|
|
21
26
|
async function main() {
|
|
27
|
+
// Persistent daemon mode: `mcp-telegram serve` (or MCP_TELEGRAM_DAEMON=1). The daemon owns
|
|
28
|
+
// the connection with no stdio attached; every other process becomes a client.
|
|
29
|
+
if (process.argv[2] === "serve" || process.env.MCP_TELEGRAM_DAEMON === "1") {
|
|
30
|
+
requireCreds();
|
|
31
|
+
console.error("[mcp-telegram] Starting in serve (daemon) mode");
|
|
32
|
+
const { runServe } = await import("./serve.js");
|
|
33
|
+
await runServe(API_ID, API_HASH, version);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
22
36
|
const isMaster = tryAcquireLock();
|
|
23
37
|
if (isMaster) {
|
|
38
|
+
requireCreds();
|
|
24
39
|
console.error("[mcp-telegram] Starting as master process");
|
|
25
40
|
const { runMaster } = await import("./master.js");
|
|
26
41
|
await runMaster(API_ID, API_HASH, version);
|
|
27
42
|
}
|
|
28
43
|
else {
|
|
29
|
-
|
|
44
|
+
// Client proxies all tool calls to the owner over IPC; the dummy TelegramService is
|
|
45
|
+
// never used for real calls, so API credentials are optional here.
|
|
46
|
+
console.error("[mcp-telegram] Starting as client process (owner already running)");
|
|
30
47
|
const { runClient } = await import("./client.js");
|
|
31
|
-
await runClient(API_ID, API_HASH, version);
|
|
48
|
+
await runClient(API_ID || 0, API_HASH ?? "", version);
|
|
32
49
|
}
|
|
33
50
|
}
|
|
34
51
|
main().catch((err) => {
|
package/dist/lock.js
CHANGED
|
@@ -37,8 +37,12 @@ export function tryAcquireLock() {
|
|
|
37
37
|
// Process is alive — another master owns the lock
|
|
38
38
|
return false;
|
|
39
39
|
}
|
|
40
|
-
catch {
|
|
41
|
-
// ESRCH: process not found — stale lock, take over
|
|
40
|
+
catch (err) {
|
|
41
|
+
// ESRCH: process not found — stale lock, take over.
|
|
42
|
+
// EPERM: process is alive but owned by another uid we can't signal —
|
|
43
|
+
// treat as a live owner and DON'T steal the lock.
|
|
44
|
+
if (err.code === "EPERM")
|
|
45
|
+
return false;
|
|
42
46
|
unlinkSync(lock);
|
|
43
47
|
}
|
|
44
48
|
}
|
|
@@ -76,8 +80,30 @@ export function releaseLock() {
|
|
|
76
80
|
export function releaseSocket() {
|
|
77
81
|
try {
|
|
78
82
|
const sock = socketPath();
|
|
79
|
-
if (existsSync(sock))
|
|
80
|
-
|
|
83
|
+
if (!existsSync(sock))
|
|
84
|
+
return;
|
|
85
|
+
// Ownership guard (mirrors releaseLock): never unlink a socket owned by a different,
|
|
86
|
+
// still-alive process. Otherwise any process that imports this module and exits (e.g. a
|
|
87
|
+
// one-shot run or a test on the same host) would delete a running daemon's socket file,
|
|
88
|
+
// leaving the daemon listening in memory but unreachable for new clients.
|
|
89
|
+
const lock = lockPath();
|
|
90
|
+
if (existsSync(lock)) {
|
|
91
|
+
const pid = Number.parseInt(readFileSync(lock, "utf-8").trim(), 10);
|
|
92
|
+
// Ignore non-positive PIDs (e.g. 0) so kill() can't probe our own process group.
|
|
93
|
+
if (!Number.isNaN(pid) && pid > 0 && pid !== process.pid) {
|
|
94
|
+
try {
|
|
95
|
+
process.kill(pid, 0); // foreign owner still alive?
|
|
96
|
+
return; // yes — leave its socket alone
|
|
97
|
+
}
|
|
98
|
+
catch (err) {
|
|
99
|
+
// ESRCH: stale owner — safe to remove. EPERM: owner is alive under a
|
|
100
|
+
// different uid (e.g. a systemd daemon) — leave its socket alone too.
|
|
101
|
+
if (err.code === "EPERM")
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
unlinkSync(sock);
|
|
81
107
|
}
|
|
82
108
|
catch { }
|
|
83
109
|
}
|
package/dist/master.d.ts
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
|
-
import { type Socket } from "node:net";
|
|
1
|
+
import { type Server, type Socket } from "node:net";
|
|
2
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
3
|
import { type McpServerInternal } from "./ipc-protocol.js";
|
|
3
4
|
import { TelegramService } from "./telegram-client.js";
|
|
4
5
|
export declare function handleClient(socket: Socket, mcpServer: McpServerInternal, telegram: TelegramService): void;
|
|
6
|
+
export interface OwnerHandle {
|
|
7
|
+
server: McpServer;
|
|
8
|
+
srv: Server;
|
|
9
|
+
gracefulExit: () => Promise<void>;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Bootstrap the connection owner shared by master (stdio) and serve (daemon) modes:
|
|
13
|
+
* build the tool registry, listen on the IPC socket, install a graceful shutdown that
|
|
14
|
+
* disconnects Telegram, and auto-connect the single client. No stdio is attached here —
|
|
15
|
+
* the caller decides whether to also serve a stdio MCP session (master) or not (serve).
|
|
16
|
+
*/
|
|
17
|
+
export declare function startOwner(telegram: TelegramService, version: string, opts?: {
|
|
18
|
+
label?: string;
|
|
19
|
+
}): Promise<OwnerHandle>;
|
|
5
20
|
export declare function runMaster(apiId: number, apiHash: string, version: string): Promise<void>;
|
package/dist/master.js
CHANGED
|
@@ -17,8 +17,6 @@ function cleanup() {
|
|
|
17
17
|
releaseSocket();
|
|
18
18
|
}
|
|
19
19
|
process.on("exit", cleanup);
|
|
20
|
-
process.on("SIGINT", () => process.exit(0));
|
|
21
|
-
process.on("SIGTERM", () => process.exit(0));
|
|
22
20
|
// Serializes tool calls with QR login — login holds the lock for up to minutes,
|
|
23
21
|
// tool calls queue behind it. Prevents tool calls from running against a stale
|
|
24
22
|
// Telegram client mid-relogin.
|
|
@@ -126,15 +124,25 @@ async function handleLoginStart(socket, req, telegram) {
|
|
|
126
124
|
unlock();
|
|
127
125
|
}
|
|
128
126
|
}
|
|
129
|
-
|
|
130
|
-
|
|
127
|
+
/**
|
|
128
|
+
* Bootstrap the connection owner shared by master (stdio) and serve (daemon) modes:
|
|
129
|
+
* build the tool registry, listen on the IPC socket, install a graceful shutdown that
|
|
130
|
+
* disconnects Telegram, and auto-connect the single client. No stdio is attached here —
|
|
131
|
+
* the caller decides whether to also serve a stdio MCP session (master) or not (serve).
|
|
132
|
+
*/
|
|
133
|
+
export async function startOwner(telegram, version, opts = {}) {
|
|
134
|
+
const label = opts.label ?? "mcp-telegram";
|
|
131
135
|
const server = new McpServer({ name: "mcp-telegram", version });
|
|
132
136
|
registerTools(server, telegram);
|
|
133
137
|
const mcpServer = server;
|
|
134
|
-
// Remove stale socket file from previous crash before attempting to listen
|
|
138
|
+
// Remove a stale socket file from a previous crash before attempting to listen.
|
|
135
139
|
releaseSocket();
|
|
136
140
|
const sock = socketPath();
|
|
137
|
-
const srv = createServer((socket) =>
|
|
141
|
+
const srv = createServer((socket) => {
|
|
142
|
+
console.error(`[${label}] client connected`);
|
|
143
|
+
socket.on("close", () => console.error(`[${label}] client disconnected`));
|
|
144
|
+
handleClient(socket, mcpServer, telegram);
|
|
145
|
+
});
|
|
138
146
|
await new Promise((resolve, reject) => {
|
|
139
147
|
srv.listen(sock, resolve);
|
|
140
148
|
srv.once("error", reject);
|
|
@@ -144,27 +152,49 @@ export async function runMaster(apiId, apiHash, version) {
|
|
|
144
152
|
await chmod(sock, 0o600);
|
|
145
153
|
}
|
|
146
154
|
catch { }
|
|
147
|
-
console.error(`[
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
155
|
+
console.error(`[${label}] IPC socket ready: ${sock}`);
|
|
156
|
+
let shuttingDown = false;
|
|
157
|
+
const gracefulExit = async () => {
|
|
158
|
+
if (shuttingDown)
|
|
159
|
+
return;
|
|
160
|
+
shuttingDown = true;
|
|
161
|
+
console.error(`[${label}] Shutting down, disconnecting from Telegram...`);
|
|
162
|
+
try {
|
|
163
|
+
await telegram.disconnect();
|
|
164
|
+
}
|
|
165
|
+
catch (err) {
|
|
166
|
+
console.error(`[${label}] Disconnect error:`, err);
|
|
167
|
+
}
|
|
168
|
+
process.exit(0);
|
|
169
|
+
};
|
|
170
|
+
process.on("SIGINT", gracefulExit);
|
|
171
|
+
process.on("SIGTERM", gracefulExit);
|
|
172
|
+
// Auto-connect with saved session — catch to avoid unhandled rejection.
|
|
156
173
|
telegram
|
|
157
174
|
.loadSession()
|
|
158
175
|
.then(async () => {
|
|
159
176
|
if (await telegram.connect()) {
|
|
160
177
|
const me = await telegram.getMe();
|
|
161
|
-
console.error(`[
|
|
178
|
+
console.error(`[${label}] connected as @${me.username}`);
|
|
162
179
|
}
|
|
163
180
|
else if (telegram.lastError) {
|
|
164
|
-
console.error(`[
|
|
181
|
+
console.error(`[${label}] ${telegram.lastError}`);
|
|
165
182
|
}
|
|
166
183
|
})
|
|
167
184
|
.catch((err) => {
|
|
168
|
-
console.error(
|
|
185
|
+
console.error(`[${label}] Auto-connect failed:`, err);
|
|
169
186
|
});
|
|
187
|
+
return { server, srv, gracefulExit };
|
|
188
|
+
}
|
|
189
|
+
export async function runMaster(apiId, apiHash, version) {
|
|
190
|
+
const telegram = new TelegramService(apiId, apiHash);
|
|
191
|
+
const { server, gracefulExit } = await startOwner(telegram, version, { label: "mcp-telegram (master)" });
|
|
192
|
+
// Parent (Claude Code / MCP client) can close stdio without sending a signal.
|
|
193
|
+
// Without this, the process keeps running as an orphan with a live Telegram connection,
|
|
194
|
+
// blocking auth_key from being reused — causes AUTH_KEY_DUPLICATED on next start.
|
|
195
|
+
process.stdin.on("end", gracefulExit);
|
|
196
|
+
// Master also serves the launching window directly over stdio.
|
|
197
|
+
const transport = new StdioServerTransport();
|
|
198
|
+
await server.connect(transport);
|
|
199
|
+
console.error("[mcp-telegram] MCP server running on stdio (master)");
|
|
170
200
|
}
|
package/dist/serve.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Persistent daemon mode: own the single Telegram connection and serve many concurrent
|
|
3
|
+
* IPC clients, with no stdio and no stdin-exit, so closing any client never tears the
|
|
4
|
+
* connection down. Intended to run under a supervisor (systemd, Docker) with Restart=always.
|
|
5
|
+
*/
|
|
6
|
+
export declare function runServe(apiId: number, apiHash: string, version: string): Promise<void>;
|
package/dist/serve.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { tryAcquireLock } from "./lock.js";
|
|
2
|
+
import { startOwner } from "./master.js";
|
|
3
|
+
import { TelegramService } from "./telegram-client.js";
|
|
4
|
+
/**
|
|
5
|
+
* Persistent daemon mode: own the single Telegram connection and serve many concurrent
|
|
6
|
+
* IPC clients, with no stdio and no stdin-exit, so closing any client never tears the
|
|
7
|
+
* connection down. Intended to run under a supervisor (systemd, Docker) with Restart=always.
|
|
8
|
+
*/
|
|
9
|
+
export async function runServe(apiId, apiHash, version) {
|
|
10
|
+
// The daemon must be the sole connection owner. If another owner already holds the lock,
|
|
11
|
+
// refuse rather than open a second client on the same session (AUTH_KEY_DUPLICATED).
|
|
12
|
+
if (!tryAcquireLock()) {
|
|
13
|
+
console.error("[serve] Another owner already holds the lock; refusing to start a second daemon.");
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
|
16
|
+
const telegram = new TelegramService(apiId, apiHash);
|
|
17
|
+
// Owner core: socket server + IPC dispatch + auto-connect + SIGINT/SIGTERM graceful shutdown.
|
|
18
|
+
// No StdioServerTransport and no process.stdin handler — the daemon's lifetime is independent
|
|
19
|
+
// of any client. The listening socket keeps the event loop alive until a termination signal.
|
|
20
|
+
await startOwner(telegram, version, { label: "serve" });
|
|
21
|
+
console.error("[serve] daemon ready — owning the Telegram connection, no stdio attached");
|
|
22
|
+
}
|
package/package.json
CHANGED