@overpod/mcp-telegram 1.24.1 → 1.26.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.
Files changed (69) hide show
  1. package/CHANGELOG.md +67 -1
  2. package/README.md +45 -13
  3. package/dist/__tests__/admin-log.test.d.ts +1 -0
  4. package/dist/__tests__/admin-log.test.js +41 -0
  5. package/dist/__tests__/approve-join-request.test.d.ts +1 -0
  6. package/dist/__tests__/approve-join-request.test.js +107 -0
  7. package/dist/__tests__/boosts.test.d.ts +1 -0
  8. package/dist/__tests__/boosts.test.js +310 -0
  9. package/dist/__tests__/broadcast-stats.test.d.ts +1 -0
  10. package/dist/__tests__/broadcast-stats.test.js +172 -0
  11. package/dist/__tests__/business-chat-links.test.d.ts +1 -0
  12. package/dist/__tests__/business-chat-links.test.js +102 -0
  13. package/dist/__tests__/get-message-buttons.test.d.ts +1 -0
  14. package/dist/__tests__/get-message-buttons.test.js +122 -0
  15. package/dist/__tests__/group-calls.test.d.ts +1 -0
  16. package/dist/__tests__/group-calls.test.js +503 -0
  17. package/dist/__tests__/inline-query-send.test.d.ts +1 -0
  18. package/dist/__tests__/inline-query-send.test.js +94 -0
  19. package/dist/__tests__/inline-query.test.d.ts +1 -0
  20. package/dist/__tests__/inline-query.test.js +115 -0
  21. package/dist/__tests__/megagroup-stats.test.d.ts +1 -0
  22. package/dist/__tests__/megagroup-stats.test.js +166 -0
  23. package/dist/__tests__/press-button.test.d.ts +1 -0
  24. package/dist/__tests__/press-button.test.js +123 -0
  25. package/dist/__tests__/quick-replies.test.d.ts +1 -0
  26. package/dist/__tests__/quick-replies.test.js +245 -0
  27. package/dist/__tests__/reactions.test.d.ts +1 -0
  28. package/dist/__tests__/reactions.test.js +23 -0
  29. package/dist/__tests__/set-chat-permissions-merge.test.d.ts +1 -0
  30. package/dist/__tests__/set-chat-permissions-merge.test.js +107 -0
  31. package/dist/__tests__/set-chat-reactions.test.d.ts +1 -0
  32. package/dist/__tests__/set-chat-reactions.test.js +129 -0
  33. package/dist/__tests__/stars-status.test.d.ts +1 -0
  34. package/dist/__tests__/stars-status.test.js +205 -0
  35. package/dist/__tests__/stars-transactions.test.d.ts +1 -0
  36. package/dist/__tests__/stars-transactions.test.js +82 -0
  37. package/dist/__tests__/stories.test.d.ts +1 -0
  38. package/dist/__tests__/stories.test.js +361 -0
  39. package/dist/__tests__/toggle-anti-spam.test.d.ts +1 -0
  40. package/dist/__tests__/toggle-anti-spam.test.js +80 -0
  41. package/dist/__tests__/toggle-channel-signatures.test.d.ts +1 -0
  42. package/dist/__tests__/toggle-channel-signatures.test.js +80 -0
  43. package/dist/__tests__/toggle-forum-mode.test.d.ts +1 -0
  44. package/dist/__tests__/toggle-forum-mode.test.js +80 -0
  45. package/dist/__tests__/toggle-prehistory-hidden.test.d.ts +1 -0
  46. package/dist/__tests__/toggle-prehistory-hidden.test.js +80 -0
  47. package/dist/__tests__/updates.test.d.ts +1 -0
  48. package/dist/__tests__/updates.test.js +221 -0
  49. package/dist/rate-limiter.d.ts +8 -2
  50. package/dist/rate-limiter.js +15 -8
  51. package/dist/telegram-client.d.ts +711 -2
  52. package/dist/telegram-client.js +2167 -99
  53. package/dist/tools/account.js +108 -0
  54. package/dist/tools/boosts.d.ts +3 -0
  55. package/dist/tools/boosts.js +65 -0
  56. package/dist/tools/chats.js +388 -1
  57. package/dist/tools/group-calls.d.ts +4 -0
  58. package/dist/tools/group-calls.js +77 -0
  59. package/dist/tools/index.js +10 -0
  60. package/dist/tools/media.js +120 -1
  61. package/dist/tools/messages.js +379 -0
  62. package/dist/tools/quick-replies.d.ts +4 -0
  63. package/dist/tools/quick-replies.js +58 -0
  64. package/dist/tools/reactions.js +102 -1
  65. package/dist/tools/stars.d.ts +4 -0
  66. package/dist/tools/stars.js +71 -0
  67. package/dist/tools/stories.d.ts +3 -0
  68. package/dist/tools/stories.js +107 -0
  69. package/package.json +1 -1
@@ -1,12 +1,17 @@
1
1
  import { registerAccountTools } from "./account.js";
2
2
  import { registerAuthTools } from "./auth.js";
3
+ import { registerBoostTools } from "./boosts.js";
3
4
  import { registerChatTools } from "./chats.js";
4
5
  import { registerContactTools } from "./contacts.js";
5
6
  import { registerExtraTools } from "./extras.js";
7
+ import { registerGroupCallTools } from "./group-calls.js";
6
8
  import { registerMediaTools } from "./media.js";
7
9
  import { registerMessageTools } from "./messages.js";
10
+ import { registerQuickRepliesTools } from "./quick-replies.js";
8
11
  import { registerReactionTools } from "./reactions.js";
12
+ import { registerStarsTools } from "./stars.js";
9
13
  import { registerStickerTools } from "./stickers.js";
14
+ import { registerStoryTools } from "./stories.js";
10
15
  export function registerTools(server, telegram) {
11
16
  registerAuthTools(server, telegram);
12
17
  registerMessageTools(server, telegram);
@@ -17,4 +22,9 @@ export function registerTools(server, telegram) {
17
22
  registerExtraTools(server, telegram);
18
23
  registerAccountTools(server, telegram);
19
24
  registerStickerTools(server, telegram);
25
+ registerStoryTools(server, telegram);
26
+ registerBoostTools(server, telegram);
27
+ registerGroupCallTools(server, telegram);
28
+ registerStarsTools(server, telegram);
29
+ registerQuickRepliesTools(server, telegram);
20
30
  }
@@ -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") },
@@ -207,4 +394,196 @@ export function registerMessageTools(server, telegram) {
207
394
  return fail(e);
208
395
  }
209
396
  });
397
+ server.registerTool("telegram-inline-query", {
398
+ description: "Query an inline bot (like @gif, @bing) in a chat context and return the compact result list. Returns queryId, cacheTime, and results[{id,type,title?,description?,url?}]. The queryId is typically valid for ~60s and can be passed to telegram-inline-query-send to deliver a chosen result. Bot must be a real bot account",
399
+ inputSchema: {
400
+ bot: z.string().describe("Inline bot username (e.g. @gif) or numeric user ID"),
401
+ chatId: z.string().describe("Chat ID or username providing context for the inline query"),
402
+ query: z.string().describe("Query text the bot should resolve (may be empty string)"),
403
+ offset: z
404
+ .string()
405
+ .optional()
406
+ .describe("Pagination offset returned by a previous call as nextOffset (empty string on first call)"),
407
+ },
408
+ annotations: READ_ONLY,
409
+ }, async ({ bot, chatId, query, offset }) => {
410
+ const err = await requireConnection(telegram);
411
+ if (err)
412
+ return fail(new Error(err));
413
+ try {
414
+ const res = await telegram.getInlineBotResults(bot, chatId, query, offset);
415
+ return ok(sanitize(JSON.stringify(res)));
416
+ }
417
+ catch (e) {
418
+ return fail(e);
419
+ }
420
+ });
421
+ server.registerTool("telegram-inline-query-send", {
422
+ description: "Send an inline bot result to a chat by queryId + resultId (as returned by telegram-inline-query). The queryId is valid for ~60s after the original query, so call this soon after telegram-inline-query. Returns the sent messageId (0 if not extractable from the update).",
423
+ inputSchema: {
424
+ chatId: z.string().describe("Target chat ID or username to send the result into"),
425
+ queryId: z
426
+ .string()
427
+ .regex(/^\d+$/, "queryId must be a numeric string")
428
+ .describe("queryId from a prior telegram-inline-query call (valid ~60s)"),
429
+ resultId: z.string().describe("id of the chosen result from telegram-inline-query results[]"),
430
+ replyTo: z.number().optional().describe("Message ID to reply to"),
431
+ silent: z.boolean().optional().describe("Send without notification"),
432
+ hideVia: z.boolean().optional().describe("Hide the 'via @bot' label on the sent message"),
433
+ clearDraft: z.boolean().optional().describe("Clear the chat draft after sending"),
434
+ },
435
+ annotations: WRITE,
436
+ }, async ({ chatId, queryId, resultId, replyTo, silent, hideVia, clearDraft }) => {
437
+ const err = await requireConnection(telegram);
438
+ if (err)
439
+ return fail(new Error(err));
440
+ try {
441
+ const { messageId } = await telegram.sendInlineBotResult(chatId, queryId, resultId, {
442
+ replyTo,
443
+ silent,
444
+ hideVia,
445
+ clearDraft,
446
+ });
447
+ const idInfo = messageId ? ` [#${messageId}]` : "";
448
+ return ok(`Inline result ${resultId} sent to ${chatId}${idInfo}`);
449
+ }
450
+ catch (e) {
451
+ return fail(e);
452
+ }
453
+ });
454
+ server.registerTool("telegram-get-message-buttons", {
455
+ description: "List the inline/reply keyboard buttons on a Telegram message with their (row, col) indices, type (e.g. KeyboardButtonCallback, KeyboardButtonUrl), label and type-specific fields (callback data as base64, url, switchQuery, userId, copyText, etc). Helper for telegram-press-button — call this first to discover indices and filter by type before pressing. Returns markupType='none' and empty buttons when the message has no keyboard",
456
+ inputSchema: {
457
+ chatId: z.string().describe("Chat ID or username where the message lives"),
458
+ messageId: z.number().describe("Message ID whose keyboard to inspect"),
459
+ },
460
+ annotations: READ_ONLY,
461
+ }, async ({ chatId, messageId }) => {
462
+ const err = await requireConnection(telegram);
463
+ if (err)
464
+ return fail(new Error(err));
465
+ try {
466
+ const result = await telegram.getMessageButtons(chatId, messageId);
467
+ return ok(sanitize(JSON.stringify(result)));
468
+ }
469
+ catch (e) {
470
+ return fail(e);
471
+ }
472
+ });
473
+ server.registerTool("telegram-press-button", {
474
+ description: "Press an inline keyboard callback button on a message. Identify the button by (row, column) from its replyMarkup, or pass raw callback_data as base64. URL, switch-inline, game and 2FA-password buttons are rejected with a clear error. Returns the bot's callback answer: {alert?, hasUrl?, nativeUi?, message?, url?, cacheTime}",
475
+ inputSchema: {
476
+ chatId: z.string().describe("Chat ID or username where the message lives"),
477
+ messageId: z.number().describe("Message ID whose inline button to press"),
478
+ row: z
479
+ .number()
480
+ .int()
481
+ .nonnegative()
482
+ .optional()
483
+ .describe("Button row index (0-based) — required unless data is provided"),
484
+ column: z
485
+ .number()
486
+ .int()
487
+ .nonnegative()
488
+ .optional()
489
+ .describe("Button column index (0-based) — required unless data is provided"),
490
+ data: z.string().optional().describe("Raw callback_data as base64 string (escape hatch — prefer row/column)"),
491
+ },
492
+ annotations: WRITE,
493
+ }, async ({ chatId, messageId, row, column, data }) => {
494
+ const err = await requireConnection(telegram);
495
+ if (err)
496
+ return fail(new Error(err));
497
+ const hasIndex = row !== undefined && column !== undefined;
498
+ if (!hasIndex && data === undefined) {
499
+ return fail(new Error("Provide either both row+column, or data (base64 callback_data)"));
500
+ }
501
+ if (hasIndex && data !== undefined) {
502
+ return fail(new Error("Provide either row+column OR data, not both"));
503
+ }
504
+ try {
505
+ const answer = await telegram.pressButton(chatId, messageId, {
506
+ buttonIndex: hasIndex ? { row: row, column: column } : undefined,
507
+ data,
508
+ });
509
+ return ok(sanitize(JSON.stringify(answer)));
510
+ }
511
+ catch (e) {
512
+ return fail(e);
513
+ }
514
+ });
515
+ server.registerTool("telegram-get-state", {
516
+ description: "Initialize the polling cursor by fetching the current Telegram updates state {pts, qts, date, seq, unreadCount}. Call once before telegram-get-updates; then persist {pts, qts, date} in your agent state and feed them into telegram-get-updates. The MCP server does NOT store the cursor — you do.",
517
+ inputSchema: {},
518
+ annotations: READ_ONLY,
519
+ }, async () => {
520
+ const err = await requireConnection(telegram);
521
+ if (err)
522
+ return fail(new Error(err));
523
+ try {
524
+ const state = await telegram.getUpdatesState();
525
+ return ok(sanitize(JSON.stringify(state)));
526
+ }
527
+ catch (e) {
528
+ return fail(e);
529
+ }
530
+ });
531
+ server.registerTool("telegram-get-updates", {
532
+ description: "Fetch new messages, deleted messages, and other updates since a previously-known {pts, qts, date} cursor (from telegram-get-state or a prior call). Returns compact newMessages[], deletedMessageIds[], otherUpdates[] (className only), and the new cursor state. isFinal=false means more updates are queued — call again with the returned state. If Telegram reports the gap is too long, a fallback hint is returned suggesting to resync via telegram-read-messages per chat. Cursor is stateless — the agent must persist {pts, qts, date} between calls.",
533
+ inputSchema: {
534
+ pts: z.number().int().describe("Last known pts (from telegram-get-state or prior telegram-get-updates)"),
535
+ qts: z.number().int().describe("Last known qts (secret-chat / encrypted stream cursor; 0 if unknown)"),
536
+ date: z.number().int().describe("Last known date (unix seconds from prior state)"),
537
+ ptsLimit: z
538
+ .number()
539
+ .int()
540
+ .positive()
541
+ .max(1000)
542
+ .optional()
543
+ .describe("Max updates per batch (default 100, capped at 1000)"),
544
+ ptsTotalLimit: z
545
+ .number()
546
+ .int()
547
+ .positive()
548
+ .max(1000)
549
+ .optional()
550
+ .describe("Max total updates across paginated slices (default 1000, capped at 1000)"),
551
+ },
552
+ annotations: READ_ONLY,
553
+ }, async ({ pts, qts, date, ptsLimit, ptsTotalLimit }) => {
554
+ const err = await requireConnection(telegram);
555
+ if (err)
556
+ return fail(new Error(err));
557
+ try {
558
+ const diff = await telegram.getUpdates({ pts, qts, date, ptsLimit, ptsTotalLimit });
559
+ return ok(sanitize(JSON.stringify(diff)));
560
+ }
561
+ catch (e) {
562
+ return fail(e);
563
+ }
564
+ });
565
+ server.registerTool("telegram-get-channel-updates", {
566
+ description: "Fetch new messages and updates for a single channel/supergroup since a known per-channel pts cursor. Separate from the global cursor used by telegram-get-updates. Returns compact newMessages[], otherUpdates[], and new {pts, isFinal, timeout?}. If the channel gap is too long, Telegram returns a dialog snapshot — this tool forwards it and hints to resync via telegram-read-messages. Cursor is stateless — the agent stores pts.",
567
+ inputSchema: {
568
+ chatId: z.string().describe("Channel or supergroup ID or username"),
569
+ pts: z.number().int().describe("Last known per-channel pts"),
570
+ limit: z.number().int().positive().optional().describe("Max updates per batch (default 100)"),
571
+ force: z
572
+ .boolean()
573
+ .optional()
574
+ .describe("Force request updates even if the client hasn't processed previous ones (rarely needed)"),
575
+ },
576
+ annotations: READ_ONLY,
577
+ }, async ({ chatId, pts, limit, force }) => {
578
+ const err = await requireConnection(telegram);
579
+ if (err)
580
+ return fail(new Error(err));
581
+ try {
582
+ const diff = await telegram.getChannelUpdates(chatId, { pts, limit, force });
583
+ return ok(sanitize(JSON.stringify(diff)));
584
+ }
585
+ catch (e) {
586
+ return fail(e);
587
+ }
588
+ });
210
589
  }
@@ -0,0 +1,4 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import type { TelegramService } from "../telegram-client.js";
3
+ export declare function isQuickRepliesEnabled(): boolean;
4
+ export declare function registerQuickRepliesTools(server: McpServer, telegram: TelegramService): void;
@@ -0,0 +1,58 @@
1
+ import { z } from "zod";
2
+ import { fail, ok, READ_ONLY, requireConnection, sanitize } from "./shared.js";
3
+ export function isQuickRepliesEnabled() {
4
+ return process.env.MCP_TELEGRAM_ENABLE_QUICK_REPLIES === "1";
5
+ }
6
+ export function registerQuickRepliesTools(server, telegram) {
7
+ if (!isQuickRepliesEnabled())
8
+ return;
9
+ server.registerTool("telegram-get-quick-replies", {
10
+ description: "Fetch the list of quick-reply shortcuts configured for the user account (messages.GetQuickReplies). Each entry has {shortcutId, shortcut, topMessage, count} — use the shortcutId with telegram-get-quick-reply-messages to inspect the stored messages. Optional `hash` implements Telegram's hash-based diff: pass the last-known aggregate hash as a decimal string and the server may respond with {notModified:true} if nothing changed. Opt-in: register only when MCP_TELEGRAM_ENABLE_QUICK_REPLIES=1. Read-only.",
11
+ inputSchema: {
12
+ hash: z
13
+ .string()
14
+ .regex(/^\d+$/)
15
+ .optional()
16
+ .describe("Aggregate hash from a previous response (decimal string); omit for a fresh fetch"),
17
+ },
18
+ annotations: READ_ONLY,
19
+ }, async ({ hash }) => {
20
+ const err = await requireConnection(telegram);
21
+ if (err)
22
+ return fail(new Error(err));
23
+ try {
24
+ const result = await telegram.getQuickReplies(hash);
25
+ return ok(sanitize(JSON.stringify(result)));
26
+ }
27
+ catch (e) {
28
+ return fail(e);
29
+ }
30
+ });
31
+ server.registerTool("telegram-get-quick-reply-messages", {
32
+ description: "Fetch messages stored under a quick-reply shortcut (messages.GetQuickReplyMessages). Use `shortcutId` from telegram-get-quick-replies. Optional `ids` narrows to specific message ids within the shortcut. Optional `hash` implements Telegram's hash-based diff: pass the last-known aggregate hash as a decimal string — the server may respond with {notModified:true, count} if nothing changed. Returns compact {count, messages[{id, date, text, isService, fromId?, replyToMsgId?}]}. Opt-in: register only when MCP_TELEGRAM_ENABLE_QUICK_REPLIES=1. Read-only.",
33
+ inputSchema: {
34
+ shortcutId: z.number().int().nonnegative().describe("Shortcut id from telegram-get-quick-replies"),
35
+ ids: z
36
+ .array(z.number().int().nonnegative())
37
+ .optional()
38
+ .describe("Optional list of message ids to fetch within the shortcut"),
39
+ hash: z
40
+ .string()
41
+ .regex(/^\d+$/)
42
+ .optional()
43
+ .describe("Aggregate hash from a previous response (decimal string); omit for a fresh fetch"),
44
+ },
45
+ annotations: READ_ONLY,
46
+ }, async ({ shortcutId, ids, hash }) => {
47
+ const err = await requireConnection(telegram);
48
+ if (err)
49
+ return fail(new Error(err));
50
+ try {
51
+ const result = await telegram.getQuickReplyMessages(shortcutId, { ids, hash });
52
+ return ok(sanitize(JSON.stringify(result)));
53
+ }
54
+ catch (e) {
55
+ return fail(e);
56
+ }
57
+ });
58
+ }