// MCP Server connection configuration let MCP_SERVER_URL = 'ws://localhost:5555'; // Default, will be auto-discovered let mcpSocket = null; let reconnectInterval = null; let reconnectAttempts = 0; let lastKnownPorts = { websocket: 5555, http: 5556 }; // Cache for port discovery // Safety Mode configuration let safetyModeEnabled = false; const WRITE_EDIT_TOOLS = [ 'element_click', 'element_fill' ]; // Load safety mode state on startup chrome.storage.local.get(['safetyMode'], (result) => { safetyModeEnabled = result.safetyMode || false; }); // Content script management for background tabs async function ensureContentScriptReady(tabId, retries = 3) { for (let attempt = 1; attempt <= retries; attempt++) { try { // Test if content script is responsive const response = await new Promise((resolve, reject) => { const timeout = setTimeout(() => { reject(new Error('Content script ping timeout')); }, 2000); chrome.tabs.sendMessage(tabId, { action: 'ping' }, (response) => { clearTimeout(timeout); if (chrome.runtime.lastError) { reject(new Error(chrome.runtime.lastError.message)); } else { resolve(response); } }); }); if (response && response.success) { console.log(`✅ Content script ready in tab ${tabId}`); return true; } } catch (error) { console.log(`⚠️ Content script not responsive in tab ${tabId}, attempt ${attempt}/${retries}`); if (attempt === retries) { // Last attempt - try to inject content script try { const tab = await chrome.tabs.get(tabId); // Check if tab URL is injectable (not chrome://, chrome-extension://, etc.) if (!isInjectableUrl(tab.url)) { throw new Error(`Cannot inject content script into ${tab.url} - restricted URL`); } console.log(`🔄 Injecting content script into tab ${tabId}`); await chrome.scripting.executeScript({ target: { tabId: tabId }, files: ['content.js'] }); // Wait a moment for script to initialize await new Promise(resolve => setTimeout(resolve, 1000)); // Test again const testResponse = await new Promise((resolve, reject) => { const timeout = setTimeout(() => reject(new Error('Timeout after injection')), 3000); chrome.tabs.sendMessage(tabId, { action: 'ping' }, (response) => { clearTimeout(timeout); if (chrome.runtime.lastError) { reject(new Error(chrome.runtime.lastError.message)); } else { resolve(response); } }); }); if (testResponse && testResponse.success) { console.log(`✅ Content script successfully injected into tab ${tabId}`); return true; } } catch (injectionError) { throw new Error(`Failed to inject content script into tab ${tabId}: ${injectionError.message}`); } } // Wait before retry if (attempt < retries) { await new Promise(resolve => setTimeout(resolve, 1000)); } } } throw new Error(`Content script not available in tab ${tabId} after ${retries} attempts`); } // Check if URL allows content script injection function isInjectableUrl(url) { if (!url) return false; const restrictedProtocols = ['chrome:', 'chrome-extension:', 'chrome-devtools:', 'edge:', 'moz-extension:']; const restrictedDomains = ['chrome.google.com']; // Check protocol if (restrictedProtocols.some(protocol => url.startsWith(protocol))) { return false; } // Check special Chrome pages if (url.startsWith('https://chrome.google.com/webstore') || url.includes('chrome://') || restrictedDomains.some(domain => url.includes(domain))) { return false; } return true; } // Get content script readiness status for a tab async function getTabContentScriptStatus(tabId) { try { const tab = await chrome.tabs.get(tabId); if (!isInjectableUrl(tab.url)) { return { ready: false, reason: 'restricted_url', url: tab.url }; } const response = await new Promise((resolve, reject) => { const timeout = setTimeout(() => resolve(null), 1000); chrome.tabs.sendMessage(tabId, { action: 'ping' }, (response) => { clearTimeout(timeout); resolve(response); }); }); if (response && response.success) { return { ready: true, reason: 'active', url: tab.url }; } else { return { ready: false, reason: 'not_loaded', url: tab.url }; } } catch (error) { return { ready: false, reason: 'tab_error', error: error.message }; } } // Port discovery function async function discoverServerPorts() { // Try common HTTP ports to find the server const commonPorts = [5556, 5557, 5558, 3001, 6001, 6002, 6003]; for (const httpPort of commonPorts) { try { const response = await fetch(`http://localhost:${httpPort}/ports`); if (response.ok) { const portInfo = await response.json(); console.log('🔍 Discovered server ports:', portInfo); lastKnownPorts = { websocket: portInfo.websocket, http: portInfo.http }; MCP_SERVER_URL = portInfo.websocketUrl; return portInfo; } } catch (error) { // Port not available or not OpenDia server, continue searching } } // Fallback to default if discovery fails console.log('⚠️ Port discovery failed, using defaults'); return null; } // Initialize WebSocket connection to MCP server async function connectToMCPServer() { if (mcpSocket && mcpSocket.readyState === WebSocket.OPEN) return; // Try port discovery if using default URL or if connection failed if (MCP_SERVER_URL === 'ws://localhost:5555' || reconnectAttempts > 2) { await discoverServerPorts(); reconnectAttempts = 0; // Reset attempts after discovery } console.log('🔗 Connecting to MCP server at', MCP_SERVER_URL); mcpSocket = new WebSocket(MCP_SERVER_URL); mcpSocket.onopen = () => { console.log('✅ Connected to MCP server'); clearInterval(reconnectInterval); const tools = getAvailableTools(); console.log(`🔧 Registering ${tools.length} tools:`, tools.map(t => t.name)); // Register available browser functions mcpSocket.send(JSON.stringify({ type: 'register', tools: tools })); }; mcpSocket.onmessage = async (event) => { const message = JSON.parse(event.data); await handleMCPRequest(message); }; mcpSocket.onclose = () => { console.log('❌ Disconnected from MCP server, will reconnect...'); reconnectAttempts++; // Clear any existing reconnect interval if (reconnectInterval) { clearInterval(reconnectInterval); } // Attempt to reconnect every 5 seconds reconnectInterval = setInterval(connectToMCPServer, 5000); }; mcpSocket.onerror = (error) => { console.log('⚠️ MCP WebSocket error:', error); reconnectAttempts++; }; } // Define available browser automation tools for MCP function getAvailableTools() { return [ /* 🎯 BACKGROUND TAB WORKFLOW GUIDE: 1. DISCOVER TABS: Use tab_list with check_content_script=true to see all tabs and their IDs 2. TARGET SPECIFIC TABS: Add tab_id parameter to any tool to work on background tabs 3. MULTI-TAB OPERATIONS: Process multiple tabs without switching between them Example Multi-Tab Workflow: - tab_list({check_content_script: true}) → Get tab IDs and readiness status - page_analyze({intent_hint: "article", tab_id: 12345}) → Analyze background research tab - page_extract_content({content_type: "article", tab_id: 12345}) → Extract content without switching - get_selected_text({tab_id: 67890}) → Get quotes from another background tab Perfect for: Research workflows, content analysis, form processing, social media management */ // Page Analysis Tools { name: "page_analyze", description: "🔍 BACKGROUND TAB READY: Analyze any tab without switching to it! Two-phase intelligent page analysis with token efficiency optimization. Use tab_id parameter to analyze background tabs while staying on current page.", inputSchema: { type: "object", examples: [ { intent_hint: "analyze", phase: "discover" }, // Current tab quick analysis { intent_hint: "login", tab_id: 12345 }, // Background tab login form analysis { intent_hint: "post_create", tab_id: 67890, phase: "detailed" } // Background tab detailed analysis ], properties: { intent_hint: { type: "string", description: "User intent: login, signup, search, post_create, comment, menu, submit, etc." }, phase: { type: "string", enum: ["discover", "detailed"], default: "discover", description: "Analysis phase: 'discover' for quick scan (<100 tokens), 'detailed' for full analysis" }, focus_areas: { type: "array", items: { type: "string" }, description: "Areas to analyze in detail: buttons, forms, navigation, search_elements" }, max_results: { type: "number", default: 5, maximum: 15, description: "Maximum number of elements to return" }, element_ids: { type: "array", items: { type: "string" }, description: "Expand specific quick match IDs from discover phase (e.g. ['q1', 'q2'])" }, tab_id: { type: "number", description: "🎯 TARGET ANY TAB: Specify tab ID to analyze background tabs without switching! Get tab IDs from tab_list. If omitted, analyzes current active tab." } }, required: ["intent_hint"] } }, { name: "page_extract_content", description: "📄 BACKGROUND TAB READY: Extract content from any tab without switching! Perfect for analyzing multiple research tabs, articles, or pages simultaneously. Use tab_id to target specific background tabs.", inputSchema: { type: "object", examples: [ { content_type: "article" }, // Extract from current tab { content_type: "article", tab_id: 12345 }, // Extract from background research tab { content_type: "posts", tab_id: 67890, max_items: 10 } // Extract social media posts from background tab ], properties: { content_type: { type: "string", enum: ["article", "search_results", "posts"], description: "Type of content to extract" }, max_items: { type: "number", description: "Maximum number of items to extract (for lists/collections)", default: 20 }, summarize: { type: "boolean", default: true, description: "Return summary instead of full content to save tokens" }, tab_id: { type: "number", description: "🎯 TARGET ANY TAB: Extract content from specific background tab without switching! Use tab_list to get tab IDs. Perfect for processing multiple research tabs." } }, required: ["content_type"] } }, // Element Interaction Tools { name: "element_click", description: "🖱️ BACKGROUND TAB READY: Click elements in any tab without switching! Perform actions on background tabs while staying on current page. Use tab_id to target specific tabs.", inputSchema: { type: "object", properties: { element_id: { type: "string", description: "Unique element identifier from page_analyze" }, click_type: { type: "string", enum: ["left", "right", "double"], default: "left" }, wait_after: { type: "number", description: "Milliseconds to wait after click", default: 500 }, tab_id: { type: "number", description: "🎯 TARGET ANY TAB: Click elements in background tabs without switching! Get tab IDs from tab_list to interact with multiple tabs efficiently." } }, required: ["element_id"] } }, { name: "element_fill", description: "✏️ BACKGROUND TAB READY: Fill forms in any tab without switching! Enhanced focus and event simulation for modern web apps. Use tab_id to fill forms in background tabs.", inputSchema: { type: "object", properties: { element_id: { type: "string", description: "Unique element identifier from page_analyze" }, value: { type: "string", description: "Text value to input" }, clear_first: { type: "boolean", description: "Clear existing content before filling", default: true }, force_focus: { type: "boolean", description: "Use enhanced focus sequence with click simulation for modern apps", default: true }, tab_id: { type: "number", description: "🎯 TARGET ANY TAB: Fill forms in background tabs without switching! Perfect for batch form filling across multiple tabs. Get tab IDs from tab_list." } }, required: ["element_id", "value"] } }, // Navigation Tools { name: "page_navigate", description: "Navigate CURRENT tab to a new URL. Use tab_create instead if you want to open a NEW tab with a URL.", inputSchema: { type: "object", properties: { url: { type: "string", description: "URL to navigate to" }, wait_for: { type: "string", description: "CSS selector to wait for after navigation (ensures page is ready)" }, timeout: { type: "number", description: "Maximum wait time in milliseconds", default: 10000 } }, required: ["url"] } }, { name: "page_wait_for", description: "Wait for specific element or condition on current page", inputSchema: { type: "object", properties: { condition_type: { type: "string", enum: ["element_visible", "text_present"], description: "Type of condition to wait for" }, selector: { type: "string", description: "CSS selector for element-based conditions" }, text: { type: "string", description: "Text to wait for (when condition_type is 'text_present')" }, timeout: { type: "number", description: "Maximum wait time in milliseconds", default: 5000 } }, required: ["condition_type"] } }, // Tab Management Tools { name: "tab_create", description: "Creates tabs. CRITICAL: For multiple identical tabs, ALWAYS use 'count' parameter! Examples: {url: 'https://x.com', count: 5} creates 5 Twitter tabs. {url: 'https://github.com', count: 10} creates 10 GitHub tabs. Single tab: {url: 'https://example.com'}. Multiple different URLs: {urls: ['url1', 'url2']}.", inputSchema: { type: "object", examples: [ { url: "https://x.com", count: 5 }, // CORRECT: Creates 5 identical Twitter tabs in one batch { url: "https://github.com", count: 10 }, // CORRECT: Creates 10 GitHub tabs { urls: ["https://x.com/post1", "https://x.com/post2", "https://google.com"] }, // CORRECT: Different URLs in batch { url: "https://example.com" } // Single tab only ], properties: { url: { type: "string", description: "Single URL to open. Can be used with 'count' to create multiple identical tabs" }, urls: { type: "array", items: { type: "string" }, description: "PREFERRED FOR MULTIPLE URLS: Array of URLs to open ALL AT ONCE in a single batch operation. Pass ALL URLs here instead of making multiple calls! Example: ['https://x.com/post1', 'https://x.com/post2', 'https://google.com']", maxItems: 100 }, count: { type: "number", default: 1, minimum: 1, maximum: 50, description: "REQUIRED FOR MULTIPLE IDENTICAL TABS: Set this to N to create N copies of the same URL. For '5 Twitter tabs' use count=5 with url='https://x.com'. DO NOT make 5 separate calls!" }, active: { type: "boolean", default: true, description: "Whether to activate the last created tab (single tab only)" }, wait_for: { type: "string", description: "CSS selector to wait for after tab creation (single tab only)" }, timeout: { type: "number", default: 10000, description: "Maximum wait time per tab in milliseconds" }, batch_settings: { type: "object", description: "Performance control settings for batch operations", properties: { chunk_size: { type: "number", default: 5, minimum: 1, maximum: 10, description: "Number of tabs to create per batch" }, delay_between_chunks: { type: "number", default: 1000, minimum: 100, maximum: 5000, description: "Delay between batches in milliseconds" }, delay_between_tabs: { type: "number", default: 200, minimum: 50, maximum: 1000, description: "Delay between individual tabs in milliseconds" } } } } } }, { name: "tab_close", description: "Close specific tab(s) by ID or close current tab", inputSchema: { type: "object", properties: { tab_id: { type: "number", description: "Specific tab ID to close (optional, closes current tab if not provided)" }, tab_ids: { type: "array", items: { type: "number" }, description: "Array of tab IDs to close multiple tabs" } } } }, { name: "tab_list", description: "📋 TAB DISCOVERY: Get list of all open tabs with IDs for background tab targeting! Shows content script readiness status and tab details. Essential for multi-tab workflows - use tab IDs with other tools to work on background tabs.", inputSchema: { type: "object", examples: [ { check_content_script: true }, // RECOMMENDED: Check which tabs are ready for background operations { current_window_only: false, check_content_script: true } // Check all tabs across windows ], properties: { current_window_only: { type: "boolean", default: true, description: "Only return tabs from the current window" }, include_details: { type: "boolean", default: true, description: "Include additional tab details (title, favicon, etc.)" }, check_content_script: { type: "boolean", default: false, description: "🔍 ESSENTIAL FOR BACKGROUND TABS: Check which tabs are ready for background operations! Set to true when planning multi-tab workflows to see which tabs can be targeted." } } } }, { name: "tab_switch", description: "Switch to a specific tab by ID", inputSchema: { type: "object", properties: { tab_id: { type: "number", description: "Tab ID to switch to" } }, required: ["tab_id"] } }, // Element State Tools { name: "element_get_state", description: "Get detailed state information for a specific element (disabled, clickable, etc.)", inputSchema: { type: "object", properties: { element_id: { type: "string", description: "Element ID from page_analyze" } }, required: ["element_id"] } }, // Workspace and Reference Management Tools { name: "get_bookmarks", description: "Get all bookmarks or search for specific bookmarks", inputSchema: { type: "object", properties: { query: { type: "string", description: "Search query for bookmarks (optional)" } } } }, { name: "add_bookmark", description: "Add a new bookmark", inputSchema: { type: "object", properties: { title: { type: "string", description: "Title of the bookmark" }, url: { type: "string", description: "URL of the bookmark" }, parentId: { type: "string", description: "ID of the parent folder (optional)" } }, required: ["title", "url"] } }, { name: "get_history", description: "Search browser history with comprehensive filters for finding previous work by date/keywords", inputSchema: { type: "object", properties: { keywords: { type: "string", description: "Search keywords to match in page titles and URLs" }, start_date: { type: "string", format: "date-time", description: "Start date for history search (ISO 8601 format)" }, end_date: { type: "string", format: "date-time", description: "End date for history search (ISO 8601 format)" }, domains: { type: "array", items: { type: "string" }, description: "Filter by specific domains (e.g., ['github.com', 'stackoverflow.com'])" }, min_visit_count: { type: "number", default: 1, description: "Minimum visit count threshold" }, max_results: { type: "number", default: 50, maximum: 500, description: "Maximum number of results to return" }, sort_by: { type: "string", enum: ["visit_time", "visit_count", "title"], default: "visit_time", description: "Sort results by visit time, visit count, or title" }, sort_order: { type: "string", enum: ["desc", "asc"], default: "desc", description: "Sort order (descending or ascending)" } } } }, { name: "get_selected_text", description: "📝 BACKGROUND TAB READY: Get selected text from any tab without switching! Perfect for collecting quotes, citations, or highlighted content from multiple research tabs simultaneously.", inputSchema: { type: "object", properties: { include_metadata: { type: "boolean", default: true, description: "Include metadata about the selection (element info, position, etc.)" }, max_length: { type: "number", default: 10000, description: "Maximum length of text to return" }, tab_id: { type: "number", description: "🎯 TARGET ANY TAB: Get selected text from background tabs without switching! Perfect for collecting quotes or snippets from multiple research tabs." } } } }, { name: "page_scroll", description: "📜 BACKGROUND TAB READY: Scroll any tab without switching! Critical for long pages. Navigate through content in background tabs while staying on current page. Use tab_id to target specific tabs.", inputSchema: { type: "object", properties: { direction: { type: "string", enum: ["up", "down", "left", "right", "top", "bottom"], default: "down", description: "Direction to scroll" }, amount: { type: "string", enum: ["small", "medium", "large", "page", "custom"], default: "medium", description: "Amount to scroll" }, pixels: { type: "number", description: "Custom pixel amount (when amount is 'custom')" }, smooth: { type: "boolean", default: true, description: "Use smooth scrolling animation" }, element_id: { type: "string", description: "Scroll to specific element (overrides direction/amount)" }, wait_after: { type: "number", default: 500, description: "Milliseconds to wait after scrolling" }, tab_id: { type: "number", description: "🎯 TARGET ANY TAB: Scroll content in background tabs without switching! Perfect for navigating long documents or pages in multiple tabs simultaneously." } } } }, { name: "get_page_links", description: "Get all hyperlinks on the current page with filtering options", inputSchema: { type: "object", properties: { link_type: { type: "string", enum: ["all", "internal", "external"], default: "all", description: "Filter by internal/external links" }, domains: { type: "array", items: { type: "string" }, description: "Filter by specific domains (optional)" }, max_results: { type: "number", default: 50, maximum: 200, description: "Maximum links to return" } } } }, ]; } // Handle MCP requests with enhanced automation tools async function handleMCPRequest(message) { const { id, method, params } = message; try { // Safety Mode check: Block write/edit tools if safety mode is enabled if (safetyModeEnabled && WRITE_EDIT_TOOLS.includes(method)) { const targetInfo = params.tab_id ? `tab ${params.tab_id}` : 'the current page'; throw new Error(`🛡️ Safety Mode is enabled. This tool (${method}) is blocked to prevent modifications to ${targetInfo}. To disable Safety Mode, open the OpenDia extension popup and toggle off "Safety Mode".`); } let result; switch (method) { // New automation tools with background tab support case "page_analyze": result = await sendToContentScript('analyze', params, params.tab_id); break; case "page_extract_content": result = await sendToContentScript('extract_content', params, params.tab_id); break; case "element_click": result = await sendToContentScript('element_click', params, params.tab_id); break; case "element_fill": result = await sendToContentScript('element_fill', params, params.tab_id); break; case "page_navigate": result = await navigateToUrl(params.url, params.wait_for, params.timeout); break; case "page_wait_for": result = await sendToContentScript('wait_for', params, params.tab_id); break; // Tab management tools case "tab_create": result = await createTab(params); break; case "tab_close": result = await closeTabs(params); break; case "tab_list": result = await listTabs(params); break; case "tab_switch": result = await switchToTab(params.tab_id); break; // Element state tools case "element_get_state": result = await sendToContentScript('get_element_state', params, params.tab_id); break; // Workspace and Reference Management Tools case "get_bookmarks": result = await getBookmarks(params); break; case "add_bookmark": result = await addBookmark(params); break; case "get_history": result = await getHistory(params); break; case "get_selected_text": result = await getSelectedText(params); break; case "page_scroll": result = await sendToContentScript('page_scroll', params, params.tab_id); break; case "get_page_links": result = await sendToContentScript('get_page_links', params, params.tab_id); break; default: throw new Error(`Unknown method: ${method}`); } // Send success response mcpSocket.send( JSON.stringify({ id, result, }) ); } catch (error) { // Send error response mcpSocket.send( JSON.stringify({ id, error: { message: error.message, code: -32603, }, }) ); } } // Enhanced content script communication with background tab support async function sendToContentScript(action, data, targetTabId = null) { let targetTab; if (targetTabId) { // Use specific tab try { targetTab = await chrome.tabs.get(targetTabId); } catch (error) { throw new Error(`Tab ${targetTabId} not found or inaccessible`); } } else { // Fallback to active tab (maintains compatibility) const [activeTab] = await chrome.tabs.query({ active: true, currentWindow: true, }); if (!activeTab) { throw new Error('No active tab found'); } targetTab = activeTab; } // Ensure content script is available in the target tab await ensureContentScriptReady(targetTab.id); return new Promise((resolve, reject) => { chrome.tabs.sendMessage(targetTab.id, { action, data }, (response) => { if (chrome.runtime.lastError) { reject(new Error(`Tab ${targetTab.id}: ${chrome.runtime.lastError.message}`)); } else if (response && response.success) { resolve(response.data); } else { reject(new Error(`Tab ${targetTab.id}: ${response?.error || 'Unknown error'}`)); } }); }); } async function navigateToUrl(url, waitFor, timeout = 10000) { const [activeTab] = await chrome.tabs.query({ active: true, currentWindow: true, }); await chrome.tabs.update(activeTab.id, { url }); // If waitFor is specified, wait for the element to appear if (waitFor) { try { await waitForElement(activeTab.id, waitFor, timeout); } catch (error) { return { success: true, tabId: activeTab.id, warning: `Navigation completed but wait condition failed: ${error.message}` }; } } return { success: true, tabId: activeTab.id, url: url }; } async function waitForElement(tabId, selector, timeout = 5000) { const startTime = Date.now(); while (Date.now() - startTime < timeout) { try { const result = await chrome.tabs.sendMessage(tabId, { action: 'wait_for', data: { condition_type: 'element_visible', selector: selector, timeout: 1000 } }); if (result.success) { return true; } } catch (error) { // Content script might not be ready yet, continue waiting } // Wait 500ms before next check await new Promise(resolve => setTimeout(resolve, 500)); } throw new Error(`Timeout waiting for element: ${selector}`); } // Enhanced Tab Management Functions with Batch Support async function createTab(params) { const { url, urls, count = 1, active = true, wait_for, timeout = 10000, batch_settings = {} } = params; // Smart hint: If creating single tab but description suggests multiple, provide guidance if (count === 1 && !urls) { console.log(`💡 Single tab creation. For multiple identical tabs, use count parameter: {"url": "${url}", "count": N}`); } // Validate parameters const validation = validateTabCreateParams(params); if (!validation.valid) { throw new Error(validation.error); } console.log(`🎯 Tab creation request:`, { url, urls, count, batch_settings, hasBatchSettings: !!batch_settings }); // Determine operation type if (urls && urls.length > 0) { // Batch creation with multiple URLs console.log(`🚀 Using batch mode with ${urls.length} URLs`); return await createTabsBatch(urls, active, wait_for, timeout, batch_settings); } else if (url && count > 1) { // Batch creation with same URL repeated console.log(`🔄 Using repeat mode: ${count} copies of ${url}`); const urlArray = Array(count).fill(url); return await createTabsBatch(urlArray, active, wait_for, timeout, batch_settings); } else { // Single tab creation (legacy behavior) console.log(`📱 Using single tab mode for: ${url || 'about:blank'}`); return await createSingleTab(url, active, wait_for, timeout); } } // Parameter validation function validateTabCreateParams(params) { const { url, urls, count = 1 } = params; // Check for conflicting parameters if (url && urls) { return { valid: false, error: "Cannot specify both 'url' and 'urls' parameters" }; } if (urls && count > 1) { return { valid: false, error: "Cannot use 'count' with 'urls' array" }; } // Allow empty URL for about:blank tabs if (!url && !urls && count > 1) { return { valid: false, error: "Must specify 'url' when using 'count' parameter" }; } // Validate URLs array if (urls) { if (!Array.isArray(urls) || urls.length === 0) { return { valid: false, error: "'urls' must be a non-empty array" }; } if (urls.length > 100) { return { valid: false, error: "Maximum 100 URLs allowed in batch operation" }; } // Validate each URL for (let i = 0; i < urls.length; i++) { if (typeof urls[i] !== 'string' || !urls[i].trim()) { return { valid: false, error: `Invalid URL at index ${i}: must be a non-empty string` }; } } } // Validate count if (count < 1 || count > 50) { return { valid: false, error: "Count must be between 1 and 50" }; } return { valid: true }; } // Single tab creation (original behavior) async function createSingleTab(url, active, wait_for, timeout) { const createProperties = { active }; if (url) { createProperties.url = url; } console.log(`🔍 Creating single tab with properties:`, createProperties); const newTab = await chrome.tabs.create(createProperties); console.log(`📝 Tab created:`, { id: newTab.id, url: newTab.url, pendingUrl: newTab.pendingUrl }); // Wait a moment for the URL to load if (url) { await new Promise(resolve => setTimeout(resolve, 500)); // Check if tab loaded correctly try { const updatedTab = await chrome.tabs.get(newTab.id); console.log(`🔄 Tab after load check:`, { id: updatedTab.id, url: updatedTab.url, status: updatedTab.status }); // If URL was provided and wait_for is specified, wait for the element if (wait_for) { try { await waitForElement(newTab.id, wait_for, timeout); } catch (error) { return { success: true, tab_id: newTab.id, url: updatedTab.url, actual_url: updatedTab.url, requested_url: url, warning: `Tab created but wait condition failed: ${error.message}` }; } } return { success: true, tab_id: newTab.id, url: updatedTab.url || updatedTab.pendingUrl || url, actual_url: updatedTab.url || updatedTab.pendingUrl, requested_url: url, active: updatedTab.active, status: updatedTab.status, title: updatedTab.title || 'New Tab', note: updatedTab.url === 'about:blank' && updatedTab.pendingUrl ? 'Tab is still loading' : undefined }; } catch (error) { console.error(`❌ Error checking tab status:`, error); return { success: true, tab_id: newTab.id, url: newTab.url || 'about:blank', actual_url: newTab.url, requested_url: url, active: newTab.active, title: newTab.title || 'New Tab', warning: `Tab created but status check failed: ${error.message}` }; } } return { success: true, tab_id: newTab.id, url: newTab.url || 'about:blank', active: newTab.active, title: newTab.title || 'New Tab' }; } // Batch tab creation with performance throttling async function createTabsBatch(urls, active, wait_for, timeout, batch_settings = {}) { console.log('🔍 createTabsBatch called with:', { urls: urls.length, batch_settings }); const { chunk_size = 5, delay_between_chunks = 1000, delay_between_tabs = 200 } = batch_settings || {}; const startTime = Date.now(); const totalTabs = urls.length; const createdTabs = []; const errors = []; // Performance warnings const warnings = []; if (totalTabs > 20) { warnings.push(`Creating ${totalTabs} tabs may impact browser performance`); } if (totalTabs > 45) { warnings.push(`Large batch (${totalTabs} tabs) may hit Chrome's tab limits or cause memory issues`); } console.log(`🚀 Starting batch tab creation: ${totalTabs} tabs in chunks of ${chunk_size}`); // Process in chunks for (let chunkStart = 0; chunkStart < urls.length; chunkStart += chunk_size) { const chunkEnd = Math.min(chunkStart + chunk_size, urls.length); const chunk = urls.slice(chunkStart, chunkEnd); const chunkIndex = Math.floor(chunkStart / chunk_size) + 1; const totalChunks = Math.ceil(urls.length / chunk_size); // Reduced logging for better performance if (totalTabs > 10) { console.log(`📦 Chunk ${chunkIndex}/${totalChunks}`); } // Create tabs in current chunk with delays for (let i = 0; i < chunk.length; i++) { const url = chunk[i]; const globalIndex = chunkStart + i; const isLastTab = globalIndex === totalTabs - 1; try { // Only activate the very last tab if active=true const shouldActivate = active && isLastTab; const tab = await chrome.tabs.create({ url: url, active: shouldActivate }); // Wait a moment and check actual URL await new Promise(resolve => setTimeout(resolve, 300)); const updatedTab = await chrome.tabs.get(tab.id); createdTabs.push({ tab_id: tab.id, url: updatedTab.url || url, requested_url: url, index: globalIndex, active: updatedTab.active, title: updatedTab.title || `Tab ${globalIndex + 1}` }); // Only log for small batches to avoid context overflow if (totalTabs <= 5) { console.log(`✅ Created tab ${globalIndex + 1}/${totalTabs}: ${url} (ID: ${tab.id})`); } // Wait between individual tabs (except last in chunk) if (i < chunk.length - 1) { await new Promise(resolve => setTimeout(resolve, delay_between_tabs)); } } catch (error) { console.error(`❌ Failed to create tab ${globalIndex + 1}: ${error.message}`); errors.push({ index: globalIndex, url: url, error: error.message }); } } // Wait between chunks (except after last chunk) if (chunkEnd < urls.length) { if (totalTabs <= 10) { console.log(`⏳ Waiting ${delay_between_chunks}ms before next chunk...`); } await new Promise(resolve => setTimeout(resolve, delay_between_chunks)); } } const executionTime = Date.now() - startTime; const successCount = createdTabs.length; const errorCount = errors.length; console.log(`🏁 Batch creation complete: ${successCount}/${totalTabs} successful in ${executionTime}ms`); // Prepare result - simplified to avoid context overflow const result = { success: errorCount === 0, batch_operation: true, summary: { total_requested: totalTabs, successful: successCount, failed: errorCount, execution_time_ms: executionTime }, // Only include full tab details for small batches created_tabs: totalTabs <= 10 ? createdTabs : createdTabs.map(tab => ({ tab_id: tab.tab_id, url: tab.url })) }; // Add warnings if any if (warnings.length > 0) { result.warnings = warnings; } // Add errors if any if (errors.length > 0) { result.errors = errors; result.partial_success = successCount > 0; } // Add active tab info const activeTabs = createdTabs.filter(tab => tab.active); if (activeTabs.length > 0) { result.active_tab = activeTabs[0]; } return result; } // Utility function to generate URLs for testing/demo purposes function generateTestUrls(baseUrl, count) { const urls = []; for (let i = 1; i <= count; i++) { urls.push(`${baseUrl}?tab=${i}`); } return urls; } // Batch operation helper functions function estimateBatchTime(urlCount, batchSettings = {}) { const { chunk_size = 5, delay_between_chunks = 1000, delay_between_tabs = 200 } = batchSettings || {}; const totalChunks = Math.ceil(urlCount / chunk_size); const timePerChunk = (chunk_size - 1) * delay_between_tabs; // delays within chunk const timeForChunks = totalChunks * timePerChunk; const timeBetweenChunks = (totalChunks - 1) * delay_between_chunks; return timeForChunks + timeBetweenChunks; // in milliseconds } async function closeTabs(params) { const { tab_id, tab_ids } = params; let tabsToClose = []; if (tab_ids && Array.isArray(tab_ids)) { // Close multiple tabs tabsToClose = tab_ids; } else if (tab_id) { // Close specific tab tabsToClose = [tab_id]; } else { // Close current tab const [activeTab] = await chrome.tabs.query({ active: true, currentWindow: true, }); if (activeTab) { tabsToClose = [activeTab.id]; } } if (tabsToClose.length === 0) { throw new Error('No tabs specified to close'); } // Close tabs await chrome.tabs.remove(tabsToClose); return { success: true, closed_tabs: tabsToClose, count: tabsToClose.length }; } async function listTabs(params) { const { current_window_only = true, include_details = true, check_content_script = false } = params; const queryOptions = {}; if (current_window_only) { queryOptions.currentWindow = true; } const tabs = await chrome.tabs.query(queryOptions); // Check content script status if requested const contentScriptStatuses = new Map(); if (check_content_script) { const statusPromises = tabs.map(async (tab) => { try { const status = await getTabContentScriptStatus(tab.id); return [tab.id, status]; } catch (error) { return [tab.id, { ready: false, reason: 'error', error: error.message }]; } }); const results = await Promise.all(statusPromises); results.forEach(([tabId, status]) => { contentScriptStatuses.set(tabId, status); }); } const tabList = tabs.map(tab => { const basicInfo = { id: tab.id, url: tab.url, active: tab.active, title: tab.title }; // Add content script status if checked if (check_content_script) { const scriptStatus = contentScriptStatuses.get(tab.id); basicInfo.content_script = { ready: scriptStatus?.ready || false, reason: scriptStatus?.reason || 'unknown', injectable: isInjectableUrl(tab.url) }; } if (include_details) { return { ...basicInfo, index: tab.index, pinned: tab.pinned, status: tab.status, favIconUrl: tab.favIconUrl, windowId: tab.windowId, incognito: tab.incognito }; } return basicInfo; }); // Calculate summary statistics const summary = { total_tabs: tabList.length, active_tab: tabs.find(tab => tab.active)?.id || null }; if (check_content_script) { const readyTabs = tabList.filter(tab => tab.content_script?.ready).length; const injectableTabs = tabList.filter(tab => tab.content_script?.injectable).length; summary.content_script_stats = { ready_count: readyTabs, injectable_count: injectableTabs, restricted_count: tabList.length - injectableTabs }; } return { success: true, tabs: tabList, count: tabList.length, summary }; } async function switchToTab(tabId) { // First, get tab info to ensure it exists const tab = await chrome.tabs.get(tabId); if (!tab) { throw new Error(`Tab with ID ${tabId} not found`); } // Switch to the tab await chrome.tabs.update(tabId, { active: true }); // Also focus the window containing the tab await chrome.windows.update(tab.windowId, { focused: true }); return { success: true, tab_id: tabId, url: tab.url, title: tab.title, window_id: tab.windowId }; } // Workspace and Reference Management Functions async function getBookmarks(params) { const { query } = params; let bookmarks; if (query) { bookmarks = await chrome.bookmarks.search(query); } else { bookmarks = await chrome.bookmarks.getTree(); } return { success: true, bookmarks, count: bookmarks.length }; } async function addBookmark(params) { const { title, url, parentId } = params; const bookmark = await chrome.bookmarks.create({ title, url, parentId }); return { success: true, bookmark }; } // History Management Function async function getHistory(params) { const { keywords = "", start_date, end_date, domains = [], min_visit_count = 1, max_results = 50, sort_by = "visit_time", sort_order = "desc" } = params; try { // Chrome History API search configuration const searchQuery = { text: keywords, maxResults: Math.min(max_results * 3, 1000), // Over-fetch for filtering }; // Add date range if specified if (start_date) { searchQuery.startTime = new Date(start_date).getTime(); } if (end_date) { searchQuery.endTime = new Date(end_date).getTime(); } // Execute history search const historyItems = await chrome.history.search(searchQuery); // Apply advanced filters let filteredItems = historyItems.filter(item => { // Domain filter if (domains.length > 0) { try { const itemDomain = new URL(item.url).hostname; if (!domains.some(domain => itemDomain.includes(domain))) { return false; } } catch (e) { // Skip items with invalid URLs return false; } } // Visit count filter if (item.visitCount < min_visit_count) { return false; } return true; }); // Sort results filteredItems.sort((a, b) => { let aVal, bVal; switch (sort_by) { case "visit_count": aVal = a.visitCount; bVal = b.visitCount; break; case "title": aVal = (a.title || "").toLowerCase(); bVal = (b.title || "").toLowerCase(); break; default: // visit_time aVal = a.lastVisitTime; bVal = b.lastVisitTime; } if (sort_order === "asc") { return aVal > bVal ? 1 : -1; } else { return aVal < bVal ? 1 : -1; } }); // Limit results const results = filteredItems.slice(0, max_results); // Format response with comprehensive metadata return { success: true, history_items: results.map(item => { let domain; try { domain = new URL(item.url).hostname; } catch (e) { domain = "invalid-url"; } return { id: item.id, url: item.url, title: item.title || "Untitled", last_visit_time: new Date(item.lastVisitTime).toISOString(), visit_count: item.visitCount, domain: domain, typed_count: item.typedCount || 0 }; }), metadata: { total_found: filteredItems.length, returned_count: results.length, search_params: { keywords: keywords || null, date_range: start_date && end_date ? `${start_date} to ${end_date}` : start_date ? `from ${start_date}` : end_date ? `until ${end_date}` : null, domains: domains.length > 0 ? domains : null, min_visit_count, sort_by, sort_order }, execution_time: new Date().toISOString(), over_fetched: historyItems.length, filters_applied: { domain_filter: domains.length > 0, visit_count_filter: min_visit_count > 1, date_filter: !!(start_date || end_date), keyword_filter: !!keywords } } }; } catch (error) { return { success: false, error: `History search failed: ${error.message}`, history_items: [], metadata: { total_found: 0, returned_count: 0, search_params: params, execution_time: new Date().toISOString() } }; } } // Selected Text Management Function async function getSelectedText(params) { const { include_metadata = true, max_length = 10000, tab_id } = params; try { let targetTab; if (tab_id) { // Use specific tab try { targetTab = await chrome.tabs.get(tab_id); } catch (error) { return { success: false, error: `Tab ${tab_id} not found or inaccessible`, selected_text: "", metadata: { execution_time: new Date().toISOString() } }; } } else { // Get the active tab const [activeTab] = await chrome.tabs.query({ active: true, currentWindow: true, }); if (!activeTab) { return { success: false, error: "No active tab found", selected_text: "", metadata: { execution_time: new Date().toISOString() } }; } targetTab = activeTab; } // Execute script to get selected text const results = await chrome.scripting.executeScript({ target: { tabId: targetTab.id }, func: () => { const selection = window.getSelection(); const selectedText = selection.toString(); if (!selectedText) { return { text: "", hasSelection: false, metadata: null }; } // Get metadata about the selection const range = selection.getRangeAt(0); const rect = range.getBoundingClientRect(); // Get parent element info const commonAncestor = range.commonAncestorContainer; const parentElement = commonAncestor.nodeType === Node.TEXT_NODE ? commonAncestor.parentElement : commonAncestor; const metadata = { length: selectedText.length, word_count: selectedText.trim().split(/\s+/).filter(word => word.length > 0).length, line_count: selectedText.split('\n').length, position: { x: rect.x, y: rect.y, width: rect.width, height: rect.height }, parent_element: { tag_name: parentElement.tagName?.toLowerCase(), class_name: parentElement.className, id: parentElement.id, text_content_length: parentElement.textContent?.length || 0 }, page_info: { url: window.location.href, title: document.title, domain: window.location.hostname }, selection_info: { anchor_offset: selection.anchorOffset, focus_offset: selection.focusOffset, range_count: selection.rangeCount, is_collapsed: selection.isCollapsed } }; return { text: selectedText, hasSelection: true, metadata: metadata }; } }); const result = results[0]?.result; if (!result) { return { success: false, error: "Failed to execute selection script", selected_text: "", metadata: { execution_time: new Date().toISOString() } }; } if (!result.hasSelection) { return { success: true, selected_text: "", has_selection: false, message: "No text is currently selected on the page", metadata: { execution_time: new Date().toISOString(), tab_info: { id: targetTab.id, url: targetTab.url, title: targetTab.title } } }; } // Truncate text if it exceeds max_length let selectedText = result.text; let truncated = false; if (selectedText.length > max_length) { selectedText = selectedText.substring(0, max_length); truncated = true; } const response = { success: true, selected_text: selectedText, has_selection: true, character_count: result.text.length, truncated: truncated, metadata: { execution_time: new Date().toISOString(), tab_info: { id: targetTab.id, url: targetTab.url, title: targetTab.title } } }; // Include detailed metadata if requested if (include_metadata && result.metadata) { response.selection_metadata = result.metadata; } return response; } catch (error) { return { success: false, error: `Failed to get selected text: ${error.message}`, selected_text: "", has_selection: false, metadata: { execution_time: new Date().toISOString(), error_details: error.stack } }; } } // Initialize connection when extension loads (with delay for server startup) setTimeout(() => { connectToMCPServer(); }, 1000); // Heartbeat to keep connection alive setInterval(() => { if (mcpSocket && mcpSocket.readyState === WebSocket.OPEN) { mcpSocket.send(JSON.stringify({ type: "ping", timestamp: Date.now() })); } }, 30000); // Every 30 seconds // Handle messages from popup chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { if (request.action === "getStatus") { sendResponse({ connected: mcpSocket && mcpSocket.readyState === WebSocket.OPEN, }); } else if (request.action === "getToolCount") { const tools = getAvailableTools(); sendResponse({ toolCount: tools.length, tools: tools.map(t => t.name) }); } else if (request.action === "reconnect") { connectToMCPServer(); sendResponse({ success: true }); } else if (request.action === "getPorts") { sendResponse({ current: lastKnownPorts, websocketUrl: MCP_SERVER_URL }); } else if (request.action === "setSafetyMode") { safetyModeEnabled = request.enabled; console.log(`🛡️ Safety Mode ${safetyModeEnabled ? 'ENABLED' : 'DISABLED'}`); sendResponse({ success: true }); } else if (request.action === "test") { if (mcpSocket && mcpSocket.readyState === WebSocket.OPEN) { mcpSocket.send(JSON.stringify({ type: "test", timestamp: Date.now() })); } sendResponse({ success: true }); } return true; // Keep the message channel open });