browser-x-mcp
Version:
AI-Powered Browser Automation with Advanced Form Testing - A Model Context Provider (MCP) server that enables intelligent browser automation with form testing, element extraction, and comprehensive logging
245 lines (207 loc) • 8.09 kB
JavaScript
/**
* XML Canvas Extractor for Browser[X]MCP
* Clean implementation using ID-based element indexing
* Ultra-compact format with coordinate lookup
*
* @author Browser[X]MCP Team
* @version 4.0.0
*/
class XMLCanvasExtractor {
constructor() {
this.viewport = this.getViewport();
this.typeMap = {
// Ultra-compact type mapping
'input': 'i',
'button': 'b',
'a': 'l',
'select': 's',
'textarea': 't',
'div': 'd',
'span': 'p',
'img': 'g',
'form': 'f'
};
this.elementIndex = 0;
this.coordinateMap = new Map(); // ID -> coordinates mapping
}
getViewport() {
return {
x: window.pageXOffset || document.documentElement.scrollLeft,
y: window.pageYOffset || document.documentElement.scrollTop,
width: window.innerWidth,
height: window.innerHeight
};
}
isElementVisible(element) {
const rect = element.getBoundingClientRect();
return (
rect.width > 0 &&
rect.height > 0 &&
rect.bottom > 0 &&
rect.right > 0 &&
rect.top < this.viewport.height &&
rect.left < this.viewport.width
);
}
getElementType(element) {
const tag = element.tagName.toLowerCase();
const type = element.getAttribute('type')?.toLowerCase();
if (tag === 'input') {
return type || 'text';
}
return this.typeMap[tag] || tag.substring(0, 1);
}
getElementContent(element) {
const text = (element.textContent || '').trim();
const placeholder = element.getAttribute('placeholder') || '';
const value = element.value || '';
const alt = element.getAttribute('alt') || '';
const title = element.getAttribute('title') || '';
const content = text || placeholder || value || alt || title || '';
return content.substring(0, 20).replace(/[<>&"'\s]/g, ''); // Clean & compact for XML
}
isInteractive(element) {
const interactiveTags = ['input', 'button', 'a', 'select', 'textarea'];
const interactiveRoles = ['button', 'link', 'tab', 'checkbox', 'radio'];
return interactiveTags.includes(element.tagName.toLowerCase()) ||
interactiveRoles.includes(element.getAttribute('role')) ||
element.hasAttribute('onclick') ||
element.tabIndex >= 0;
}
generateElementId() {
return (this.elementIndex++).toString(36); // Base36 for ultra-compact IDs: 0,1,2...9,a,b,c...z,10,11...
}
extractElements() {
const allElements = document.querySelectorAll('*');
const elements = [];
this.coordinateMap.clear();
this.elementIndex = 0;
allElements.forEach((element) => {
if (!this.isElementVisible(element)) return;
const rect = element.getBoundingClientRect();
const interactive = this.isInteractive(element);
// Skip tiny elements and non-interactive unless important
if (rect.width < 5 || rect.height < 5) {
if (!interactive) return;
}
const id = this.generateElementId();
const centerX = Math.round(rect.left + rect.width / 2);
const centerY = Math.round(rect.top + rect.height / 2);
// Store coordinates in lookup map
this.coordinateMap.set(id, {
x: centerX,
y: centerY,
w: Math.round(rect.width),
h: Math.round(rect.height),
left: Math.round(rect.left),
top: Math.round(rect.top)
});
const elementData = {
id,
type: this.getElementType(element),
interactive,
content: this.getElementContent(element)
};
elements.push(elementData);
});
return elements;
}
generateCompactXML() {
const elements = this.extractElements();
const interactiveElements = elements.filter(el => el.interactive);
// Start with minimal page container
let xml = `<p w="${this.viewport.width}" h="${this.viewport.height}">`;
// Add interactive elements with minimal attributes
interactiveElements.forEach((el) => {
xml += `<e id="${el.id}" t="${el.type}"`;
if (el.content) {
xml += ` c="${el.content}"`;
}
xml += `/>`;
});
xml += '</p>';
return xml;
}
getScrollInfo() {
const documentHeight = Math.max(
document.body.scrollHeight,
document.documentElement.scrollHeight
);
const maxScrollY = Math.max(0, documentHeight - this.viewport.height);
return {
scrollable: documentHeight > this.viewport.height,
current_y: this.viewport.y,
max_y: maxScrollY,
progress: maxScrollY > 0 ? this.viewport.y / maxScrollY : 0
};
}
// Convert element ID to click coordinates
getClickCoordinates(elementId) {
// Build XML and coordinate map if not already done
if (!this.coordinateMap || this.coordinateMap.size === 0) {
this.buildXML();
}
const coords = this.coordinateMap.get(elementId);
if (!coords) {
throw new Error(`Element ID ${elementId} not found in coordinate map`);
}
return {
x: coords.x,
y: coords.y,
bounds: {
left: coords.left,
top: coords.top,
width: coords.w,
height: coords.h
}
};
}
// Get all available element IDs with their types
getElementIndex() {
const elements = this.extractElements();
const index = {};
elements.filter(el => el.interactive).forEach(el => {
index[el.id] = {
type: el.type,
content: el.content,
coords: this.coordinateMap.get(el.id)
};
});
return index;
}
extract() {
const xml = this.generateCompactXML();
const elements = this.extractElements();
const interactiveElements = elements.filter(el => el.interactive);
const scrollInfo = this.getScrollInfo();
const elementIndex = this.getElementIndex();
// Calculate efficiency
const xmlSize = new Blob([xml]).size;
const coordinateMapSize = new Blob([JSON.stringify(Object.fromEntries(this.coordinateMap))]).size;
const totalSize = xmlSize + coordinateMapSize;
const estimatedScreenshotSize = this.viewport.width * this.viewport.height * 3; // RGB
const compressionRatio = Math.round(estimatedScreenshotSize / totalSize);
return {
format: 'compact_xml',
xml: xml,
coordinate_map: Object.fromEntries(this.coordinateMap),
element_index: elementIndex,
scroll: scrollInfo,
stats: {
total_elements: elements.length,
interactive_elements: interactiveElements.length,
xml_size: xmlSize,
coordinate_map_size: coordinateMapSize,
total_size: totalSize,
estimated_screenshot_size: estimatedScreenshotSize,
compression_ratio: compressionRatio,
efficiency_gain: Math.round((1 - totalSize / estimatedScreenshotSize) * 100)
},
timestamp: Date.now()
};
}
}
// Make available globally
if (typeof window !== 'undefined') {
window.XMLCanvasExtractor = XMLCanvasExtractor;
}