diff --git a/backend/app/controller/tool_controller.py b/backend/app/controller/tool_controller.py index 4a18857f..fd8d3521 100644 --- a/backend/app/controller/tool_controller.py +++ b/backend/app/controller/tool_controller.py @@ -2,7 +2,7 @@ from loguru import logger from app.utils.toolkit.notion_mcp_toolkit import NotionMCPToolkit from app.utils.toolkit.google_calendar_toolkit import GoogleCalendarToolkit - +from app.utils.toolkit.google_gmail_native_toolkit import GoogleGmailNativeToolkit router = APIRouter(tags=["task"]) @@ -80,10 +80,32 @@ async def install_tool(tool: str): status_code=500, detail=f"Failed to install {tool}: {str(e)}" ) + elif tool == "gmail": + try: + # Use a dummy task_id for installation, as this is just for pre-authentication + toolkit = GoogleGmailNativeToolkit("install_auth") + + # Get available tools to verify connection + tools = [tool_func.func.__name__ for tool_func in toolkit.get_tools()] + logger.info(f"Successfully pre-instantiated {tool} toolkit with {len(tools)} tools") + + return { + "success": True, + "tools": tools, + "message": f"Successfully installed {tool} toolkit", + "count": len(tools), + "toolkit_name": "GoogleGmailNativeToolkit" + } + except Exception as e: + logger.error(f"Failed to install {tool} toolkit: {e}") + raise HTTPException( + status_code=500, + detail=f"Failed to install {tool}: {str(e)}" + ) 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', 'gmail']" ) @@ -110,6 +132,13 @@ async def list_available_tools(): "description": "Google Calendar integration for managing events and schedules", "toolkit_class": "GoogleCalendarToolkit", "requires_auth": True + }, + { + "name": "gmail", + "display_name": "Gmail", + "description": "Gmail integration for sending, reading, and managing emails", + "toolkit_class": "GoogleGmailNativeToolkit", + "requires_auth": True } ] } diff --git a/backend/app/service/chat_service.py b/backend/app/service/chat_service.py index a17a3451..fc7ad9d4 100644 --- a/backend/app/service/chat_service.py +++ b/backend/app/service/chat_service.py @@ -438,12 +438,12 @@ async def construct_workforce(options: Chat) -> tuple[Workforce, ListenChatAgent "generate new images from text prompts.", multi_modaler, ) - # workforce.add_single_agent_worker( - # "Social Media Agent: A social media management assistant for " - # "handling tasks related to WhatsApp, Twitter, LinkedIn, Reddit, " - # "Notion, Slack, and other social platforms.", - # await social_medium_agent(options), - # ) + workforce.add_single_agent_worker( + "Social Media Agent: A social media management assistant for " + "handling tasks related to WhatsApp, Twitter, LinkedIn, Reddit, " + "Notion, Slack, and other social platforms.", + await social_medium_agent(options), + ) mcp = await mcp_agent(options) # workforce.add_single_agent_worker( # "MCP Agent: A Model Context Protocol agent that provides access " diff --git a/backend/app/utils/agent.py b/backend/app/utils/agent.py index 64ac7486..b3d52212 100644 --- a/backend/app/utils/agent.py +++ b/backend/app/utils/agent.py @@ -24,7 +24,7 @@ from app.utils.toolkit.file_write_toolkit import FileToolkit from app.utils.toolkit.google_calendar_toolkit import GoogleCalendarToolkit from app.utils.toolkit.google_drive_mcp_toolkit import GoogleDriveMCPToolkit -from app.utils.toolkit.google_gmail_mcp_toolkit import GoogleGmailMCPToolkit +from app.utils.toolkit.google_gmail_native_toolkit import GoogleGmailNativeToolkit from app.utils.toolkit.human_toolkit import HumanToolkit from app.utils.toolkit.markitdown_toolkit import MarkItDownToolkit from app.utils.toolkit.mcp_search_toolkit import McpSearchToolkit @@ -1262,7 +1262,7 @@ async def social_medium_agent(options: Chat): *RedditToolkit.get_can_use_tools(options.task_id), *await NotionMCPToolkit.get_can_use_tools(options.task_id), # *SlackToolkit.get_can_use_tools(options.task_id), - *await GoogleGmailMCPToolkit.get_can_use_tools(options.task_id, options.get_bun_env()), + *GoogleGmailNativeToolkit.get_can_use_tools(options.task_id), *GoogleCalendarToolkit.get_can_use_tools(options.task_id), *HumanToolkit.get_can_use_tools(options.task_id, Agents.social_medium_agent), *TerminalToolkit(options.task_id, agent_name=Agents.social_medium_agent, clone_current_env=False).get_tools(), @@ -1357,7 +1357,7 @@ async def social_medium_agent(options: Chat): LinkedInToolkit.toolkit_name(), RedditToolkit.toolkit_name(), NotionMCPToolkit.toolkit_name(), - GoogleGmailMCPToolkit.toolkit_name(), + GoogleGmailNativeToolkit.toolkit_name(), GoogleCalendarToolkit.toolkit_name(), HumanToolkit.toolkit_name(), TerminalToolkit.toolkit_name(), @@ -1437,7 +1437,7 @@ async def get_toolkits(tools: list[str], agent_name: str, api_task_id: str): "github_toolkit": GithubToolkit, "google_calendar_toolkit": GoogleCalendarToolkit, "google_drive_mcp_toolkit": GoogleDriveMCPToolkit, - "google_gmail_mcp_toolkit": GoogleGmailMCPToolkit, + "google_gmail_native_toolkit": GoogleGmailNativeToolkit, "image_analysis_toolkit": ImageAnalysisToolkit, "linkedin_toolkit": LinkedInToolkit, "mcp_search_toolkit": McpSearchToolkit, diff --git a/backend/app/utils/toolkit/google_gmail_mcp_toolkit.py b/backend/app/utils/toolkit/google_gmail_mcp_toolkit.py index 68aec649..ab23ab5f 100644 --- a/backend/app/utils/toolkit/google_gmail_mcp_toolkit.py +++ b/backend/app/utils/toolkit/google_gmail_mcp_toolkit.py @@ -1,3 +1,10 @@ +""" +DEPRECATED: This MCP-based Gmail toolkit is no longer used. +Use GoogleGmailNativeToolkit instead for Gmail integration. + +This file is kept for reference only and is not imported anywhere in the codebase. +""" + from camel.toolkits import BaseToolkit, FunctionTool, MCPToolkit from app.component.environment import env, env_or_fail from app.component.command import bun @@ -6,6 +13,11 @@ class GoogleGmailMCPToolkit(BaseToolkit, AbstractToolkit): + """ + DEPRECATED: Use GoogleGmailNativeToolkit instead. + + This MCP-based implementation is no longer integrated into the agent system. + """ agent_name: str = Agents.social_medium_agent def __init__( diff --git a/backend/app/utils/toolkit/google_gmail_native_toolkit.py b/backend/app/utils/toolkit/google_gmail_native_toolkit.py new file mode 100644 index 00000000..6e55d8df --- /dev/null +++ b/backend/app/utils/toolkit/google_gmail_native_toolkit.py @@ -0,0 +1,295 @@ +from typing import Any, Dict, List, Literal, Optional, Union + +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 + + +class GoogleGmailNativeToolkit(BaseGmailToolkit, AbstractToolkit): + """Eigent wrapper for CAMEL's native Gmail toolkit.""" + + agent_name: str = Agents.social_medium_agent + + def __init__( + self, + api_task_id: str, + timeout: Optional[float] = None, + ): + """Initialize the Gmail toolkit. + + Args: + api_task_id: The task ID for tracking + timeout: Optional timeout for API requests + """ + self.api_task_id = api_task_id + super().__init__(timeout=timeout) + + # Email Sending Operations + @listen_toolkit( + BaseGmailToolkit.send_email, + lambda _, to, subject, **kwargs: f"Sending email to '{to}' with subject '{subject}'" + ) + def send_email( + self, + to: Union[str, List[str]], + subject: str, + body: str, + cc: Optional[Union[str, List[str]]] = None, + bcc: Optional[Union[str, List[str]]] = None, + attachments: Optional[List[str]] = None, + is_html: bool = False, + ) -> Dict[str, Any]: + return super().send_email(to, subject, body, cc, bcc, attachments, is_html) + + @listen_toolkit( + BaseGmailToolkit.reply_to_email, + lambda _, message_id, reply_body, **kwargs: f"Replying to message {message_id}" + ) + def reply_to_email( + self, + message_id: str, + reply_body: str, + reply_all: bool = False, + is_html: bool = False, + ) -> Dict[str, Any]: + return super().reply_to_email(message_id, reply_body, reply_all, is_html) + + @listen_toolkit( + BaseGmailToolkit.forward_email, + lambda _, message_id, to, **kwargs: f"Forwarding message {message_id} to '{to}'" + ) + def forward_email( + self, + message_id: str, + to: Union[str, List[str]], + forward_body: Optional[str] = None, + cc: Optional[Union[str, List[str]]] = None, + bcc: Optional[Union[str, List[str]]] = None, + include_attachments: bool = True, + ) -> Dict[str, Any]: + return super().forward_email(message_id, to, forward_body, cc, bcc, include_attachments) + + # Draft Operations + @listen_toolkit( + BaseGmailToolkit.create_email_draft, + lambda _, to, subject, **kwargs: f"Creating draft to '{to}' with subject '{subject}'" + ) + def create_email_draft( + self, + to: Union[str, List[str]], + subject: str, + body: str, + cc: Optional[Union[str, List[str]]] = None, + bcc: Optional[Union[str, List[str]]] = None, + attachments: Optional[List[str]] = None, + is_html: bool = False, + ) -> Dict[str, Any]: + return super().create_email_draft(to, subject, body, cc, bcc, attachments, is_html) + + @listen_toolkit( + BaseGmailToolkit.send_draft, + lambda _, draft_id: f"Sending draft {draft_id}" + ) + def send_draft(self, draft_id: str) -> Dict[str, Any]: + return super().send_draft(draft_id) + + @listen_toolkit( + BaseGmailToolkit.list_drafts, + lambda _, max_results=10: f"Listing {max_results} drafts" + ) + def list_drafts(self, max_results: int = 10) -> Dict[str, Any]: + return super().list_drafts(max_results) + + # Email Fetching Operations + @listen_toolkit( + BaseGmailToolkit.fetch_emails, + lambda _, query="", max_results=10, **kwargs: f"Fetching {max_results} emails with query '{query}'" + ) + def fetch_emails( + self, + query: str = "", + max_results: int = 10, + include_spam_trash: bool = False, + label_ids: Optional[List[str]] = None, + ) -> Dict[str, Any]: + return super().fetch_emails(query, max_results, include_spam_trash, label_ids) + + @listen_toolkit( + BaseGmailToolkit.fetch_thread_by_id, + lambda _, thread_id: f"Fetching thread {thread_id}" + ) + def fetch_thread_by_id(self, thread_id: str) -> Dict[str, Any]: + return super().fetch_thread_by_id(thread_id) + + @listen_toolkit( + BaseGmailToolkit.list_threads, + lambda _, query="", max_results=10, **kwargs: f"Listing {max_results} threads with query '{query}'" + ) + def list_threads( + self, + query: str = "", + max_results: int = 10, + include_spam_trash: bool = False, + label_ids: Optional[List[str]] = None, + ) -> Dict[str, Any]: + return super().list_threads(query, max_results, include_spam_trash, label_ids) + + # Label Management + @listen_toolkit( + BaseGmailToolkit.modify_email_labels, + lambda _, message_id, add_labels=None, remove_labels=None: + f"Modifying labels on message {message_id} (add: {add_labels}, remove: {remove_labels})" + ) + def modify_email_labels( + self, + message_id: str, + add_labels: Optional[List[str]] = None, + remove_labels: Optional[List[str]] = None, + ) -> Dict[str, Any]: + return super().modify_email_labels(message_id, add_labels, remove_labels) + + @listen_toolkit( + BaseGmailToolkit.modify_thread_labels, + lambda _, thread_id, add_labels=None, remove_labels=None: + f"Modifying labels on thread {thread_id} (add: {add_labels}, remove: {remove_labels})" + ) + def modify_thread_labels( + self, + thread_id: str, + add_labels: Optional[List[str]] = None, + remove_labels: Optional[List[str]] = None, + ) -> Dict[str, Any]: + return super().modify_thread_labels(thread_id, add_labels, remove_labels) + + @listen_toolkit( + BaseGmailToolkit.list_gmail_labels, + lambda _: "Listing all Gmail labels" + ) + def list_gmail_labels(self) -> Dict[str, Any]: + return super().list_gmail_labels() + + @listen_toolkit( + BaseGmailToolkit.create_label, + lambda _, name, **kwargs: f"Creating label '{name}'" + ) + def create_label( + self, + name: str, + label_list_visibility: Literal["labelShow", "labelHide"] = "labelShow", + message_list_visibility: Literal["show", "hide"] = "show", + ) -> Dict[str, Any]: + return super().create_label(name, label_list_visibility, message_list_visibility) + + @listen_toolkit( + BaseGmailToolkit.delete_label, + lambda _, label_id: f"Deleting label {label_id}" + ) + def delete_label(self, label_id: str) -> Dict[str, Any]: + return super().delete_label(label_id) + + # Utility Operations + @listen_toolkit( + BaseGmailToolkit.move_to_trash, + lambda _, message_id: f"Moving message {message_id} to trash" + ) + def move_to_trash(self, message_id: str) -> Dict[str, Any]: + return super().move_to_trash(message_id) + + @listen_toolkit( + BaseGmailToolkit.get_attachment, + lambda _, message_id, attachment_id, **kwargs: + f"Getting attachment {attachment_id} from message {message_id}" + ) + def get_attachment( + self, + message_id: str, + attachment_id: str, + save_path: Optional[str] = None, + ) -> Dict[str, Any]: + return super().get_attachment(message_id, attachment_id, save_path) + + @listen_toolkit( + BaseGmailToolkit.get_profile, + lambda _: "Getting Gmail profile" + ) + def get_profile(self) -> Dict[str, Any]: + return super().get_profile() + + # Contact Operations + @listen_toolkit( + BaseGmailToolkit.get_contacts, + lambda _, query="", max_results=100: f"Getting contacts with query '{query}' (max: {max_results})" + ) + def get_contacts( + self, + query: str = "", + max_results: int = 100, + ) -> Dict[str, Any]: + return super().get_contacts(query, max_results) + + @listen_toolkit( + BaseGmailToolkit.search_people, + lambda _, query, max_results=10: f"Searching people with query '{query}' (max: {max_results})" + ) + def search_people( + self, + query: str, + max_results: int = 10, + ) -> Dict[str, Any]: + return super().search_people(query, max_results) + + @classmethod + def get_can_use_tools(cls, api_task_id: str) -> list[FunctionTool]: + """Check if Gmail toolkit can be used and return available tools. + + Requires GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET environment variables. + """ + if not env("GOOGLE_CLIENT_ID") or not env("GOOGLE_CLIENT_SECRET"): + logger.warning( + "Gmail toolkit unavailable: GOOGLE_CLIENT_ID or GOOGLE_CLIENT_SECRET not set" + ) + return [] + + try: + toolkit = cls(api_task_id) + tools = toolkit.get_tools() + + # Mark each tool with the toolkit name for tracking + for tool in tools: + setattr(tool, "_toolkit_name", cls.__name__) + + logger.info(f"Gmail toolkit initialized with {len(tools)} tools") + return tools + + except Exception as e: + logger.error(f"Failed to initialize Gmail toolkit: {e}") + return [] + + def get_tools(self) -> List[FunctionTool]: + """Return all available Gmail tools.""" + return [ + FunctionTool(self.send_email), + FunctionTool(self.reply_to_email), + FunctionTool(self.forward_email), + FunctionTool(self.create_email_draft), + FunctionTool(self.send_draft), + FunctionTool(self.list_drafts), + FunctionTool(self.fetch_emails), + FunctionTool(self.fetch_thread_by_id), + FunctionTool(self.list_threads), + FunctionTool(self.modify_email_labels), + FunctionTool(self.modify_thread_labels), + FunctionTool(self.list_gmail_labels), + FunctionTool(self.create_label), + FunctionTool(self.delete_label), + FunctionTool(self.move_to_trash), + FunctionTool(self.get_attachment), + FunctionTool(self.get_profile), + FunctionTool(self.get_contacts), + FunctionTool(self.search_people), + ] \ No newline at end of file diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 05049886..bc7310df 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -5,7 +5,7 @@ description = "Add your description here" readme = "README.md" requires-python = "==3.10.16" dependencies = [ - "camel-ai[eigent]==0.2.76a13", + "camel-ai[eigent] @ git+https://github.com/camel-ai/camel.git@gmail-toolkit", "fastapi>=0.115.12", "fastapi-babel>=1.0.0", "uvicorn[standard]>=0.34.2", diff --git a/backend/tests/unit/utils/test_agent.py b/backend/tests/unit/utils/test_agent.py index 161db996..76ec4029 100644 --- a/backend/tests/unit/utils/test_agent.py +++ b/backend/tests/unit/utils/test_agent.py @@ -673,7 +673,8 @@ async def test_social_medium_agent_creation(self, sample_chat_data): patch('app.utils.agent.LinkedInToolkit') as mock_linkedin_toolkit, \ patch('app.utils.agent.RedditToolkit') as mock_reddit_toolkit, \ patch('app.utils.agent.NotionMCPToolkit') as mock_notion_mcp_toolkit, \ - patch('app.utils.agent.GoogleGmailMCPToolkit') as mock_gmail_toolkit, \ + # patch('app.utils.agent.GoogleGmailMCPToolkit') as mock_gmail_mcp_toolkit, \ # Deprecated - MCP version + patch('app.utils.agent.GoogleGmailNativeToolkit') as mock_gmail_toolkit, \ patch('app.utils.agent.GoogleCalendarToolkit') as mock_calendar_toolkit, \ patch('app.utils.agent.HumanToolkit') as mock_human_toolkit, \ patch('app.utils.agent.TerminalToolkit') as mock_terminal_toolkit, \ @@ -685,7 +686,8 @@ async def test_social_medium_agent_creation(self, sample_chat_data): mock_linkedin_toolkit.get_can_use_tools.return_value = [] mock_reddit_toolkit.get_can_use_tools.return_value = [] mock_notion_mcp_toolkit.get_can_use_tools = AsyncMock(return_value=[]) - mock_gmail_toolkit.get_can_use_tools = AsyncMock(return_value=[]) + # mock_gmail_mcp_toolkit.get_can_use_tools = AsyncMock(return_value=[]) # Deprecated - MCP version + mock_gmail_toolkit.get_can_use_tools.return_value = [] # Native toolkit (not async) mock_calendar_toolkit.get_can_use_tools.return_value = [] mock_human_toolkit.get_can_use_tools.return_value = [] mock_terminal_toolkit.return_value.get_tools.return_value = [] diff --git a/backend/uv.lock b/backend/uv.lock index cfe0b277..679861da 100644 --- a/backend/uv.lock +++ b/backend/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 2 +revision = 3 requires-python = "==3.10.16" [[package]] @@ -122,6 +122,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c7/d1/69d02ce34caddb0a7ae088b84c356a625a93cd4ff57b2f97644c03fad905/asgiref-3.9.2-py3-none-any.whl", hash = "sha256:0b61526596219d70396548fc003635056856dba5d0d086f86476f10b33c75960", size = 23788, upload-time = "2025-09-23T15:00:53.627Z" }, ] +[[package]] +name = "astor" +version = "0.8.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/21/75b771132fee241dfe601d39ade629548a9626d1d39f333fde31bc46febe/astor-0.8.1.tar.gz", hash = "sha256:6a6effda93f4e1ce9f618779b2dd1d9d84f1e32812c23a29b3fff6fd7f63fa5e", size = 35090, upload-time = "2019-12-10T01:50:35.51Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c3/88/97eef84f48fa04fbd6750e62dcceafba6c63c81b7ac1420856c8dcc0a3f9/astor-0.8.1-py2.py3-none-any.whl", hash = "sha256:070a54e890cefb5b3739d19f30f5a5ec840ffc9c50ffa7d23cc9fc1a38ebbfc5", size = 27488, upload-time = "2019-12-10T01:50:33.628Z" }, +] + [[package]] name = "async-timeout" version = "5.0.1" @@ -240,7 +249,7 @@ dev = [ [package.metadata] requires-dist = [ { name = "aiofiles", specifier = ">=24.1.0" }, - { name = "camel-ai", extras = ["eigent"], specifier = "==0.2.76a13" }, + { name = "camel-ai", extras = ["eigent"], git = "https://github.com/camel-ai/camel.git?rev=gmail-toolkit" }, { name = "fastapi", specifier = ">=0.115.12" }, { name = "fastapi-babel", specifier = ">=1.0.0" }, { name = "httpx", extras = ["socks"], specifier = ">=0.28.1" }, @@ -324,9 +333,10 @@ wheels = [ [[package]] name = "camel-ai" -version = "0.2.76a13" -source = { registry = "https://pypi.org/simple" } +version = "0.2.77" +source = { git = "https://github.com/camel-ai/camel.git?rev=gmail-toolkit#8f4d073c49f494ec8801c5f5874fd3dec014f0dc" } dependencies = [ + { name = "astor" }, { name = "colorama" }, { name = "docstring-parser" }, { name = "httpx" }, @@ -339,10 +349,6 @@ dependencies = [ { name = "tiktoken" }, { name = "websockets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f7/7c/0145edf0307e360557917de28691eb0c41b36b017a28c6b67e58a729a6da/camel_ai-0.2.76a13.tar.gz", hash = "sha256:487570c36a39a333ae8000783babd5a82350a829aaa8aa2ae712470b596cafe1", size = 950278, upload-time = "2025-10-06T06:09:46.064Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/04/46/9886106669491737631178830bce79bd7bf63391db4d2200f645089dd9df/camel_ai-0.2.76a13-py3-none-any.whl", hash = "sha256:b860412e4a5b5fc31b0cc3d4b1eeefcd02382d9a5aced252856a1eff0285a97b", size = 1400549, upload-time = "2025-10-06T06:09:43.291Z" }, -] [package.optional-dependencies] eigent = [ diff --git a/server/app/model/config/config.py b/server/app/model/config/config.py index 022a520d..9e5e8d2d 100644 --- a/server/app/model/config/config.py +++ b/server/app/model/config/config.py @@ -120,6 +120,13 @@ class ConfigInfo: ], "toolkit": "google_calendar_toolkit", }, + ConfigGroup.GMAIL.value: { + "env_vars": [ + "GOOGLE_CLIENT_ID", + "GOOGLE_CLIENT_SECRET", + ], + "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 ba7b6605..980fdad4 100644 --- a/server/app/type/config_group.py +++ b/server/app/type/config_group.py @@ -21,7 +21,8 @@ class ConfigGroup(str, Enum): GITHUB = "Github" GOOGLE_CALENDAR = "Google Calendar" GOOGLE_DRIVE_MCP = "Google Drive MCP" - GOOGLE_GMAIL_MCP = "Google Gmail MCP" + # GOOGLE_GMAIL_MCP = "Google Gmail MCP" # Deprecated - use GMAIL instead + GMAIL = "Gmail" IMAGE_ANALYSIS = "Image Analysis" MCP_SEARCH = "MCP Search" PPTX = "PPTX" diff --git a/src/pages/Setting/MCP.tsx b/src/pages/Setting/MCP.tsx index 4f7cee6b..0e148670 100644 --- a/src/pages/Setting/MCP.tsx +++ b/src/pages/Setting/MCP.tsx @@ -102,6 +102,29 @@ export default function SettingMCP() { ), }, + { + key: "Gmail", + name: "Gmail", + env_vars: ["GOOGLE_CLIENT_ID", "GOOGLE_CLIENT_SECRET"], + desc: ( + <> + {t("setting.environmental-variables-required")}: GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET +
+ + Use your Google Cloud OAuth 2.0 Client ID and Secret. Create at {""} + { + window.location.href = "https://console.cloud.google.com/apis/credentials"; + }} + className="underline text-blue-500" + > + Google Cloud Console + + + + ), + onInstall: () => {}, // Empty function - actual handling is in IntegrationList.tsx + }, ]); // get integrations diff --git a/src/pages/Setting/components/IntegrationList.tsx b/src/pages/Setting/components/IntegrationList.tsx index 20fb328a..a5a9208c 100644 --- a/src/pages/Setting/components/IntegrationList.tsx +++ b/src/pages/Setting/components/IntegrationList.tsx @@ -9,6 +9,7 @@ import { proxyFetchGet, proxyFetchPost, proxyFetchDelete, + fetchPost, } from "@/api/http"; import React, { useState, useCallback, useEffect, useRef } from "react"; @@ -18,6 +19,7 @@ import { MCPEnvDialog } from "./MCPEnvDialog"; import { useAuthStore } from "@/store/authStore"; import { OAuth } from "@/lib/oauth"; import { useTranslation } from "react-i18next"; +import { toast } from "sonner"; interface IntegrationItem { key: string; name: string; @@ -102,8 +104,21 @@ export default function IntegrationList({ config_name: envVarKey, config_value: value, }; - await proxyFetchPost("/api/configs", configPayload); + console.log("📤 Sending config to API:", configPayload); + const response = await proxyFetchPost("/api/configs", configPayload); + console.log("📥 API response:", response); + + // Check for errors + if (response && response.error) { + console.error("❌ API ERROR DETAILS:", response.error); + console.error("❌ Full error object:", JSON.stringify(response.error, null, 2)); + } + if (response && response.code === 100) { + console.error("❌ API returned error code 100"); + } + if (window.electronAPI?.envWrite) { + console.log("💻 Writing to electron env:", { key: envVarKey, value }); await window.electronAPI.envWrite(email, { key: envVarKey, value }); } }; @@ -228,38 +243,80 @@ export default function IntegrationList({ return; } - if (item.key === "Google Calendar") { - let mcp = { - name: "Google Calendar", - key: "Google Calendar", - install_command: { - env: {} as any, - }, - id: 14, - }; - item.env_vars.map((key) => { - mcp.install_command.env[key] = ""; - }); - setActiveMcp(mcp); - setShowEnvConfig(true); - return; - } + if (item.key === "Google Calendar") { + let mcp = { + name: "Google Calendar", + key: "Google Calendar", + install_command: { + env: {} as any, + }, + id: 14, + }; + item.env_vars.map((key) => { + mcp.install_command.env[key] = ""; + }); + setActiveMcp(mcp); + setShowEnvConfig(true); + return; + } - if (installed[item.key]) return; + if (item.key === "Gmail") { + let mcp = { + name: "Gmail", + key: "Gmail", + install_command: { + env: {} as any, + }, + id: 15, + }; + item.env_vars.map((key) => { + mcp.install_command.env[key] = ""; + }); + setActiveMcp(mcp); + setShowEnvConfig(true); + return; + } + + if (installed[item.key]) return; await item.onInstall(); }, [installed] ); const onConnect = async (mcp: any) => { - console.log(mcp); + console.log("🔌 onConnect called with MCP:", mcp); + console.log("🔑 Env values to save:", mcp.install_command.env); + await Promise.all( Object.keys(mcp.install_command.env).map((key) => { + console.log(`💾 Saving ${key}:`, mcp.install_command.env[key]); return saveEnvAndConfig(mcp.key, key, mcp.install_command.env[key]); }) ); + console.log("✅ All env values saved"); + + // Trigger OAuth flow for Gmail + if (mcp.key && mcp.key === "Gmail") { + console.log("🔐 Triggering Gmail installation..."); + try { + const response = await fetchPost("/install/tool/gmail"); + if (response.success) { + console.log("✅ Gmail toolkit installed successfully!"); + toast.success("Gmail toolkit installed successfully!"); + setInstalled((prev) => ({ ...prev, [mcp.key]: true })); + } else { + console.error("❌ Installation failed:", response.error); + toast.error("Gmail installation failed: " + (response.error || "Unknown error")); + } + } catch (error: any) { + console.error("❌ Installation error:", error); + toast.error("Failed to install Gmail: " + error.message); + } + } + console.log("🔄 Fetching installed integrations..."); fetchInstalled(); + console.log("🚪 Closing dialog"); onClose(); }; const onClose = () => {