@@ -576,6 +576,9 @@ async def list_messages(
576576 "date" : msg .date ,
577577 "text" : sanitize_user_content (msg .message ),
578578 }
579+ grouped_id = getattr (msg , "grouped_id" , None )
580+ if grouped_id is not None :
581+ record ["grouped_id" ] = grouped_id
579582 reply_to_id = getattr (msg .reply_to , "reply_to_msg_id" , None ) if msg .reply_to else None
580583 if reply_to_id :
581584 record ["reply_to" ] = reply_to_id
@@ -639,6 +642,9 @@ async def get_message_context(
639642 "is_target" : msg .id == message_id ,
640643 "text" : sanitize_user_content (msg .message ),
641644 }
645+ grouped_id = getattr (msg , "grouped_id" , None )
646+ if grouped_id is not None :
647+ record ["grouped_id" ] = grouped_id
642648
643649 # Check if this message is a reply and get the replied message
644650 if msg .reply_to and msg .reply_to .reply_to_msg_id :
@@ -683,19 +689,72 @@ async def get_message_context(
683689@validate_id ("from_chat_id" , "to_chat_id" )
684690async def forward_message (
685691 from_chat_id : Union [int , str ],
686- message_id : int ,
692+ message_id : Union [ int , List [ int ]] ,
687693 to_chat_id : Union [int , str ],
688694 account : str = None ,
695+ expand_album : bool = True ,
689696) -> str :
690697 """
691- Forward a message from one chat to another.
698+ Forward a message (or several) from a source chat to a destination chat.
699+
700+ When forwarding a single int message_id, the server automatically detects
701+ Telegram albums (multi-photo/video posts sharing a `grouped_id`) and
702+ forwards the ENTIRE album as one grouped batch — so the destination
703+ receives the album intact with "Forwarded from <source>", not a single
704+ detached photo. This is the desired behavior in almost all cases.
705+
706+ Set expand_album=False to forward only the exact message you specified
707+ (useful if you really want one photo out of an album).
708+
709+ To forward a specific set of unrelated messages, pass a list of ints.
710+ Album expansion is not applied to list inputs — the list is treated as
711+ the explicit batch.
712+
713+ Args:
714+ from_chat_id: Source chat (id or @username).
715+ message_id: A single message id (int) OR a list of ids. Single ints
716+ are auto-expanded to the full album when applicable.
717+ to_chat_id: Destination chat (id or @username).
718+ account: Optional account label for multi-account mode.
719+ expand_album: If True (default) and message_id is a single int, the
720+ server expands albums automatically. No effect on list inputs.
692721 """
693722 try :
694723 cl = get_client (account )
695724 from_entity = await resolve_entity (from_chat_id , cl )
696725 to_entity = await resolve_entity (to_chat_id , cl )
697- await cl .forward_messages (to_entity , message_id , from_entity )
698- return f"Message { message_id } forwarded from { from_chat_id } to { to_chat_id } ."
726+
727+ ids_to_forward = message_id
728+ expanded_from_album = False
729+ if expand_album and isinstance (message_id , int ):
730+ anchor = await cl .get_messages (from_entity , ids = message_id )
731+ grouped_id = getattr (anchor , "grouped_id" , None ) if anchor else None
732+ if grouped_id is not None :
733+ # Album ids are allocated contiguously by Telegram; a small
734+ # window around the anchor reliably captures all siblings.
735+ window = list (range (message_id - 9 , message_id + 10 ))
736+ neighbors = await cl .get_messages (from_entity , ids = window )
737+ sibling_ids = sorted (
738+ {
739+ m .id
740+ for m in neighbors
741+ if m is not None and getattr (m , "grouped_id" , None ) == grouped_id
742+ }
743+ )
744+ if len (sibling_ids ) > 1 :
745+ ids_to_forward = sibling_ids
746+ expanded_from_album = True
747+
748+ await cl .forward_messages (to_entity , ids_to_forward , from_entity )
749+ count = len (ids_to_forward ) if isinstance (ids_to_forward , list ) else 1
750+ if count == 1 :
751+ return f"Message { message_id } forwarded from { from_chat_id } to { to_chat_id } ."
752+ if expanded_from_album :
753+ return (
754+ f"Album of { count } messages forwarded from { from_chat_id } "
755+ f"to { to_chat_id } (auto-expanded from message { message_id } )."
756+ )
757+ return f"{ count } messages forwarded from { from_chat_id } to { to_chat_id } ."
699758 except Exception as e :
700759 return log_and_format_error (
701760 "forward_message" ,
@@ -706,6 +765,58 @@ async def forward_message(
706765 )
707766
708767
768+ @mcp .tool (
769+ annotations = ToolAnnotations (
770+ title = "Forward Messages (batch)" , openWorldHint = True , destructiveHint = True
771+ )
772+ )
773+ @with_account (readonly = False )
774+ @validate_id ("from_chat_id" , "to_chat_id" )
775+ async def forward_messages (
776+ from_chat_id : Union [int , str ],
777+ message_ids : List [int ],
778+ to_chat_id : Union [int , str ],
779+ account : str = None ,
780+ ) -> str :
781+ """
782+ Forward a BATCH of messages from a source chat to a destination chat in
783+ a single atomic call.
784+
785+ Use this whenever you need to forward more than one message. Pass all
786+ message ids as a list (e.g. message_ids=[12345, 12346, 12347]). Calling
787+ this once with a list is strictly better than calling forward_message
788+ multiple times: it preserves Telegram album grouping (siblings sharing
789+ `grouped_id` arrive as one grouped album), is atomic, and counts as a
790+ single forward op for Telegram rate limits.
791+
792+ For exactly one message, you may use either this tool with a one-item
793+ list or `forward_message` with an int.
794+
795+ Args:
796+ from_chat_id: Source chat (id or @username).
797+ message_ids: List of message ids to forward, in any order
798+ (e.g. [12345, 12346]). Must contain at least one id.
799+ to_chat_id: Destination chat (id or @username).
800+ account: Optional account label for multi-account mode.
801+ """
802+ try :
803+ if not message_ids :
804+ return "Error: message_ids must contain at least one id."
805+ cl = get_client (account )
806+ from_entity = await resolve_entity (from_chat_id , cl )
807+ to_entity = await resolve_entity (to_chat_id , cl )
808+ await cl .forward_messages (to_entity , list (message_ids ), from_entity )
809+ return f"{ len (message_ids )} messages forwarded from " f"{ from_chat_id } to { to_chat_id } ."
810+ except Exception as e :
811+ return log_and_format_error (
812+ "forward_messages" ,
813+ e ,
814+ from_chat_id = from_chat_id ,
815+ message_ids = message_ids ,
816+ to_chat_id = to_chat_id ,
817+ )
818+
819+
709820@mcp .tool (
710821 annotations = ToolAnnotations (
711822 title = "Edit Message" , openWorldHint = True , destructiveHint = True , idempotentHint = True
0 commit comments