Skip to main content
Glama
taylorwilsdon

Google Workspace MCP Server - Control Gmail, Calendar, Docs, Sheets, Slides, Chat, Forms & Drive

get_gmail_message_content

Retrieve the complete content of a specific Gmail message, including subject, sender, and body text, using the message ID and user email.

Instructions

Retrieves the full content (subject, sender, plain text body) of a specific Gmail message.

Args:
    message_id (str): The unique ID of the Gmail message to retrieve.
    user_google_email (str): The user's Google email address. Required.

Returns:
    str: The message details including subject, sender, and body content.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
message_idYes
serviceYes
user_google_emailYes

Implementation Reference

  • The primary handler function for the 'get_gmail_message_content' tool. It fetches message metadata and full payload using Gmail API, extracts headers, bodies (text/HTML), and attachments, then formats and returns the message content.
    @server.tool()
    @handle_http_errors("get_gmail_message_content", is_read_only=True, service_type="gmail")
    @require_google_service("gmail", "gmail_read")
    async def get_gmail_message_content(
        service, message_id: str, user_google_email: str
    ) -> str:
        """
        Retrieves the full content (subject, sender, recipients, plain text body) of a specific Gmail message.
    
        Args:
            message_id (str): The unique ID of the Gmail message to retrieve.
            user_google_email (str): The user's Google email address. Required.
    
        Returns:
            str: The message details including subject, sender, recipients (To, Cc), and body content.
        """
        logger.info(
            f"[get_gmail_message_content] Invoked. Message ID: '{message_id}', Email: '{user_google_email}'"
        )
    
        logger.info(f"[get_gmail_message_content] Using service for: {user_google_email}")
    
        # Fetch message metadata first to get headers
        message_metadata = await asyncio.to_thread(
            service.users()
            .messages()
            .get(
                userId="me",
                id=message_id,
                format="metadata",
                metadataHeaders=["Subject", "From", "To", "Cc"],
            )
            .execute
        )
    
        headers = {
            h["name"]: h["value"]
            for h in message_metadata.get("payload", {}).get("headers", [])
        }
        subject = headers.get("Subject", "(no subject)")
        sender = headers.get("From", "(unknown sender)")
        to = headers.get("To", "")
        cc = headers.get("Cc", "")
    
        # Now fetch the full message to get the body parts
        message_full = await asyncio.to_thread(
            service.users()
            .messages()
            .get(
                userId="me",
                id=message_id,
                format="full",  # Request full payload for body
            )
            .execute
        )
    
        # Extract both text and HTML bodies using enhanced helper function
        payload = message_full.get("payload", {})
        bodies = _extract_message_bodies(payload)
        text_body = bodies.get("text", "")
        html_body = bodies.get("html", "")
    
        # Format body content with HTML fallback
        body_data = _format_body_content(text_body, html_body)
    
        # Extract attachment metadata
        attachments = _extract_attachments(payload)
    
        content_lines = [
            f"Subject: {subject}",
            f"From:    {sender}",
        ]
    
        if to:
            content_lines.append(f"To:      {to}")
        if cc:
            content_lines.append(f"Cc:      {cc}")
    
        content_lines.append(f"\n--- BODY ---\n{body_data or '[No text/plain body found]'}")
    
        # Add attachment information if present
        if attachments:
            content_lines.append("\n--- ATTACHMENTS ---")
            for i, att in enumerate(attachments, 1):
                size_kb = att['size'] / 1024
                content_lines.append(
                    f"{i}. {att['filename']} ({att['mimeType']}, {size_kb:.1f} KB)\n"
                    f"   Attachment ID: {att['attachmentId']}\n"
                    f"   Use get_gmail_attachment_content(message_id='{message_id}', attachment_id='{att['attachmentId']}') to download"
                )
    
        return "\n".join(content_lines)
  • Key helper function used by the handler to extract plain text and HTML bodies from the Gmail message payload by traversing MIME parts.
    def _extract_message_bodies(payload):
        """
        Helper function to extract both plain text and HTML bodies from a Gmail message payload.
    
        Args:
            payload (dict): The message payload from Gmail API
    
        Returns:
            dict: Dictionary with 'text' and 'html' keys containing body content
        """
        text_body = ""
        html_body = ""
        parts = [payload] if "parts" not in payload else payload.get("parts", [])
    
        part_queue = list(parts)  # Use a queue for BFS traversal of parts
        while part_queue:
            part = part_queue.pop(0)
            mime_type = part.get("mimeType", "")
            body_data = part.get("body", {}).get("data")
    
            if body_data:
                try:
                    decoded_data = base64.urlsafe_b64decode(body_data).decode("utf-8", errors="ignore")
                    if mime_type == "text/plain" and not text_body:
                        text_body = decoded_data
                    elif mime_type == "text/html" and not html_body:
                        html_body = decoded_data
                except Exception as e:
                    logger.warning(f"Failed to decode body part: {e}")
    
            # Add sub-parts to queue for multipart messages
            if mime_type.startswith("multipart/") and "parts" in part:
                part_queue.extend(part.get("parts", []))
    
        # Check the main payload if it has body data directly
        if payload.get("body", {}).get("data"):
            try:
                decoded_data = base64.urlsafe_b64decode(payload["body"]["data"]).decode("utf-8", errors="ignore")
                mime_type = payload.get("mimeType", "")
                if mime_type == "text/plain" and not text_body:
                    text_body = decoded_data
                elif mime_type == "text/html" and not html_body:
                    html_body = decoded_data
            except Exception as e:
                logger.warning(f"Failed to decode main payload body: {e}")
    
        return {
            "text": text_body,
            "html": html_body
        }
  • Helper function used to format the extracted body content, preferring plain text, falling back to HTML (truncated if too long).
    def _format_body_content(text_body: str, html_body: str) -> str:
        """
        Helper function to format message body content with HTML fallback and truncation.
    
        Args:
            text_body: Plain text body content
            html_body: HTML body content
    
        Returns:
            Formatted body content string
        """
        if text_body.strip():
            return text_body
        elif html_body.strip():
            # Truncate very large HTML to keep responses manageable
            if len(html_body) > HTML_BODY_TRUNCATE_LIMIT:
                html_body = html_body[:HTML_BODY_TRUNCATE_LIMIT] + "\n\n[HTML content truncated...]"
            return f"[HTML Content Converted]\n{html_body}"
        else:
            return "[No readable content found]"
  • Helper function to recursively extract attachment metadata (filename, MIME type, size, attachment ID) from the message payload.
    def _extract_attachments(payload: dict) -> List[Dict[str, Any]]:
        """
        Extract attachment metadata from a Gmail message payload.
    
        Args:
            payload: The message payload from Gmail API
    
        Returns:
            List of attachment dictionaries with filename, mimeType, size, and attachmentId
        """
        attachments = []
    
        def search_parts(part):
            """Recursively search for attachments in message parts"""
            # Check if this part is an attachment
            if part.get("filename") and part.get("body", {}).get("attachmentId"):
                attachments.append({
                    "filename": part["filename"],
                    "mimeType": part.get("mimeType", "application/octet-stream"),
                    "size": part.get("body", {}).get("size", 0),
                    "attachmentId": part["body"]["attachmentId"]
                })
    
            # Recursively search sub-parts
            if "parts" in part:
                for subpart in part["parts"]:
                    search_parts(subpart)
    
        # Start searching from the root payload
        search_parts(payload)
        return attachments
  • The @server.tool() decorator registers this function as an MCP tool named 'get_gmail_message_content', with additional decorators for error handling and auth.
    @server.tool()
Behavior3/5

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

With no annotations provided, the description carries full burden. It discloses the read operation nature ('retrieves') and output format ('message details including subject, sender, and body content'), but doesn't mention authentication requirements, rate limits, error conditions, or whether it returns HTML vs plain text. It adds some behavioral context but leaves gaps for a tool accessing user data.

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 perfectly structured with a clear purpose statement followed by Args and Returns sections. Every sentence earns its place - the first sentence states purpose, the Args section documents parameters, and Returns section describes output. No wasted words or redundancy.

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

Completeness3/5

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

For a 3-parameter tool with no annotations and no output schema, the description is moderately complete. It covers purpose, 2/3 parameters, and return format, but leaves the 'service' parameter unexplained and lacks behavioral details like authentication, error handling, or content limitations. Given the complexity of accessing user email data, more context would be beneficial.

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

Parameters3/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. It documents 2 of 3 parameters (message_id and user_google_email) with basic semantics, but omits the 'service' parameter entirely. The description adds meaning for documented parameters but fails to address the undocumented third parameter, resulting in incomplete coverage.

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 ('retrieves'), resource ('full content of a specific Gmail message'), and scope ('subject, sender, plain text body'). It distinguishes from siblings like get_gmail_messages_content_batch (batch retrieval) and get_gmail_thread_content (thread-level).

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 by specifying 'specific Gmail message' and listing required parameters, but doesn't explicitly state when to use this vs alternatives like get_gmail_messages_content_batch or search_gmail_messages. It provides clear prerequisites (message_id and user_google_email) but lacks explicit exclusions or comparison guidance.

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

Related 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/taylorwilsdon/google_workspace_mcp'

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