from fastapi import APIRouter, HTTPException
from app.utils.toolkit.notion_mcp_toolkit import NotionMCPToolkit
from app.utils.toolkit.google_calendar_toolkit import GoogleCalendarToolkit
from utils import traceroot_wrapper as traceroot
from camel.toolkits.hybrid_browser_toolkit.hybrid_browser_toolkit_ts import (
HybridBrowserToolkit as BaseHybridBrowserToolkit,
)
from app.utils.cookie_manager import CookieManager
import os
import uuid
logger = traceroot.get_logger("tool_controller")
router = APIRouter(tags=["task"])
@router.post("/install/tool/{tool}", name="install tool")
async def install_tool(tool: str):
"""
Install and pre-instantiate a specific MCP tool for authentication
Args:
tool: Tool name to install (notion)
Returns:
Installation result with tool information
"""
if tool == "notion":
try:
# Use a dummy task_id for installation, as this is just for pre-authentication
toolkit = NotionMCPToolkit("install_auth")
try:
# Pre-instantiate by connecting (this completes authentication)
await toolkit.connect()
# 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")
# Disconnect, authentication info is saved
await toolkit.disconnect()
return {
"success": True,
"tools": tools,
"message": f"Successfully installed and authenticated {tool} toolkit",
"count": len(tools),
"toolkit_name": "NotionMCPToolkit"
}
except Exception as connect_error:
logger.warning(
f"Could not connect to {tool} MCP server: {connect_error}")
# Even if connection fails, mark as installed so user can use it later
return {
"success": True,
"tools": [],
"message": f"{tool} toolkit installed but not connected. Will connect when needed.",
"count": 0,
"toolkit_name": "NotionMCPToolkit",
"warning": "Could not connect to Notion MCP server. You may need to authenticate when using the tool."
}
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)}"
)
elif tool == "google_calendar":
try:
# Use a dummy task_id for installation, as this is just for pre-authentication
toolkit = GoogleCalendarToolkit("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": "GoogleCalendarToolkit"
}
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']"
)
@router.get("/tools/available", name="list available tools")
async def list_available_tools():
"""
List all available MCP tools that can be installed
Returns:
List of available tools with their information
"""
return {
"tools": [
{
"name": "notion",
"display_name": "Notion MCP",
"description": "Notion workspace integration for reading and managing Notion pages",
"toolkit_class": "NotionMCPToolkit",
"requires_auth": True
},
{
"name": "google_calendar",
"display_name": "Google Calendar",
"description": "Google Calendar integration for managing events and schedules",
"toolkit_class": "GoogleCalendarToolkit",
"requires_auth": True
}
]
}
@router.post("/browser/login", name="open browser for login")
async def open_browser_login():
"""
Open an Electron-based Chrome browser for user login with a dedicated user data directory
Returns:
Browser session information
"""
try:
import subprocess
import platform
import socket
import json
# Use fixed profile name for persistent logins (no port suffix)
session_id = "user_login"
cdp_port = 9223
# IMPORTANT: Use dedicated profile for tool_controller browser
# This is the SOURCE OF TRUTH for login data
# On Eigent startup, this data will be copied to WebView partition (one-way sync)
browser_profiles_base = os.path.expanduser("~/.eigent/browser_profiles")
user_data_dir = os.path.join(browser_profiles_base, "profile_user_login")
os.makedirs(user_data_dir, exist_ok=True)
logger.info(
f"Creating browser session {session_id} with profile at: {user_data_dir}")
# Check if browser is already running on this port
def is_port_in_use(port):
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
return s.connect_ex(('localhost', port)) == 0
if is_port_in_use(cdp_port):
logger.info(f"Browser already running on port {cdp_port}")
return {
"success": True,
"session_id": session_id,
"user_data_dir": user_data_dir,
"cdp_port": cdp_port,
"message": "Browser already running. Use existing window to log in.",
"note": "Your login data will be saved in the profile."
}
# Create Electron browser script with .cjs extension for CommonJS
electron_script_path = os.path.join(os.path.dirname(__file__), "electron_browser.cjs")
electron_script_content = '''
const { app, BrowserWindow, ipcMain } = require('electron');
const path = require('path');
// Parse command line arguments
const args = process.argv.slice(2);
const userDataDir = args[0];
const cdpPort = args[1];
const startUrl = args[2] || 'https://www.google.com';
// This must be called before app.ready
app.commandLine.appendSwitch('remote-debugging-port', cdpPort);
console.log('[ELECTRON BROWSER] Starting with:');
console.log(' Chrome version:', process.versions.chrome);
console.log(' User data dir (requested):', userDataDir);
console.log(' CDP port:', cdpPort);
console.log(' Start URL:', startUrl);
// Set app paths - must be done before app.ready
// Do NOT use commandLine.appendSwitch('user-data-dir') as it conflicts with setPath
app.setPath('userData', userDataDir);
app.setPath('sessionData', userDataDir);
app.whenReady().then(async () => {
const { session } = require('electron');
const fs = require('fs');
const path = require('path');
// Log actual paths being used
console.log('[ELECTRON BROWSER] Actual paths:');
console.log(' app.getPath("userData"):', app.getPath('userData'));
console.log(' app.getPath("sessionData"):', app.getPath('sessionData'));
console.log(' app.getPath("cache"):', app.getPath('cache'));
console.log(' app.getPath("temp"):', app.getPath('temp'));
console.log(' process.argv:', process.argv);
// Check command line switches
console.log('[ELECTRON BROWSER] Command line switches:');
console.log(' user-data-dir:', app.commandLine.getSwitchValue('user-data-dir'));
console.log(' remote-debugging-port:', app.commandLine.getSwitchValue('remote-debugging-port'));
// Log partition session info
const userLoginSession = session.fromPartition('persist:user_login');
console.log('[ELECTRON BROWSER] Session info:');
console.log(' Partition: persist:user_login');
console.log(' Session storage path:', userLoginSession.getStoragePath());
// Check if Cookies file exists
const cookiesPath = path.join(app.getPath('userData'), 'Partitions', 'user_login', 'Cookies');
console.log('[ELECTRON BROWSER] Cookies path:', cookiesPath);
console.log('[ELECTRON BROWSER] Cookies exists:', fs.existsSync(cookiesPath));
if (fs.existsSync(cookiesPath)) {
const stats = fs.statSync(cookiesPath);
console.log('[ELECTRON BROWSER] Cookies file size:', stats.size, 'bytes');
}
const win = new BrowserWindow({
width: 1400,
height: 900,
title: 'Eigent Browser - Login',
webPreferences: {
nodeIntegration: true,
contextIsolation: false,
webviewTag: true
}
});
// Create navigation bar and webview
const html = `
`;
win.loadURL('data:text/html;charset=UTF-8,' + encodeURIComponent(html));
// Show window when ready
win.once('ready-to-show', () => {
win.show();
// Log cookies periodically to track changes
setInterval(async () => {
try {
const cookies = await userLoginSession.cookies.get({});
console.log('[ELECTRON BROWSER] Current cookies count:', cookies.length);
if (cookies.length > 0) {
console.log('[ELECTRON BROWSER] Cookie domains:', [...new Set(cookies.map(c => c.domain))]);
}
} catch (error) {
console.error('[ELECTRON BROWSER] Failed to get cookies:', error);
}
}, 5000); // Check every 5 seconds
});
win.on('closed', async () => {
console.log('[ELECTRON BROWSER] Window closed, preparing to quit...');
// Flush storage data before quitting to ensure cookies are saved
try {
const { session } = require('electron');
const fs = require('fs');
const path = require('path');
const userLoginSession = session.fromPartition('persist:user_login');
// Log cookies before flush
const cookiesBeforeFlush = await userLoginSession.cookies.get({});
console.log('[ELECTRON BROWSER] Cookies count before flush:', cookiesBeforeFlush.length);
// Flush storage
console.log('[ELECTRON BROWSER] Flushing storage data...');
await userLoginSession.flushStorageData();
console.log('[ELECTRON BROWSER] Storage data flushed successfully');
// Check cookies file after flush
const cookiesPath = path.join(app.getPath('userData'), 'Partitions', 'user_login', 'Cookies');
if (fs.existsSync(cookiesPath)) {
const stats = fs.statSync(cookiesPath);
console.log('[ELECTRON BROWSER] Cookies file size after flush:', stats.size, 'bytes');
} else {
console.log('[ELECTRON BROWSER] WARNING: Cookies file does not exist after flush!');
}
} catch (error) {
console.error('[ELECTRON BROWSER] Failed to flush storage data:', error);
}
app.quit();
});
});
let isQuitting = false;
app.on('before-quit', async (event) => {
if (isQuitting) return;
// Prevent immediate quit to allow storage flush and cookie sync
event.preventDefault();
isQuitting = true;
console.log('[ELECTRON BROWSER] before-quit event triggered');
try {
const { session } = require('electron');
const fs = require('fs');
const path = require('path');
const userLoginSession = session.fromPartition('persist:user_login');
// Log cookies before flush
const cookiesBeforeQuit = await userLoginSession.cookies.get({});
console.log('[ELECTRON BROWSER] Cookies count before quit:', cookiesBeforeQuit.length);
if (cookiesBeforeQuit.length > 0) {
console.log('[ELECTRON BROWSER] Cookie domains before quit:', [...new Set(cookiesBeforeQuit.map(c => c.domain))]);
}
// Flush storage
console.log('[ELECTRON BROWSER] Flushing storage on quit...');
await userLoginSession.flushStorageData();
console.log('[ELECTRON BROWSER] Storage data flushed on quit');
} catch (error) {
console.error('[ELECTRON BROWSER] Failed to sync cookies:', error);
} finally {
console.log('[ELECTRON BROWSER] Exiting now...');
// Force quit after sync
app.exit(0);
}
});
app.on('window-all-closed', () => {
if (!isQuitting) {
app.quit();
}
});
'''
# Write the Electron script
with open(electron_script_path, 'w') as f:
f.write(electron_script_content)
# Find Electron executable
# Try to use the same Electron version as the main app
electron_cmd = "npx"
electron_args = [
electron_cmd,
"electron",
electron_script_path,
user_data_dir,
str(cdp_port),
"https://www.google.com"
]
# Get the app's directory to run npx in the right context
app_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(__file__))))
logger.info(f"[PROFILE USER LOGIN] Launching Electron browser with CDP on port {cdp_port}")
logger.info(f"[PROFILE USER LOGIN] Working directory: {app_dir}")
logger.info(f"[PROFILE USER LOGIN] userData path: {user_data_dir}")
logger.info(f"[PROFILE USER LOGIN] Electron args: {electron_args}")
# Start process and capture output in real-time
process = subprocess.Popen(
electron_args,
cwd=app_dir,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT, # Redirect stderr to stdout
universal_newlines=True,
bufsize=1 # Line buffered
)
# Create async task to log Electron output
async def log_electron_output():
for line in iter(process.stdout.readline, ''):
if line:
logger.info(f"[ELECTRON OUTPUT] {line.strip()}")
import asyncio
asyncio.create_task(log_electron_output())
# Wait a bit for Electron to start
import asyncio
await asyncio.sleep(3)
# Clean up the script file after a delay
async def cleanup_script():
await asyncio.sleep(10)
try:
os.remove(electron_script_path)
except:
pass
asyncio.create_task(cleanup_script())
logger.info(f"[PROFILE USER LOGIN] Electron browser launched with PID {process.pid}")
return {
"success": True,
"session_id": session_id,
"user_data_dir": user_data_dir,
"cdp_port": cdp_port,
"pid": process.pid,
"chrome_version": "130.0.6723.191", # Electron 33's Chrome version
"message": "Electron browser opened successfully. Please log in to your accounts.",
"note": "The browser will remain open for you to log in. Your login data will be saved in the profile."
}
except Exception as e:
logger.error(f"Failed to open Electron browser for login: {e}")
raise HTTPException(
status_code=500,
detail=f"Failed to open browser: {str(e)}"
)
@router.get("/browser/cookies", name="list cookie domains")
async def list_cookie_domains(search: str = None):
"""
list cookie domains
Args:
search: url
Returns:
list of cookie domains
"""
try:
# Use tool_controller browser's user data directory (source of truth)
user_data_base = os.path.expanduser("~/.eigent/browser_profiles")
user_data_dir = os.path.join(user_data_base, "profile_user_login")
logger.info(f"[COOKIES CHECK] Tool controller user_data_dir: {user_data_dir}")
logger.info(f"[COOKIES CHECK] Tool controller user_data_dir exists: {os.path.exists(user_data_dir)}")
# Check partition path
partition_path = os.path.join(user_data_dir, "Partitions", "user_login")
logger.info(f"[COOKIES CHECK] partition path: {partition_path}")
logger.info(f"[COOKIES CHECK] partition exists: {os.path.exists(partition_path)}")
# Check cookies file
cookies_file = os.path.join(partition_path, "Cookies")
logger.info(f"[COOKIES CHECK] cookies file: {cookies_file}")
logger.info(f"[COOKIES CHECK] cookies file exists: {os.path.exists(cookies_file)}")
if os.path.exists(cookies_file):
stat = os.stat(cookies_file)
logger.info(f"[COOKIES CHECK] cookies file size: {stat.st_size} bytes")
# Try to read actual cookie count
try:
import sqlite3
conn = sqlite3.connect(cookies_file)
cursor = conn.cursor()
cursor.execute("SELECT COUNT(*) FROM cookies")
count = cursor.fetchone()[0]
logger.info(f"[COOKIES CHECK] actual cookie count in database: {count}")
conn.close()
except Exception as e:
logger.error(f"[COOKIES CHECK] failed to read cookie count: {e}")
if not os.path.exists(user_data_dir):
return {
"success": True,
"domains": [],
"message": "No browser profile found. Please login first using /browser/login."
}
cookie_manager = CookieManager(user_data_dir)
if search:
domains = cookie_manager.search_cookies(search)
else:
domains = cookie_manager.get_cookie_domains()
return {
"success": True,
"domains": domains,
"total": len(domains),
"user_data_dir": user_data_dir
}
except Exception as e:
logger.error(f"Failed to list cookie domains: {e}")
raise HTTPException(
status_code=500,
detail=f"Failed to list cookies: {str(e)}"
)
@router.get("/browser/cookies/{domain}", name="get domain cookies")
async def get_domain_cookies(domain: str):
"""
get domain cookies
Args:
domain
Returns:
cookies
"""
try:
user_data_base = os.path.expanduser("~/.eigent/browser_profiles")
user_data_dir = os.path.join(user_data_base, "profile_user_login")
if not os.path.exists(user_data_dir):
raise HTTPException(
status_code=404,
detail="No browser profile found. Please login first using /browser/login."
)
cookie_manager = CookieManager(user_data_dir)
cookies = cookie_manager.get_cookies_for_domain(domain)
return {
"success": True,
"domain": domain,
"cookies": cookies,
"count": len(cookies)
}
except HTTPException:
raise
except Exception as e:
logger.error(f"Failed to get cookies for domain {domain}: {e}")
raise HTTPException(
status_code=500,
detail=f"Failed to get cookies: {str(e)}"
)
@router.delete("/browser/cookies/{domain}", name="delete domain cookies")
async def delete_domain_cookies(domain: str):
"""
Delete cookies
Args:
domain
Returns:
deleted cookies
"""
try:
user_data_base = os.path.expanduser("~/.eigent/browser_profiles")
user_data_dir = os.path.join(user_data_base, "profile_user_login")
if not os.path.exists(user_data_dir):
raise HTTPException(
status_code=404,
detail="No browser profile found. Please login first using /browser/login."
)
cookie_manager = CookieManager(user_data_dir)
success = cookie_manager.delete_cookies_for_domain(domain)
if success:
return {
"success": True,
"message": f"Successfully deleted cookies for domain: {domain}"
}
else:
raise HTTPException(
status_code=500,
detail=f"Failed to delete cookies for domain: {domain}"
)
except HTTPException:
raise
except Exception as e:
logger.error(f"Failed to delete cookies for domain {domain}: {e}")
raise HTTPException(
status_code=500,
detail=f"Failed to delete cookies: {str(e)}"
)
@router.delete("/browser/cookies", name="delete all cookies")
async def delete_all_cookies():
"""
delete all cookies
Returns:
deleted cookies
"""
try:
user_data_base = os.path.expanduser("~/.eigent/browser_profiles")
user_data_dir = os.path.join(user_data_base, "profile_user_login")
if not os.path.exists(user_data_dir):
raise HTTPException(
status_code=404,
detail="No browser profile found."
)
cookie_manager = CookieManager(user_data_dir)
success = cookie_manager.delete_all_cookies()
if success:
return {
"success": True,
"message": "Successfully deleted all cookies"
}
else:
raise HTTPException(
status_code=500,
detail="Failed to delete all cookies"
)
except HTTPException:
raise
except Exception as e:
logger.error(f"Failed to delete all cookies: {e}")
raise HTTPException(
status_code=500,
detail=f"Failed to delete cookies: {str(e)}"
)