2025-04-27 14:30:11 -04:00
import logging
import os
2025-05-06 09:36:48 -04:00
import sys
2025-05-11 17:39:15 -04:00
from typing import Dict , Any , Optional
2025-05-06 09:36:48 -04:00
2025-05-11 15:37:44 -04:00
from fastapi import Request
from fastapi . responses import HTMLResponse
2025-05-11 10:07:37 -04:00
# Import MCP types for proper response formatting
2025-05-11 15:37:44 -04:00
from mcp import types
2025-05-11 10:07:37 -04:00
from mcp . server . fastmcp import FastMCP
2025-05-06 09:36:48 -04:00
2025-04-27 14:30:11 -04:00
from google . auth . exceptions import RefreshError
2025-05-13 12:36:53 -04:00
from auth . google_auth import handle_auth_callback , CONFIG_CLIENT_SECRETS_PATH # Import handle_auth_callback and CONFIG_CLIENT_SECRETS_PATH
2025-05-11 17:15:05 -04:00
# auth_session_manager is no longer used here with the simplified flow
2025-04-27 14:30:11 -04:00
2025-05-13 12:36:53 -04:00
# Import shared configuration
from config . google_config import (
OAUTH_STATE_TO_SESSION_ID_MAP ,
USERINFO_EMAIL_SCOPE ,
OPENID_SCOPE ,
CALENDAR_READONLY_SCOPE ,
CALENDAR_EVENTS_SCOPE ,
DRIVE_READONLY_SCOPE ,
DRIVE_FILE_SCOPE ,
GMAIL_READONLY_SCOPE ,
GMAIL_SEND_SCOPE ,
BASE_SCOPES ,
CALENDAR_SCOPES ,
DRIVE_SCOPES ,
GMAIL_SCOPES ,
2025-05-14 09:35:48 -04:00
DOCS_READONLY_SCOPE ,
DOCS_WRITE_SCOPE ,
2025-05-13 12:36:53 -04:00
SCOPES
)
2025-04-27 14:30:11 -04:00
# Configure logging
logging . basicConfig ( level = logging . INFO )
2025-05-13 12:36:53 -04:00
# OAUTH_STATE_TO_SESSION_ID_MAP is now imported from config.google_config
2025-04-27 14:30:11 -04:00
logger = logging . getLogger ( __name__ )
2025-05-11 15:37:44 -04:00
DEFAULT_PORT = 8000
2025-04-27 12:34:22 -04:00
# Basic MCP server instance
2025-05-11 10:07:37 -04:00
server = FastMCP (
name = " google_workspace " ,
2025-05-11 15:37:44 -04:00
server_url = f " http://localhost: { DEFAULT_PORT } /gworkspace " , # Add absolute URL for Gemini native function calling
2025-05-11 10:07:37 -04:00
host = " 0.0.0.0 " , # Listen on all interfaces
2025-05-11 15:37:44 -04:00
port = DEFAULT_PORT , # Default port for HTTP server
2025-05-11 10:07:37 -04:00
stateless_http = False # Enable stateful sessions (default)
)
# Configure OAuth redirect URI to use the MCP server's port
2025-05-11 15:37:44 -04:00
OAUTH_REDIRECT_URI = f " http://localhost: { DEFAULT_PORT } /oauth2callback "
# Register OAuth callback as a custom route
@server.custom_route ( " /oauth2callback " , methods = [ " GET " ] )
async def oauth2_callback ( request : Request ) - > HTMLResponse :
"""
Handle OAuth2 callback from Google via a custom route.
2025-05-11 17:15:05 -04:00
This endpoint exchanges the authorization code for credentials and saves them.
It then displays a success or error page to the user.
2025-05-11 15:37:44 -04:00
"""
2025-05-11 17:15:05 -04:00
# State is used by google-auth-library for CSRF protection and should be present.
# We don't need to track it ourselves in this simplified flow.
2025-05-11 15:37:44 -04:00
state = request . query_params . get ( " state " )
code = request . query_params . get ( " code " )
error = request . query_params . get ( " error " )
if error :
2025-05-11 17:15:05 -04:00
error_message = f " Authentication failed: Google returned an error: { error } . State: { state } . "
logger . error ( error_message )
2025-05-11 15:37:44 -04:00
return HTMLResponse ( content = f """
<html><head><title>Authentication Error</title></head>
2025-05-11 17:15:05 -04:00
<body><h2>Authentication Error</h2><p> { error_message } </p>
<p>Please ensure you grant the requested permissions. You can close this window and try again.</p></body></html>
2025-05-11 15:37:44 -04:00
""" , status_code = 400 )
if not code :
2025-05-11 17:15:05 -04:00
error_message = " Authentication failed: No authorization code received from Google. "
logger . error ( error_message )
2025-05-11 15:37:44 -04:00
return HTMLResponse ( content = f """
<html><head><title>Authentication Error</title></head>
2025-05-11 17:15:05 -04:00
<body><h2>Authentication Error</h2><p> { error_message } </p><p>You can close this window and try again.</p></body></html>
2025-05-11 15:37:44 -04:00
""" , status_code = 400 )
2025-05-11 10:07:37 -04:00
try :
2025-05-13 12:36:53 -04:00
# Use the centralized CONFIG_CLIENT_SECRETS_PATH
client_secrets_path = CONFIG_CLIENT_SECRETS_PATH
2025-05-11 15:37:44 -04:00
if not os . path . exists ( client_secrets_path ) :
2025-05-11 17:15:05 -04:00
logger . error ( f " OAuth client secrets file not found at { client_secrets_path } " )
# This is a server configuration error, should not happen in a deployed environment.
return HTMLResponse ( content = " Server Configuration Error: Client secrets not found. " , status_code = 500 )
2025-05-11 15:37:44 -04:00
2025-05-11 17:15:05 -04:00
logger . info ( f " OAuth callback: Received code (state: { state } ). Attempting to exchange for tokens. " )
2025-05-13 12:36:53 -04:00
2025-05-11 17:39:15 -04:00
mcp_session_id : Optional [ str ] = OAUTH_STATE_TO_SESSION_ID_MAP . pop ( state , None )
if mcp_session_id :
logger . info ( f " OAuth callback: Retrieved MCP session ID ' { mcp_session_id } ' for state ' { state } ' . " )
else :
logger . warning ( f " OAuth callback: No MCP session ID found for state ' { state } ' . Auth will not be tied to a specific session directly via this callback. " )
2025-05-11 17:15:05 -04:00
# Exchange code for credentials. handle_auth_callback will save them.
# The user_id returned here is the Google-verified email.
verified_user_id , credentials = handle_auth_callback (
2025-05-11 10:07:37 -04:00
client_secrets_path = client_secrets_path ,
2025-05-11 17:15:05 -04:00
scopes = SCOPES , # Ensure all necessary scopes are requested
authorization_response = str ( request . url ) ,
2025-05-11 17:39:15 -04:00
redirect_uri = OAUTH_REDIRECT_URI ,
session_id = mcp_session_id # Pass session_id if available
2025-05-11 10:07:37 -04:00
)
2025-05-13 12:36:53 -04:00
2025-05-11 17:39:15 -04:00
log_session_part = f " (linked to session: { mcp_session_id } ) " if mcp_session_id else " "
logger . info ( f " OAuth callback: Successfully authenticated user: { verified_user_id } (state: { state } ) { log_session_part } . " )
2025-05-13 12:36:53 -04:00
2025-05-11 17:15:05 -04:00
# Return a more informative success page
success_page_content = f """
2025-05-11 10:07:37 -04:00
<html>
<head>
<title>Authentication Successful</title>
<style>
2025-05-11 17:15:05 -04:00
body {{ font-family: -apple-system, BlinkMacSystemFont, " Segoe UI " , Roboto, Helvetica, Arial, sans-serif; max-width: 600px; margin: 40px auto; padding: 20px; text-align: center; color: #333; border: 1px solid #ddd; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }}
.status {{ color: #4CAF50; font-size: 24px; margin-bottom: 15px; }}
.message {{ margin-bottom: 20px; line-height: 1.6; }}
.user-id {{ font-weight: bold; color: #2a2a2a; }}
.button {{ background-color: #4CAF50; color: white; padding: 12px 25px; border: none; border-radius: 4px; cursor: pointer; font-size: 16px; text-decoration: none; display: inline-block; margin-top: 10px; }}
.note {{ font-size: 0.9em; color: #555; margin-top: 25px; }}
2025-05-11 10:07:37 -04:00
</style>
2025-05-11 17:15:05 -04:00
<script> setTimeout(function() {{ window.close(); }} , 10000); </script>
2025-05-11 10:07:37 -04:00
</head>
<body>
2025-05-11 17:15:05 -04:00
<div class= " status " >✅ Authentication Successful</div>
<div class= " message " >
You have successfully authenticated as <span class= " user-id " > { verified_user_id } </span>.
Credentials have been saved.
</div>
<div class= " message " >
You can now close this window and **retry your original command** in the application.
</div>
2025-05-11 10:07:37 -04:00
<button class= " button " onclick= " window.close() " >Close Window</button>
2025-05-11 17:15:05 -04:00
<div class= " note " >This window will close automatically in 10 seconds.</div>
2025-05-11 10:07:37 -04:00
</body>
</html>
2025-05-11 17:15:05 -04:00
"""
return HTMLResponse ( content = success_page_content )
2025-05-13 12:36:53 -04:00
2025-05-11 10:07:37 -04:00
except Exception as e :
2025-05-11 17:15:05 -04:00
error_message_detail = f " Error processing OAuth callback (state: { state } ): { str ( e ) } "
2025-05-11 15:37:44 -04:00
logger . error ( error_message_detail , exc_info = True )
2025-05-11 17:15:05 -04:00
# Generic error page for any other issues during token exchange or credential saving
2025-05-11 15:37:44 -04:00
return HTMLResponse ( content = f """
2025-05-11 10:07:37 -04:00
<html>
2025-05-11 17:15:05 -04:00
<head><title>Authentication Processing Error</title></head>
2025-05-11 10:07:37 -04:00
<body>
2025-05-11 15:37:44 -04:00
<h2 style= " color: #d32f2f; " >Authentication Processing Error</h2>
2025-05-11 17:15:05 -04:00
<p>An unexpected error occurred while processing your authentication: { str ( e ) } </p>
<p>Please try again. You can close this window.</p>
2025-05-11 10:07:37 -04:00
</body>
</html>
2025-05-11 15:37:44 -04:00
""" , status_code = 500 )
2025-04-27 14:30:11 -04:00
2025-05-11 17:15:05 -04:00
# The @server.tool("oauth2callback") is removed as it's redundant with the custom HTTP route
# and the simplified "authorize and retry" flow.