unify authentication logic

This commit is contained in:
Taylor Wilsdon
2025-05-13 12:36:53 -04:00
parent 4e13196802
commit 0bebaee051
5 changed files with 320 additions and 450 deletions

View File

@@ -12,27 +12,33 @@ from typing import List, Optional, Any # Keep Any
from mcp import types
from fastapi import Header
# Remove unused Resource import
# from googleapiclient.discovery import Resource as GoogleApiServiceResource
from googleapiclient.discovery import build # Import build
from googleapiclient.errors import HttpError
# Import the decorator, config path, AND the context variable
from auth.auth_flow import require_google_auth, CONFIG_CLIENT_SECRETS_PATH, current_google_service
from core.server import server
# Use functions directly from google_auth
from auth.google_auth import get_credentials, start_auth_flow, CONFIG_CLIENT_SECRETS_PATH # Import get_credentials, start_auth_flow, CONFIG_CLIENT_SECRETS_PATH
# Remove imports from auth.auth_flow
# from auth.auth_flow import require_google_auth, CONFIG_CLIENT_SECRETS_PATH, current_google_service
from core.server import server, OAUTH_REDIRECT_URI # Import OAUTH_REDIRECT_URI
from core.server import (
GMAIL_READONLY_SCOPE,
GMAIL_SEND_SCOPE,
SCOPES # Import SCOPES for auth flow initiation
)
logger = logging.getLogger(__name__)
# CONFIG_CLIENT_SECRETS_PATH is now imported from auth.google_auth
@server.tool()
@require_google_auth(
required_scopes=[GMAIL_READONLY_SCOPE],
service_name="Gmail",
api_name="gmail",
api_version="v1"
)
# Remove the decorator
# @require_google_auth(
# required_scopes=[GMAIL_READONLY_SCOPE],
# service_name="Gmail",
# api_name="gmail",
# api_version="v1"
# )
async def search_gmail_messages( # Signature cleaned - no google_service param
query: str,
user_google_email: Optional[str] = None,
@@ -41,45 +47,49 @@ async def search_gmail_messages( # Signature cleaned - no google_service param
) -> types.CallToolResult:
"""
Searches messages in a user's Gmail account based on a query.
Authentication and service object are handled by the @require_google_auth decorator using context variables.
Authentication is handled by get_credentials and start_auth_flow.
Args:
query (str): The search query. Supports standard Gmail search operators.
user_google_email (Optional[str]): The user's Google email address (used for context/logging).
user_google_email (Optional[str]): The user's Google email address. Required if the MCP session is not already authenticated for Gmail access.
page_size (int): The maximum number of messages to return. Defaults to 10.
mcp_session_id (Optional[str]): The active MCP session ID (used for context/logging).
mcp_session_id (Optional[str]): The active MCP session ID (automatically injected by FastMCP from the Mcp-Session-Id header). Used for session-based authentication.
Returns:
types.CallToolResult: Contains a list of found message IDs or an error/auth guidance message.
"""
# *** Add logging and check here ***
tool_name = "search_gmail_messages"
logger.debug(f"[{tool_name}] Entered function. Attempting to get service from context var...")
google_service = current_google_service.get()
logger.debug(f"[{tool_name}] Service retrieved from context var. Type: {type(google_service)}, id: {id(google_service)}")
logger.info(f"[{tool_name}] Invoked. Session: '{mcp_session_id}', Email: '{user_google_email}', Query: '{query}'")
if not google_service:
logger.error(f"[{tool_name}] Google service retrieved from context is None!")
return types.CallToolResult(isError=True, content=[types.TextContent(type="text", text="Internal error: Google service unavailable in tool context.")])
# Use get_credentials to fetch credentials
credentials = await asyncio.to_thread(
get_credentials,
user_google_email=user_google_email,
required_scopes=[GMAIL_READONLY_SCOPE], # Specify required scopes for this tool
client_secrets_path=CONFIG_CLIENT_SECRETS_PATH, # Use imported constant
session_id=mcp_session_id
)
logger.info(f"[{tool_name}] Session: '{mcp_session_id}', Email: '{user_google_email}', Query: '{query}'")
try:
# More robust way to extract email from credentials
id_token = getattr(google_service._http.credentials, 'id_token', None)
if isinstance(id_token, dict) and 'email' in id_token:
user_email_from_creds = id_token.get('email')
# Check if credentials are valid, initiate auth flow if not
if not credentials or not credentials.valid:
logger.warning(f"[{tool_name}] No valid credentials. Session: '{mcp_session_id}', Email: '{user_google_email}'.")
if user_google_email and '@' in user_google_email:
logger.info(f"[{tool_name}] Valid email '{user_google_email}' provided, initiating auth flow for this email (requests all SCOPES).")
# Use the centralized start_auth_flow
return await start_auth_flow(mcp_session_id=mcp_session_id, user_google_email=user_google_email, service_name="Gmail", redirect_uri=OAUTH_REDIRECT_URI)
else:
# Fallback to user_google_email parameter or default
user_email_from_creds = user_google_email or "Unknown (Gmail)"
logger.info(f"[{tool_name}] Using service for: {user_email_from_creds}")
except AttributeError as e:
logger.error(f"[{tool_name}] Error accessing credentials/email from google_service: {e}", exc_info=True)
user_email_from_creds = user_google_email or "Unknown (Gmail - Error)"
error_msg = "Gmail Authentication required. No active authenticated session, and no valid 'user_google_email' provided. LLM: Please ask the user for their Google email address and retry, or use the 'start_auth' tool with their email."
logger.info(f"[{tool_name}] {error_msg}")
return types.CallToolResult(isError=True, content=[types.TextContent(type="text", text=error_msg)])
try:
# Build the service object directly
service = build('gmail', 'v1', credentials=credentials)
user_email_from_creds = credentials.id_token.get('email') if credentials.id_token else 'Unknown (Gmail)'
logger.info(f"[{tool_name}] Using service for: {user_email_from_creds}")
response = await asyncio.to_thread(
google_service.users().messages().list(
service.users().messages().list(
userId='me',
q=query,
maxResults=page_size
@@ -96,20 +106,21 @@ async def search_gmail_messages( # Signature cleaned - no google_service param
return types.CallToolResult(content=[types.TextContent(type="text", text="\n".join(lines))])
except HttpError as e:
logger.error(f"Gmail API error searching messages: {e}", exc_info=True)
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}")])
except Exception as e:
logger.exception(f"Unexpected error searching Gmail messages: {e}")
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}")])
@server.tool()
@require_google_auth(
required_scopes=[GMAIL_READONLY_SCOPE],
service_name="Gmail",
api_name="gmail",
api_version="v1"
)
# Remove the decorator
# @require_google_auth(
# required_scopes=[GMAIL_READONLY_SCOPE],
# service_name="Gmail",
# api_name="gmail",
# api_version="v1"
# )
async def get_gmail_message_content( # Signature cleaned - no google_service param
message_id: str,
user_google_email: Optional[str] = None,
@@ -117,45 +128,49 @@ async def get_gmail_message_content( # Signature cleaned - no google_service par
) -> types.CallToolResult:
"""
Retrieves the full content (subject, sender, plain text body) of a specific Gmail message.
Authentication and service object are handled by the @require_google_auth decorator using context variables.
Authentication is handled by get_credentials and start_auth_flow.
Args:
message_id (str): The unique ID of the Gmail message to retrieve.
user_google_email (Optional[str]): The user's Google email address (used for context/logging).
mcp_session_id (Optional[str]): The active MCP session ID (used for context/logging).
user_google_email (Optional[str]): The user's Google email address. Required if the MCP session is not already authenticated for Gmail access.
mcp_session_id (Optional[str]): The active MCP session ID (automatically injected by FastMCP from the Mcp-Session-Id header). Used for session-based authentication.
Returns:
types.CallToolResult: Contains the message details or an error/auth guidance message.
"""
# *** Add logging and check here ***
tool_name = "get_gmail_message_content"
logger.debug(f"[{tool_name}] Entered function. Attempting to get service from context var...")
google_service = current_google_service.get()
logger.debug(f"[{tool_name}] Service retrieved from context var. Type: {type(google_service)}, id: {id(google_service)}")
logger.info(f"[{tool_name}] Invoked. Message ID: '{message_id}', Session: '{mcp_session_id}', Email: '{user_google_email}'")
if not google_service:
logger.error(f"[{tool_name}] Google service retrieved from context is None!")
return types.CallToolResult(isError=True, content=[types.TextContent(type="text", text="Internal error: Google service unavailable in tool context.")])
# Use get_credentials to fetch credentials
credentials = await asyncio.to_thread(
get_credentials,
user_google_email=user_google_email,
required_scopes=[GMAIL_READONLY_SCOPE], # Specify required scopes for this tool
client_secrets_path=CONFIG_CLIENT_SECRETS_PATH, # Use imported constant
session_id=mcp_session_id
)
logger.info(f"[{tool_name}] Message ID: '{message_id}', Session: '{mcp_session_id}', Email: '{user_google_email}'")
try:
# More robust way to extract email from credentials
id_token = getattr(google_service._http.credentials, 'id_token', None)
if isinstance(id_token, dict) and 'email' in id_token:
user_email_from_creds = id_token.get('email')
# Check if credentials are valid, initiate auth flow if not
if not credentials or not credentials.valid:
logger.warning(f"[{tool_name}] No valid credentials. Session: '{mcp_session_id}', Email: '{user_google_email}'.")
if user_google_email and '@' in user_google_email:
logger.info(f"[{tool_name}] Valid email '{user_google_email}' provided, initiating auth flow for this email (requests all SCOPES).")
# Use the centralized start_auth_flow
return await start_auth_flow(mcp_session_id=mcp_session_id, user_google_email=user_google_email, service_name="Gmail", redirect_uri=OAUTH_REDIRECT_URI)
else:
# Fallback to user_google_email parameter or default
user_email_from_creds = user_google_email or "Unknown (Gmail)"
logger.info(f"[{tool_name}] Using service for: {user_email_from_creds}")
except AttributeError as e:
logger.error(f"[{tool_name}] Error accessing credentials/email from google_service: {e}", exc_info=True)
user_email_from_creds = user_google_email or "Unknown (Gmail - Error)"
error_msg = "Gmail Authentication required. No active authenticated session, and no valid 'user_google_email' provided. LLM: Please ask the user for their Google email address and retry, or use the 'start_auth' tool with their email."
logger.info(f"[{tool_name}] {error_msg}")
return types.CallToolResult(isError=True, content=[types.TextContent(type="text", text=error_msg)])
try:
# Build the service object directly
service = build('gmail', 'v1', credentials=credentials)
user_email_from_creds = credentials.id_token.get('email') if credentials.id_token else 'Unknown (Gmail)'
logger.info(f"[{tool_name}] Using service for: {user_email_from_creds}")
# Fetch message metadata first to get headers
message_metadata = await asyncio.to_thread(
google_service.users().messages().get(
service.users().messages().get(
userId='me',
id=message_id,
format='metadata',
@@ -169,7 +184,7 @@ async def get_gmail_message_content( # Signature cleaned - no google_service par
# Now fetch the full message to get the body parts
message_full = await asyncio.to_thread(
google_service.users().messages().get(
service.users().messages().get(
userId='me',
id=message_id,
format='full' # Request full payload for body
@@ -205,10 +220,10 @@ async def get_gmail_message_content( # Signature cleaned - no google_service par
return types.CallToolResult(content=[types.TextContent(type="text", text=content_text)])
except HttpError as e:
logger.error(f"Gmail API error getting message content: {e}", exc_info=True)
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}")])
except Exception as e:
logger.exception(f"Unexpected error getting Gmail message content: {e}")
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}")])
# Note: send_gmail_message tool would need GMAIL_SEND_SCOPE and similar refactoring if added.