2025-05-12 17:22:58 -04:00
"""
Google Gmail MCP Tools
This module provides MCP tools for interacting with the Gmail API.
"""
2025-05-22 14:21:52 -04:00
2025-05-12 17:22:58 -04:00
import logging
import asyncio
import base64
2025-05-26 19:00:27 -04:00
from typing import Optional , List , Dict , Literal
2025-05-12 17:22:58 -04:00
2025-05-19 20:57:11 -07:00
from email . mime . text import MIMEText
2025-05-12 17:22:58 -04:00
from mcp import types
2025-05-24 10:43:55 -04:00
from fastapi import Body
2025-05-12 17:22:58 -04:00
from googleapiclient . errors import HttpError
2025-05-24 10:43:55 -04:00
from auth . google_auth import get_authenticated_google_service
2025-05-13 12:36:53 -04:00
2025-05-12 17:22:58 -04:00
from core . server import (
GMAIL_READONLY_SCOPE ,
GMAIL_SEND_SCOPE ,
2025-05-22 00:02:51 +05:30
GMAIL_COMPOSE_SCOPE ,
2025-05-22 14:21:52 -04:00
server ,
2025-05-12 17:22:58 -04:00
)
logger = logging . getLogger ( __name__ )
2025-05-22 14:21:52 -04:00
2025-05-24 00:08:19 +08:00
def _extract_message_body ( payload ) :
"""
Helper function to extract plain text body from a Gmail message payload.
Args:
payload (dict): The message payload from Gmail API
Returns:
str: The plain text body content, or empty string if not found
"""
body_data = " "
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 )
if part . get ( " mimeType " ) == " text/plain " and part . get ( " body " , { } ) . get ( " data " ) :
data = base64 . urlsafe_b64decode ( part [ " body " ] [ " data " ] )
body_data = data . decode ( " utf-8 " , errors = " ignore " )
break # Found plain text body
elif part . get ( " mimeType " , " " ) . startswith ( " multipart/ " ) and " parts " in part :
part_queue . extend ( part . get ( " parts " , [ ] ) ) # Add sub-parts to the queue
# If no plain text found, check the main payload body if it exists
if (
not body_data
and payload . get ( " mimeType " ) == " text/plain "
and payload . get ( " body " , { } ) . get ( " data " )
) :
data = base64 . urlsafe_b64decode ( payload [ " body " ] [ " data " ] )
body_data = data . decode ( " utf-8 " , errors = " ignore " )
return body_data
2025-05-26 19:00:27 -04:00
def _extract_headers ( payload : dict , header_names : List [ str ] ) - > Dict [ str , str ] :
"""
Extract specified headers from a Gmail message payload.
Args:
payload: The message payload from Gmail API
header_names: List of header names to extract
Returns:
Dict mapping header names to their values
"""
headers = { }
for header in payload . get ( " headers " , [ ] ) :
if header [ " name " ] in header_names :
headers [ header [ " name " ] ] = header [ " value " ]
return headers
2025-05-25 17:35:58 +08:00
def _generate_gmail_web_url ( item_id : str , account_index : int = 0 ) - > str :
"""
Generate Gmail web interface URL for a message or thread ID.
Uses #all to access messages from any Gmail folder/label (not just inbox).
Args:
item_id: Gmail message ID or thread ID
account_index: Google account index (default 0 for primary account)
Returns:
Gmail web interface URL that opens the message/thread in Gmail web interface
"""
return f " https://mail.google.com/mail/u/ { account_index } /#all/ { item_id } "
2025-05-26 17:51:34 -04:00
def _format_gmail_results_plain ( messages : list , query : str ) - > str :
""" Format Gmail search results in clean, LLM-friendly plain text. """
if not messages :
return f " No messages found for query: ' { query } ' "
lines = [
f " Found { len ( messages ) } messages matching ' { query } ' : " ,
" " ,
" 📧 MESSAGES: " ,
]
for i , msg in enumerate ( messages , 1 ) :
message_url = _generate_gmail_web_url ( msg [ " id " ] )
thread_url = _generate_gmail_web_url ( msg [ " threadId " ] )
lines . extend ( [
f " { i } . Message ID: { msg [ ' id ' ] } " ,
f " Web Link: { message_url } " ,
f " Thread ID: { msg [ ' threadId ' ] } " ,
f " Thread Link: { thread_url } " ,
" "
] )
lines . extend ( [
" 💡 USAGE: " ,
2025-05-26 19:00:27 -04:00
" • Pass the Message IDs **as a list** to get_gmail_messages_content_batch() " ,
" e.g. get_gmail_messages_content_batch(message_ids=[...]) " ,
" • Pass the Thread IDs to get_gmail_thread_content() (single) _or_ " ,
" get_gmail_threads_content_batch() (coming soon) "
2025-05-26 17:51:34 -04:00
] )
return " \n " . join ( lines )
2025-05-12 17:22:58 -04:00
@server.tool ( )
2025-05-20 10:43:47 -04:00
async def search_gmail_messages (
2025-05-12 17:22:58 -04:00
query : str ,
2025-05-24 10:43:55 -04:00
user_google_email : str ,
2025-05-12 17:22:58 -04:00
page_size : int = 10 ,
) - > types . CallToolResult :
"""
Searches messages in a user ' s Gmail account based on a query.
2025-05-25 17:35:58 +08:00
Returns both Message IDs and Thread IDs for each found message, along with Gmail web interface links for manual verification.
2025-05-12 17:22:58 -04:00
Args:
query (str): The search query. Supports standard Gmail search operators.
2025-05-24 10:43:55 -04:00
user_google_email (str): The user ' s Google email address. Required.
2025-05-12 17:22:58 -04:00
page_size (int): The maximum number of messages to return. Defaults to 10.
Returns:
2025-05-26 17:51:34 -04:00
types.CallToolResult: Contains LLM-friendly structured results with Message IDs, Thread IDs, and clickable Gmail web interface URLs for each found message, or an error/auth guidance message.
2025-05-12 17:22:58 -04:00
"""
tool_name = " search_gmail_messages "
2025-05-22 14:21:52 -04:00
logger . info (
2025-05-24 10:43:55 -04:00
f " [ { tool_name } ] Invoked. Email: ' { user_google_email } ' , Query: ' { query } ' "
2025-05-22 14:21:52 -04:00
)
2025-05-13 12:36:53 -04:00
2025-05-24 10:43:55 -04:00
auth_result = await get_authenticated_google_service (
service_name = " gmail " ,
version = " v1 " ,
tool_name = tool_name ,
2025-05-13 12:36:53 -04:00
user_google_email = user_google_email ,
2025-05-20 10:43:47 -04:00
required_scopes = [ GMAIL_READONLY_SCOPE ] ,
2025-05-13 12:36:53 -04:00
)
2025-05-24 10:43:55 -04:00
if isinstance ( auth_result , types . CallToolResult ) :
2025-06-03 14:16:19 -04:00
return auth_result
2025-05-24 10:43:55 -04:00
service , user_email = auth_result
2025-05-12 17:22:58 -04:00
try :
response = await asyncio . to_thread (
2025-05-22 14:21:52 -04:00
service . users ( )
. messages ( )
. list ( userId = " me " , q = query , maxResults = page_size )
. execute
2025-05-12 17:22:58 -04:00
)
2025-05-22 14:21:52 -04:00
messages = response . get ( " messages " , [ ] )
2025-05-26 17:51:34 -04:00
formatted_output = _format_gmail_results_plain ( messages , query )
2025-05-25 17:35:58 +08:00
2025-05-22 14:21:52 -04:00
return types . CallToolResult (
2025-05-26 17:51:34 -04:00
content = [ types . TextContent ( type = " text " , text = formatted_output ) ]
2025-05-22 14:21:52 -04:00
)
2025-05-12 17:22:58 -04:00
except HttpError as e :
2025-05-22 14:21:52 -04:00
logger . error (
f " [ { tool_name } ] Gmail API error searching messages: { e } " , exc_info = True
)
return types . CallToolResult (
isError = True ,
content = [ types . TextContent ( type = " text " , text = f " Gmail API error: { e } " ) ] ,
)
2025-05-12 17:22:58 -04:00
except Exception as e :
2025-05-22 14:21:52 -04:00
logger . exception (
f " [ { tool_name } ] Unexpected error searching Gmail messages: { e } "
)
return types . CallToolResult (
isError = True ,
content = [ types . TextContent ( type = " text " , text = f " Unexpected error: { e } " ) ] ,
)
2025-05-12 17:22:58 -04:00
@server.tool ( )
2025-05-20 10:43:47 -04:00
async def get_gmail_message_content (
2025-05-12 17:22:58 -04:00
message_id : str ,
2025-05-24 10:43:55 -04:00
user_google_email : str ,
2025-05-12 17:22:58 -04:00
) - > types . CallToolResult :
"""
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.
2025-05-24 10:43:55 -04:00
user_google_email (str): The user ' s Google email address. Required.
2025-05-12 17:22:58 -04:00
Returns:
types.CallToolResult: Contains the message details or an error/auth guidance message.
"""
tool_name = " get_gmail_message_content "
2025-05-22 14:21:52 -04:00
logger . info (
2025-05-24 10:43:55 -04:00
f " [ { tool_name } ] Invoked. Message ID: ' { message_id } ' , Email: ' { user_google_email } ' "
2025-05-22 14:21:52 -04:00
)
2025-05-13 12:36:53 -04:00
2025-05-24 10:43:55 -04:00
auth_result = await get_authenticated_google_service (
service_name = " gmail " ,
version = " v1 " ,
tool_name = tool_name ,
2025-05-13 12:36:53 -04:00
user_google_email = user_google_email ,
2025-05-20 10:43:47 -04:00
required_scopes = [ GMAIL_READONLY_SCOPE ] ,
2025-05-13 12:36:53 -04:00
)
2025-05-24 10:43:55 -04:00
if isinstance ( auth_result , types . CallToolResult ) :
2025-06-04 18:48:17 -04:00
return auth_result
2025-05-24 10:43:55 -04:00
service , user_email = auth_result
2025-05-12 17:22:58 -04:00
try :
2025-05-24 10:43:55 -04:00
logger . info ( f " [ { tool_name } ] Using service for: { user_google_email } " )
2025-05-12 17:22:58 -04:00
# Fetch message metadata first to get headers
message_metadata = await asyncio . to_thread (
2025-05-22 14:21:52 -04:00
service . users ( )
. messages ( )
. get (
userId = " me " ,
2025-05-12 17:22:58 -04:00
id = message_id ,
2025-05-22 14:21:52 -04:00
format = " metadata " ,
metadataHeaders = [ " Subject " , " From " ] ,
)
. execute
2025-05-12 17:22:58 -04:00
)
2025-05-22 14:21:52 -04:00
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) " )
2025-05-12 17:22:58 -04:00
# Now fetch the full message to get the body parts
message_full = await asyncio . to_thread (
2025-05-22 14:21:52 -04:00
service . users ( )
. messages ( )
. get (
userId = " me " ,
2025-05-12 17:22:58 -04:00
id = message_id ,
2025-05-22 14:21:52 -04:00
format = " full " , # Request full payload for body
)
. execute
2025-05-12 17:22:58 -04:00
)
2025-05-24 00:08:19 +08:00
# Extract the plain text body using helper function
2025-05-22 14:21:52 -04:00
payload = message_full . get ( " payload " , { } )
2025-05-24 00:08:19 +08:00
body_data = _extract_message_body ( payload )
2025-05-22 14:21:52 -04:00
content_text = " \n " . join (
[
f " Subject: { subject } " ,
f " From: { sender } " ,
f " \n --- BODY --- \n { body_data or ' [No text/plain body found] ' } " ,
]
)
return types . CallToolResult (
content = [ types . TextContent ( type = " text " , text = content_text ) ]
)
2025-05-12 17:22:58 -04:00
except HttpError as e :
2025-05-22 14:21:52 -04:00
logger . error (
f " [ { tool_name } ] Gmail API error getting message content: { e } " , exc_info = True
)
return types . CallToolResult (
isError = True ,
content = [ types . TextContent ( type = " text " , text = f " Gmail API error: { e } " ) ] ,
)
2025-05-12 17:22:58 -04:00
except Exception as e :
2025-05-22 14:21:52 -04:00
logger . exception (
f " [ { tool_name } ] Unexpected error getting Gmail message content: { e } "
)
return types . CallToolResult (
isError = True ,
content = [ types . TextContent ( type = " text " , text = f " Unexpected error: { e } " ) ] ,
2025-05-26 19:00:27 -04:00
)
@server.tool ( )
async def get_gmail_messages_content_batch (
message_ids : List [ str ] ,
user_google_email : str ,
format : Literal [ " full " , " metadata " ] = " full " ,
) - > types . CallToolResult :
"""
Retrieves the content of multiple Gmail messages in a single batch request.
Supports up to 100 messages per request using Google ' s batch API.
Args:
message_ids (List[str]): List of Gmail message IDs to retrieve (max 100).
user_google_email (str): The user ' s Google email address. Required.
format (Literal[ " full " , " metadata " ]): Message format. " full " includes body, " metadata " only headers.
Returns:
types.CallToolResult: Contains a list of message contents or error details.
"""
tool_name = " get_gmail_messages_content_batch "
logger . info (
f " [ { tool_name } ] Invoked. Message count: { len ( message_ids ) } , Email: ' { user_google_email } ' "
)
if not message_ids :
return types . CallToolResult (
content = [ types . TextContent ( type = " text " , text = " No message IDs provided " ) ]
)
auth_result = await get_authenticated_google_service (
service_name = " gmail " ,
version = " v1 " ,
tool_name = tool_name ,
user_google_email = user_google_email ,
required_scopes = [ GMAIL_READONLY_SCOPE ] ,
)
if isinstance ( auth_result , types . CallToolResult ) :
2025-06-04 18:48:17 -04:00
return auth_result
2025-05-26 19:00:27 -04:00
service , user_email = auth_result
try :
output_messages = [ ]
# Process in chunks of 100 (Gmail batch limit)
for chunk_start in range ( 0 , len ( message_ids ) , 100 ) :
chunk_ids = message_ids [ chunk_start : chunk_start + 100 ]
results : Dict [ str , Dict ] = { }
def _batch_callback ( request_id , response , exception ) :
""" Callback for batch requests """
results [ request_id ] = { " data " : response , " error " : exception }
# Try to use batch API
try :
batch = service . new_batch_http_request ( callback = _batch_callback )
for mid in chunk_ids :
if format == " metadata " :
req = service . users ( ) . messages ( ) . get (
userId = " me " ,
id = mid ,
format = " metadata " ,
metadataHeaders = [ " Subject " , " From " ]
)
else :
req = service . users ( ) . messages ( ) . get (
userId = " me " ,
id = mid ,
format = " full "
)
batch . add ( req , request_id = mid )
# Execute batch request
await asyncio . to_thread ( batch . execute )
except Exception as batch_error :
# Fallback to asyncio.gather if batch API fails
logger . warning (
f " [ { tool_name } ] Batch API failed, falling back to asyncio.gather: { batch_error } "
)
async def fetch_message ( mid : str ) :
try :
if format == " metadata " :
msg = await asyncio . to_thread (
service . users ( ) . messages ( ) . get (
userId = " me " ,
id = mid ,
format = " metadata " ,
metadataHeaders = [ " Subject " , " From " ]
) . execute
)
else :
msg = await asyncio . to_thread (
service . users ( ) . messages ( ) . get (
userId = " me " ,
id = mid ,
format = " full "
) . execute
)
return mid , msg , None
except Exception as e :
return mid , None , e
# Fetch all messages in parallel
fetch_results = await asyncio . gather (
* [ fetch_message ( mid ) for mid in chunk_ids ] ,
return_exceptions = False
)
# Convert to results format
for mid , msg , error in fetch_results :
results [ mid ] = { " data " : msg , " error " : error }
# Process results for this chunk
for mid in chunk_ids :
entry = results . get ( mid , { " data " : None , " error " : " No result " } )
if entry [ " error " ] :
output_messages . append (
f " ⚠️ Message { mid } : { entry [ ' error ' ] } \n "
)
else :
message = entry [ " data " ]
if not message :
output_messages . append (
f " ⚠️ Message { mid } : No data returned \n "
)
continue
# Extract content based on format
payload = message . get ( " payload " , { } )
if format == " metadata " :
headers = _extract_headers ( payload , [ " Subject " , " From " ] )
subject = headers . get ( " Subject " , " (no subject) " )
sender = headers . get ( " From " , " (unknown sender) " )
output_messages . append (
f " Message ID: { mid } \n "
f " Subject: { subject } \n "
f " From: { sender } \n "
f " Web Link: { _generate_gmail_web_url ( mid ) } \n "
)
else :
# Full format - extract body too
headers = _extract_headers ( payload , [ " Subject " , " From " ] )
subject = headers . get ( " Subject " , " (no subject) " )
sender = headers . get ( " From " , " (unknown sender) " )
body = _extract_message_body ( payload )
output_messages . append (
f " Message ID: { mid } \n "
f " Subject: { subject } \n "
f " From: { sender } \n "
f " Web Link: { _generate_gmail_web_url ( mid ) } \n "
f " \n { body or ' [No text/plain body found] ' } \n "
)
# Combine all messages with separators
final_output = f " Retrieved { len ( message_ids ) } messages: \n \n "
final_output + = " \n --- \n \n " . join ( output_messages )
return types . CallToolResult (
content = [ types . TextContent ( type = " text " , text = final_output ) ]
)
except HttpError as e :
logger . error (
f " [ { tool_name } ] Gmail API error in batch retrieval: { e } " , exc_info = True
)
return types . CallToolResult (
isError = True ,
content = [ types . TextContent ( type = " text " , text = f " Gmail API error: { e } " ) ] ,
)
except Exception as e :
logger . exception (
f " [ { tool_name } ] Unexpected error in batch retrieval: { e } "
)
return types . CallToolResult (
isError = True ,
content = [ types . TextContent ( type = " text " , text = f " Unexpected error: { e } " ) ] ,
2025-05-22 14:21:52 -04:00
)
2025-05-12 17:22:58 -04:00
2025-05-19 20:57:11 -07:00
@server.tool ( )
async def send_gmail_message (
2025-05-24 10:43:55 -04:00
user_google_email : str ,
2025-05-19 20:57:11 -07:00
to : str = Body ( . . . , description = " Recipient email address. " ) ,
subject : str = Body ( . . . , description = " Email subject. " ) ,
body : str = Body ( . . . , description = " Email body (plain text). " ) ,
) - > types . CallToolResult :
"""
Sends an email using the user ' s Gmail account.
Args:
to (str): Recipient email address.
subject (str): Email subject.
body (str): Email body (plain text).
2025-05-24 10:43:55 -04:00
user_google_email (str): The user ' s Google email address. Required.
2025-05-19 20:57:11 -07:00
Returns:
types.CallToolResult: Contains the message ID of the sent email, or an error/auth guidance message.
"""
tool_name = " send_gmail_message "
2025-05-24 10:43:55 -04:00
auth_result = await get_authenticated_google_service (
service_name = " gmail " ,
version = " v1 " ,
tool_name = tool_name ,
user_google_email = user_google_email ,
required_scopes = [ GMAIL_SEND_SCOPE ] ,
)
if isinstance ( auth_result , types . CallToolResult ) :
2025-06-04 18:48:17 -04:00
return auth_result
2025-05-24 10:43:55 -04:00
service , user_email = auth_result
try :
2025-05-19 20:57:11 -07:00
# Prepare the email
message = MIMEText ( body )
message [ " to " ] = to
message [ " subject " ] = subject
raw_message = base64 . urlsafe_b64encode ( message . as_bytes ( ) ) . decode ( )
send_body = { " raw " : raw_message }
# Send the message
sent_message = await asyncio . to_thread (
service . users ( ) . messages ( ) . send ( userId = " me " , body = send_body ) . execute
)
message_id = sent_message . get ( " id " )
2025-05-22 14:21:52 -04:00
return types . CallToolResult (
content = [
types . TextContent (
type = " text " , text = f " Email sent! Message ID: { message_id } "
)
]
)
2025-05-19 20:57:11 -07:00
except HttpError as e :
2025-05-22 14:21:52 -04:00
logger . error (
f " [ { tool_name } ] Gmail API error sending message: { e } " , exc_info = True
)
return types . CallToolResult (
isError = True ,
content = [ types . TextContent ( type = " text " , text = f " Gmail API error: { e } " ) ] ,
)
2025-05-19 20:57:11 -07:00
except Exception as e :
logger . exception ( f " [ { tool_name } ] Unexpected error sending Gmail message: { e } " )
2025-05-22 14:21:52 -04:00
return types . CallToolResult (
isError = True ,
content = [ types . TextContent ( type = " text " , text = f " Unexpected error: { e } " ) ] ,
)
2025-05-22 00:02:51 +05:30
@server.tool ( )
async def draft_gmail_message (
2025-05-24 10:43:55 -04:00
user_google_email : str ,
2025-05-22 00:02:51 +05:30
subject : str = Body ( . . . , description = " Email subject. " ) ,
body : str = Body ( . . . , description = " Email body (plain text). " ) ,
to : Optional [ str ] = Body ( None , description = " Optional recipient email address. " ) ,
) - > types . CallToolResult :
"""
Creates a draft email in the user ' s Gmail account.
Args:
2025-05-24 10:43:55 -04:00
user_google_email (str): The user ' s Google email address. Required.
2025-05-22 00:02:51 +05:30
subject (str): Email subject.
body (str): Email body (plain text).
to (Optional[str]): Optional recipient email address. Can be left empty for drafts.
Returns:
types.CallToolResult: Contains the draft ID of the created email, or an error/auth guidance message.
"""
tool_name = " draft_gmail_message "
2025-05-22 14:21:52 -04:00
logger . info (
2025-05-24 10:43:55 -04:00
f " [ { tool_name } ] Invoked. Email: ' { user_google_email } ' , Subject: ' { subject } ' "
2025-05-22 14:21:52 -04:00
)
2025-05-24 10:43:55 -04:00
auth_result = await get_authenticated_google_service (
service_name = " gmail " ,
version = " v1 " ,
tool_name = tool_name ,
2025-05-22 14:21:52 -04:00
user_google_email = user_google_email ,
required_scopes = [ GMAIL_COMPOSE_SCOPE ] ,
)
2025-05-24 10:43:55 -04:00
if isinstance ( auth_result , types . CallToolResult ) :
2025-06-04 18:48:17 -04:00
return auth_result
2025-05-24 10:43:55 -04:00
service , user_email = auth_result
2025-05-22 00:02:51 +05:30
2025-05-22 14:21:52 -04:00
try :
2025-05-22 00:02:51 +05:30
# Prepare the email
message = MIMEText ( body )
message [ " subject " ] = subject
2025-05-22 14:21:52 -04:00
2025-05-22 00:02:51 +05:30
# Add recipient if provided
if to :
message [ " to " ] = to
2025-05-22 14:21:52 -04:00
2025-05-22 00:02:51 +05:30
raw_message = base64 . urlsafe_b64encode ( message . as_bytes ( ) ) . decode ( )
2025-05-22 14:21:52 -04:00
2025-05-22 00:02:51 +05:30
# Create a draft instead of sending
2025-05-22 14:21:52 -04:00
draft_body = { " message " : { " raw " : raw_message } }
2025-05-22 00:02:51 +05:30
# Create the draft
created_draft = await asyncio . to_thread (
service . users ( ) . drafts ( ) . create ( userId = " me " , body = draft_body ) . execute
)
draft_id = created_draft . get ( " id " )
2025-05-22 14:21:52 -04:00
return types . CallToolResult (
content = [
types . TextContent (
type = " text " , text = f " Draft created! Draft ID: { draft_id } "
)
]
)
2025-05-22 00:02:51 +05:30
except HttpError as e :
2025-05-22 14:21:52 -04:00
logger . error (
f " [ { tool_name } ] Gmail API error creating draft: { e } " , exc_info = True
)
return types . CallToolResult (
isError = True ,
content = [ types . TextContent ( type = " text " , text = f " Gmail API error: { e } " ) ] ,
)
2025-05-24 00:08:19 +08:00
except Exception as e :
logger . exception ( f " [ { tool_name } ] Unexpected error creating Gmail draft: { e } " )
return types . CallToolResult (
isError = True ,
content = [ types . TextContent ( type = " text " , text = f " Unexpected error: { e } " ) ] ,
)
@server.tool ( )
async def get_gmail_thread_content (
thread_id : str ,
2025-05-24 10:43:55 -04:00
user_google_email : str ,
2025-05-24 00:08:19 +08:00
) - > types . CallToolResult :
"""
Retrieves the complete content of a Gmail conversation thread, including all messages.
Args:
thread_id (str): The unique ID of the Gmail thread to retrieve.
2025-05-24 10:43:55 -04:00
user_google_email (str): The user ' s Google email address. Required.
2025-05-24 00:08:19 +08:00
Returns:
types.CallToolResult: Contains the complete thread content with all messages or an error/auth guidance message.
"""
tool_name = " get_gmail_thread_content "
logger . info (
2025-05-24 10:43:55 -04:00
f " [ { tool_name } ] Invoked. Thread ID: ' { thread_id } ' , Email: ' { user_google_email } ' "
2025-05-24 00:08:19 +08:00
)
2025-05-24 10:43:55 -04:00
auth_result = await get_authenticated_google_service (
service_name = " gmail " ,
version = " v1 " ,
tool_name = tool_name ,
2025-05-24 00:08:19 +08:00
user_google_email = user_google_email ,
required_scopes = [ GMAIL_READONLY_SCOPE ] ,
)
2025-05-24 10:43:55 -04:00
if isinstance ( auth_result , types . CallToolResult ) :
2025-06-04 18:48:17 -04:00
return auth_result
2025-05-24 10:43:55 -04:00
service , user_email = auth_result
2025-05-24 00:08:19 +08:00
try :
# Fetch the complete thread with all messages
thread_response = await asyncio . to_thread (
service . users ( )
. threads ( )
. get ( userId = " me " , id = thread_id , format = " full " )
. execute
)
messages = thread_response . get ( " messages " , [ ] )
if not messages :
return types . CallToolResult (
content = [
types . TextContent (
type = " text " , text = f " No messages found in thread ' { thread_id } ' . "
)
]
)
# Extract thread subject from the first message
first_message = messages [ 0 ]
first_headers = {
h [ " name " ] : h [ " value " ]
for h in first_message . get ( " payload " , { } ) . get ( " headers " , [ ] )
}
thread_subject = first_headers . get ( " Subject " , " (no subject) " )
# Build the thread content
content_lines = [
f " Thread ID: { thread_id } " ,
f " Subject: { thread_subject } " ,
f " Messages: { len ( messages ) } " ,
" " ,
]
# Process each message in the thread
for i , message in enumerate ( messages , 1 ) :
# Extract headers
headers = {
h [ " name " ] : h [ " value " ]
for h in message . get ( " payload " , { } ) . get ( " headers " , [ ] )
}
sender = headers . get ( " From " , " (unknown sender) " )
date = headers . get ( " Date " , " (unknown date) " )
subject = headers . get ( " Subject " , " (no subject) " )
# Extract message body
payload = message . get ( " payload " , { } )
body_data = _extract_message_body ( payload )
# Add message to content
content_lines . extend (
[
f " === Message { i } === " ,
f " From: { sender } " ,
f " Date: { date } " ,
]
)
# Only show subject if it's different from thread subject
if subject != thread_subject :
content_lines . append ( f " Subject: { subject } " )
content_lines . extend (
[
" " ,
body_data or " [No text/plain body found] " ,
" " ,
]
)
content_text = " \n " . join ( content_lines )
return types . CallToolResult (
content = [ types . TextContent ( type = " text " , text = content_text ) ]
)
except HttpError as e :
logger . error (
f " [ { tool_name } ] Gmail API error getting thread content: { e } " , exc_info = True
)
return types . CallToolResult (
isError = True ,
content = [ types . TextContent ( type = " text " , text = f " Gmail API error: { e } " ) ] ,
)
2025-05-22 00:02:51 +05:30
except Exception as e :
2025-05-22 14:21:52 -04:00
logger . exception (
2025-05-24 00:08:19 +08:00
f " [ { tool_name } ] Unexpected error getting Gmail thread content: { e } "
2025-05-22 14:21:52 -04:00
)
return types . CallToolResult (
isError = True ,
content = [ types . TextContent ( type = " text " , text = f " Unexpected error: { e } " ) ] ,
)