@overpod/mcp-telegram 1.25.0 → 1.26.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.
Files changed (40) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/README.md +35 -5
  3. package/dist/index.js +4 -1
  4. package/dist/rate-limiter.d.ts +9 -3
  5. package/dist/rate-limiter.js +23 -16
  6. package/dist/telegram-client.d.ts +134 -18
  7. package/dist/telegram-client.js +601 -136
  8. package/dist/telegram-helpers.d.ts +470 -0
  9. package/dist/telegram-helpers.js +870 -0
  10. package/dist/tools/account.js +22 -6
  11. package/dist/tools/boosts.d.ts +3 -0
  12. package/dist/tools/boosts.js +65 -0
  13. package/dist/tools/chats.js +155 -5
  14. package/dist/tools/contacts.js +3 -3
  15. package/dist/tools/extras.js +3 -3
  16. package/dist/tools/group-calls.d.ts +4 -0
  17. package/dist/tools/group-calls.js +77 -0
  18. package/dist/tools/index.js +10 -0
  19. package/dist/tools/messages.js +203 -11
  20. package/dist/tools/quick-replies.d.ts +4 -0
  21. package/dist/tools/quick-replies.js +58 -0
  22. package/dist/tools/reactions.js +45 -2
  23. package/dist/tools/shared.d.ts +3 -3
  24. package/dist/tools/shared.js +8 -7
  25. package/dist/tools/stars.d.ts +4 -0
  26. package/dist/tools/stars.js +71 -0
  27. package/dist/tools/stickers.js +5 -5
  28. package/dist/tools/stories.d.ts +3 -0
  29. package/dist/tools/stories.js +107 -0
  30. package/package.json +1 -1
  31. package/dist/__tests__/admin-log.test.d.ts +0 -1
  32. package/dist/__tests__/admin-log.test.js +0 -41
  33. package/dist/__tests__/rate-limiter.test.d.ts +0 -1
  34. package/dist/__tests__/rate-limiter.test.js +0 -81
  35. package/dist/__tests__/reactions.test.d.ts +0 -1
  36. package/dist/__tests__/reactions.test.js +0 -23
  37. package/dist/__tests__/set-chat-permissions-merge.test.d.ts +0 -1
  38. package/dist/__tests__/set-chat-permissions-merge.test.js +0 -107
  39. package/dist/__tests__/tools/shared.test.d.ts +0 -1
  40. package/dist/__tests__/tools/shared.test.js +0 -110
@@ -1,5 +1,5 @@
1
1
  import { z } from "zod";
2
- import { DESTRUCTIVE, fail, formatReactions, ok, READ_ONLY, requireConnection, sanitize, WRITE } from "./shared.js";
2
+ import { DESTRUCTIVE, fail, formatReactions, ok, READ_ONLY, requireConnection, WRITE } from "./shared.js";
3
3
  export function registerMessageTools(server, telegram) {
4
4
  server.registerTool("telegram-send-message", {
5
5
  description: "Send a message to a Telegram chat",
@@ -45,7 +45,7 @@ export function registerMessageTools(server, telegram) {
45
45
  const text = messages
46
46
  .map((m) => `[#${m.id}] [${m.date}] ${m.sender}: ${m.text}${m.media ? ` [${m.media.type}${m.media.fileName ? `: ${m.media.fileName}` : ""}]` : ""}${formatReactions(m.reactions)}`)
47
47
  .join("\n\n");
48
- return ok(sanitize(text) || "No messages");
48
+ return ok(text || "No messages");
49
49
  }
50
50
  catch (e) {
51
51
  return fail(e);
@@ -70,7 +70,7 @@ export function registerMessageTools(server, telegram) {
70
70
  const text = messages
71
71
  .map((m) => `[#${m.id}] [${m.date}] ${m.sender}: ${m.text}${m.media ? ` [${m.media.type}${m.media.fileName ? `: ${m.media.fileName}` : ""}]` : ""}${formatReactions(m.reactions)}`)
72
72
  .join("\n\n");
73
- return ok(sanitize(text) || "No messages found");
73
+ return ok(text || "No messages found");
74
74
  }
75
75
  catch (e) {
76
76
  return fail(e);
@@ -94,7 +94,7 @@ export function registerMessageTools(server, telegram) {
94
94
  const text = messages
95
95
  .map((m) => `[#${m.id}] [${m.date}] [${m.chat.type === "channel" ? "C" : m.chat.type === "group" ? "G" : "P"} ${m.chat.name}${m.chat.username ? ` @${m.chat.username}` : ""}] ${m.sender}: ${m.text}${m.media ? ` [${m.media.type}${m.media.fileName ? `: ${m.media.fileName}` : ""}]` : ""}${formatReactions(m.reactions)}`)
96
96
  .join("\n\n");
97
- return ok(sanitize(text) || "No messages found");
97
+ return ok(text || "No messages found");
98
98
  }
99
99
  catch (e) {
100
100
  return fail(e);
@@ -185,7 +185,7 @@ export function registerMessageTools(server, telegram) {
185
185
  return line;
186
186
  })
187
187
  .join("\n");
188
- return ok(sanitize(text) || "No unread chats");
188
+ return ok(text || "No unread chats");
189
189
  }
190
190
  catch (e) {
191
191
  return fail(e);
@@ -206,7 +206,7 @@ export function registerMessageTools(server, telegram) {
206
206
  const text = messages
207
207
  .map((m) => `[#${m.id}] [${m.date}] ${m.text}${m.media ? ` [${m.media.type}${m.media.fileName ? `: ${m.media.fileName}` : ""}]` : ""}`)
208
208
  .join("\n\n");
209
- return ok(sanitize(text) || "No scheduled messages");
209
+ return ok(text || "No scheduled messages");
210
210
  }
211
211
  catch (e) {
212
212
  return fail(e);
@@ -252,7 +252,7 @@ export function registerMessageTools(server, telegram) {
252
252
  const text = messages
253
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
254
  .join("\n\n");
255
- return ok(sanitize(text) || "No replies");
255
+ return ok(text || "No replies");
256
256
  }
257
257
  catch (e) {
258
258
  return fail(e);
@@ -272,7 +272,7 @@ export function registerMessageTools(server, telegram) {
272
272
  return fail(new Error(err));
273
273
  try {
274
274
  const link = await telegram.getMessageLink(chatId, messageId, thread);
275
- return ok(sanitize(link));
275
+ return ok(link);
276
276
  }
277
277
  catch (e) {
278
278
  return fail(e);
@@ -294,7 +294,7 @@ export function registerMessageTools(server, telegram) {
294
294
  const text = messages
295
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
296
  .join("\n\n");
297
- return ok(sanitize(text) || "No unread mentions");
297
+ return ok(text || "No unread mentions");
298
298
  }
299
299
  catch (e) {
300
300
  return fail(e);
@@ -316,7 +316,7 @@ export function registerMessageTools(server, telegram) {
316
316
  const text = messages
317
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
318
  .join("\n\n");
319
- return ok(sanitize(text) || "No unread reactions");
319
+ return ok(text || "No unread reactions");
320
320
  }
321
321
  catch (e) {
322
322
  return fail(e);
@@ -346,7 +346,7 @@ export function registerMessageTools(server, telegram) {
346
346
  const text = translations.length === messageIds.length
347
347
  ? translations.map((t, i) => `[#${messageIds[i]}] ${t}`).join("\n\n")
348
348
  : translations.join("\n\n");
349
- return ok(sanitize(text) || "No translations");
349
+ return ok(text || "No translations");
350
350
  }
351
351
  catch (e) {
352
352
  const msg = e.message ?? "";
@@ -394,4 +394,196 @@ export function registerMessageTools(server, telegram) {
394
394
  return fail(e);
395
395
  }
396
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(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(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(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(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(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(JSON.stringify(diff));
584
+ }
585
+ catch (e) {
586
+ return fail(e);
587
+ }
588
+ });
397
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 } 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(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(JSON.stringify(result));
53
+ }
54
+ catch (e) {
55
+ return fail(e);
56
+ }
57
+ });
58
+ }
@@ -92,7 +92,50 @@ export function registerReactionTools(server, telegram) {
92
92
  const reactions = await telegram.getTopReactions(limit);
93
93
  if (reactions.length === 0)
94
94
  return ok("No top reactions available");
95
- return ok(sanitize(reactions.map((r) => r.emoji).join(" ")));
95
+ return ok(reactions.map((r) => r.emoji).join(" "));
96
+ }
97
+ catch (e) {
98
+ return fail(e);
99
+ }
100
+ });
101
+ server.registerTool("telegram-set-chat-reactions", {
102
+ description: "Set which reactions are available in a chat. type='all' allows all standard emoji (set allowCustom=true to also permit custom emoji for Premium users), type='some' restricts to a specific emoji list, type='none' disables reactions entirely. Requires admin",
103
+ inputSchema: {
104
+ chatId: z.string().describe("Chat ID or username (group, supergroup, or channel)"),
105
+ reactions: z
106
+ .discriminatedUnion("type", [
107
+ z.object({
108
+ type: z.literal("all"),
109
+ allowCustom: z
110
+ .boolean()
111
+ .optional()
112
+ .describe("If true, also allow custom emoji reactions (requires Premium users)"),
113
+ }),
114
+ z.object({
115
+ type: z.literal("some"),
116
+ emoji: z
117
+ .array(z.string().min(1).max(8))
118
+ .min(1)
119
+ .max(100)
120
+ .describe("List of allowed reaction emoji (e.g. ['👍','❤️','🔥'])"),
121
+ }),
122
+ z.object({ type: z.literal("none") }),
123
+ ])
124
+ .describe("Reaction policy for the chat"),
125
+ },
126
+ annotations: WRITE,
127
+ }, async ({ chatId, reactions }) => {
128
+ const err = await requireConnection(telegram);
129
+ if (err)
130
+ return fail(new Error(err));
131
+ try {
132
+ await telegram.setChatAvailableReactions(chatId, reactions);
133
+ const summary = reactions.type === "all"
134
+ ? `all reactions${reactions.allowCustom ? " (incl. custom)" : ""}`
135
+ : reactions.type === "none"
136
+ ? "no reactions"
137
+ : `${reactions.emoji.length} reaction(s): ${sanitize(reactions.emoji.join(" "))}`;
138
+ return ok(`Set ${summary} for ${chatId}`);
96
139
  }
97
140
  catch (e) {
98
141
  return fail(e);
@@ -112,7 +155,7 @@ export function registerReactionTools(server, telegram) {
112
155
  const reactions = await telegram.getRecentReactions(limit);
113
156
  if (reactions.length === 0)
114
157
  return ok("No recent reactions");
115
- return ok(sanitize(reactions.map((r) => r.emoji).join(" ")));
158
+ return ok(reactions.map((r) => r.emoji).join(" "));
116
159
  }
117
160
  catch (e) {
118
161
  return fail(e);
@@ -13,7 +13,9 @@ export declare const DESTRUCTIVE: {
13
13
  readonly destructiveHint: true;
14
14
  readonly openWorldHint: true;
15
15
  };
16
- /** Helper: success response */
16
+ /** Remove unpaired UTF-16 surrogates that break JSON serialization */
17
+ export declare function sanitize(text: string): string;
18
+ /** Helper: success response — always sanitizes to prevent surrogate crashes */
17
19
  export declare function ok(text: string): {
18
20
  content: {
19
21
  type: "text";
@@ -28,8 +30,6 @@ export declare function fail(e: unknown): {
28
30
  }[];
29
31
  isError: true;
30
32
  };
31
- /** Remove unpaired UTF-16 surrogates that break JSON serialization */
32
- export declare function sanitize(text: string): string;
33
33
  /** Format reactions array into compact text like: [👍×5 ❤️×3(me) 🔥×1] */
34
34
  export declare function formatReactions(reactions?: {
35
35
  emoji: string;
@@ -2,17 +2,18 @@
2
2
  export const READ_ONLY = { readOnlyHint: true, openWorldHint: true };
3
3
  export const WRITE = { readOnlyHint: false, openWorldHint: true };
4
4
  export const DESTRUCTIVE = { readOnlyHint: false, destructiveHint: true, openWorldHint: true };
5
- /** Helper: success response */
5
+ /** Remove unpaired UTF-16 surrogates that break JSON serialization */
6
+ export function sanitize(text) {
7
+ return text.replace(/[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/g, "\uFFFD");
8
+ }
9
+ /** Helper: success response — always sanitizes to prevent surrogate crashes */
6
10
  export function ok(text) {
7
- return { content: [{ type: "text", text }] };
11
+ return { content: [{ type: "text", text: sanitize(text) }] };
8
12
  }
9
13
  /** Helper: error response with isError flag */
10
14
  export function fail(e) {
11
- return { content: [{ type: "text", text: `Error: ${e.message}` }], isError: true };
12
- }
13
- /** Remove unpaired UTF-16 surrogates that break JSON serialization */
14
- export function sanitize(text) {
15
- return text.replace(/[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/g, "\uFFFD");
15
+ const msg = e instanceof Error ? e.message : String(e);
16
+ return { content: [{ type: "text", text: `Error: ${sanitize(msg)}` }], isError: true };
16
17
  }
17
18
  /** Format reactions array into compact text like: [👍×5 ❤️×3(me) 🔥×1] */
18
19
  export function formatReactions(reactions) {
@@ -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 isStarsEnabled(): boolean;
4
+ export declare function registerStarsTools(server: McpServer, telegram: TelegramService): void;
@@ -0,0 +1,71 @@
1
+ import { z } from "zod";
2
+ import { fail, ok, READ_ONLY, requireConnection } from "./shared.js";
3
+ export function isStarsEnabled() {
4
+ return process.env.MCP_TELEGRAM_ENABLE_STARS === "1";
5
+ }
6
+ export function registerStarsTools(server, telegram) {
7
+ if (!isStarsEnabled())
8
+ return;
9
+ server.registerTool("telegram-get-stars-status", {
10
+ description: "Fetch the current Telegram Stars balance and recent activity for a peer (payments.GetStarsStatus). Pass 'me' / '@me' (or the user's own id) to inspect your own wallet, or a bot/channel peer you own/administrate. Returns {balance:{amount,nanos}, subscriptions?[], subscriptionsNextOffset?, subscriptionsMissingBalance?, history?[], nextOffset?}. Each transaction has id, stars, date, peer (kind: appStore|playMarket|premiumBot|fragment|ads|api|peer|unsupported), and flags (refund/pending/failed/gift/reaction). Use telegram-get-stars-transactions for full paginated history. Opt-in: register only when MCP_TELEGRAM_ENABLE_STARS=1. Read-only.",
11
+ inputSchema: {
12
+ peer: z
13
+ .string()
14
+ .describe("Peer whose Stars wallet to inspect — 'me'/'@me' or user id for self, or @username/id for a bot/channel you control"),
15
+ },
16
+ annotations: READ_ONLY,
17
+ }, async ({ peer }) => {
18
+ const err = await requireConnection(telegram);
19
+ if (err)
20
+ return fail(new Error(err));
21
+ try {
22
+ const result = await telegram.getStarsStatus(peer);
23
+ return ok(JSON.stringify(result));
24
+ }
25
+ catch (e) {
26
+ return fail(e);
27
+ }
28
+ });
29
+ server.registerTool("telegram-get-stars-transactions", {
30
+ description: "Fetch a paginated Telegram Stars transaction history for a peer (payments.GetStarsTransactions). Pass 'me'/'@me' (or your own user id) for your wallet, or a bot/channel peer you own/administrate. Filters: inbound (credits only), outbound (debits only), ascending (chronological; default descending), subscriptionId (scope to a single subscription). Paginate with offset (cursor string from a prior response's nextOffset) and limit (default 50). Returns {balance, history[], nextOffset?, subscriptions?[], ...} — same shape as telegram-get-stars-status but focused on transactions. Opt-in: register only when MCP_TELEGRAM_ENABLE_STARS=1. Read-only.",
31
+ inputSchema: {
32
+ peer: z
33
+ .string()
34
+ .describe("Peer whose Stars transactions to fetch — 'me'/'@me' or user id for self, or @username/id for a bot/channel you control"),
35
+ inbound: z.boolean().optional().describe("If true, return only inbound (credit) transactions"),
36
+ outbound: z.boolean().optional().describe("If true, return only outbound (debit) transactions"),
37
+ ascending: z
38
+ .boolean()
39
+ .optional()
40
+ .describe("If true, return transactions in chronological (oldest first) order; default: newest first"),
41
+ subscriptionId: z.string().optional().describe("If set, filter transactions to a single subscription id"),
42
+ offset: z
43
+ .string()
44
+ .optional()
45
+ .describe("Pagination cursor — pass the nextOffset from a previous response; empty/undefined for first page"),
46
+ limit: z.number().int().min(1).max(100).optional().describe("Max transactions per page (default 50, max 100)"),
47
+ },
48
+ annotations: READ_ONLY,
49
+ }, async ({ peer, inbound, outbound, ascending, subscriptionId, offset, limit }) => {
50
+ const err = await requireConnection(telegram);
51
+ if (err)
52
+ return fail(new Error(err));
53
+ if (inbound && outbound) {
54
+ return fail(new Error("inbound and outbound are mutually exclusive — provide at most one"));
55
+ }
56
+ try {
57
+ const result = await telegram.getStarsTransactions(peer, {
58
+ inbound,
59
+ outbound,
60
+ ascending,
61
+ subscriptionId,
62
+ offset,
63
+ limit,
64
+ });
65
+ return ok(JSON.stringify(result));
66
+ }
67
+ catch (e) {
68
+ return fail(e);
69
+ }
70
+ });
71
+ }
@@ -1,5 +1,5 @@
1
1
  import { z } from "zod";
2
- import { fail, ok, READ_ONLY, requireConnection, sanitize, WRITE } from "./shared.js";
2
+ import { fail, ok, READ_ONLY, requireConnection, WRITE } from "./shared.js";
3
3
  export function registerStickerTools(server, telegram) {
4
4
  server.registerTool("telegram-get-sticker-set", {
5
5
  description: "Get all stickers from a sticker set by its short name. Returns each sticker with index and emoji. Use the index with telegram-send-sticker to send a specific sticker",
@@ -25,7 +25,7 @@ export function registerStickerTools(server, telegram) {
25
25
  }
26
26
  lines.push("");
27
27
  lines.push(`Send a sticker: telegram-send-sticker(chatId, stickerSet="${set.shortName}", index=N)`);
28
- return ok(sanitize(lines.join("\n")));
28
+ return ok(lines.join("\n"));
29
29
  }
30
30
  catch (e) {
31
31
  return fail(e);
@@ -54,7 +54,7 @@ export function registerStickerTools(server, telegram) {
54
54
  }
55
55
  lines.push("");
56
56
  lines.push("Use telegram-get-sticker-set(shortName) to see individual stickers.");
57
- return ok(sanitize(lines.join("\n")));
57
+ return ok(lines.join("\n"));
58
58
  }
59
59
  catch (e) {
60
60
  return fail(e);
@@ -79,7 +79,7 @@ export function registerStickerTools(server, telegram) {
79
79
  lines.push(`• ${set.title} — ${set.count} stickers`);
80
80
  lines.push(` Short name: ${set.shortName}`);
81
81
  }
82
- return ok(sanitize(lines.join("\n")));
82
+ return ok(lines.join("\n"));
83
83
  }
84
84
  catch (e) {
85
85
  return fail(e);
@@ -128,7 +128,7 @@ export function registerStickerTools(server, telegram) {
128
128
  for (let i = 0; i < stickers.length; i++) {
129
129
  lines.push(`[${i}] ${stickers[i].emoji}`);
130
130
  }
131
- return ok(sanitize(lines.join("\n")));
131
+ return ok(lines.join("\n"));
132
132
  }
133
133
  catch (e) {
134
134
  return fail(e);
@@ -0,0 +1,3 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import type { TelegramService } from "../telegram-client.js";
3
+ export declare function registerStoryTools(server: McpServer, telegram: TelegramService): void;