Gmail MCP
- gmail_mcp
#!/usr/bin/env python3
import argparse
import json
import os
import sys
from pathlib import Path
from typing import Any, Dict, List, Optional
from dotenv import load_dotenv
from mcp.server.fastmcp import FastMCP
from .gmail_client import GmailClient
# Find and load .env file
env_path = Path(__file__).parent.parent / ".env"
if env_path.exists():
load_dotenv(env_path)
else:
print(f"Warning: No .env file found at {env_path}", file=sys.stderr)
# Constants
DEFAULT_CREDS_PATH = "credentials.json"
DEFAULT_TOKEN_PATH = "token.json"
# Initialize FastMCP server
mcp = FastMCP("gmail")
# Initialize global client
gmail_client = None
def get_default_paths() -> Dict[str, str]:
"""Get default paths for credentials and token files.
Returns:
Dictionary containing default paths for credentials and token files
"""
current_dir = os.getcwd()
return {
"creds": os.path.join(current_dir, DEFAULT_CREDS_PATH),
"token": os.path.join(current_dir, DEFAULT_TOKEN_PATH),
}
def validate_path(file_path: str) -> Optional[Path]:
"""Validate and normalize a file path.
Args:
file_path: The path to validate
Returns:
Path object if valid, None if invalid
"""
try:
path = Path(file_path).resolve()
return path
except Exception:
return None
@mcp.tool()
async def search_emails(
search_type: str,
search_value: str,
max_results: int = 10,
page: int = 1,
) -> str:
"""Search for emails based on different criteria.
Args:
search_type: Type of search (keyword, to, from)
search_value: Value to search for
max_results: Maximum number of results to return per page
page: Page number (1-based)
"""
global gmail_client
if not gmail_client:
return "Error: Gmail client not initialized"
if not search_value:
return "Error: Missing search value"
try:
# Calculate offset based on page number
offset = (page - 1) * max_results
# Get all messages that match the query
all_messages = gmail_client.list_messages(query=search_value)
total_results = len(all_messages)
# Slice messages for current page
page_messages = all_messages[offset : offset + max_results]
# Get only metadata for the current page
results = []
for msg in page_messages:
msg_details = gmail_client.get_message(msg_id=msg["id"])
if msg_details:
# Only include metadata, not the full body
results.append(
{
"id": msg_details["id"],
"subject": msg_details["subject"],
"sender": msg_details["sender"],
"snippet": msg_details["snippet"],
}
)
has_next = total_results > offset + max_results
has_previous = page > 1
return json.dumps(
{
"message": f"Found {total_results} emails matching your search (showing page {page})",
"results": results,
"pagination": {
"current_page": page,
"total_pages": (total_results + max_results - 1) // max_results,
"has_next": has_next,
"has_previous": has_previous,
"total_results": total_results,
},
}
)
except Exception as e:
return f"Error: Failed to search emails - {str(e)}"
@mcp.tool()
async def get_email_content(msg_id: str) -> str:
"""Get the full content of a specific email.
Args:
msg_id: Message ID to retrieve
"""
global gmail_client
if not gmail_client:
return "Error: Gmail client not initialized"
if not msg_id:
return "Error: Missing message ID"
try:
message = gmail_client.get_message(msg_id=msg_id)
if not message:
return "Error: Could not retrieve message"
return json.dumps({"message": "Retrieved email content", "email": message})
except Exception as e:
return f"Error: Failed to get email content - {str(e)}"
@mcp.tool()
async def list_messages(query: str = "", max_results: int = 10) -> str:
"""List messages from Gmail inbox.
Args:
query: Query string to filter messages
max_results: Maximum number of results to return
"""
global gmail_client
if not gmail_client:
return "Error: Gmail client not initialized"
try:
messages = gmail_client.list_messages(query=query, max_results=max_results)
return json.dumps(
{"message": f"Retrieved {len(messages)} messages", "messages": messages}
)
except Exception as e:
return f"Error: Failed to list messages - {str(e)}"
def initialize_client(creds_path: str, token_path: str) -> None:
"""Initialize the Gmail client.
Args:
creds_path: Path to the credentials file
token_path: Path to the token file
"""
global gmail_client
try:
gmail_client = GmailClient.create(creds_path, token_path)
except Exception as e:
print(f"Error initializing Gmail client: {str(e)}", file=sys.stderr)
sys.exit(1)
def auth_command(creds_path: str, token_path: str) -> None:
"""Run the authorization flow.
Args:
creds_path: Path to the credentials file
token_path: Path to the token file
"""
try:
GmailClient.authorize(creds_path, token_path)
except Exception as e:
print(f"Error during authorization: {str(e)}", file=sys.stderr)
sys.exit(1)
def main():
parser = argparse.ArgumentParser(description="Gmail MCP CLI")
parser.add_argument(
"--creds-path",
help="Path to the credentials.json file",
default=os.environ.get("GMAIL_CREDS_PATH"),
)
parser.add_argument(
"--token-path",
help="Path to the token.json file",
default=os.environ.get("GMAIL_TOKEN_PATH"),
)
parser.add_argument(
"command",
nargs="?",
choices=["auth", "serve"],
default="serve",
help="Command to run (auth or serve)",
)
args = parser.parse_args()
# Get default paths if not provided
default_paths = get_default_paths()
creds_path = args.creds_path or default_paths["creds"]
token_path = args.token_path or default_paths["token"]
# Validate paths
if not validate_path(creds_path):
print(f"Error: Invalid credentials path: {creds_path}", file=sys.stderr)
sys.exit(1)
if not validate_path(token_path):
print(f"Error: Invalid token path: {token_path}", file=sys.stderr)
sys.exit(1)
if args.command == "auth":
auth_command(creds_path, token_path)
else: # serve
initialize_client(creds_path, token_path)
mcp.run(transport="stdio")
if __name__ == "__main__":
main()