diff --git a/backend/app/controller/tool_controller.py b/backend/app/controller/tool_controller.py index b64941cf2..2ba17e967 100644 --- a/backend/app/controller/tool_controller.py +++ b/backend/app/controller/tool_controller.py @@ -329,6 +329,43 @@ async def uninstall_tool(tool: str): oauth_state_manager._states.pop("google_calendar", None) logger.info("Cleared Google Calendar OAuth state cache") + return { + "success": True, + "message": f"Successfully uninstalled {tool} and cleaned up authentication tokens" + } + except Exception as e: + logger.error(f"Failed to uninstall {tool}: {e}") + raise HTTPException( + status_code=500, + detail=f"Failed to uninstall {tool}: {str(e)}" + ) + elif tool == "google_gmail": + try: + # Clean up Google Gmail token directories (user-scoped + legacy) + token_dirs = set() + try: + token_dirs.add(os.path.dirname(GoogleGmailNativeToolkit._build_canonical_token_path())) + except Exception as e: + logger.warning(f"Failed to resolve canonical Google Gmail token path: {e}") + + token_dirs.add(os.path.join(os.path.expanduser("~"), ".eigent", "tokens", "google_gmail")) + + for token_dir in token_dirs: + if os.path.exists(token_dir): + shutil.rmtree(token_dir) + logger.info(f"Removed Google Gmail token directory: {token_dir}") + + # Clear OAuth state manager cache (this is the key fix!) + # This removes the cached credentials from memory + state = oauth_state_manager.get_state("google_gmail") + if state: + if state.status in ["pending", "authorizing"]: + state.cancel() + logger.info("Cancelled ongoing Google Gmail authorization") + # Clear the state completely to remove cached credentials + oauth_state_manager._states.pop("google_gmail", None) + logger.info("Cleared Google Gmail OAuth state cache") + return { "success": True, "message": f"Successfully uninstalled {tool} and cleaned up authentication tokens" @@ -342,7 +379,7 @@ async def uninstall_tool(tool: str): else: raise HTTPException( status_code=404, - detail=f"Tool '{tool}' not found. Available tools: ['notion', 'google_calendar']" + detail=f"Tool '{tool}' not found. Available tools: ['notion', 'google_calendar', 'google_gmail']" ) diff --git a/backend/app/utils/toolkit/google_gmail_native_toolkit.py b/backend/app/utils/toolkit/google_gmail_native_toolkit.py index 467c9f745..7f6a70c7c 100644 --- a/backend/app/utils/toolkit/google_gmail_native_toolkit.py +++ b/backend/app/utils/toolkit/google_gmail_native_toolkit.py @@ -4,13 +4,15 @@ from camel.toolkits import GmailToolkit as BaseGmailToolkit from camel.toolkits.function_tool import FunctionTool -from loguru import logger from app.component.environment import env from app.service.task import Agents from app.utils.listen.toolkit_listen import listen_toolkit from app.utils.toolkit.abstract_toolkit import AbstractToolkit from app.utils.oauth_state_manager import oauth_state_manager +from utils import traceroot_wrapper as traceroot + +logger = traceroot.get_logger("main") SCOPES = [ 'https://www.googleapis.com/auth/gmail.readonly', @@ -19,7 +21,6 @@ 'https://www.googleapis.com/auth/gmail.compose', 'https://www.googleapis.com/auth/gmail.labels', 'https://www.googleapis.com/auth/contacts.readonly', - 'https://www.googleapis.com/auth/people.readonly' ] @@ -41,16 +42,26 @@ def __init__( """ self.api_task_id = api_task_id self._token_path = ( - os.environ.get("GOOGLE_GMAIL_TOKEN_PATH") + env("GOOGLE_GMAIL_TOKEN_PATH") or os.path.join( os.path.expanduser("~"), ".eigent", "tokens", "google_gmail", - f"google_gmail_token_{api_task_id}.json", + "google_gmail_token.json", ) ) super().__init__(timeout=timeout) + + @classmethod + def _build_canonical_token_path(cls) -> str: + return env("GOOGLE_GMAIL_TOKEN_PATH") or os.path.join( + os.path.expanduser("~"), + ".eigent", + "tokens", + "google_gmail", + "google_gmail_token.json", + ) # Email Sending Operations @listen_toolkit( @@ -159,8 +170,9 @@ def list_threads( max_results: int = 10, include_spam_trash: bool = False, label_ids: Optional[List[str]] = None, + page_token: Optional[str] = None, ) -> Dict[str, Any]: - return super().list_threads(query, max_results, include_spam_trash, label_ids) + return super().list_threads(query, max_results, include_spam_trash, label_ids, page_token) # Label Management @listen_toolkit( @@ -303,10 +315,10 @@ def _authenticate(self): # If no token file, try environment variables if not creds: - client_id = os.environ.get("GOOGLE_CLIENT_ID") - client_secret = os.environ.get("GOOGLE_CLIENT_SECRET") - refresh_token = os.environ.get("GOOGLE_REFRESH_TOKEN") - token_uri = os.environ.get("GOOGLE_TOKEN_URI", "https://oauth2.googleapis.com/token") + client_id = env("GOOGLE_CLIENT_ID") + client_secret = env("GOOGLE_CLIENT_SECRET") + refresh_token = env("GOOGLE_REFRESH_TOKEN") + token_uri = env("GOOGLE_TOKEN_URI", "https://oauth2.googleapis.com/token") if refresh_token and client_id and client_secret: logger.info("Creating credentials from environment variables") @@ -378,9 +390,9 @@ def auth_flow(): state.status = "authorizing" oauth_state_manager.update_status("google_gmail", "authorizing") - client_id = os.environ.get("GOOGLE_CLIENT_ID") - client_secret = os.environ.get("GOOGLE_CLIENT_SECRET") - token_uri = os.environ.get("GOOGLE_TOKEN_URI", "https://oauth2.googleapis.com/token") + client_id = env("GOOGLE_CLIENT_ID") + client_secret = env("GOOGLE_CLIENT_SECRET") + token_uri = env("GOOGLE_TOKEN_URI", "https://oauth2.googleapis.com/token") logger.info(f"Google Gmail auth - client_id present: {bool(client_id)}, client_secret present: {bool(client_secret)}") @@ -437,7 +449,7 @@ def auth_flow(): ".eigent", "tokens", "google_gmail", - f"google_gmail_token_{api_task_id}.json", + f"google_gmail_token.json", ) try: diff --git a/backend/app/utils/toolkit/google_gmail_toolkit.py b/backend/app/utils/toolkit/google_gmail_toolkit.py deleted file mode 100644 index 482194d6e..000000000 --- a/backend/app/utils/toolkit/google_gmail_toolkit.py +++ /dev/null @@ -1,379 +0,0 @@ -# ========= Copyright 2023-2024 @ CAMEL-AI.org. All Rights Reserved. ========= -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# ========= Copyright 2023-2024 @ CAMEL-AI.org. All Rights Reserved. ========= - -import os -import threading -from typing import Optional - -from app.component.environment import env -from app.service.task import Agents -from app.utils.listen.toolkit_listen import auto_listen_toolkit -from app.utils.toolkit.abstract_toolkit import AbstractToolkit -from app.utils.oauth_state_manager import oauth_state_manager -from utils import traceroot_wrapper as traceroot - -from camel.toolkits import GmailToolkit as BaseGmailToolkit - -logger = traceroot.get_logger("main") - -SCOPES = [ - 'https://www.googleapis.com/auth/gmail.readonly', - 'https://www.googleapis.com/auth/gmail.send', - 'https://www.googleapis.com/auth/gmail.modify', - 'https://www.googleapis.com/auth/gmail.compose', - 'https://www.googleapis.com/auth/gmail.labels', - 'https://www.googleapis.com/auth/contacts.readonly', - 'https://www.googleapis.com/auth/userinfo.profile', -] - - -@auto_listen_toolkit(BaseGmailToolkit) -class GoogleGmailToolkit(BaseGmailToolkit, AbstractToolkit): - r"""A comprehensive toolkit for Gmail operations integrated with Eigent. - - This class provides methods for Gmail operations including sending emails, - managing drafts, fetching messages, managing labels, and handling contacts. - API keys can be accessed in google cloud console (https://console.cloud.google.com/) - """ - - agent_name: str = Agents.social_medium_agent - - def __init__(self, api_task_id: str, timeout: Optional[float] = None): - r"""Initializes a new instance of the GoogleGmailToolkit class. - - Args: - api_task_id (str): The API task identifier for per-task token management. - timeout (Optional[float]): The timeout value for API requests - in seconds. If None, no timeout is applied. - (default: :obj:`None`) - """ - self.api_task_id = api_task_id - self._token_path = ( - env("GOOGLE_GMAIL_TOKEN_PATH") - or os.path.join( - os.path.expanduser("~"), - ".eigent", - "tokens", - "gmail", - f"gmail_token_{api_task_id}.json", - ) - ) - - # Authenticate and initialize parent class - self._credentials = self._authenticate() - - # Initialize parent with timeout - super().__init__(timeout=timeout) - - # Override the services with our authenticated credentials - self.gmail_service = self._get_gmail_service() - self._people_service = None - - @property - def people_service(self): - r"""Lazily initialize and return the Google People service.""" - if self._people_service is None: - self._people_service = self._get_people_service() - return self._people_service - - @people_service.setter - def people_service(self, service): - r"""Allow overriding/injecting the People service (e.g., in tests).""" - self._people_service = service - - @classmethod - def get_can_use_tools(cls, api_task_id: str): - from dotenv import load_dotenv - - # Force reload environment variables - default_env_path = os.path.join(os.path.expanduser("~"), ".eigent", ".env") - if os.path.exists(default_env_path): - load_dotenv(dotenv_path=default_env_path, override=True) - - if os.environ.get("GOOGLE_CLIENT_ID") and os.environ.get("GOOGLE_CLIENT_SECRET"): - return cls(api_task_id).get_tools() - else: - return [] - - def _get_gmail_service(self): - r"""Get Gmail service object.""" - from googleapiclient.discovery import build - from google.auth.transport.requests import Request - - creds = self._credentials - - if creds and creds.expired and creds.refresh_token: - creds.refresh(Request()) - try: - os.makedirs(os.path.dirname(self._token_path), exist_ok=True) - with open(self._token_path, "w") as f: - f.write(creds.to_json()) - except Exception: - pass - - try: - # Build service with optional timeout - if self.timeout is not None: - import httplib2 - - http = httplib2.Http(timeout=self.timeout) - http = creds.authorize(http) - service = build('gmail', 'v1', http=http) - else: - service = build('gmail', 'v1', credentials=creds) - return service - except Exception as e: - raise ValueError(f"Failed to build Gmail service: {e}") from e - - def _get_people_service(self): - r"""Get People service object.""" - from googleapiclient.discovery import build - from google.auth.transport.requests import Request - - creds = self._credentials - - if creds and creds.expired and creds.refresh_token: - creds.refresh(Request()) - try: - os.makedirs(os.path.dirname(self._token_path), exist_ok=True) - with open(self._token_path, "w") as f: - f.write(creds.to_json()) - except Exception: - pass - - try: - # Build service with optional timeout - if self.timeout is not None: - import httplib2 - - http = httplib2.Http(timeout=self.timeout) - http = creds.authorize(http) - service = build('people', 'v1', http=http) - else: - service = build('people', 'v1', credentials=creds) - return service - except Exception as e: - raise ValueError(f"Failed to build People service: {e}") from e - - def _authenticate(self): - r"""Authenticate with Google APIs using OAuth2. - - Automatically saves and loads credentials from token file - to avoid repeated browser logins. - """ - from google.oauth2.credentials import Credentials - from google_auth_oauthlib.flow import InstalledAppFlow - from google.auth.transport.requests import Request - from dotenv import load_dotenv - - # Force reload environment variables from default .env file - default_env_path = os.path.join(os.path.expanduser("~"), ".eigent", ".env") - if os.path.exists(default_env_path): - load_dotenv(dotenv_path=default_env_path, override=True) - - creds = None - - # First, try to load from token file - try: - if os.path.exists(self._token_path): - logger.info(f"Loading Gmail credentials from token file: {self._token_path}") - creds = Credentials.from_authorized_user_file(self._token_path, SCOPES) - logger.info("Successfully loaded Gmail credentials from token file") - except Exception as e: - logger.warning(f"Could not load from token file: {e}") - creds = None - - # If no token file, try environment variables - if not creds: - client_id = os.environ.get("GOOGLE_CLIENT_ID") - client_secret = os.environ.get("GOOGLE_CLIENT_SECRET") - refresh_token = os.environ.get("GOOGLE_REFRESH_TOKEN") - token_uri = os.environ.get("GOOGLE_TOKEN_URI") or "https://oauth2.googleapis.com/token" - - if refresh_token and client_id and client_secret: - logger.info("Creating Gmail credentials from environment variables") - creds = Credentials( - None, - refresh_token=refresh_token, - token_uri=token_uri, - client_id=client_id, - client_secret=client_secret, - scopes=SCOPES, - ) - - # If still no creds, check background authorization - if not creds: - state = oauth_state_manager.get_state("google_gmail") - if state and state.status == "success" and state.result: - logger.info("Using Gmail credentials from background authorization") - creds = state.result - else: - # No credentials available - raise ValueError("No credentials available. Please run authorization first via /api/install/tool/google_gmail_toolkit") - - # Refresh if expired - if creds and creds.expired and creds.refresh_token: - try: - logger.info("Gmail token expired, refreshing...") - creds.refresh(Request()) - logger.info("Gmail token refreshed successfully") - except Exception as e: - logger.error(f"Failed to refresh Gmail token: {e}") - raise ValueError("Failed to refresh expired token. Please re-authorize.") - - # Save credentials - try: - os.makedirs(os.path.dirname(self._token_path), exist_ok=True) - with open(self._token_path, "w") as f: - f.write(creds.to_json()) - except Exception as e: - logger.warning(f"Could not save Gmail credentials: {e}") - - return creds - - @staticmethod - def start_background_auth(api_task_id: str = "install_auth") -> str: - """ - Start background OAuth authorization flow with timeout - Returns the status of the authorization - """ - from google_auth_oauthlib.flow import InstalledAppFlow - from dotenv import load_dotenv - - # Force reload environment variables from default .env file - default_env_path = os.path.join(os.path.expanduser("~"), ".eigent", ".env") - if os.path.exists(default_env_path): - logger.info(f"Reloading environment variables from {default_env_path}") - load_dotenv(dotenv_path=default_env_path, override=True) - - # Check if there's an existing authorization and force stop it - old_state = oauth_state_manager.get_state("google_gmail") - if old_state and old_state.status in ["pending", "authorizing"]: - logger.info("Found existing Gmail authorization, forcing shutdown...") - old_state.cancel() - # Try to shutdown the old server if it exists - if hasattr(old_state, 'server') and old_state.server: - try: - old_state.server.shutdown() - logger.info("Old Gmail server shutdown successfully") - except Exception as e: - logger.warning(f"Could not shutdown old Gmail server: {e}") - - # Create new state for this authorization - state = oauth_state_manager.create_state("google_gmail") - - def auth_flow(): - try: - state.status = "authorizing" - oauth_state_manager.update_status("google_gmail", "authorizing") - - # Reload environment variables in this thread - from dotenv import load_dotenv - default_env_path = os.path.join(os.path.expanduser("~"), ".eigent", ".env") - if os.path.exists(default_env_path): - load_dotenv(dotenv_path=default_env_path, override=True) - - client_id = os.environ.get("GOOGLE_CLIENT_ID") - client_secret = os.environ.get("GOOGLE_CLIENT_SECRET") - token_uri = os.environ.get("GOOGLE_TOKEN_URI") or "https://oauth2.googleapis.com/token" - - logger.info(f"Gmail auth - client_id present: {bool(client_id)}, client_secret present: {bool(client_secret)}") - - if not client_id or not client_secret: - error_msg = "GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET must be set in environment variables" - logger.error(error_msg) - raise ValueError(error_msg) - - client_config = { - "installed": { - "client_id": client_id, - "client_secret": client_secret, - "auth_uri": "https://accounts.google.com/o/oauth2/auth", - "token_uri": token_uri, - "redirect_uris": ["http://localhost"], - } - } - logger.debug(f"Gmail client_config initialized with client_id: {client_id[:10]}...") - flow = InstalledAppFlow.from_client_config(client_config, SCOPES) - - # Check for cancellation before starting - if state.is_cancelled(): - logger.info("Gmail authorization cancelled before starting") - return - - # This will automatically open browser and wait for user authorization - logger.info("=" * 80) - logger.info(f"[Thread {threading.current_thread().name}] Starting local server for Gmail authorization") - logger.info("Browser should open automatically in a moment...") - logger.info("=" * 80) - - # Run local server - this will block until authorization completes - # Note: Each call uses a random port (port=0), so multiple concurrent attempts won't conflict - try: - creds = flow.run_local_server( - port=0, - authorization_prompt_message="", - success_message="
You can close this window and return to Eigent.
", - open_browser=True - ) - logger.info("Gmail authorization flow completed successfully!") - except Exception as server_error: - logger.error(f"Error during Gmail run_local_server: {server_error}") - raise - - # Check for cancellation after auth - if state.is_cancelled(): - logger.info("Gmail authorization cancelled after completion") - return - - # Save credentials to token file - token_path = os.path.join( - os.path.expanduser("~"), - ".eigent", - "tokens", - "gmail", - f"gmail_token_{api_task_id}.json", - ) - - try: - os.makedirs(os.path.dirname(token_path), exist_ok=True) - with open(token_path, "w") as f: - f.write(creds.to_json()) - logger.info(f"Saved Gmail credentials to {token_path}") - except Exception as e: - logger.warning(f"Could not save Gmail credentials: {e}") - - # Update state with success - oauth_state_manager.update_status("google_gmail", "success", result=creds) - logger.info("Gmail authorization successful!") - - except Exception as e: - if state.is_cancelled(): - logger.info("Gmail authorization was cancelled") - oauth_state_manager.update_status("google_gmail", "cancelled") - else: - error_msg = str(e) - logger.error(f"Gmail authorization failed: {error_msg}") - oauth_state_manager.update_status("google_gmail", "failed", error=error_msg) - finally: - # Clean up server reference - state.server = None - - # Start authorization in background thread - thread = threading.Thread(target=auth_flow, daemon=True, name=f"Gmail-OAuth-{state.started_at.timestamp()}") - state.thread = thread - thread.start() - - logger.info("Started background Gmail authorization") - return "authorizing" diff --git a/server/app/model/config/config.py b/server/app/model/config/config.py index 126af30c0..593ec6423 100644 --- a/server/app/model/config/config.py +++ b/server/app/model/config/config.py @@ -120,14 +120,6 @@ class ConfigInfo: ], "toolkit": "google_calendar_toolkit", }, - ConfigGroup.GOOGLE_GMAIL.value: { - "env_vars": [ - "GOOGLE_CLIENT_ID", - "GOOGLE_CLIENT_SECRET", - "GOOGLE_REFRESH_TOKEN", - ], - "toolkit": "google_gmail_native_toolkit", - }, ConfigGroup.GOOGLE_DRIVE_MCP.value: { "env_vars": [], "toolkit": "google_drive_mcp_toolkit", diff --git a/server/app/type/config_group.py b/server/app/type/config_group.py index ef2f14366..ba7b66050 100644 --- a/server/app/type/config_group.py +++ b/server/app/type/config_group.py @@ -20,9 +20,8 @@ class ConfigGroup(str, Enum): FILE_WRITE = "File Write" GITHUB = "Github" GOOGLE_CALENDAR = "Google Calendar" - GOOGLE_GMAIL = "Google Gmail" GOOGLE_DRIVE_MCP = "Google Drive MCP" - GOOGLE_GMAIL_MCP = "Google Gmail" + GOOGLE_GMAIL_MCP = "Google Gmail MCP" IMAGE_ANALYSIS = "Image Analysis" MCP_SEARCH = "MCP Search" PPTX = "PPTX" diff --git a/src/components/AddWorker/ToolSelect.tsx b/src/components/AddWorker/ToolSelect.tsx index d18d0a4cc..d0f3a5537 100644 --- a/src/components/AddWorker/ToolSelect.tsx +++ b/src/components/AddWorker/ToolSelect.tsx @@ -168,7 +168,7 @@ const ToolSelect = forwardRef< } }; - } else if (key.toLowerCase() === 'google gmail') { + } else if (key.toLowerCase() === 'google gmail mcp') { onInstall = async () => { try { const response = await fetchPost("/install/tool/google_gmail"); @@ -177,13 +177,13 @@ const ToolSelect = forwardRef< const existingConfigs = await proxyFetchGet("/api/configs"); const existing = Array.isArray(existingConfigs) ? existingConfigs.find((c: any) => - c.config_group?.toLowerCase() === "google gmail" && + c.config_group?.toLowerCase() === "google gmail mcp" && c.config_name === "GOOGLE_REFRESH_TOKEN" ) : null; const configPayload = { - config_group: "Google Gmail", + config_group: "Google Gmail MCP", //According to backend config config_name: "GOOGLE_REFRESH_TOKEN", config_value: "exists", }; @@ -197,7 +197,7 @@ const ToolSelect = forwardRef< console.log("Google Gmail installed successfully"); // After successful installation, add to selected tools const gmailItem = { - id: 0, // Use 0 for integration items + id: 1, // Use 1 for integration items key: key, name: key, description: "Google Gmail integration for managing emails and contacts", @@ -244,7 +244,7 @@ const ToolSelect = forwardRef< ? t("layout.notion-workspace-integration") : key.toLowerCase() === 'google calendar' ? t("layout.google-calendar-integration") - : key.toLowerCase() === 'google gmail' + : key.toLowerCase() === 'google gmail mcp' ? "Google Gmail integration for managing emails and contacts" : "", onInstall, @@ -522,7 +522,7 @@ const ToolSelect = forwardRef< } // Trigger instantiation for Gmail - if (activeMcp.key === "Gmail") { + if (activeMcp.key === "Google Gmail MCP") { console.log("[ToolSelect installMcp] Starting Gmail installation"); try { const response = await fetchPost("/install/tool/google_gmail"); @@ -533,13 +533,13 @@ const ToolSelect = forwardRef< const existingConfigs = await proxyFetchGet("/api/configs"); const existing = Array.isArray(existingConfigs) ? existingConfigs.find((c: any) => - c.config_group?.toLowerCase() === "gmail" && + c.config_group?.toLowerCase() === "google gmail mcp" && c.config_name === "GOOGLE_REFRESH_TOKEN" ) : null; const configPayload = { - config_group: "Gmail", + config_group: "Google Gmail MCP", config_name: "GOOGLE_REFRESH_TOKEN", config_value: "exists", }; @@ -558,7 +558,7 @@ const ToolSelect = forwardRef< key: activeMcp.key, name: activeMcp.name, description: "Gmail integration for managing emails, drafts, labels, and contacts", - toolkit: "google_gmail_toolkit", + toolkit: "google_gmail_native_toolkit", isLocal: true }; addOption(selectedItem, true); @@ -585,13 +585,13 @@ const ToolSelect = forwardRef< const existingConfigs = await proxyFetchGet("/api/configs"); const existing = Array.isArray(existingConfigs) ? existingConfigs.find((c: any) => - c.config_group?.toLowerCase() === "gmail" && + c.config_group?.toLowerCase() === "google gmail mcp" && c.config_name === "GOOGLE_REFRESH_TOKEN" ) : null; const configPayload = { - config_group: "Gmail", + config_group: "Google Gmail MCP", config_name: "GOOGLE_REFRESH_TOKEN", config_value: "exists", }; diff --git a/src/components/AddWorker/index.tsx b/src/components/AddWorker/index.tsx index 1d83ccb21..855c18314 100644 --- a/src/components/AddWorker/index.tsx +++ b/src/components/AddWorker/index.tsx @@ -164,7 +164,7 @@ export function AddWorker({ // call ToolSelect's install method if (toolSelectRef.current) { try { - if (activeMcp.key === "EXA Search" || activeMcp.key === "Google Calendar" || activeMcp.key === "Google Gmail") { + if (activeMcp.key === "EXA Search" || activeMcp.key === "Google Calendar" || activeMcp.key === "Google Gmail MCP") { await toolSelectRef.current.installMcp( activeMcp.id, { ...envValues }, @@ -179,7 +179,7 @@ export function AddWorker({ } // For Google Calendar, close dialog after installMcp completes - if (activeMcp.key === "Google Calendar") { + if (activeMcp.key === "Google Calendar" || activeMcp.key === "Google Gmail MCP") { setShowEnvConfig(false); } diff --git a/src/components/IntegrationList/index.tsx b/src/components/IntegrationList/index.tsx index 24098f960..b187eba16 100644 --- a/src/components/IntegrationList/index.tsx +++ b/src/components/IntegrationList/index.tsx @@ -108,6 +108,17 @@ export default function IntegrationList({ setShowEnvConfig(true); } return; + } + + if (item.key === "Google Gmail MCP") { + const mcp = createMcpFromItem(item, 15); + if (isSelectMode) { + onShowEnvConfig?.(mcp); + } else { + setActiveMcp(mcp); + setShowEnvConfig(true); + } + return; } if (installed[item.key]) return; @@ -199,6 +210,64 @@ export default function IntegrationList({ console.log("[IntegrationList onConnect] Polling timeout"); return; } + } else if (mcp.key === "Google Gmail MCP") { + console.log( + "[IntegrationList onConnect] Google Gmail detected, starting auth flow" + ); + + // Trigger install/authorization + const gmailItem = items.find((item) => item.key === "Google Gmail MCP"); + try { + if (gmailItem && gmailItem.onInstall) { + await gmailItem.onInstall(); + } else { + await fetchPost("/install/tool/google_gmail"); + } + } catch (_) {} + + // Select mode: poll OAuth status + if (isSelectMode) { + console.log( + "[IntegrationList onConnect] Starting OAuth status polling" + ); + + const start = Date.now(); + const timeoutMs = 5 * 60 * 1000; // 5 minutes + while (Date.now() - start < timeoutMs) { + try { + const statusRes: any = await fetchGet( + "/oauth/status/google_gmail" + ); + console.log( + "[IntegrationList onConnect] OAuth status:", + statusRes?.status + ); + + if (statusRes?.status === "success") { + console.log( + "[IntegrationList onConnect] Success! Closing dialog" + ); + await fetchInstalled(); + onClose(); + return; + } + if ( + statusRes?.status === "failed" || + statusRes?.status === "cancelled" + ) { + console.log( + "[IntegrationList onConnect] Failed/cancelled, keeping dialog open" + ); + return; + } + } catch (err) { + console.log("[IntegrationList onConnect] Polling error:", err); + } + await new Promise((r) => setTimeout(r, 1500)); + } + console.log("[IntegrationList onConnect] Polling timeout"); + return; + } } // Select mode: add to tools and close diff --git a/src/hooks/useIntegrationManagement.ts b/src/hooks/useIntegrationManagement.ts index b3ffd0663..6b03e881f 100644 --- a/src/hooks/useIntegrationManagement.ts +++ b/src/hooks/useIntegrationManagement.ts @@ -63,11 +63,11 @@ export function useIntegrationManagement(items: IntegrationItem[]) { const map: { [key: string]: boolean } = {}; items.forEach((item) => { - if (item.key === "Google Calendar") { + if (item.key === "Google Calendar" || item.key === "Google Gmail MCP") { // Only mark installed when refresh token is present (auth completed) const hasRefreshToken = configs.some( (c: any) => - c.config_group?.toLowerCase() === "google calendar" && + c.config_group?.toLowerCase() === item.key.toLowerCase() && c.config_name === "GOOGLE_REFRESH_TOKEN" && c.config_value && String(c.config_value).length > 0 ); @@ -240,7 +240,7 @@ export function useIntegrationManagement(items: IntegrationItem[]) { } } - // Clean up authentication tokens for Google Calendar and Notion + // Clean up authentication tokens for Google Calendar, Google Gmail, and Notion if (item.key === "Google Calendar") { try { await fetchDelete("/uninstall/tool/google_calendar"); @@ -248,6 +248,13 @@ export function useIntegrationManagement(items: IntegrationItem[]) { } catch (e) { console.log("Failed to clean up Google Calendar tokens:", e); } + } else if (item.key === "Google Gmail MCP") { + try { + await fetchDelete("/uninstall/tool/google_gmail"); + console.log("Cleaned up Google Gmail authentication tokens"); + } catch (e) { + console.log("Failed to clean up Google Gmail tokens:", e); + } } else if (item.key === "Notion") { try { await fetchDelete("/uninstall/tool/notion"); diff --git a/src/pages/Setting/MCP.tsx b/src/pages/Setting/MCP.tsx index 2c631f64f..cd46e4849 100644 --- a/src/pages/Setting/MCP.tsx +++ b/src/pages/Setting/MCP.tsx @@ -30,13 +30,13 @@ import { SelectItem, SelectItemWithButton } from "@/components/ui/select"; import { Tag as TagComponent } from "@/components/ui/tag"; export const GMAIL_CONFIG = { - "Google Gmail": { + "Google Gmail MCP": { "env_vars": [ "GOOGLE_CLIENT_ID", "GOOGLE_CLIENT_SECRET", "GOOGLE_REFRESH_TOKEN" ], - "toolkit": "google_gmail_mcp_toolkit" + "toolkit": "google_gmail_native_toolkit" } } @@ -327,7 +327,7 @@ export default function SettingMCP() { ); } } - } else if (key.toLowerCase() === 'google gmail') { + } else if (key.toLowerCase() === 'google gmail mcp') { onInstall = async () => { try { const response = await fetchPost("/install/tool/google_gmail"); @@ -336,13 +336,13 @@ export default function SettingMCP() { const existingConfigs = await proxyFetchGet("/api/configs"); const existing = Array.isArray(existingConfigs) ? existingConfigs.find((c: any) => - c.config_group?.toLowerCase() === "google gmail" && + c.config_group?.toLowerCase() === "google gmail mcp" && c.config_name === "GOOGLE_REFRESH_TOKEN" ) : null; const configPayload = { - config_group: "Google Gmail", + config_group: "Google Gmail MCP", config_name: "GOOGLE_REFRESH_TOKEN", config_value: "exists", }; @@ -374,13 +374,13 @@ export default function SettingMCP() { const configs = await proxyFetchGet("/api/configs"); const existing = Array.isArray(configs) ? configs.find((c: any) => - c.config_group?.toLowerCase() === "google gmail" && + c.config_group?.toLowerCase() === "google gmail mcp" && c.config_name === "GOOGLE_REFRESH_TOKEN" ) : null; const payload = { - config_group: "Google Gmail", + config_group: "Google Gmail MCP", config_name: "GOOGLE_REFRESH_TOKEN", config_value: "exists", }; @@ -436,7 +436,7 @@ export default function SettingMCP() { ? t("setting.notion-workspace-integration") : key.toLowerCase() === "google calendar" ? t("setting.google-calendar-integration") - : key.toLowerCase() === 'google gmail' + : key.toLowerCase() === 'google gmail mcp' ? "Google Gmail integration for managing emails, drafts, and contacts" : "", onInstall,