@overpod/mcp-telegram 1.28.1 → 1.32.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.
@@ -1,5 +1,26 @@
1
- import type bigInt from "big-integer";
1
+ import bigInt from "big-integer";
2
2
  import { Api } from "telegram/tl/index.js";
3
+ /**
4
+ * Build an InputReplyToMessage from optional replyTo / topicId, matching the shape used by
5
+ * raw messages.SendMedia. Returns undefined when neither is set so the caller can spread-omit it.
6
+ */
7
+ export declare function buildReplyTo(replyTo?: number, topicId?: number): Api.InputReplyToMessage | undefined;
8
+ /** Cryptographically random 64-bit bigInt for TL randomId (SendMedia/SendMultiMedia require it). */
9
+ export declare function generateRandomBigInt(): bigInt.BigInteger;
10
+ /**
11
+ * Extract the server-assigned message ID from an Updates envelope returned by SendMedia/SendMessage.
12
+ * Prefers UpdateMessageID (authoritative for SendMedia), falls back to UpdateNewMessage /
13
+ * UpdateNewChannelMessage for safety. Returns undefined when no ID is found.
14
+ */
15
+ export declare function extractMessageId(result: Api.TypeUpdates | Api.Message | Api.UpdateShortSentMessage | undefined): number | undefined;
16
+ /**
17
+ * Extract the MessageMediaDice value and captured message ID from a SendMedia dice envelope.
18
+ * Value is only present in UpdateNewMessage/UpdateNewChannelMessage; UpdateMessageID carries the ID only.
19
+ */
20
+ export declare function extractDiceResult(result: Api.TypeUpdates | undefined): {
21
+ id: number;
22
+ value?: number;
23
+ } | undefined;
3
24
  export declare function describeAdminLogAction(action: Api.TypeChannelAdminLogEventAction): string;
4
25
  export declare function describeAdminLogDetails(action: Api.TypeChannelAdminLogEventAction, describeUser: (userId: bigInt.BigInteger) => string): string;
5
26
  export declare function reactionToEmoji(reaction: Api.TypeReaction): string | null;
@@ -467,4 +488,93 @@ export declare function summarizePeerStories(ps: Api.TypePeerStories): PeerStori
467
488
  export declare function summarizeStoriesById(result: Api.stories.TypeStories): StoriesByIdSummary;
468
489
  export declare function summarizeStoryView(view: Api.TypeStoryView): StoryViewSummary;
469
490
  export declare function summarizeStoryViewsList(result: Api.stories.TypeStoryViewsList): StoryViewsListSummary;
491
+ export type StoryPrivacy = "everyone" | "contacts" | "close_friends" | "selected";
492
+ export declare function detectMediaType(filePath: string): "photo" | "video";
493
+ export declare function buildStoryPrivacyRules(privacy: StoryPrivacy, allowUserIds?: string[], disallowUserIds?: string[]): Api.TypeInputPrivacyRule[];
494
+ export declare function extractStoryIdFromUpdates(result: Api.TypeUpdates | undefined): number;
495
+ export type DiscussionMessageSummary = {
496
+ discussionGroupId: string;
497
+ discussionMsgId: number;
498
+ unreadCount: number;
499
+ readInboxMaxId?: number;
500
+ readOutboxMaxId?: number;
501
+ topMessage?: {
502
+ id: number;
503
+ text?: string;
504
+ date: number;
505
+ };
506
+ };
507
+ export declare function summarizeDiscussionMessage(result: Api.messages.DiscussionMessage): DiscussionMessageSummary;
508
+ export type GroupsForDiscussionSummary = {
509
+ groups: Array<{
510
+ id: string;
511
+ title: string;
512
+ username?: string;
513
+ participantsCount?: number;
514
+ }>;
515
+ };
516
+ export declare function summarizeGroupsForDiscussion(result: Api.messages.TypeChats): GroupsForDiscussionSummary;
517
+ export type ReadParticipantsSummary = {
518
+ messageId: number;
519
+ readers: Array<{
520
+ userId: string;
521
+ readAt: string;
522
+ }>;
523
+ count: number;
524
+ };
525
+ export declare function summarizeReadParticipants(list: Api.TypeReadParticipantDate[], messageId: number): ReadParticipantsSummary;
526
+ export type ReportResultSummary = {
527
+ kind: "reported";
528
+ } | {
529
+ kind: "chooseOption";
530
+ title?: string;
531
+ options: Array<{
532
+ text: string;
533
+ option: string;
534
+ }>;
535
+ } | {
536
+ kind: "addComment";
537
+ optional?: boolean;
538
+ };
539
+ export declare function summarizeReportResult(result: Api.TypeReportResult): ReportResultSummary;
540
+ export type PollSummary = {
541
+ question: string;
542
+ isClosed: boolean;
543
+ isQuiz: boolean;
544
+ isMulti: boolean;
545
+ totalVoters: number;
546
+ options: Array<{
547
+ index: number;
548
+ text: string;
549
+ votes: number;
550
+ percent: number;
551
+ chosen: boolean;
552
+ correct?: boolean;
553
+ }>;
554
+ };
555
+ export declare function summarizePoll(poll: Api.Poll, results?: Api.PollResults): PollSummary;
556
+ export declare function extractPollMediaFromUpdates(updates: Api.TypeUpdates): {
557
+ poll: Api.Poll;
558
+ results?: Api.PollResults;
559
+ } | null;
560
+ export declare function extractPeerId(peer: Api.TypePeer): string;
561
+ export type EmojiStatusSummary = {
562
+ kind: "default" | "collectible" | "empty";
563
+ documentId?: string;
564
+ collectibleId?: string;
565
+ until?: number;
566
+ title?: string;
567
+ slug?: string;
568
+ };
569
+ export declare function summarizeEmojiStatus(s: Api.TypeEmojiStatus): EmojiStatusSummary;
570
+ export type PeerSummary = {
571
+ id: string;
572
+ type: "user" | "chat" | "channel";
573
+ };
574
+ export declare function summarizePeer(peer: Api.TypePeer): PeerSummary;
575
+ export type ResolvedBusinessChatLinkSummary = {
576
+ peer: PeerSummary;
577
+ message: string;
578
+ entityCount: number;
579
+ };
470
580
  export declare function summarizeAllStories(result: Api.stories.TypeAllStories): AllStoriesSummary;
@@ -1,4 +1,77 @@
1
+ import { randomBytes } from "node:crypto";
2
+ import bigInt from "big-integer";
1
3
  import { Api } from "telegram/tl/index.js";
4
+ /**
5
+ * Build an InputReplyToMessage from optional replyTo / topicId, matching the shape used by
6
+ * raw messages.SendMedia. Returns undefined when neither is set so the caller can spread-omit it.
7
+ */
8
+ export function buildReplyTo(replyTo, topicId) {
9
+ if (!replyTo && !topicId)
10
+ return undefined;
11
+ // Telegram expects replyToMsgId to equal topicId when replying to the topic root
12
+ // (posting into a topic without quoting a specific message inside it).
13
+ return new Api.InputReplyToMessage({
14
+ replyToMsgId: (replyTo ?? topicId),
15
+ topMsgId: topicId,
16
+ });
17
+ }
18
+ /** Cryptographically random 64-bit bigInt for TL randomId (SendMedia/SendMultiMedia require it). */
19
+ export function generateRandomBigInt() {
20
+ return bigInt(randomBytes(8).toString("hex"), 16);
21
+ }
22
+ /**
23
+ * Extract the server-assigned message ID from an Updates envelope returned by SendMedia/SendMessage.
24
+ * Prefers UpdateMessageID (authoritative for SendMedia), falls back to UpdateNewMessage /
25
+ * UpdateNewChannelMessage for safety. Returns undefined when no ID is found.
26
+ */
27
+ export function extractMessageId(result) {
28
+ if (!result)
29
+ return undefined;
30
+ if (result instanceof Api.Message)
31
+ return result.id;
32
+ if (result instanceof Api.UpdateShortSentMessage)
33
+ return result.id;
34
+ if (result instanceof Api.Updates || result instanceof Api.UpdatesCombined) {
35
+ for (const u of result.updates) {
36
+ if (u instanceof Api.UpdateMessageID)
37
+ return u.id;
38
+ }
39
+ for (const u of result.updates) {
40
+ if (u instanceof Api.UpdateNewMessage || u instanceof Api.UpdateNewChannelMessage) {
41
+ if (u.message instanceof Api.Message)
42
+ return u.message.id;
43
+ }
44
+ }
45
+ }
46
+ return undefined;
47
+ }
48
+ /**
49
+ * Extract the MessageMediaDice value and captured message ID from a SendMedia dice envelope.
50
+ * Value is only present in UpdateNewMessage/UpdateNewChannelMessage; UpdateMessageID carries the ID only.
51
+ */
52
+ export function extractDiceResult(result) {
53
+ if (!result)
54
+ return undefined;
55
+ if (!(result instanceof Api.Updates) && !(result instanceof Api.UpdatesCombined))
56
+ return undefined;
57
+ let id;
58
+ let value;
59
+ for (const u of result.updates) {
60
+ if (u instanceof Api.UpdateMessageID && id === undefined)
61
+ id = u.id;
62
+ if (u instanceof Api.UpdateNewMessage || u instanceof Api.UpdateNewChannelMessage) {
63
+ if (u.message instanceof Api.Message) {
64
+ if (id === undefined)
65
+ id = u.message.id;
66
+ if (u.message.media instanceof Api.MessageMediaDice)
67
+ value = u.message.media.value;
68
+ }
69
+ }
70
+ }
71
+ if (id === undefined)
72
+ return undefined;
73
+ return { id, value };
74
+ }
2
75
  export function describeAdminLogAction(action) {
3
76
  const prefix = "ChannelAdminLogEventAction";
4
77
  const raw = action.className.startsWith(prefix) ? action.className.slice(prefix.length) : action.className;
@@ -840,6 +913,203 @@ export function summarizeStoryViewsList(result) {
840
913
  nextOffset: list.nextOffset,
841
914
  };
842
915
  }
916
+ export function detectMediaType(filePath) {
917
+ const ext = filePath.toLowerCase().split(".").pop() ?? "";
918
+ if (["jpg", "jpeg", "png", "webp", "heic", "heif"].includes(ext))
919
+ return "photo";
920
+ return "video";
921
+ }
922
+ export function buildStoryPrivacyRules(privacy, allowUserIds, disallowUserIds) {
923
+ const rules = [];
924
+ switch (privacy) {
925
+ case "everyone":
926
+ rules.push(new Api.InputPrivacyValueAllowAll());
927
+ break;
928
+ case "contacts":
929
+ rules.push(new Api.InputPrivacyValueAllowContacts());
930
+ break;
931
+ case "close_friends":
932
+ rules.push(new Api.InputPrivacyValueAllowCloseFriends());
933
+ break;
934
+ case "selected":
935
+ rules.push(new Api.InputPrivacyValueAllowUsers({
936
+ users: (allowUserIds ?? []).map((id) => new Api.InputUser({ userId: bigInt(id), accessHash: bigInt(0) })),
937
+ }));
938
+ break;
939
+ }
940
+ if (disallowUserIds?.length && privacy !== "selected") {
941
+ rules.push(new Api.InputPrivacyValueDisallowUsers({
942
+ users: disallowUserIds.map((id) => new Api.InputUser({ userId: bigInt(id), accessHash: bigInt(0) })),
943
+ }));
944
+ }
945
+ return rules;
946
+ }
947
+ export function extractStoryIdFromUpdates(result) {
948
+ if (!result)
949
+ return 0;
950
+ if (result instanceof Api.Updates || result instanceof Api.UpdatesCombined) {
951
+ for (const u of result.updates) {
952
+ if (u instanceof Api.UpdateStoryID)
953
+ return u.id;
954
+ }
955
+ for (const u of result.updates) {
956
+ if (u instanceof Api.UpdateStory && u.story instanceof Api.StoryItem)
957
+ return u.story.id;
958
+ }
959
+ }
960
+ return 0;
961
+ }
962
+ export function summarizeDiscussionMessage(result) {
963
+ const topMsg = result.messages?.[0];
964
+ let discussionGroupId = "";
965
+ for (const chat of result.chats ?? []) {
966
+ const isBroadcast = "broadcast" in chat && chat.broadcast;
967
+ if (!isBroadcast) {
968
+ discussionGroupId = `-100${chat.id.toString()}`;
969
+ break;
970
+ }
971
+ }
972
+ const discussionMsgId = topMsg instanceof Api.Message || topMsg instanceof Api.MessageService ? topMsg.id : 0;
973
+ const topMessage = topMsg instanceof Api.Message
974
+ ? {
975
+ id: topMsg.id,
976
+ text: topMsg.message?.slice(0, 200),
977
+ date: topMsg.date,
978
+ }
979
+ : undefined;
980
+ return {
981
+ discussionGroupId,
982
+ discussionMsgId,
983
+ unreadCount: result.unreadCount ?? 0,
984
+ readInboxMaxId: result.readInboxMaxId,
985
+ readOutboxMaxId: result.readOutboxMaxId,
986
+ topMessage,
987
+ };
988
+ }
989
+ export function summarizeGroupsForDiscussion(result) {
990
+ const chats = "chats" in result ? result.chats : [];
991
+ return {
992
+ groups: chats.map((c) => {
993
+ const id = `-100${c.id.toString()}`;
994
+ const title = "title" in c ? c.title : "";
995
+ const username = "username" in c ? (c.username ?? undefined) : undefined;
996
+ const participantsCount = "participantsCount" in c ? (c.participantsCount ?? undefined) : undefined;
997
+ return { id, title, username, participantsCount };
998
+ }),
999
+ };
1000
+ }
1001
+ export function summarizeReadParticipants(list, messageId) {
1002
+ return {
1003
+ messageId,
1004
+ readers: list.map((r) => ({
1005
+ userId: r.userId.toString(),
1006
+ readAt: new Date(r.date * 1000).toISOString(),
1007
+ })),
1008
+ count: list.length,
1009
+ };
1010
+ }
1011
+ export function summarizeReportResult(result) {
1012
+ if (result instanceof Api.ReportResultReported)
1013
+ return { kind: "reported" };
1014
+ if (result instanceof Api.ReportResultAddComment)
1015
+ return { kind: "addComment", optional: result.optional };
1016
+ if (result instanceof Api.ReportResultChooseOption) {
1017
+ return {
1018
+ kind: "chooseOption",
1019
+ title: result.title,
1020
+ options: (result.options ?? []).map((o) => {
1021
+ const opt = o;
1022
+ return {
1023
+ text: opt.text,
1024
+ option: Buffer.from(opt.option).toString("base64"),
1025
+ };
1026
+ }),
1027
+ };
1028
+ }
1029
+ throw new Error(`unknown ReportResult type: ${result.className ?? "unknown"}`);
1030
+ }
1031
+ export function summarizePoll(poll, results) {
1032
+ const total = results?.totalVoters ?? 0;
1033
+ const answerResults = results?.results ?? [];
1034
+ const options = poll.answers.map((answer, index) => {
1035
+ // Match by option bytes
1036
+ const v = answerResults.find((r) => {
1037
+ const rOpt = Buffer.from(r.option);
1038
+ const aOpt = Buffer.from(answer.option);
1039
+ return rOpt.equals(aOpt);
1040
+ });
1041
+ const votes = v?.voters ?? 0;
1042
+ const percent = total > 0 ? Math.round((votes / total) * 1000) / 10 : 0;
1043
+ return {
1044
+ index,
1045
+ text: answer.text.text,
1046
+ votes,
1047
+ percent,
1048
+ chosen: v?.chosen ?? false,
1049
+ correct: poll.quiz ? (v?.correct ?? false) : undefined,
1050
+ };
1051
+ });
1052
+ return {
1053
+ question: poll.question.text,
1054
+ isClosed: poll.closed ?? false,
1055
+ isQuiz: poll.quiz ?? false,
1056
+ isMulti: poll.multipleChoice ?? false,
1057
+ totalVoters: total,
1058
+ options,
1059
+ };
1060
+ }
1061
+ export function extractPollMediaFromUpdates(updates) {
1062
+ let list = [];
1063
+ if (updates instanceof Api.Updates || updates instanceof Api.UpdatesCombined) {
1064
+ list = updates.updates;
1065
+ }
1066
+ else if (updates instanceof Api.UpdateShort) {
1067
+ list = [updates.update];
1068
+ }
1069
+ for (const u of list) {
1070
+ if (u instanceof Api.UpdateMessagePoll) {
1071
+ if (u.poll instanceof Api.Poll) {
1072
+ return {
1073
+ poll: u.poll,
1074
+ results: u.results instanceof Api.PollResults ? u.results : undefined,
1075
+ };
1076
+ }
1077
+ }
1078
+ }
1079
+ return null;
1080
+ }
1081
+ export function extractPeerId(peer) {
1082
+ if (peer instanceof Api.PeerUser)
1083
+ return peer.userId.toString();
1084
+ if (peer instanceof Api.PeerChat)
1085
+ return peer.chatId.toString();
1086
+ if (peer instanceof Api.PeerChannel)
1087
+ return peer.channelId.toString();
1088
+ return "0";
1089
+ }
1090
+ export function summarizeEmojiStatus(s) {
1091
+ if (s instanceof Api.EmojiStatusCollectible) {
1092
+ return {
1093
+ kind: "collectible",
1094
+ collectibleId: s.collectibleId.toString(),
1095
+ documentId: s.documentId?.toString(),
1096
+ title: s.title,
1097
+ slug: s.slug,
1098
+ until: s.until,
1099
+ };
1100
+ }
1101
+ if (s instanceof Api.EmojiStatus) {
1102
+ return { kind: "default", documentId: s.documentId.toString(), until: s.until };
1103
+ }
1104
+ return { kind: "empty" };
1105
+ }
1106
+ export function summarizePeer(peer) {
1107
+ if (peer instanceof Api.PeerUser)
1108
+ return { id: peer.userId.toString(), type: "user" };
1109
+ if (peer instanceof Api.PeerChat)
1110
+ return { id: peer.chatId.toString(), type: "chat" };
1111
+ return { id: peer.channelId.toString(), type: "channel" };
1112
+ }
843
1113
  export function summarizeAllStories(result) {
844
1114
  const stealthMode = result.stealthMode
845
1115
  ? {
@@ -1,5 +1,5 @@
1
1
  import { z } from "zod";
2
- import { DESTRUCTIVE, fail, ok, READ_ONLY, requireConnection, WRITE } from "./shared.js";
2
+ import { ABSOLUTE_PATH_ERROR, DESTRUCTIVE, fail, isSafeAbsolutePath, ok, READ_ONLY, requireConnection, WRITE, } from "./shared.js";
3
3
  const MUTE_FOREVER_UNTIL = 2147483647; // max 32-bit signed int
4
4
  export function registerAccountTools(server, telegram) {
5
5
  server.registerTool("telegram-mute-chat", {
@@ -369,17 +369,232 @@ export function registerAccountTools(server, telegram) {
369
369
  return fail(e);
370
370
  }
371
371
  });
372
- server.registerTool("telegram-get-business-chat-links", {
373
- description: "List Telegram Business chat links configured for the account (account.GetBusinessChatLinks). Each entry includes the t.me/... link, the prefilled message, optional title (admin-facing label), views count, and entityCount (number of formatting entities in the message). Requires a Telegram Business-enabled account — returns an empty list when the account has none configured. Read-only.",
374
- inputSchema: {},
372
+ // ─── Profile write tools (v1.32.0) ─────────────────────────────────────────
373
+ server.registerTool("telegram-set-emoji-status", {
374
+ description: "Set your profile emoji status (custom animated emoji shown next to your name). Requires Telegram Premium. Pass documentId or collectibleId to set — omit both to clear the status. Use telegram-list-emoji-statuses to browse available IDs.",
375
+ inputSchema: {
376
+ documentId: z
377
+ .string()
378
+ .optional()
379
+ .describe("Custom emoji document ID (stringified long). Omit to clear the status."),
380
+ collectibleId: z
381
+ .string()
382
+ .optional()
383
+ .describe("Collectible emoji ID (stringified long) — for paid unique emoji. Exactly one of documentId/collectibleId may be set."),
384
+ untilUnix: z
385
+ .number()
386
+ .int()
387
+ .positive()
388
+ .optional()
389
+ .describe("Unix timestamp when status expires. Omit for permanent."),
390
+ },
391
+ annotations: WRITE,
392
+ }, async ({ documentId, collectibleId, untilUnix }) => {
393
+ const err = await requireConnection(telegram);
394
+ if (err)
395
+ return fail(new Error(err));
396
+ if (documentId && collectibleId) {
397
+ return fail(new Error("Only one of documentId or collectibleId may be set"));
398
+ }
399
+ try {
400
+ await telegram.setEmojiStatus({ documentId, collectibleId, untilUnix });
401
+ if (!documentId && !collectibleId)
402
+ return ok("Emoji status cleared");
403
+ const id = collectibleId ?? documentId;
404
+ const until = untilUnix ? ` until ${new Date(untilUnix * 1000).toISOString()}` : "";
405
+ return ok(`Emoji status set: ${id}${until}`);
406
+ }
407
+ catch (e) {
408
+ return fail(e);
409
+ }
410
+ });
411
+ server.registerTool("telegram-list-emoji-statuses", {
412
+ description: "List default or recently-used emoji statuses available for your account. Useful for finding a documentId to pass to telegram-set-emoji-status.",
413
+ inputSchema: {
414
+ kind: z
415
+ .enum(["default", "recent", "channel_default", "collectible"])
416
+ .default("default")
417
+ .describe("Which list: default (popular set), recent (your recent usage), channel_default (for channels), collectible (paid unique)"),
418
+ limit: z.number().int().positive().max(200).default(50).describe("Max items to return"),
419
+ },
375
420
  annotations: READ_ONLY,
421
+ }, async ({ kind, limit }) => {
422
+ const err = await requireConnection(telegram);
423
+ if (err)
424
+ return fail(new Error(err));
425
+ try {
426
+ const items = await telegram.listEmojiStatuses(kind, limit);
427
+ if (!items.length)
428
+ return ok(`[${kind}]\n(no statuses)`);
429
+ const lines = items.map((s) => {
430
+ const id = s.collectibleId ?? s.documentId ?? "empty";
431
+ const until = s.until ? ` until=${s.until}` : " until=permanent";
432
+ const extra = s.title ? ` title="${s.title}"` : "";
433
+ return `${s.kind} id=${id}${until}${extra}`;
434
+ });
435
+ return ok(`[${kind}]\n${lines.join("\n")}`);
436
+ }
437
+ catch (e) {
438
+ return fail(e);
439
+ }
440
+ });
441
+ server.registerTool("telegram-clear-recent-emoji-statuses", {
442
+ description: "Clear your recently-used emoji status list (the 'recent' section in the emoji status picker).",
443
+ inputSchema: {},
444
+ annotations: WRITE,
376
445
  }, async () => {
377
446
  const err = await requireConnection(telegram);
378
447
  if (err)
379
448
  return fail(new Error(err));
380
449
  try {
381
- const result = await telegram.getBusinessChatLinks();
382
- return ok(JSON.stringify(result));
450
+ await telegram.clearRecentEmojiStatuses();
451
+ return ok("Recent emoji statuses cleared");
452
+ }
453
+ catch (e) {
454
+ return fail(e);
455
+ }
456
+ });
457
+ server.registerTool("telegram-set-profile-color", {
458
+ description: "Set your profile name color or profile background color. Requires Telegram Premium for colors above index 6 and for profile background patterns. Omit color to reset to default.",
459
+ inputSchema: {
460
+ forProfile: z
461
+ .boolean()
462
+ .default(false)
463
+ .describe("true = profile page color + background pattern (Premium); false = name color in chat lists"),
464
+ color: z
465
+ .number()
466
+ .int()
467
+ .min(0)
468
+ .max(20)
469
+ .optional()
470
+ .describe("Color index (0-6 free palette; 7+ Premium custom). Omit to reset to default."),
471
+ backgroundEmojiId: z
472
+ .string()
473
+ .optional()
474
+ .describe("Custom emoji document ID (stringified long) for profile background pattern (Premium). Omit to remove."),
475
+ },
476
+ annotations: WRITE,
477
+ }, async ({ forProfile, color, backgroundEmojiId }) => {
478
+ const err = await requireConnection(telegram);
479
+ if (err)
480
+ return fail(new Error(err));
481
+ try {
482
+ await telegram.setProfileColor({ forProfile, color, backgroundEmojiId });
483
+ if (color === undefined && !backgroundEmojiId)
484
+ return ok("Profile color reset");
485
+ return ok(`Profile color updated: forProfile=${forProfile} color=${color ?? "default"} bg=${backgroundEmojiId ?? "none"}`);
486
+ }
487
+ catch (e) {
488
+ return fail(e);
489
+ }
490
+ });
491
+ server.registerTool("telegram-set-birthday", {
492
+ description: "Set your birthday in your Telegram profile. Year is optional (omit to hide age). Pass clear=true to remove birthday. Requires day and month unless clearing.",
493
+ inputSchema: {
494
+ day: z.number().int().min(1).max(31).optional().describe("Day of month (1-31)"),
495
+ month: z.number().int().min(1).max(12).optional().describe("Month (1-12)"),
496
+ year: z.number().int().min(1900).max(2100).optional().describe("Year (optional — omit to hide age)"),
497
+ clear: z.boolean().optional().describe("Pass true to remove birthday from profile"),
498
+ },
499
+ annotations: WRITE,
500
+ }, async ({ day, month, year, clear }) => {
501
+ const err = await requireConnection(telegram);
502
+ if (err)
503
+ return fail(new Error(err));
504
+ if (!clear && (!day || !month)) {
505
+ return fail(new Error("day and month are required when not clearing"));
506
+ }
507
+ try {
508
+ await telegram.setBirthday({ day, month, year, clear });
509
+ if (clear)
510
+ return ok("Birthday cleared");
511
+ const yearStr = year ? `/${year}` : "";
512
+ return ok(`Birthday set: ${day}/${month}${yearStr}`);
513
+ }
514
+ catch (e) {
515
+ return fail(e);
516
+ }
517
+ });
518
+ server.registerTool("telegram-set-personal-channel", {
519
+ description: "Set the channel displayed on your profile as 'Personal Channel'. Pass clear=true to remove. Pass channelId or @username of a channel you own or are subscribed to.",
520
+ inputSchema: {
521
+ channelId: z.string().optional().describe("Channel ID or @username to feature on profile"),
522
+ clear: z.boolean().optional().describe("Pass true to remove personal channel from profile"),
523
+ },
524
+ annotations: WRITE,
525
+ }, async ({ channelId, clear }) => {
526
+ const err = await requireConnection(telegram);
527
+ if (err)
528
+ return fail(new Error(err));
529
+ if (!clear && !channelId) {
530
+ return fail(new Error("channelId is required when not clearing"));
531
+ }
532
+ if (clear && channelId) {
533
+ return fail(new Error("Cannot set channelId and clear=true simultaneously"));
534
+ }
535
+ try {
536
+ const title = await telegram.setPersonalChannel({ channelId, clear });
537
+ if (clear)
538
+ return ok("Personal channel cleared");
539
+ return ok(`Personal channel set to ${title ?? channelId}`);
540
+ }
541
+ catch (e) {
542
+ return fail(e);
543
+ }
544
+ });
545
+ server.registerTool("telegram-set-profile-photo", {
546
+ description: "Upload and set a new profile photo from a local file. Supports JPEG/PNG for static avatar or MP4 for animated avatar (square, up to 10s). Optionally set as fallback photo shown to users who cannot see your main photo.",
547
+ inputSchema: {
548
+ filePath: z
549
+ .string()
550
+ .min(1)
551
+ .refine(isSafeAbsolutePath, ABSOLUTE_PATH_ERROR)
552
+ .describe("Absolute local filesystem path to photo (JPEG/PNG) or video (MP4, square) to upload as avatar. URLs are rejected."),
553
+ isVideo: z.boolean().default(false).describe("true if file is an MP4 animated avatar; false for static photo"),
554
+ videoStartTs: z
555
+ .number()
556
+ .min(0)
557
+ .optional()
558
+ .describe("For video avatar: timestamp in seconds to use as still preview frame"),
559
+ fallback: z
560
+ .boolean()
561
+ .default(false)
562
+ .describe("true = set as fallback photo (shown to users who cannot see your main photo due to privacy settings)"),
563
+ },
564
+ annotations: WRITE,
565
+ }, async ({ filePath, isVideo, videoStartTs, fallback }) => {
566
+ const err = await requireConnection(telegram);
567
+ if (err)
568
+ return fail(new Error(err));
569
+ try {
570
+ const { id } = await telegram.setProfilePhoto({ filePath, isVideo, videoStartTs, fallback });
571
+ const label = fallback ? "Fallback profile photo" : "Profile photo";
572
+ return ok(`${label} updated [id=${id}]`);
573
+ }
574
+ catch (e) {
575
+ return fail(e);
576
+ }
577
+ });
578
+ server.registerTool("telegram-delete-profile-photo", {
579
+ description: "Delete one or more profile photos by their photo IDs. Use telegram-get-profile-photo to obtain the current photo ID. Returns which IDs were deleted and which were not found.",
580
+ inputSchema: {
581
+ photoIds: z
582
+ .array(z.string().regex(/^\d{1,20}$/, "must be a numeric photo ID"))
583
+ .min(1)
584
+ .max(100)
585
+ .describe("Array of photo IDs (stringified long) to delete from your profile photo history"),
586
+ },
587
+ annotations: WRITE,
588
+ }, async ({ photoIds }) => {
589
+ const err = await requireConnection(telegram);
590
+ if (err)
591
+ return fail(new Error(err));
592
+ try {
593
+ const { deleted, missing } = await telegram.deleteProfilePhotos(photoIds);
594
+ const parts = [`Deleted ${deleted.length} profile photo(s): ${deleted.join(", ")}`];
595
+ if (missing.length)
596
+ parts.push(`Not found: ${missing.join(", ")}`);
597
+ return ok(parts.join(". "));
383
598
  }
384
599
  catch (e) {
385
600
  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 registerBusinessTools(server: McpServer, telegram: TelegramService): void;