Skip to main content
Glama
Jem-HR
by Jem-HR

send_message

Send text messages to WhatsApp users with optional headers, footers, URL previews, and reply functionality through the PyWA MCP Server.

Instructions

Send a text message to a WhatsApp user.

Args: to: Phone number (with country code) or WhatsApp ID text: The text message content header: Optional header text (for interactive messages) footer: Optional footer text (for interactive messages) preview_url: Whether to show URL previews (default False) reply_to_message_id: Message ID to reply to

Returns: Dictionary with success status and message ID

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
toYes
textYes
headerNo
footerNo
preview_urlNo
reply_to_message_idNo

Output Schema

TableJSON Schema
NameRequiredDescriptionDefault

No arguments

Implementation Reference

  • The send_message tool handler - an async function decorated with @mcp.tool() that sends text messages to WhatsApp users. It accepts parameters like 'to', 'text', 'header', 'footer', 'preview_url', and 'reply_to_message_id', calls wa_client.send_message(), and returns a dict with success status and message ID.
    @mcp.tool()
    async def send_message(
        to: str,
        text: str,
        header: Optional[str] = None,
        footer: Optional[str] = None,
        *,
        preview_url: bool = False,
        reply_to_message_id: Optional[str] = None,
    ) -> dict:
        """
        Send a text message to a WhatsApp user.
        
        Args:
            to: Phone number (with country code) or WhatsApp ID
            text: The text message content
            header: Optional header text (for interactive messages)
            footer: Optional footer text (for interactive messages)
            preview_url: Whether to show URL previews (default False)
            reply_to_message_id: Message ID to reply to
        
        Returns:
            Dictionary with success status and message ID
        """
        try:
            result = wa_client.send_message(
                to=to,
                text=text,
                header=header,
                footer=footer,
                preview_url=preview_url,
                reply_to_message_id=reply_to_message_id,
            )
            
            logger.info(f"Message sent to {to}")
            # Extract just the message ID if result is a complex object
            message_id = getattr(result, 'id', str(result)) if result else None
            
            return {
                "success": True,
                "message_id": message_id,
                "to": to,
            }
        except Exception as e:
            logger.error(f"Failed to send message: {str(e)}")
            return {"success": False, "error": str(e)}
  • The register_messaging_tools function that registers all messaging tools including send_message with the MCP server. This function is called from tools/__init__.py to register tools at startup.
    def register_messaging_tools(mcp, wa_client: WhatsApp):
        """Register all messaging-related tools."""
        
        @mcp.tool()
        async def send_message(
            to: str,
            text: str,
            header: Optional[str] = None,
            footer: Optional[str] = None,
            *,
            preview_url: bool = False,
            reply_to_message_id: Optional[str] = None,
        ) -> dict:
            """
            Send a text message to a WhatsApp user.
            
            Args:
                to: Phone number (with country code) or WhatsApp ID
                text: The text message content
                header: Optional header text (for interactive messages)
                footer: Optional footer text (for interactive messages)
                preview_url: Whether to show URL previews (default False)
                reply_to_message_id: Message ID to reply to
            
            Returns:
                Dictionary with success status and message ID
            """
            try:
                result = wa_client.send_message(
                    to=to,
                    text=text,
                    header=header,
                    footer=footer,
                    preview_url=preview_url,
                    reply_to_message_id=reply_to_message_id,
                )
                
                logger.info(f"Message sent to {to}")
                # Extract just the message ID if result is a complex object
                message_id = getattr(result, 'id', str(result)) if result else None
                
                return {
                    "success": True,
                    "message_id": message_id,
                    "to": to,
                }
            except Exception as e:
                logger.error(f"Failed to send message: {str(e)}")
                return {"success": False, "error": str(e)}
        
        
        @mcp.tool()
        async def send_image(
            to: str,
            image: str,
            caption: Optional[str] = None,
            footer: Optional[str] = None,
            *,
            reply_to_message_id: Optional[str] = None,
        ) -> dict:
            """
            Send an image message.
            
            Args:
                to: Phone number or WhatsApp ID
                image: Image URL or media ID
                caption: Optional image caption
                footer: Optional footer text
                reply_to_message_id: Message ID to reply to
            
            Returns:
                Dictionary with success status and message ID
            """
            try:
                result = wa_client.send_image(
                    to=to,
                    image=image,
                    caption=caption,
                    footer=footer,
                    reply_to_message_id=reply_to_message_id,
                )
                
                logger.info(f"Image sent to {to}")
                message_id = getattr(result, 'id', str(result)) if result else None
                return {"success": True, "message_id": message_id}
            except Exception as e:
                logger.error(f"Failed to send image: {str(e)}")
                return {"success": False, "error": str(e)}
        
        
        @mcp.tool()
        async def send_video(
            to: str,
            video: str,
            caption: Optional[str] = None,
            footer: Optional[str] = None,
            *,
            reply_to_message_id: Optional[str] = None,
        ) -> dict:
            """
            Send a video message.
            
            Args:
                to: Phone number or WhatsApp ID
                video: Video URL or media ID
                caption: Optional video caption
                footer: Optional footer text
                reply_to_message_id: Message ID to reply to
            
            Returns:
                Dictionary with success status and message ID
            """
            try:
                result = wa_client.send_video(
                    to=to,
                    video=video,
                    caption=caption,
                    footer=footer,
                    reply_to_message_id=reply_to_message_id,
                )
                
                logger.info(f"Video sent to {to}")
                message_id = getattr(result, 'id', str(result)) if result else None
                return {"success": True, "message_id": message_id}
            except Exception as e:
                logger.error(f"Failed to send video: {str(e)}")
                return {"success": False, "error": str(e)}
        
        
        @mcp.tool()
        async def send_document(
            to: str,
            document: str,
            filename: Optional[str] = None,
            caption: Optional[str] = None,
            footer: Optional[str] = None,
            *,
            reply_to_message_id: Optional[str] = None,
        ) -> dict:
            """
            Send a document message.
            
            Args:
                to: Phone number or WhatsApp ID
                document: Document URL or media ID
                filename: Optional filename for the document
                caption: Optional document caption
                footer: Optional footer text
                reply_to_message_id: Message ID to reply to
            
            Returns:
                Dictionary with success status and message ID
            """
            try:
                result = wa_client.send_document(
                    to=to,
                    document=document,
                    filename=filename,
                    caption=caption,
                    footer=footer,
                    reply_to_message_id=reply_to_message_id,
                )
                
                logger.info(f"Document sent to {to}")
                message_id = getattr(result, 'id', str(result)) if result else None
                return {"success": True, "message_id": message_id}
            except Exception as e:
                logger.error(f"Failed to send document: {str(e)}")
                return {"success": False, "error": str(e)}
        
        
        @mcp.tool()
        async def send_audio(
            to: str,
            audio: str,
            *,
            reply_to_message_id: Optional[str] = None,
        ) -> dict:
            """
            Send an audio message.
            
            Args:
                to: Phone number or WhatsApp ID
                audio: Audio URL or media ID
                reply_to_message_id: Message ID to reply to
            
            Returns:
                Dictionary with success status and message ID
            """
            try:
                result = wa_client.send_audio(
                    to=to,
                    audio=audio,
                    reply_to_message_id=reply_to_message_id,
                )
                
                logger.info(f"Audio sent to {to}")
                message_id = getattr(result, 'id', str(result)) if result else None
                return {"success": True, "message_id": message_id}
            except Exception as e:
                logger.error(f"Failed to send audio: {str(e)}")
                return {"success": False, "error": str(e)}
        
        
        @mcp.tool()
        async def send_sticker(
            to: str,
            sticker: str,
            *,
            reply_to_message_id: Optional[str] = None,
        ) -> dict:
            """
            Send a sticker message.
            
            Args:
                to: Phone number or WhatsApp ID
                sticker: Sticker URL or media ID (must be webp format)
                reply_to_message_id: Message ID to reply to
            
            Returns:
                Dictionary with success status and message ID
            """
            try:
                result = wa_client.send_sticker(
                    to=to,
                    sticker=sticker,
                    reply_to_message_id=reply_to_message_id,
                )
                
                logger.info(f"Sticker sent to {to}")
                message_id = getattr(result, 'id', str(result)) if result else None
                return {"success": True, "message_id": message_id}
            except Exception as e:
                logger.error(f"Failed to send sticker: {str(e)}")
                return {"success": False, "error": str(e)}
        
        
        @mcp.tool()
        async def send_location(
            to: str,
            latitude: float,
            longitude: float,
            name: Optional[str] = None,
            address: Optional[str] = None,
            *,
            reply_to_message_id: Optional[str] = None,
        ) -> dict:
            """
            Send a location message.
            
            Args:
                to: Phone number or WhatsApp ID
                latitude: Latitude of the location
                longitude: Longitude of the location
                name: Optional location name
                address: Optional location address
                reply_to_message_id: Message ID to reply to
            
            Returns:
                Dictionary with success status and message ID
            """
            try:
                result = wa_client.send_location(
                    to=to,
                    latitude=latitude,
                    longitude=longitude,
                    name=name,
                    address=address,
                    reply_to_message_id=reply_to_message_id,
                )
                
                logger.info(f"Location sent to {to}")
                message_id = getattr(result, 'id', str(result)) if result else None
                return {"success": True, "message_id": message_id}
            except Exception as e:
                logger.error(f"Failed to send location: {str(e)}")
                return {"success": False, "error": str(e)}
        
        
        @mcp.tool()
        async def request_location(
            to: str,
            text: str,
            *,
            reply_to_message_id: Optional[str] = None,
        ) -> dict:
            """
            Request user's location.
            
            Args:
                to: Phone number or WhatsApp ID
                text: Message text asking for location
                reply_to_message_id: Message ID to reply to
            
            Returns:
                Dictionary with success status and message ID
            """
            try:
                result = wa_client.request_location(
                    to=to,
                    text=text,
                    reply_to_message_id=reply_to_message_id,
                )
                
                logger.info(f"Location request sent to {to}")
                message_id = getattr(result, 'id', str(result)) if result else None
                return {"success": True, "message_id": message_id}
            except Exception as e:
                logger.error(f"Failed to request location: {str(e)}")
                return {"success": False, "error": str(e)}
        
        
        @mcp.tool()
        async def send_contact(
            to: str,
            contact_name: str,
            contact_phone: str,
            *,
            reply_to_message_id: Optional[str] = None,
        ) -> dict:
            """
            Send a contact card.
            
            Args:
                to: Phone number or WhatsApp ID
                contact_name: Name of the contact
                contact_phone: Phone number of the contact
                reply_to_message_id: Message ID to reply to
            
            Returns:
                Dictionary with success status and message ID
            """
            try:
                # Create Contact object
                contact = Contact(
                    name=Contact.Name(formatted_name=contact_name),
                    phones=[Contact.Phone(phone=contact_phone)]
                )
                
                result = wa_client.send_contact(
                    to=to,
                    contact=contact,
                    reply_to_message_id=reply_to_message_id,
                )
                
                logger.info(f"Contact sent to {to}")
                message_id = getattr(result, 'id', str(result)) if result else None
                return {"success": True, "message_id": message_id}
            except Exception as e:
                logger.error(f"Failed to send contact: {str(e)}")
                return {"success": False, "error": str(e)}
        
        
        @mcp.tool()
        async def send_reaction(
            to: str,
            emoji: str,
            message_id: str,
            *,
            sender: Optional[str] = None,
        ) -> dict:
            """
            Send a reaction to a message.
            
            Args:
                to: Phone number or WhatsApp ID
                emoji: Reaction emoji
                message_id: ID of message to react to
                sender: Optional sender phone ID
            
            Returns:
                Dictionary with success status
            """
            try:
                result = wa_client.send_reaction(
                    to=to,
                    emoji=emoji,
                    message_id=message_id,
                    sender=sender,
                )
                
                logger.info(f"Reaction sent to message {message_id}")
                result_data = str(result) if result else None
                return {"success": True, "result": result_data}
            except Exception as e:
                logger.error(f"Failed to send reaction: {str(e)}")
                return {"success": False, "error": str(e)}
        
        
        @mcp.tool()
        async def remove_reaction(
            to: str,
            message_id: str,
            *,
            sender: Optional[str] = None,
        ) -> dict:
            """
            Remove a reaction from a message.
            
            Args:
                to: Phone number or WhatsApp ID
                message_id: ID of message to remove reaction from
                sender: Optional sender phone ID
            
            Returns:
                Dictionary with success status
            """
            try:
                result = wa_client.remove_reaction(
                    to=to,
                    message_id=message_id,
                    sender=sender,
                )
                
                logger.info(f"Reaction removed from message {message_id}")
                result_data = str(result) if result else None
                return {"success": True, "result": result_data}
            except Exception as e:
                logger.error(f"Failed to remove reaction: {str(e)}")
                return {"success": False, "error": str(e)}
        
        
        @mcp.tool()
        async def mark_message_as_read(
            message_id: str,
            *,
            sender: Optional[str] = None,
        ) -> dict:
            """
            Mark a message as read.
            
            Args:
                message_id: The WhatsApp message ID to mark as read
                sender: Optional phone ID
            
            Returns:
                Dictionary with success status
            """
            try:
                result = wa_client.mark_message_as_read(
                    message_id=message_id,
                    sender=sender,
                )
                
                logger.info(f"Message {message_id} marked as read")
                result_data = str(result) if result else None
                return {"success": True, "result": result_data}
            except Exception as e:
                logger.error(f"Failed to mark message as read: {str(e)}")
                return {"success": False, "error": str(e)}
        
        
        @mcp.tool()
        async def indicate_typing(
            message_id: str,
            *,
            sender: Optional[str] = None,
        ) -> dict:
            """
            Show typing indicator to WhatsApp user.
            
            This marks a message as read and displays a typing indicator to show
            the user that you are preparing a response. Best practice when it will
            take a few seconds to respond.
            
            IMPORTANT NOTES:
            - Typing indicator lasts max 25 seconds or until you send a message
            - Only use if you are actually going to respond  
            - Will be dismissed when you send the next message
            - Improves user experience for delayed responses
            
            EXAMPLE:
            {
              "message_id": "wamid.HBgNMjc2NTY4NjY5MDUVAgARGBI5QTNDMEM3RjVBMzY2Q0Y4AA=="
            }
            
            Args:
                message_id: The WhatsApp message ID to respond to (from incoming message)
                sender: Optional phone ID (defaults to client's phone ID)
            
            Returns:
                Dictionary with success status
            """
            try:
                result = wa_client.indicate_typing(
                    message_id=message_id,
                    sender=sender,
                )
                
                logger.info(f"Typing indicator shown for message {message_id}")
                # SuccessResult has a success boolean attribute
                success_status = getattr(result, 'success', bool(result))
                
                return {
                    "success": True, 
                    "typing_indicated": success_status,
                    "message_id": message_id
                }
            except Exception as e:
                logger.error(f"Failed to indicate typing: {str(e)}")
                return {"success": False, "error": str(e)}
        
        
        @mcp.tool()
        async def upload_media(
            media_path: str,
            *,
            mime_type: Optional[str] = None,
        ) -> dict:
            """
            Upload media file to WhatsApp servers.
            
            Args:
                media_path: Path to media file
                mime_type: Optional MIME type
            
            Returns:
                Dictionary with media ID
            """
            try:
                media_obj = wa_client.upload_media(
                    media=media_path,
                    mime_type=mime_type,
                )
                
                logger.info("Media uploaded successfully")
                return {
                    "success": True, 
                    "media_id": str(media_obj),
                    "media_type": type(media_obj).__name__
                }
            except Exception as e:
                logger.error(f"Failed to upload media: {str(e)}")
                return {"success": False, "error": str(e)}
  • The register_all_tools function that orchestrates tool registration, calling register_messaging_tools which in turn registers the send_message tool via the @mcp.tool() decorator.
    def register_all_tools(mcp, wa_client):
        """Register all available tools with the MCP server."""
        register_messaging_tools(mcp, wa_client)
        register_interactive_tools(mcp, wa_client)
        register_template_tools(mcp, wa_client)
Behavior3/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations provided, the description carries the full burden. It discloses the action is a 'send' operation (implying mutation/write) and describes the return format, which is good. However, it lacks details about authentication requirements, rate limits, error conditions, or whether the message is queued vs. sent immediately—important behavioral context for a messaging tool.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is efficiently structured: a clear purpose statement upfront, followed by well-organized parameter explanations in bullet-like format, and ending with return value information. Every sentence adds value with no redundancy or fluff.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the tool's complexity (6 parameters, mutation operation) and no annotations, the description does well by explaining all parameters and the return format. However, it lacks some behavioral context (like error handling or delivery guarantees) that would be helpful. The presence of an output schema reduces the need to fully document returns, but more operational context would improve completeness.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters5/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 0%, so the description must compensate fully. It provides clear semantic explanations for all 6 parameters beyond just their names: specifying format requirements ('Phone number with country code'), purpose ('for interactive messages'), defaults ('default False'), and relationships ('Message ID to reply to'). This adds significant value over the bare schema.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the specific action ('Send a text message') and target resource ('to a WhatsApp user'), distinguishing it from sibling tools like send_audio, send_image, or send_template which handle different message types. It's precise and unambiguous about its function.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description implies usage context for sending basic text messages, and the parameter explanations (e.g., 'for interactive messages') provide some situational guidance. However, it doesn't explicitly state when to use this versus alternatives like send_message_with_buttons or send_template, which would be helpful for sibling differentiation.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Jem-HR/pywa-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server