UNPKG

camoufox-mcp-server

Version:

MCP server for browser automation using Camoufox - a privacy-focused Firefox fork with advanced anti-detection features

97 lines (96 loc) 3.8 kB
import { redactUrl } from "../utils.js"; export async function extractLinks(page, maxLinks, selector) { return page.evaluate(({ maxItems, cssSelector }) => { const root = cssSelector ? document.querySelector(cssSelector) : document.body ?? document.documentElement; if (!root) { return { links: [], truncated: false, found: false }; } function cssIdent(value) { if (typeof CSS !== "undefined" && typeof CSS.escape === "function") { return CSS.escape(value); } return value.replace(/[^a-zA-Z0-9_-]/g, "\\$&"); } function selectorFor(element) { if (element.id) { return `#${cssIdent(element.id)}`; } const path = []; let current = element; while (current && current !== document.documentElement && path.length < 8) { let part = current.tagName.toLowerCase(); if (current.classList.length > 0) { part += `.${Array.from(current.classList).slice(0, 2).map(cssIdent).join(".")}`; } const parent = current.parentElement; if (parent) { const sameTagSiblings = Array.from(parent.children).filter((child) => child.tagName === current?.tagName); if (sameTagSiblings.length > 1) { part += `:nth-of-type(${sameTagSiblings.indexOf(current) + 1})`; } } path.unshift(part); current = parent; } return path.join(" > "); } function textOf(element) { return (element.textContent ?? element.getAttribute("aria-label") ?? element.getAttribute("title") ?? "") .replace(/\s+/g, " ") .trim() .slice(0, 500); } function isVisible(element) { const rect = element.getBoundingClientRect(); const style = window.getComputedStyle(element); return rect.width > 0 && rect.height > 0 && style.display !== "none" && style.visibility !== "hidden" && style.visibility !== "collapse"; } const candidates = [ ...(root.matches("a[href]") ? [root] : []), ...Array.from(root.querySelectorAll("a[href]")), ]; const links = []; let truncated = false; const seen = new Set(); for (const link of candidates) { const href = link.href; if (!href || seen.has(href)) { continue; } const visible = isVisible(link); const text = textOf(link); if (!text && !visible) { continue; } if (links.length >= maxItems) { truncated = true; break; } seen.add(href); links.push({ text, href, selector: selectorFor(link), visible, confidence: visible && text ? 0.95 : visible || text ? 0.75 : 0.5, }); } return { links, truncated, found: true }; }, { maxItems: maxLinks, cssSelector: selector }); } export async function buildLinksPayload(page, response, maxLinks, selector) { const extracted = await extractLinks(page, maxLinks, selector); return { url: redactUrl(page.url()), title: await page.title(), status: response?.status(), contentType: response?.headers()["content-type"], selector, selectorFound: extracted.found, links: extracted.links, truncated: extracted.truncated, maxLinks, }; }