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 = () => {