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
JavaScript
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,
};
}