@overpod/mcp-telegram 1.24.1 → 1.25.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.
@@ -258,6 +258,98 @@ export function registerAccountTools(server, telegram) {
258
258
  return fail(e);
259
259
  }
260
260
  });
261
+ server.registerTool("telegram-save-draft", {
262
+ description: "Save or clear a message draft for a chat. Pass empty text to clear the draft. Optional replyTo sets the message being replied to",
263
+ inputSchema: {
264
+ chatId: z.string().describe("Chat ID or username"),
265
+ text: z.string().describe("Draft text. Empty string clears the draft"),
266
+ replyTo: z.number().int().positive().optional().describe("Message ID this draft replies to"),
267
+ },
268
+ annotations: WRITE,
269
+ }, async ({ chatId, text, replyTo }) => {
270
+ const err = await requireConnection(telegram);
271
+ if (err)
272
+ return fail(new Error(err));
273
+ try {
274
+ await telegram.saveDraft(chatId, text, replyTo);
275
+ return ok(text === "" ? `Draft cleared for ${chatId}` : `Draft saved for ${chatId}`);
276
+ }
277
+ catch (e) {
278
+ return fail(e);
279
+ }
280
+ });
281
+ server.registerTool("telegram-get-drafts", {
282
+ description: "Get all saved message drafts across chats",
283
+ inputSchema: {},
284
+ annotations: READ_ONLY,
285
+ }, async () => {
286
+ const err = await requireConnection(telegram);
287
+ if (err)
288
+ return fail(new Error(err));
289
+ try {
290
+ const drafts = await telegram.getAllDrafts();
291
+ if (drafts.length === 0)
292
+ return ok("No drafts");
293
+ const text = drafts.map((d) => `[${d.chatId}] ${d.chatTitle} (${d.date})\n ${d.text}`).join("\n\n");
294
+ return ok(sanitize(text));
295
+ }
296
+ catch (e) {
297
+ return fail(e);
298
+ }
299
+ });
300
+ server.registerTool("telegram-clear-drafts", {
301
+ description: "Delete saved message drafts. Pass chatId to clear the draft for a single chat. Without chatId, clears drafts in ALL chats — requires confirmAllChats: true",
302
+ inputSchema: {
303
+ chatId: z.string().optional().describe("Chat ID or username. If provided, clears draft only for this chat"),
304
+ confirmAllChats: z
305
+ .boolean()
306
+ .optional()
307
+ .describe("Must be true to wipe drafts across ALL chats when chatId is omitted"),
308
+ },
309
+ annotations: DESTRUCTIVE,
310
+ }, async ({ chatId, confirmAllChats }) => {
311
+ const err = await requireConnection(telegram);
312
+ if (err)
313
+ return fail(new Error(err));
314
+ try {
315
+ if (chatId !== undefined) {
316
+ if (confirmAllChats) {
317
+ return fail(new Error("Pass either chatId or confirmAllChats=true, not both"));
318
+ }
319
+ await telegram.saveDraft(chatId, "");
320
+ return ok(`Draft cleared for ${chatId}`);
321
+ }
322
+ if (!confirmAllChats) {
323
+ return fail(new Error("Refusing to clear drafts in ALL chats without explicit confirmation. Pass chatId for a single chat, or confirmAllChats=true to wipe all drafts"));
324
+ }
325
+ await telegram.clearAllDrafts();
326
+ return ok("All drafts cleared");
327
+ }
328
+ catch (e) {
329
+ return fail(e);
330
+ }
331
+ });
332
+ server.registerTool("telegram-get-saved-dialogs", {
333
+ description: "Get Saved Messages dialogs (Telegram's per-sender grouping of messages forwarded to your Saved Messages)",
334
+ inputSchema: {
335
+ limit: z.number().int().positive().default(20).describe("Max dialogs to return"),
336
+ },
337
+ annotations: READ_ONLY,
338
+ }, async ({ limit }) => {
339
+ const err = await requireConnection(telegram);
340
+ if (err)
341
+ return fail(new Error(err));
342
+ try {
343
+ const dialogs = await telegram.getSavedDialogs(limit);
344
+ if (dialogs.length === 0)
345
+ return ok("No saved dialogs");
346
+ const text = dialogs.map((d) => `[${d.peerId}] ${d.peerTitle} — last msg #${d.lastMsgId}`).join("\n");
347
+ return ok(sanitize(text));
348
+ }
349
+ catch (e) {
350
+ return fail(e);
351
+ }
352
+ });
261
353
  server.registerTool("telegram-revoke-invite-link", {
262
354
  description: "Revoke an invite link for a group or channel",
263
355
  inputSchema: {
@@ -1,5 +1,5 @@
1
1
  import { z } from "zod";
2
- import { fail, ok, READ_ONLY, requireConnection, sanitize, WRITE } from "./shared.js";
2
+ import { DESTRUCTIVE, fail, ok, READ_ONLY, requireConnection, sanitize, WRITE } from "./shared.js";
3
3
  export function registerChatTools(server, telegram) {
4
4
  server.registerTool("telegram-list-chats", {
5
5
  description: "List Telegram chats with unread counts, type indicators, and contact status",
@@ -309,6 +309,89 @@ export function registerChatTools(server, telegram) {
309
309
  return fail(e);
310
310
  }
311
311
  });
312
+ server.registerTool("telegram-archive-chat", {
313
+ description: "Archive or unarchive a Telegram dialog (moves to/from the Archive folder)",
314
+ inputSchema: {
315
+ chatId: z.string().describe("Chat ID or username"),
316
+ archive: z.boolean().describe("true to archive, false to unarchive"),
317
+ },
318
+ annotations: WRITE,
319
+ }, async ({ chatId, archive }) => {
320
+ const err = await requireConnection(telegram);
321
+ if (err)
322
+ return fail(new Error(err));
323
+ try {
324
+ await telegram.archiveChat(chatId, archive);
325
+ return ok(`${archive ? "Archived" : "Unarchived"} ${chatId}`);
326
+ }
327
+ catch (e) {
328
+ return fail(e);
329
+ }
330
+ });
331
+ server.registerTool("telegram-pin-chat", {
332
+ description: "Pin or unpin a Telegram dialog in the dialog list",
333
+ inputSchema: {
334
+ chatId: z.string().describe("Chat ID or username"),
335
+ pin: z.boolean().describe("true to pin, false to unpin"),
336
+ },
337
+ annotations: WRITE,
338
+ }, async ({ chatId, pin }) => {
339
+ const err = await requireConnection(telegram);
340
+ if (err)
341
+ return fail(new Error(err));
342
+ try {
343
+ await telegram.pinDialog(chatId, pin);
344
+ return ok(`${pin ? "Pinned" : "Unpinned"} ${chatId}`);
345
+ }
346
+ catch (e) {
347
+ return fail(e);
348
+ }
349
+ });
350
+ server.registerTool("telegram-mark-dialog-unread", {
351
+ description: "Mark a Telegram dialog as unread (or clear the unread mark)",
352
+ inputSchema: {
353
+ chatId: z.string().describe("Chat ID or username"),
354
+ unread: z.boolean().describe("true to mark as unread, false to clear the mark"),
355
+ },
356
+ annotations: WRITE,
357
+ }, async ({ chatId, unread }) => {
358
+ const err = await requireConnection(telegram);
359
+ if (err)
360
+ return fail(new Error(err));
361
+ try {
362
+ await telegram.markDialogUnread(chatId, unread);
363
+ return ok(`Marked ${chatId} as ${unread ? "unread" : "read"}`);
364
+ }
365
+ catch (e) {
366
+ return fail(e);
367
+ }
368
+ });
369
+ server.registerTool("telegram-get-admin-log", {
370
+ description: "Get the admin action log (recent event history) of a supergroup or channel. Includes bans, edits, pins, and role changes",
371
+ inputSchema: {
372
+ chatId: z.string().describe("Chat ID or username (supergroup or channel)"),
373
+ limit: z.number().int().min(1).max(100).default(20).describe("Number of events to return (1-100)"),
374
+ q: z.string().optional().describe("Optional text filter for events"),
375
+ },
376
+ annotations: READ_ONLY,
377
+ }, async ({ chatId, limit, q }) => {
378
+ const err = await requireConnection(telegram);
379
+ if (err)
380
+ return fail(new Error(err));
381
+ try {
382
+ const events = await telegram.getAdminLog(chatId, limit, q);
383
+ const text = events
384
+ .map((e) => {
385
+ const details = e.details ? ` — ${e.details}` : "";
386
+ return `[#${e.id}] [${e.date}] ${e.userName}: ${e.action}${details}`;
387
+ })
388
+ .join("\n");
389
+ return ok(sanitize(text) || "No admin log events");
390
+ }
391
+ catch (e) {
392
+ return fail(e);
393
+ }
394
+ });
312
395
  server.registerTool("telegram-remove-admin", {
313
396
  description: "Remove admin rights from a user in a supergroup or channel",
314
397
  inputSchema: {
@@ -328,4 +411,158 @@ export function registerChatTools(server, telegram) {
328
411
  return fail(e);
329
412
  }
330
413
  });
414
+ server.registerTool("telegram-set-chat-permissions", {
415
+ description: "Set the default permissions for all non-admin members of a group, supergroup, or channel. Omitted flags keep their current state; true = allowed, false = denied",
416
+ inputSchema: {
417
+ chatId: z.string().describe("Chat ID or username"),
418
+ sendMessages: z.boolean().optional().describe("Allow sending text messages"),
419
+ sendMedia: z.boolean().optional().describe("Allow sending photos/videos/documents"),
420
+ sendStickers: z.boolean().optional().describe("Allow sending stickers"),
421
+ sendGifs: z.boolean().optional().describe("Allow sending GIFs"),
422
+ sendPolls: z.boolean().optional().describe("Allow sending polls"),
423
+ sendInline: z.boolean().optional().describe("Allow inline bot usage"),
424
+ embedLinks: z.boolean().optional().describe("Allow link previews"),
425
+ changeInfo: z.boolean().optional().describe("Allow changing chat info (title, photo, description)"),
426
+ inviteUsers: z.boolean().optional().describe("Allow inviting new members"),
427
+ pinMessages: z.boolean().optional().describe("Allow pinning messages"),
428
+ },
429
+ annotations: DESTRUCTIVE,
430
+ }, async ({ chatId, ...permissions }) => {
431
+ const err = await requireConnection(telegram);
432
+ if (err)
433
+ return fail(new Error(err));
434
+ try {
435
+ await telegram.setChatPermissions(chatId, permissions);
436
+ const changed = Object.entries(permissions)
437
+ .filter(([, v]) => v !== undefined)
438
+ .map(([k, v]) => `${k}=${v ? "allow" : "deny"}`);
439
+ return ok(changed.length > 0
440
+ ? `Updated default permissions for ${chatId}: ${changed.join(", ")}`
441
+ : `No permission changes for ${chatId}`);
442
+ }
443
+ catch (e) {
444
+ return fail(e);
445
+ }
446
+ });
447
+ server.registerTool("telegram-set-slow-mode", {
448
+ description: "Set slow mode for a supergroup (minimum interval between messages per user). Allowed values: 0, 10, 30, 60, 300, 900, 3600 seconds (0 disables slow mode)",
449
+ inputSchema: {
450
+ chatId: z.string().describe("Chat ID or username (supergroup)"),
451
+ seconds: z
452
+ .union([
453
+ z.literal(0),
454
+ z.literal(10),
455
+ z.literal(30),
456
+ z.literal(60),
457
+ z.literal(300),
458
+ z.literal(900),
459
+ z.literal(3600),
460
+ ])
461
+ .describe("Interval in seconds: 0 (off), 10, 30, 60, 300, 900, or 3600"),
462
+ },
463
+ annotations: WRITE,
464
+ }, async ({ chatId, seconds }) => {
465
+ const err = await requireConnection(telegram);
466
+ if (err)
467
+ return fail(new Error(err));
468
+ try {
469
+ await telegram.setSlowMode(chatId, seconds);
470
+ return ok(seconds === 0 ? `Disabled slow mode in ${chatId}` : `Set slow mode to ${seconds}s in ${chatId}`);
471
+ }
472
+ catch (e) {
473
+ return fail(e);
474
+ }
475
+ });
476
+ server.registerTool("telegram-create-topic", {
477
+ description: "Create a new forum topic in a forum-enabled supergroup",
478
+ inputSchema: {
479
+ chatId: z.string().describe("Chat ID or username of the forum supergroup"),
480
+ title: z.string().describe("Topic title"),
481
+ iconColor: z
482
+ .union([
483
+ z.literal(7322096),
484
+ z.literal(16766590),
485
+ z.literal(13338331),
486
+ z.literal(9367192),
487
+ z.literal(16749490),
488
+ z.literal(16225862),
489
+ ])
490
+ .optional()
491
+ .describe("Optional icon color (one of 7322096, 16766590, 13338331, 9367192, 16749490, 16225862)"),
492
+ iconEmojiId: z
493
+ .string()
494
+ .regex(/^\d+$/, "iconEmojiId must be a numeric string")
495
+ .optional()
496
+ .describe("Optional custom emoji document ID for the icon (numeric string)"),
497
+ },
498
+ annotations: WRITE,
499
+ }, async ({ chatId, title, iconColor, iconEmojiId }) => {
500
+ const err = await requireConnection(telegram);
501
+ if (err)
502
+ return fail(new Error(err));
503
+ try {
504
+ const topic = await telegram.createForumTopic(chatId, title, iconColor, iconEmojiId);
505
+ return ok(sanitize(`Created topic "${topic.title}" (id=${topic.id}) in ${chatId}`));
506
+ }
507
+ catch (e) {
508
+ return fail(e);
509
+ }
510
+ });
511
+ server.registerTool("telegram-edit-topic", {
512
+ description: "Edit a forum topic — rename, change icon emoji, open/close, or show/hide",
513
+ inputSchema: {
514
+ chatId: z.string().describe("Chat ID or username of the forum supergroup"),
515
+ topicId: z.number().describe("Topic ID (get from telegram-list-topics)"),
516
+ title: z.string().optional().describe("New topic title"),
517
+ iconEmojiId: z
518
+ .string()
519
+ .regex(/^\d+$/, "iconEmojiId must be a numeric string")
520
+ .optional()
521
+ .describe("New custom emoji document ID for the icon (numeric string)"),
522
+ closed: z.boolean().optional().describe("Close (true) or reopen (false) the topic"),
523
+ hidden: z.boolean().optional().describe("Hide (true) or show (false) the General topic"),
524
+ },
525
+ annotations: WRITE,
526
+ }, async ({ chatId, topicId, title, iconEmojiId, closed, hidden }) => {
527
+ const err = await requireConnection(telegram);
528
+ if (err)
529
+ return fail(new Error(err));
530
+ try {
531
+ await telegram.editForumTopic(chatId, topicId, { title, iconEmojiId, closed, hidden });
532
+ const changes = [];
533
+ if (title !== undefined)
534
+ changes.push(`title="${title}"`);
535
+ if (iconEmojiId !== undefined)
536
+ changes.push(`iconEmojiId=${iconEmojiId}`);
537
+ if (closed !== undefined)
538
+ changes.push(closed ? "closed" : "reopened");
539
+ if (hidden !== undefined)
540
+ changes.push(hidden ? "hidden" : "shown");
541
+ return ok(sanitize(changes.length > 0
542
+ ? `Updated topic ${topicId} in ${chatId}: ${changes.join(", ")}`
543
+ : `No changes for topic ${topicId} in ${chatId}`));
544
+ }
545
+ catch (e) {
546
+ return fail(e);
547
+ }
548
+ });
549
+ server.registerTool("telegram-delete-topic", {
550
+ description: "Delete a forum topic and all its message history",
551
+ inputSchema: {
552
+ chatId: z.string().describe("Chat ID or username of the forum supergroup"),
553
+ topicId: z.number().describe("Topic ID to delete"),
554
+ },
555
+ annotations: DESTRUCTIVE,
556
+ }, async ({ chatId, topicId }) => {
557
+ const err = await requireConnection(telegram);
558
+ if (err)
559
+ return fail(new Error(err));
560
+ try {
561
+ await telegram.deleteForumTopic(chatId, topicId);
562
+ return ok(`Deleted topic ${topicId} in ${chatId}`);
563
+ }
564
+ catch (e) {
565
+ return fail(e);
566
+ }
567
+ });
331
568
  }
@@ -1,5 +1,5 @@
1
1
  import { z } from "zod";
2
- import { fail, ok, READ_ONLY, requireConnection, WRITE } from "./shared.js";
2
+ import { fail, ok, READ_ONLY, requireConnection, sanitize, WRITE } from "./shared.js";
3
3
  export function registerMediaTools(server, telegram) {
4
4
  server.registerTool("telegram-send-file", {
5
5
  description: "Send a file (photo, document, video, etc.) to a Telegram chat",
@@ -81,4 +81,123 @@ export function registerMediaTools(server, telegram) {
81
81
  return fail(e);
82
82
  }
83
83
  });
84
+ server.registerTool("telegram-get-web-preview", {
85
+ description: "Fetch Telegram's web-page preview metadata (type, title, description, site name) for a URL",
86
+ inputSchema: {
87
+ url: z
88
+ .string()
89
+ .url()
90
+ .refine((u) => {
91
+ try {
92
+ const p = new URL(u);
93
+ if (p.protocol !== "http:" && p.protocol !== "https:")
94
+ return false;
95
+ const host = p.hostname
96
+ .toLowerCase()
97
+ .replace(/^\[|\]$/g, "")
98
+ .replace(/\.$/, "");
99
+ if (host === "localhost" ||
100
+ // Trailing-dot and subdomain forms of localhost (e.g. "localhost.", "foo.localhost")
101
+ host.endsWith(".localhost") ||
102
+ // Unspecified: 0.0.0.0/8
103
+ /^0\./.test(host) ||
104
+ // IPv4 loopback
105
+ /^127\./.test(host) ||
106
+ // IPv6 loopback and unspecified address
107
+ host === "::1" ||
108
+ host === "::" ||
109
+ // Link-local (AWS metadata, etc.)
110
+ /^169\.254\./.test(host) ||
111
+ // RFC1918 private ranges
112
+ /^10\./.test(host) ||
113
+ /^172\.(1[6-9]|2\d|3[01])\./.test(host) ||
114
+ /^192\.168\./.test(host) ||
115
+ // IETF Protocol Assignments: 192.0.0.0/24
116
+ /^192\.0\.0\./.test(host) ||
117
+ // Documentation ranges (TEST-NET-1/2/3, RFC 5737): 192.0.2.0/24, 198.51.100.0/24, 203.0.113.0/24
118
+ /^192\.0\.2\./.test(host) ||
119
+ /^198\.51\.100\./.test(host) ||
120
+ /^203\.0\.113\./.test(host) ||
121
+ // CGNAT (RFC 6598): 100.64.0.0/10
122
+ /^100\.(6[4-9]|[7-9]\d|1[01]\d|12[0-7])\./.test(host) ||
123
+ // Benchmark testing (RFC 2544): 198.18.0.0/15
124
+ /^198\.1[89]\./.test(host) ||
125
+ // IPv4 multicast: 224.0.0.0/4
126
+ /^2(2[4-9]|3\d)\./.test(host) ||
127
+ // Reserved (future use): 240.0.0.0/4 and broadcast
128
+ /^(24[0-9]|25[0-5])\./.test(host)) {
129
+ return false;
130
+ }
131
+ // IPv4-mapped IPv6 (e.g. ::ffff:127.0.0.1 or Node-normalized ::ffff:7f00:1)
132
+ if (/^::ffff:/i.test(host)) {
133
+ let v4 = host.replace(/^::ffff:/i, "");
134
+ // Node.js normalizes ::ffff:a.b.c.d to ::ffff:XXYY:ZZWW (hex pairs).
135
+ // Convert hex-pair form back to dotted decimal before range checks.
136
+ const hexPair = /^([0-9a-f]{1,4}):([0-9a-f]{1,4})$/i.exec(v4);
137
+ if (hexPair) {
138
+ const hi = hexPair[1].padStart(4, "0");
139
+ const lo = hexPair[2].padStart(4, "0");
140
+ v4 = [
141
+ parseInt(hi.slice(0, 2), 16),
142
+ parseInt(hi.slice(2, 4), 16),
143
+ parseInt(lo.slice(0, 2), 16),
144
+ parseInt(lo.slice(2, 4), 16),
145
+ ].join(".");
146
+ }
147
+ if (/^0\./.test(v4) ||
148
+ /^127\./.test(v4) ||
149
+ /^10\./.test(v4) ||
150
+ /^172\.(1[6-9]|2\d|3[01])\./.test(v4) ||
151
+ /^192\.168\./.test(v4) ||
152
+ /^192\.0\.0\./.test(v4) ||
153
+ /^192\.0\.2\./.test(v4) ||
154
+ /^198\.51\.100\./.test(v4) ||
155
+ /^203\.0\.113\./.test(v4) ||
156
+ /^169\.254\./.test(v4) ||
157
+ /^100\.(6[4-9]|[7-9]\d|1[01]\d|12[0-7])\./.test(v4) ||
158
+ /^198\.1[89]\./.test(v4) ||
159
+ /^2(2[4-9]|3\d)\./.test(v4) ||
160
+ /^(24[0-9]|25[0-5])\./.test(v4)) {
161
+ return false;
162
+ }
163
+ }
164
+ // Private IPv6: ULA fc00::/7, link-local fe80::/10, multicast ff00::/8, documentation 2001:db8::/32
165
+ if (/^f[cd][0-9a-f]/i.test(host) ||
166
+ /^fe[89ab][0-9a-f]/i.test(host) ||
167
+ /^ff[0-9a-f]{2}/i.test(host) ||
168
+ /^2001:0?db8:/i.test(host)) {
169
+ return false;
170
+ }
171
+ return true;
172
+ }
173
+ catch {
174
+ return false;
175
+ }
176
+ }, "Only http:// and https:// URLs are allowed; literal loopback, private, link-local, and reserved IP addresses are blocked (DNS-backed hostnames that resolve to private ranges are not checked)")
177
+ .describe("URL to preview (http:// or https://; literal private/loopback/reserved IPs rejected)"),
178
+ },
179
+ annotations: READ_ONLY,
180
+ }, async ({ url }) => {
181
+ const err = await requireConnection(telegram);
182
+ if (err)
183
+ return fail(new Error(err));
184
+ try {
185
+ const preview = await telegram.getWebPreview(url);
186
+ if (!preview)
187
+ return ok("No preview available");
188
+ const lines = [`type: ${preview.type}`];
189
+ if (preview.url)
190
+ lines.push(`url: ${preview.url}`);
191
+ if (preview.siteName)
192
+ lines.push(`site: ${sanitize(preview.siteName)}`);
193
+ if (preview.title)
194
+ lines.push(`title: ${sanitize(preview.title)}`);
195
+ if (preview.description)
196
+ lines.push(`description: ${sanitize(preview.description)}`);
197
+ return ok(lines.join("\n"));
198
+ }
199
+ catch (e) {
200
+ return fail(e);
201
+ }
202
+ });
84
203
  }
@@ -191,6 +191,193 @@ export function registerMessageTools(server, telegram) {
191
191
  return fail(e);
192
192
  }
193
193
  });
194
+ server.registerTool("telegram-get-scheduled", {
195
+ description: "List scheduled messages in a Telegram chat",
196
+ inputSchema: {
197
+ chatId: z.string().describe("Chat ID or username"),
198
+ },
199
+ annotations: READ_ONLY,
200
+ }, async ({ chatId }) => {
201
+ const err = await requireConnection(telegram);
202
+ if (err)
203
+ return fail(new Error(err));
204
+ try {
205
+ const messages = await telegram.getScheduledMessages(chatId);
206
+ const text = messages
207
+ .map((m) => `[#${m.id}] [${m.date}] ${m.text}${m.media ? ` [${m.media.type}${m.media.fileName ? `: ${m.media.fileName}` : ""}]` : ""}`)
208
+ .join("\n\n");
209
+ return ok(sanitize(text) || "No scheduled messages");
210
+ }
211
+ catch (e) {
212
+ return fail(e);
213
+ }
214
+ });
215
+ server.registerTool("telegram-delete-scheduled", {
216
+ description: "Delete scheduled messages in a Telegram chat",
217
+ inputSchema: {
218
+ chatId: z.string().describe("Chat ID or username"),
219
+ messageIds: z
220
+ .array(z.number().int().positive())
221
+ .min(1)
222
+ .max(100)
223
+ .describe("Array of scheduled message IDs to delete (1-100)"),
224
+ },
225
+ annotations: DESTRUCTIVE,
226
+ }, async ({ chatId, messageIds }) => {
227
+ const err = await requireConnection(telegram);
228
+ if (err)
229
+ return fail(new Error(err));
230
+ try {
231
+ await telegram.deleteScheduledMessages(chatId, messageIds);
232
+ return ok(`Deleted ${messageIds.length} scheduled message(s) in ${chatId}`);
233
+ }
234
+ catch (e) {
235
+ return fail(e);
236
+ }
237
+ });
238
+ server.registerTool("telegram-get-replies", {
239
+ description: "Get replies/comments under a Telegram channel post or discussion message",
240
+ inputSchema: {
241
+ chatId: z.string().describe("Chat ID or username (channel or linked discussion group)"),
242
+ messageId: z.number().describe("ID of the message whose replies to fetch"),
243
+ limit: z.number().default(20).describe("Number of replies to return"),
244
+ },
245
+ annotations: READ_ONLY,
246
+ }, async ({ chatId, messageId, limit }) => {
247
+ const err = await requireConnection(telegram);
248
+ if (err)
249
+ return fail(new Error(err));
250
+ try {
251
+ const messages = await telegram.getReplies(chatId, messageId, limit);
252
+ const text = messages
253
+ .map((m) => `[#${m.id}] [${m.date}] ${m.sender}: ${m.text}${m.media ? ` [${m.media.type}${m.media.fileName ? `: ${m.media.fileName}` : ""}]` : ""}${formatReactions(m.reactions)}`)
254
+ .join("\n\n");
255
+ return ok(sanitize(text) || "No replies");
256
+ }
257
+ catch (e) {
258
+ return fail(e);
259
+ }
260
+ });
261
+ server.registerTool("telegram-get-message-link", {
262
+ description: "Get a t.me link to a specific message in a Telegram channel or supergroup",
263
+ inputSchema: {
264
+ chatId: z.string().describe("Chat ID or username (channel or supergroup)"),
265
+ messageId: z.number().describe("ID of the message to link to"),
266
+ thread: z.boolean().default(false).describe("Link to the message thread instead of the message itself"),
267
+ },
268
+ annotations: READ_ONLY,
269
+ }, async ({ chatId, messageId, thread }) => {
270
+ const err = await requireConnection(telegram);
271
+ if (err)
272
+ return fail(new Error(err));
273
+ try {
274
+ const link = await telegram.getMessageLink(chatId, messageId, thread);
275
+ return ok(sanitize(link));
276
+ }
277
+ catch (e) {
278
+ return fail(e);
279
+ }
280
+ });
281
+ server.registerTool("telegram-get-unread-mentions", {
282
+ description: "Get unread @mentions addressed to you in a Telegram chat. Marks all mentions as read on the server when all unread mentions fit within the requested limit.",
283
+ inputSchema: {
284
+ chatId: z.string().describe("Chat ID or username"),
285
+ limit: z.number().default(20).describe("Max number of mentions to return"),
286
+ },
287
+ annotations: WRITE,
288
+ }, async ({ chatId, limit }) => {
289
+ const err = await requireConnection(telegram);
290
+ if (err)
291
+ return fail(new Error(err));
292
+ try {
293
+ const messages = await telegram.getUnreadMentions(chatId, limit);
294
+ const text = messages
295
+ .map((m) => `[#${m.id}] [${m.date}] ${m.sender}: ${m.text}${m.media ? ` [${m.media.type}${m.media.fileName ? `: ${m.media.fileName}` : ""}]` : ""}${formatReactions(m.reactions)}`)
296
+ .join("\n\n");
297
+ return ok(sanitize(text) || "No unread mentions");
298
+ }
299
+ catch (e) {
300
+ return fail(e);
301
+ }
302
+ });
303
+ server.registerTool("telegram-get-unread-reactions", {
304
+ description: "Get messages with unread reactions on your posts in a Telegram chat. Marks all reactions as read on the server when all unread reactions fit within the requested limit.",
305
+ inputSchema: {
306
+ chatId: z.string().describe("Chat ID or username"),
307
+ limit: z.number().default(20).describe("Max number of messages to return"),
308
+ },
309
+ annotations: WRITE,
310
+ }, async ({ chatId, limit }) => {
311
+ const err = await requireConnection(telegram);
312
+ if (err)
313
+ return fail(new Error(err));
314
+ try {
315
+ const messages = await telegram.getUnreadReactions(chatId, limit);
316
+ const text = messages
317
+ .map((m) => `[#${m.id}] [${m.date}] ${m.sender}: ${m.text}${m.media ? ` [${m.media.type}${m.media.fileName ? `: ${m.media.fileName}` : ""}]` : ""}${formatReactions(m.reactions)}`)
318
+ .join("\n\n");
319
+ return ok(sanitize(text) || "No unread reactions");
320
+ }
321
+ catch (e) {
322
+ return fail(e);
323
+ }
324
+ });
325
+ server.registerTool("telegram-translate-message", {
326
+ description: "Translate one or more Telegram messages to a target language (requires Telegram Premium). Consumes account translation quota.",
327
+ inputSchema: {
328
+ chatId: z.string().describe("Chat ID or username"),
329
+ messageIds: z
330
+ .array(z.number().int().positive())
331
+ .min(1)
332
+ .max(100)
333
+ .describe("Array of message IDs to translate (1-100)"),
334
+ toLang: z
335
+ .string()
336
+ .regex(/^[a-z]{2,3}(-[A-Z]{2})?$/)
337
+ .describe("ISO 639-1 (e.g. 'en', 'ru') or locale (e.g. 'en-US')"),
338
+ },
339
+ annotations: WRITE,
340
+ }, async ({ chatId, messageIds, toLang }) => {
341
+ const err = await requireConnection(telegram);
342
+ if (err)
343
+ return fail(new Error(err));
344
+ try {
345
+ const translations = await telegram.translateText(chatId, messageIds, toLang);
346
+ const text = translations.length === messageIds.length
347
+ ? translations.map((t, i) => `[#${messageIds[i]}] ${t}`).join("\n\n")
348
+ : translations.join("\n\n");
349
+ return ok(sanitize(text) || "No translations");
350
+ }
351
+ catch (e) {
352
+ const msg = e.message ?? "";
353
+ if (/PREMIUM|PAYMENT_REQUIRED|TRANSLATE_REQ/i.test(msg)) {
354
+ return fail(new Error("Message translation requires Telegram Premium on this account"));
355
+ }
356
+ return fail(e);
357
+ }
358
+ });
359
+ server.registerTool("telegram-send-typing", {
360
+ description: "Send a typing/upload indicator to a Telegram chat (or cancel it)",
361
+ inputSchema: {
362
+ chatId: z.string().describe("Chat ID or username"),
363
+ action: z
364
+ .enum(["typing", "upload_photo", "upload_document", "cancel"])
365
+ .default("typing")
366
+ .describe("Typing action to broadcast"),
367
+ },
368
+ annotations: WRITE,
369
+ }, async ({ chatId, action }) => {
370
+ const err = await requireConnection(telegram);
371
+ if (err)
372
+ return fail(new Error(err));
373
+ try {
374
+ await telegram.sendTyping(chatId, action);
375
+ return ok(`Typing indicator (${action}) sent to ${chatId}`);
376
+ }
377
+ catch (e) {
378
+ return fail(e);
379
+ }
380
+ });
194
381
  server.registerTool("telegram-mark-as-read", {
195
382
  description: "Mark a Telegram chat as read",
196
383
  inputSchema: { chatId: z.string().describe("Chat ID or username") },