Skip to main content
Glama

autonomous-frontend-browser-tools

background.js41.2 kB
// Listen for messages from the devtools panel chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { if (message.type === "GET_CURRENT_URL" && message.tabId) { getCurrentTabUrl(message.tabId) .then((url) => { sendResponse({ success: true, url: url }); }) .catch((error) => { sendResponse({ success: false, error: error.message }); }); return true; // Required to use sendResponse asynchronously } // Handle explicit request to update the server with the URL if (message.type === "UPDATE_SERVER_URL" && message.tabId && message.url) { console.log( `Background: Received request to update server with URL for tab ${message.tabId}: ${message.url}` ); updateServerWithUrl( message.tabId, message.url, message.source || "explicit_update" ) .then(() => { if (sendResponse) sendResponse({ success: true }); }) .catch((error) => { console.error("Background: Error updating server with URL:", error); if (sendResponse) sendResponse({ success: false, error: error.message }); }); return true; // Required to use sendResponse asynchronously } if (message.type === "CAPTURE_SCREENSHOT" && message.tabId) { console.log("Background: Received CAPTURE_SCREENSHOT message:", message); // First get the server settings chrome.storage.local.get(["browserConnectorSettings"], (result) => { const settings = result.browserConnectorSettings || { serverHost: "localhost", serverPort: 3025, }; // Validate server identity first validateServerIdentity(settings.serverHost, settings.serverPort) .then((isValid) => { console.log("Background: Server validation result:", isValid); if (!isValid) { console.error( "Cannot capture screenshot: Not connected to a valid browser tools server" ); sendResponse({ success: false, error: "Not connected to a valid browser tools server. Please check your connection settings.", }); return; } // Continue with screenshot capture console.log("Background: Proceeding with screenshot capture"); captureAndSendScreenshot(message, settings, sendResponse); }) .catch((error) => { console.error("Error validating server:", error); // Still attempt to capture screenshot even if validation fails, but log the error console.warn("Proceeding with screenshot capture despite validation error"); captureAndSendScreenshot(message, settings, sendResponse); }); }); return true; // Required to use sendResponse asynchronously } if (message.type === "NAVIGATE_TAB" && message.url) { console.log("Background: Received navigation request:", message); const targetTabId = message.tabId || chrome.devtools?.inspectedWindow?.tabId; if (!targetTabId) { console.error("Background: No target tab ID available for navigation"); sendResponse({ success: false, error: "No target tab ID available" }); return true; } // Navigate the tab to the specified URL chrome.tabs.update(targetTabId, { url: message.url }, (tab) => { if (chrome.runtime.lastError) { console.error( "Background: Navigation failed:", chrome.runtime.lastError ); sendResponse({ success: false, error: chrome.runtime.lastError.message, }); } else { console.log("Background: Navigation successful to:", message.url); // Update our cache with the new URL tabUrls.set(targetTabId, message.url); sendResponse({ success: true, url: message.url }); } }); return true; // Required to use sendResponse asynchronously } if (message.type === "PERFORM_DOM_ACTION" && message.tabId && message.payload) { try { const { tabId, payload } = message; // Default to DOM-injection path using scripting API chrome.scripting.executeScript( { target: { tabId }, world: "MAIN", func: performDomAction, args: [payload], }, (results) => { if (chrome.runtime.lastError) { sendResponse({ success: false, error: chrome.runtime.lastError.message }); return; } try { const first = Array.isArray(results) ? results[0] : results; const value = first && first.result ? first.result : first; sendResponse(value || { success: false, error: "No result returned" }); } catch (e) { sendResponse({ success: false, error: e?.message || "Failed to parse result" }); } } ); return true; // async sendResponse } catch (e) { sendResponse({ success: false, error: e?.message || "Unexpected error" }); return true; } } if (message.type === "RETRIEVE_AUTH_TOKEN") { // message: { origin?: string, storageType: 'localStorage'|'sessionStorage'|'cookies', tokenKey: string, tabId?, requestId? } const targetTabId = message.tabId || chrome.devtools?.inspectedWindow?.tabId; const storageType = message.storageType; const tokenKey = message.tokenKey; const origin = message.origin; // required for cookies; preferred context URL for storage if (!storageType || !tokenKey) { sendResponse({ success: false, error: "Missing storageType or tokenKey" }); return true; } // Helper to respond uniformly const respond = (ok, payload) => { if (ok) sendResponse({ success: true, token: payload }); else sendResponse({ success: false, error: payload }); }; try { if (storageType === "cookies") { // Use chrome.cookies API; requires hostPermissions try { const query = {}; if (origin) query.url = origin; // If origin not provided, best-effort: infer from current tab URL const ensureUrl = async () => { if (query.url) return query.url; const url = await getCurrentTabUrl(targetTabId); return url || undefined; }; (async () => { const url = await ensureUrl(); if (!url) { respond(false, "Unable to resolve URL for cookie lookup"); return; } chrome.cookies.get({ url, name: tokenKey }, (cookie) => { if (chrome.runtime.lastError) { respond(false, chrome.runtime.lastError.message); return; } if (cookie && cookie.value) respond(true, cookie.value); else respond(false, "Cookie not found"); }); })(); } catch (e) { respond(false, e?.message || "Cookie retrieval failed"); } return true; } // For localStorage/sessionStorage we must execute in page context; if origin provided, try to find/match that tab first const executeInTab = (tabId) => chrome.scripting.executeScript( { target: { tabId }, world: "MAIN", func: (type, key) => { try { if (type === "localStorage") { return { ok: true, value: window.localStorage.getItem(key) }; } if (type === "sessionStorage") { return { ok: true, value: window.sessionStorage.getItem(key) }; } return { ok: false, error: "Unsupported storageType" }; } catch (e) { return { ok: false, error: e?.message || "Storage access error" }; } }, args: [storageType, tokenKey], }, (results) => { if (chrome.runtime.lastError) { respond(false, chrome.runtime.lastError.message); return; } try { const first = Array.isArray(results) ? results[0] : results; const r = first && first.result ? first.result : first; if (r && r.ok && typeof r.value === "string" && r.value.length > 0) { respond(true, r.value); } else { respond(false, r?.error || "Token not found"); } } catch (e) { respond(false, e?.message || "Failed to parse result"); } } ); // Strategy: if origin provided, try to locate a tab with matching origin (host); // else use provided tabId or active tab. if (origin) { chrome.tabs.query({}, (tabs) => { const match = (tabs || []).find((t) => { try { const u = new URL(t.url || ""); const want = new URL(origin); return u.origin === want.origin; } catch { return false; } }); if (match && match.id) { executeInTab(match.id); } else if (targetTabId) { executeInTab(targetTabId); } else { respond(false, "No matching tab for provided origin"); } }); return true; } if (!targetTabId) { respond(false, "No target tab ID available"); return true; } executeInTab(targetTabId); return true; } catch (e) { respond(false, e?.message || "Unexpected error"); return true; } } }); // In-page function executed via chrome.scripting.executeScript function performDomAction(payload) { const wait = (ms) => new Promise((r) => setTimeout(r, ms)); const isVisible = (el) => { if (!el) return false; const rect = el.getBoundingClientRect(); const style = window.getComputedStyle(el); return ( rect.width > 0 && rect.height > 0 && style.visibility !== "hidden" && style.display !== "none" ); }; const isDisabled = (el) => !!(el && (el.disabled || el.getAttribute('aria-disabled') === 'true')); const hasPointerEvents = (el) => { const style = window.getComputedStyle(el); return style.pointerEvents !== 'none'; }; const isCenterOnTop = (el) => { const rect = el.getBoundingClientRect(); const cx = rect.left + rect.width / 2; const cy = rect.top + rect.height / 2; const topEl = document.elementFromPoint(cx, cy); return topEl && (topEl === el || el.contains(topEl)); }; const isScrollable = (el) => { if (!el) return false; const style = window.getComputedStyle(el); const overflowY = style.overflowY; const overflowX = style.overflowX; const canScrollY = (overflowY === 'auto' || overflowY === 'scroll') && el.scrollHeight > el.clientHeight; const canScrollX = (overflowX === 'auto' || overflowX === 'scroll') && el.scrollWidth > el.clientWidth; return canScrollY || canScrollX; }; const findPrimaryScrollContainer = () => { // Prefer explicit containers const candidates = [ document.querySelector('main'), document.querySelector('[role="main"]'), document.querySelector('[data-scroll-container]') ].filter(Boolean); for (const c of candidates) if (isScrollable(c)) return c; // Heuristic: pick visible element with largest (scrollHeight - clientHeight) let best = null; let bestDelta = 0; const all = document.querySelectorAll('*'); for (const el of all) { if (!isVisible(el)) continue; const delta = Math.max(0, el.scrollHeight - el.clientHeight); if (delta > bestDelta && isScrollable(el)) { best = el; bestDelta = delta; } } return best || document.scrollingElement || document.documentElement; }; const closestClickable = (el) => { if (!el) return null; const selector = [ 'button', 'input[type="button"]', 'input[type="submit"]', 'input[type="reset"]', '[role="button"]', 'a[href]', '[role="link"]', '[role="tab"]', '[role="menuitem"]', '[role="menuitemcheckbox"]', '[role="menuitemradio"]', '[role="option"]', '[role="switch"]', '[onclick]', '[tabindex]' ].join(', '); const node = el.closest(selector) || el; return node; }; const getAccessibleName = (el) => { if (!el) return ''; return ( el.getAttribute('aria-label') || (el.getAttribute('aria-labelledby') ? (document.getElementById(el.getAttribute('aria-labelledby'))?.textContent || '') : '') || el.innerText || el.textContent || '' ).trim(); }; const byText = (text, exact) => { const hay = exact ? [text] : [text, text.trim()]; // Prefer interactive candidates first (broad set) const interactive = document.querySelectorAll("button, input[type='button'], input[type='submit'], input[type='reset'], [role='button'], a[href], [role='link'], [role='tab'], [role='menuitem'], [role='menuitemcheckbox'], [role='menuitemradio'], [role='option'], [role='switch'], [onclick], [tabindex]"); for (const el of interactive) { const name = getAccessibleName(el); if (!name || isDisabled(el) || !isVisible(el)) continue; if (hay.some((t) => t && name.includes(t))) return el; } // Fallback: any element containing text const all = document.querySelectorAll("*"); for (const el of all) { const name = getAccessibleName(el); if (!name) continue; if (hay.some((t) => t && name.includes(t))) return el; } return null; }; const byLabel = (labelText, exact) => { const labels = Array.from(document.querySelectorAll("label")); const match = labels.find((l) => exact ? l.textContent === labelText : (l.textContent || "").includes(labelText) ); if (!match) return null; const forId = match.getAttribute("for"); if (forId) return document.getElementById(forId); const input = match.querySelector("input,textarea,select"); return input || null; }; const byRole = (role, name, exact) => { const qAll = Array.from(document.querySelectorAll("*")); let candidates = []; const matchRole = (el, role) => { if (el.getAttribute("role") === role) return true; if (role === 'button' && el.tagName === 'BUTTON') return true; if (role === 'link' && (el.tagName === 'A' && el.hasAttribute('href'))) return true; return false; }; candidates = qAll.filter((el) => matchRole(el, role)); // Prefer enabled candidates candidates = candidates.filter((el) => !isDisabled(el) && isVisible(el)); if (role === 'tab') { // Prefer tabs that are not already selected const unselected = candidates.filter((el) => el.getAttribute('aria-selected') !== 'true'); if (unselected.length) candidates = unselected; } if (!name) return candidates[0] || null; const pick = candidates.find((el) => { const txt = getAccessibleName(el); return exact ? txt === name : txt.includes(name); }); return pick || null; }; const byTestId = (value) => document.querySelector(`[data-testid="${CSS.escape(value)}"]`); const byPlaceholder = (value, exact) => Array.from(document.querySelectorAll("input,textarea")) .find((el) => { const ph = el.getAttribute("placeholder") || ""; return exact ? ph === value : ph.includes(value); }) || null; const byName = (value, exact) => Array.from(document.querySelectorAll("input,textarea,select")) .find((el) => { const nm = el.getAttribute("name") || ""; return exact ? nm === value : nm.includes(value); }) || null; const queryWithin = (root, sel) => { const { by, value, exact } = sel || {}; if (!by || !value) return null; if (by === "testid") return root.querySelector(`[data-testid="${CSS.escape(value)}"]`); if (by === "role") { const parts = value.split(":"); const role = parts[0]; const name = parts.slice(1).join(":") || undefined; let candidates = Array.from(root.querySelectorAll("*")) .filter((el) => el.getAttribute("role") === role || (role === "button" && (el.tagName === "BUTTON" || el.getAttribute("role") === "button"))); candidates = candidates.filter((el) => el.getAttribute('aria-disabled') !== 'true' && !el.disabled); if (role === 'tab') { const unselected = candidates.filter((el) => el.getAttribute('aria-selected') !== 'true'); if (unselected.length) candidates = unselected; } if (!name) return candidates[0] || null; return candidates.find((el) => { const txt = getAccessibleName(el); return exact ? txt === name : txt.includes(name); }) || null; } if (by === "label") { const labels = Array.from(root.querySelectorAll("label")); const match = labels.find((l) => exact ? l.textContent === value : (l.textContent || "").includes(value)); if (!match) return null; const forId = match.getAttribute("for"); if (forId) return root.getElementById ? root.getElementById(forId) : document.getElementById(forId); return match.querySelector("input,textarea,select"); } if (by === "placeholder") return Array.from(root.querySelectorAll("input,textarea")).find((el) => (el.getAttribute("placeholder") || "").includes(value)) || null; if (by === "name") return Array.from(root.querySelectorAll("input,textarea,select")).find((el) => (el.getAttribute("name") || "").includes(value)) || null; if (by === "text") { const hay = exact ? [value] : [value, value.trim()]; const interactive = root.querySelectorAll("button, [role='tab'], [role='button'], a, [onclick], [tabindex]"); for (const el of interactive) { const name = getAccessibleName(el); if (hay.some((t) => t && name.includes(t))) return el; } const all = root.querySelectorAll("*"); for (const el of all) { const name = getAccessibleName(el); if (hay.some((t) => t && name.includes(t))) return el; } return null; } if (by === "css") return root.querySelector(value); if (by === "xpath") { try { const r = document.evaluate(value, root === document ? document : root, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null); return r.singleNodeValue; } catch (_) { return null; } } return null; }; const resolveElement = (target, scope) => { const { by, value, exact } = target || {}; if (!by || !value) return null; let root = document; if (scope && scope.by) { const scoped = queryWithin(document, scope); if (scoped) root = scoped.shadowRoot || scoped; } return queryWithin(root, target); }; const { action, target, value, options = {} } = payload || {}; const timeoutMs = typeof options.timeoutMs === "number" ? options.timeoutMs : 5000; const waitForVisible = options.waitForVisible !== false; const waitForEnabled = options.waitForEnabled !== false; const start = performance.now(); let el = null; return (async () => { while (performance.now() - start < timeoutMs) { el = resolveElement(target, payload.scopeTarget); if (el && (!waitForVisible || isVisible(el))) break; await wait(100); } if (!el) return { success: false, error: "ELEMENT_NOT_FOUND" }; el.scrollIntoView({ block: "center", inline: "center" }); await wait(50); if (waitForEnabled && (el.disabled || el.getAttribute("aria-disabled") === "true")) { return { success: false, error: "NOT_ENABLED", details: { selectorUsed: target?.by + "=" + target?.value } }; } const rect = el.getBoundingClientRect(); const clickSequence = (node) => { // Ensure in-viewport and not covered node.scrollIntoView({ block: 'center', inline: 'center' }); if (!hasPointerEvents(node)) return; const rect2 = node.getBoundingClientRect(); // If center point is not on the element (covered), try top-left fallback if (!isCenterOnTop(node)) { const ev = (type, opts) => node.dispatchEvent(new MouseEvent(type, { bubbles: true, ...opts })); ev('mousemove', { clientX: rect2.left + 1, clientY: rect2.top + 1 }); ev('mousedown', { clientX: rect2.left + 1, clientY: rect2.top + 1 }); ev('mouseup', { clientX: rect2.left + 1, clientY: rect2.top + 1 }); ev('click', { clientX: rect2.left + 1, clientY: rect2.top + 1 }); } else { node.dispatchEvent(new PointerEvent("pointerdown", { bubbles: true })); node.dispatchEvent(new MouseEvent("mousedown", { bubbles: true })); node.dispatchEvent(new MouseEvent("mouseup", { bubbles: true })); node.dispatchEvent(new MouseEvent("click", { bubbles: true })); } if (typeof node.click === 'function') node.click(); }; if (action === "click") { const node = closestClickable(el) || el; if (node.getAttribute && (node.getAttribute('aria-disabled') === 'true')) { return { success: false, error: "DISABLED_ELEMENT" }; } clickSequence(node); // Optional assertion phase (e.g., wait for panel or URL change) if (options && (options.assertTarget || options.assertUrlContains)) { const endBy = performance.now() + (options.assertTimeoutMs || 5000); while (performance.now() < endBy) { let ok = true; if (options.assertTarget) { const assertEl = resolveElement(options.assertTarget, payload.scopeTarget); ok = !!assertEl && (!waitForVisible || isVisible(assertEl)); } if (ok && options.assertUrlContains) { ok = (location.href || '').includes(options.assertUrlContains); } if (ok) break; await wait(100); } } if (options && options.tabChangeWaitMs) await wait(options.tabChangeWaitMs); return { success: true, details: { selectorUsed: `${target.by}=${target.value}`, matchedCount: 1, boundingBox: { x: rect.x, y: rect.y, width: rect.width, height: rect.height } } }; } if (action === "type") { el.focus(); if (typeof value === "string") { if ("value" in el) { el.value = value; } else { el.textContent = value; } el.dispatchEvent(new Event("input", { bubbles: true })); el.dispatchEvent(new Event("change", { bubbles: true })); } return { success: true, details: { selectorUsed: `${target.by}=${target.value}` } }; } if (action === "select") { if (el.tagName === "SELECT" && typeof value === "string") { const sel = el; const opt = Array.from(sel.options).find(o => o.value === value || o.text === value); if (opt) { sel.value = opt.value; sel.dispatchEvent(new Event("input", { bubbles: true })); sel.dispatchEvent(new Event("change", { bubbles: true })); return { success: true, details: { selectorUsed: `${target.by}=${target.value}` } }; } return { success: false, error: "OPTION_NOT_FOUND" }; } return { success: false, error: "UNSUPPORTED_SELECT_TARGET" }; } if (action === "check" || action === "uncheck") { if (el.tagName === "INPUT" && el.type === "checkbox") { const shouldBe = action === "check"; if (el.checked !== shouldBe) { el.checked = shouldBe; el.dispatchEvent(new Event("input", { bubbles: true })); el.dispatchEvent(new Event("change", { bubbles: true })); } return { success: true, details: { selectorUsed: `${target.by}=${target.value}` } }; } return { success: false, error: "UNSUPPORTED_CHECK_TARGET" }; } if (action === "keypress") { const key = typeof value === "string" ? value : "Enter"; el.focus(); el.dispatchEvent(new KeyboardEvent("keydown", { key, bubbles: true })); el.dispatchEvent(new KeyboardEvent("keypress", { key, bubbles: true })); el.dispatchEvent(new KeyboardEvent("keyup", { key, bubbles: true })); return { success: true, details: { selectorUsed: `${target.by}=${target.value}` } }; } if (action === "hover") { el.dispatchEvent(new MouseEvent("mouseover", { bubbles: true })); el.dispatchEvent(new MouseEvent("mousemove", { bubbles: true })); return { success: true, details: { selectorUsed: `${target.by}=${target.value}` } }; } if (action === "scroll") { const behavior = options.smooth ? "smooth" : "auto"; const hasOffsets = typeof options.scrollX === "number" || typeof options.scrollY === "number"; const dx = typeof options.scrollX === "number" ? options.scrollX : 0; const dy = typeof options.scrollY === "number" ? options.scrollY : 0; // If a specific target is given and exists if (target && el) { if (hasOffsets && isScrollable(el)) { el.scrollBy({ left: dx, top: dy, behavior }); return { success: true, details: { selectorUsed: `${target.by}=${target.value}`, offset: { x: dx, y: dy } } }; } if (options && options.to === "top") { if (isScrollable(el)) { el.scrollTo({ top: 0, behavior }); } else { el.scrollIntoView({ behavior, block: 'start', inline: 'nearest' }); } return { success: true, details: { selectorUsed: `${target.by}=${target.value}`, to: "top" } }; } if (options && options.to === "bottom") { if (isScrollable(el)) { el.scrollTo({ top: el.scrollHeight, behavior }); } else { el.scrollIntoView({ behavior, block: 'end', inline: 'nearest' }); } return { success: true, details: { selectorUsed: `${target.by}=${target.value}`, to: "bottom" } }; } // Default: bring it into view el.scrollIntoView({ behavior, block: "center", inline: "center" }); return { success: true, details: { selectorUsed: `${target.by}=${target.value}` } }; } // No specific target: use primary scroll container or window const container = findPrimaryScrollContainer(); if (hasOffsets) { if (container && container !== document.scrollingElement && container !== document.documentElement && container !== document.body && isScrollable(container)) { container.scrollBy({ left: dx, top: dy, behavior }); } else { window.scrollBy({ left: dx, top: dy, behavior }); } return { success: true, details: { offset: { x: dx, y: dy }, container: container?.tagName?.toLowerCase() || 'window' } }; } if (options && options.to === "top") { if (container && container !== document.scrollingElement && isScrollable(container)) { container.scrollTo({ top: 0, behavior }); } else { window.scrollTo({ top: 0, behavior }); } return { success: true, details: { to: "top", container: container?.tagName?.toLowerCase() || 'window' } }; } if (options && options.to === "bottom") { if (container && container !== document.scrollingElement && isScrollable(container)) { container.scrollTo({ top: container.scrollHeight, behavior }); } else { window.scrollTo({ top: document.body.scrollHeight, behavior }); } return { success: true, details: { to: "bottom", container: container?.tagName?.toLowerCase() || 'window' } }; } return { success: false, error: "INVALID_SCROLL_PARAMS" }; } if (action === "waitForSelector") { return { success: true, details: { selectorUsed: `${target.by}=${target.value}`, matchedCount: el ? 1 : 0 } }; } return { success: false, error: "UNSUPPORTED_ACTION" }; })(); } // Validate server identity async function validateServerIdentity(host, port) { try { const response = await fetch(`http://${host}:${port}/.identity`, { signal: AbortSignal.timeout(3000), // 3 second timeout }); if (!response.ok) { console.error(`Invalid server response: ${response.status}`); return false; } const identity = await response.json(); // Validate the server signature if (identity.signature !== "mcp-browser-connector-24x7") { console.error("Invalid server signature - not the browser tools server"); return false; } return true; } catch (error) { console.error("Error validating server identity:", error); return false; } } // Track URLs for each tab const tabUrls = new Map(); // Function to get the current URL for a tab async function getCurrentTabUrl(tabId) { try { console.log("Background: Getting URL for tab", tabId); // First check if we have it cached if (tabUrls.has(tabId)) { const cachedUrl = tabUrls.get(tabId); console.log("Background: Found cached URL:", cachedUrl); return cachedUrl; } // Otherwise get it from the tab try { const tab = await chrome.tabs.get(tabId); if (tab && tab.url) { // Cache the URL tabUrls.set(tabId, tab.url); console.log("Background: Got URL from tab:", tab.url); return tab.url; } else { console.log("Background: Tab exists but no URL found"); } } catch (tabError) { console.error("Background: Error getting tab:", tabError); } // If we can't get the tab directly, try querying for active tabs try { const tabs = await chrome.tabs.query({ active: true, currentWindow: true, }); if (tabs && tabs.length > 0 && tabs[0].url) { const activeUrl = tabs[0].url; console.log("Background: Got URL from active tab:", activeUrl); // Cache this URL as well tabUrls.set(tabId, activeUrl); return activeUrl; } } catch (queryError) { console.error("Background: Error querying tabs:", queryError); } console.log("Background: Could not find URL for tab", tabId); return null; } catch (error) { console.error("Background: Error getting tab URL:", error); return null; } } // Listen for tab updates to detect page refreshes and URL changes chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => { // Track URL changes if (changeInfo.url) { console.log(`URL changed in tab ${tabId} to ${changeInfo.url}`); tabUrls.set(tabId, changeInfo.url); // Send URL update to server if possible updateServerWithUrl(tabId, changeInfo.url, "tab_url_change"); } // Check if this is a page refresh (status becoming "complete") if (changeInfo.status === "complete") { // Update URL in our cache if (tab.url) { tabUrls.set(tabId, tab.url); // Send URL update to server if possible updateServerWithUrl(tabId, tab.url, "page_complete"); } retestConnectionOnRefresh(tabId); } }); // Listen for tab activation (switching between tabs) chrome.tabs.onActivated.addListener((activeInfo) => { const tabId = activeInfo.tabId; console.log(`Tab activated: ${tabId}`); // Get the URL of the newly activated tab chrome.tabs.get(tabId, (tab) => { if (chrome.runtime.lastError) { console.error("Error getting tab info:", chrome.runtime.lastError); return; } if (tab && tab.url) { console.log(`Active tab changed to ${tab.url}`); // Update our cache tabUrls.set(tabId, tab.url); // Send URL update to server updateServerWithUrl(tabId, tab.url, "tab_activated"); } }); }); // Function to update the server with the current URL async function updateServerWithUrl(tabId, url, source = "background_update") { if (!url) { console.error("Cannot update server with empty URL"); return; } console.log(`Updating server with URL for tab ${tabId}: ${url}`); // Get the saved settings chrome.storage.local.get(["browserConnectorSettings"], async (result) => { const settings = result.browserConnectorSettings || { serverHost: "localhost", serverPort: 3025, }; // Enhanced retry logic for autonomous operation reliability const maxRetries = 5; // Increased for autonomous workflows let retryCount = 0; let success = false; let backoffDelay = 500; // Start with 500ms, will increase exponentially while (retryCount < maxRetries && !success) { try { // Validate server connection before attempting URL update const isServerValid = await validateServerIdentity( settings.serverHost, settings.serverPort ); if (!isServerValid) { console.warn( `Server validation failed on attempt ${ retryCount + 1 }, trying anyway...` ); } // Send the URL to the server const serverUrl = `http://${settings.serverHost}:${settings.serverPort}/current-url`; console.log( `Attempt ${ retryCount + 1 }/${maxRetries} to update server with URL: ${url} (source: ${source})` ); const response = await fetch(serverUrl, { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ url: url, tabId: tabId, timestamp: Date.now(), source: source, }), // Longer timeout for autonomous operation stability signal: AbortSignal.timeout(10000), }); if (response.ok) { const responseData = await response.json(); console.log( `Successfully updated server with URL: ${url} (attempt ${ retryCount + 1 })`, responseData ); success = true; } else { console.error( `Server returned error: ${response.status} ${ response.statusText } (attempt ${retryCount + 1})` ); retryCount++; // Exponential backoff for better autonomous recovery if (retryCount < maxRetries) { console.log(`Waiting ${backoffDelay}ms before retry...`); await new Promise((resolve) => setTimeout(resolve, backoffDelay)); backoffDelay = Math.min(backoffDelay * 2, 5000); // Cap at 5 seconds } } } catch (error) { console.error( `Error updating server with URL (attempt ${retryCount + 1}): ${ error.message }` ); retryCount++; // Exponential backoff for network errors too if (retryCount < maxRetries) { console.log( `Network error, waiting ${backoffDelay}ms before retry...` ); await new Promise((resolve) => setTimeout(resolve, backoffDelay)); backoffDelay = Math.min(backoffDelay * 2, 5000); } } } if (!success) { console.error( `Failed to update server with URL after ${maxRetries} attempts` ); } }); } // Clean up when tabs are closed chrome.tabs.onRemoved.addListener((tabId) => { tabUrls.delete(tabId); }); // Function to retest connection when a page is refreshed async function retestConnectionOnRefresh(tabId) { console.log(`Page refreshed in tab ${tabId}, retesting connection...`); // Get the saved settings chrome.storage.local.get(["browserConnectorSettings"], async (result) => { const settings = result.browserConnectorSettings || { serverHost: "localhost", serverPort: 3025, }; // Test the connection with the last known host and port const isConnected = await validateServerIdentity( settings.serverHost, settings.serverPort ); // Notify all devtools instances about the connection status (safely handle no receiver) chrome.runtime.sendMessage( { type: "CONNECTION_STATUS_UPDATE", isConnected: isConnected, tabId: tabId, }, () => { if (chrome.runtime.lastError) { const msg = chrome.runtime.lastError.message || ""; if (msg.includes("Receiving end does not exist")) { console.log( "Background: No receiver for CONNECTION_STATUS_UPDATE (DevTools likely closed). Suppressing." ); } else { console.warn( "Background: sendMessage callback error (CONNECTION_STATUS_UPDATE):", chrome.runtime.lastError ); } } } ); // Always notify for page refresh, whether connected or not // This ensures any ongoing discovery is cancelled and restarted console.log( `Background: Attempting to send INITIATE_AUTO_DISCOVERY (reason: page_refresh, tabId: ${tabId})` ); chrome.runtime.sendMessage( { type: "INITIATE_AUTO_DISCOVERY", reason: "page_refresh", tabId: tabId, forceRestart: true, // Force restart any ongoing processes }, () => { if (chrome.runtime.lastError) { const msg = chrome.runtime.lastError.message || ""; if (msg.includes("Receiving end does not exist")) { console.log( "Background: Suppressed 'Receiving end does not exist' for INITIATE_AUTO_DISCOVERY (DevTools likely not ready yet)" ); } else { console.warn( "Background: sendMessage callback error (INITIATE_AUTO_DISCOVERY):", chrome.runtime.lastError ); } } } ); if (!isConnected) { console.log( "Connection test failed after page refresh, initiating auto-discovery..." ); } else { console.log("Connection test successful after page refresh"); } }); } // Function to capture and send screenshot function captureAndSendScreenshot(message, settings, sendResponse) { console.log("Background: In captureAndSendScreenshot function"); // Get the inspected window's tab chrome.tabs.get(message.tabId, (tab) => { if (chrome.runtime.lastError) { console.error("Error getting tab:", chrome.runtime.lastError); sendResponse({ success: false, error: chrome.runtime.lastError.message, }); return; } console.log("Background: Got tab info:", tab); // Get all windows to find the one containing our tab chrome.windows.getAll({ populate: true }, (windows) => { const targetWindow = windows.find((w) => w.tabs.some((t) => t.id === message.tabId) ); console.log("Background: Found target window:", targetWindow); if (!targetWindow) { console.error("Could not find window containing the inspected tab"); sendResponse({ success: false, error: "Could not find window containing the inspected tab", }); return; } // Capture screenshot of the window containing our tab chrome.tabs.captureVisibleTab( targetWindow.id, { format: "png" }, (dataUrl) => { console.log("Background: Screenshot captured, sending to server"); // Ignore DevTools panel capture error if it occurs if ( chrome.runtime.lastError && !chrome.runtime.lastError.message.includes("devtools://") ) { console.error( "Error capturing screenshot:", chrome.runtime.lastError ); sendResponse({ success: false, error: chrome.runtime.lastError.message, }); return; } // Send screenshot data to browser connector using configured settings const serverUrl = `http://${settings.serverHost}:${settings.serverPort}/screenshot`; console.log(`Sending screenshot to ${serverUrl}`); fetch(serverUrl, { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ data: dataUrl, path: message.screenshotPath, url: tab.url, // Added tab.url for filename generation }), }) .then((response) => response.json()) .then((result) => { if (result.error) { console.error("Error from server:", result.error); sendResponse({ success: false, error: result.error }); } else { console.log("Screenshot saved successfully:", result.path); // Send success response even if DevTools capture failed sendResponse({ success: true, path: result.path, title: tab.title || "Current Tab", }); } }) .catch((error) => { console.error("Error sending screenshot data:", error); sendResponse({ success: false, error: error.message || "Failed to save screenshot", }); }); } ); }); }); }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Winds-AI/Frontend-development-MCP-tools-public'

If you have feedback or need assistance with the MCP directory API, please join our Discord server