supapup
Version:
β‘ Lightning-fast MCP browser dev tool. Navigate β Get instant structured data. No screenshots needed! Puppeteer: πΈ β CSS selectors β JS eval. Supapup: semantic IDs ready to use. 10x faster, 90% fewer tokens.
863 lines (831 loc) β’ 40.4 kB
JavaScript
export class DevToolsElements {
page = null;
client = null;
constructor() { }
async initialize(page, client) {
this.page = page;
this.client = client;
// console.log('[DevToolsElements] Initialized');
}
isInitialized() {
return this.page !== null && this.client !== null;
}
async inspectElement(args) {
if (!this.page || !this.client) {
return {
content: [{
type: 'text',
text: 'β DevTools not initialized. Please navigate to a page first.'
}],
};
}
try {
// Convert agent page ID to CSS selector if needed
let selector = args.selector;
// If it looks like an agent page ID (no CSS selector characters)
if (!selector.includes('.') && !selector.includes('#') && !selector.includes('[') && !selector.includes(' ') && !selector.includes('>')) {
// Try as data-mcp-id first
selector = `[data-mcp-id="${args.selector}"]`;
}
// Get element handle
let element = await this.page.$(selector);
// If not found and we tried data-mcp-id, try the original selector as-is
if (!element && selector.startsWith('[data-mcp-id=')) {
element = await this.page.$(args.selector);
}
if (!element) {
return {
content: [{
type: 'text',
text: `β Element not found: ${args.selector}\n` +
(selector !== args.selector ? `Also tried: ${selector}` : '')
}],
};
}
// Get all element information directly from the page context
const elementInfo = await element.evaluate(el => {
const rect = el.getBoundingClientRect();
const styles = window.getComputedStyle(el);
// Get all attributes
const attributes = {};
Array.from(el.attributes).forEach(attr => {
attributes[attr.name] = attr.value;
});
// Get inline styles
const inlineStyles = {};
const styleAttr = el.getAttribute('style');
if (styleAttr) {
styleAttr.split(';').forEach(style => {
const [key, value] = style.split(':').map(s => s.trim());
if (key && value) {
inlineStyles[key] = value;
}
});
}
// Get ALL computed styles
const computedStyles = {};
for (let i = 0; i < styles.length; i++) {
const propName = styles[i];
computedStyles[propName] = styles.getPropertyValue(propName);
}
// Calculate box model
const paddingTop = parseFloat(styles.paddingTop);
const paddingRight = parseFloat(styles.paddingRight);
const paddingBottom = parseFloat(styles.paddingBottom);
const paddingLeft = parseFloat(styles.paddingLeft);
const borderTop = parseFloat(styles.borderTopWidth);
const borderRight = parseFloat(styles.borderRightWidth);
const borderBottom = parseFloat(styles.borderBottomWidth);
const borderLeft = parseFloat(styles.borderLeftWidth);
const marginTop = parseFloat(styles.marginTop);
const marginRight = parseFloat(styles.marginRight);
const marginBottom = parseFloat(styles.marginBottom);
const marginLeft = parseFloat(styles.marginLeft);
return {
tagName: el.tagName.toLowerCase(),
id: el.id,
className: el.className,
textContent: el.textContent?.substring(0, 100),
innerHTML: el.innerHTML.substring(0, 200),
isVisible: rect.width > 0 && rect.height > 0,
dimensions: {
width: rect.width,
height: rect.height,
top: rect.top,
left: rect.left,
},
// Key computed styles
display: styles.display,
position: styles.position,
color: styles.color,
backgroundColor: styles.backgroundColor,
font: styles.font,
margin: styles.margin,
padding: styles.padding,
border: styles.border,
attributes,
inlineStyles,
computedStyles,
boxModel: {
content: { width: rect.width - paddingLeft - paddingRight - borderLeft - borderRight, height: rect.height - paddingTop - paddingBottom - borderTop - borderBottom },
padding: { top: paddingTop, right: paddingRight, bottom: paddingBottom, left: paddingLeft },
border: { top: borderTop, right: borderRight, bottom: borderBottom, left: borderLeft },
margin: { top: marginTop, right: marginRight, bottom: marginBottom, left: marginLeft },
}
};
});
return {
content: [{
type: 'text',
text: `π Element Inspection Results for: ${args.selector}
π·οΈ Basic Info:
β’ Tag: <${elementInfo.tagName}>
β’ ID: ${elementInfo.id || '(none)'}
β’ Class: ${elementInfo.className || '(none)'}
β’ Visible: ${elementInfo.isVisible ? 'Yes' : 'No'}
π Dimensions:
β’ Width: ${elementInfo.dimensions.width}px
β’ Height: ${elementInfo.dimensions.height}px
β’ Position: ${elementInfo.dimensions.top}px from top, ${elementInfo.dimensions.left}px from left
π¨ Key Styles:
β’ Display: ${elementInfo.display}
β’ Position: ${elementInfo.position}
β’ Color: ${elementInfo.color}
β’ Background: ${elementInfo.backgroundColor}
β’ Font: ${elementInfo.font}
β’ Margin: ${elementInfo.margin}
β’ Padding: ${elementInfo.padding}
β’ Border: ${elementInfo.border}
π Attributes: ${JSON.stringify(elementInfo.attributes, null, 2)}
π¬ Text Content: ${elementInfo.textContent || '(empty)'}
π§ Inline Styles: ${Object.keys(elementInfo.inlineStyles).length > 0 ? JSON.stringify(elementInfo.inlineStyles, null, 2) : '(none)'}
π¦ Box Model:
β’ Content: ${elementInfo.boxModel.content.width.toFixed(1)} Γ ${elementInfo.boxModel.content.height.toFixed(1)}
β’ Padding: T:${elementInfo.boxModel.padding.top} R:${elementInfo.boxModel.padding.right} B:${elementInfo.boxModel.padding.bottom} L:${elementInfo.boxModel.padding.left}
β’ Border: T:${elementInfo.boxModel.border.top} R:${elementInfo.boxModel.border.right} B:${elementInfo.boxModel.border.bottom} L:${elementInfo.boxModel.border.left}
β’ Margin: T:${elementInfo.boxModel.margin.top} R:${elementInfo.boxModel.margin.right} B:${elementInfo.boxModel.margin.bottom} L:${elementInfo.boxModel.margin.left}
π― Computed Styles (${Object.keys(elementInfo.computedStyles).length} properties):
${Object.entries(elementInfo.computedStyles)
.sort(([a], [b]) => a.localeCompare(b))
.slice(0, 50) // Show first 50 for brevity
.map(([prop, value]) => ` β’ ${prop}: ${value}`)
.join('\n')}
${Object.keys(elementInfo.computedStyles).length > 50 ? `\n ... and ${Object.keys(elementInfo.computedStyles).length - 50} more properties` : ''}`
}],
};
}
catch (error) {
// console.error('[DevToolsElements] Inspect error:', error);
return {
content: [{
type: 'text',
text: `β Failed to inspect element: ${error.message}`
}],
};
}
}
async modifyCSS(args) {
if (!this.page || !this.client) {
return {
content: [{
type: 'text',
text: 'β DevTools Elements not initialized. Please wait a few seconds after navigation.'
}],
};
}
try {
// Use page.evaluate to modify CSS directly
const result = await this.page.evaluate(({ selector, property, value }) => {
const element = document.querySelector(selector);
if (!element) {
// Get available elements for better error message
const availableElements = Array.from(document.querySelectorAll('*'))
.filter(el => el.tagName && !['SCRIPT', 'STYLE', 'META', 'LINK', 'HEAD', 'HTML'].includes(el.tagName))
.map(el => {
const tag = el.tagName.toLowerCase();
const id = el.id ? `#${el.id}` : '';
const classes = el.className ? `.${el.className.split(' ').join('.')}` : '';
const text = el.textContent?.trim().substring(0, 30) || '';
return `${tag}${id}${classes} ${text ? `"${text}..."` : ''}`.trim();
})
.slice(0, 10); // Limit to first 10 for readability
return {
success: false,
error: 'Element not found',
selector: selector,
availableElements: availableElements
};
}
// Store original value
const originalValue = element.style.getPropertyValue(property);
// Apply new style
element.style.setProperty(property, value, 'important');
// Verify change
const computedStyle = window.getComputedStyle(element);
const newValue = computedStyle.getPropertyValue(property);
return {
success: true,
selector,
property,
originalValue: originalValue || computedStyle.getPropertyValue(property),
newValue,
applied: element.style.getPropertyValue(property),
};
}, { selector: args.selector, property: args.property, value: args.value });
if (!result.success) {
let errorMessage = `β Element not found: "${args.selector}"\n\n`;
if (result.availableElements && result.availableElements.length > 0) {
errorMessage += `π Available elements on this page:\n`;
result.availableElements.forEach((el) => {
errorMessage += ` β’ ${el}\n`;
});
errorMessage += `\nπ‘ Try using one of these selectors instead, or use devtools_visual_element_map to see all elements visually.`;
}
else {
errorMessage += `π‘ No suitable elements found. Try:\n`;
errorMessage += ` 1. Use devtools_visual_element_map to see all page elements\n`;
errorMessage += ` 2. Check if you're on the right page\n`;
errorMessage += ` 3. Use more specific selectors like #id or .class`;
}
return {
content: [{
type: 'text',
text: errorMessage
}],
};
}
return {
content: [{
type: 'text',
text: `β
CSS Modified Successfully!
π― Selector: ${args.selector}
π¨ Property: ${args.property}
π Original Value: ${result.originalValue}
β¨ New Value: ${result.newValue}
π§ Applied Style: ${result.applied}`
}],
};
}
catch (error) {
// console.error('[DevToolsElements] Modify CSS error:', error);
return {
content: [{
type: 'text',
text: `β Failed to modify CSS: ${error.message}`
}],
};
}
}
async highlightElement(args) {
if (!this.page || !this.client) {
return {
content: [{
type: 'text',
text: 'β DevTools not initialized. Please navigate to a page first.'
}],
};
}
try {
const duration = args.duration || 3000;
// Apply highlight styles and get element info
const elementInfo = await this.page.evaluate(({ selector, duration }) => {
const element = document.querySelector(selector);
if (!element) {
return null;
}
// Store original styles
const originalStyles = {
border: element.style.border,
outline: element.style.outline,
boxShadow: element.style.boxShadow,
backgroundColor: element.style.backgroundColor,
opacity: element.style.opacity,
};
// Apply highlight styles
element.style.setProperty('border', '3px solid #ff0000', 'important');
element.style.setProperty('outline', '3px solid #00ff00', 'important');
element.style.setProperty('outline-offset', '3px', 'important');
element.style.setProperty('box-shadow', '0 0 20px rgba(255, 0, 0, 0.8)', 'important');
element.style.setProperty('background-color', 'rgba(255, 255, 0, 0.2)', 'important');
// Add tracking attribute
element.setAttribute('data-mcp-highlighted', 'true');
element.setAttribute('data-mcp-highlight-id', Date.now().toString());
// Scroll into view
element.scrollIntoView({ behavior: 'smooth', block: 'center' });
// Get element information
const rect = element.getBoundingClientRect();
const computed = window.getComputedStyle(element);
// Auto-remove highlight after duration
setTimeout(() => {
element.style.border = originalStyles.border;
element.style.outline = originalStyles.outline;
element.style.boxShadow = originalStyles.boxShadow;
element.style.backgroundColor = originalStyles.backgroundColor;
element.style.opacity = originalStyles.opacity;
element.removeAttribute('data-mcp-highlighted');
element.removeAttribute('data-mcp-highlight-id');
}, duration);
return {
tagName: element.tagName.toLowerCase(),
id: element.id,
className: element.className,
rect: {
top: rect.top,
left: rect.left,
width: rect.width,
height: rect.height,
},
styles: {
display: computed.display,
position: computed.position,
zIndex: computed.zIndex,
},
attributes: Array.from(element.attributes).reduce((acc, attr) => {
acc[attr.name] = attr.value;
return acc;
}, {}),
};
}, { selector: args.selector, duration });
if (!elementInfo) {
return {
content: [{
type: 'text',
text: `β Element not found: ${args.selector}`
}],
};
}
// Take a screenshot of the highlighted element
await new Promise(resolve => setTimeout(resolve, 100)); // Wait for styles to apply
const screenshot = await this.page.screenshot({
encoding: 'base64',
type: 'png',
clip: {
x: Math.max(0, elementInfo.rect.left - 10),
y: Math.max(0, elementInfo.rect.top - 10),
width: elementInfo.rect.width + 20,
height: elementInfo.rect.height + 20,
}
});
// Validate element highlight screenshot
if (!screenshot || screenshot.length === 0) {
throw new Error(`Element highlight screenshot failed: Empty or invalid screenshot buffer for selector ${args.selector}`);
}
return {
content: [{
type: 'text',
text: `π¦ Element Highlighted: ${args.selector}
π Element Info:
β’ Tag: <${elementInfo.tagName}>
β’ ID: ${elementInfo.id || '(none)'}
β’ Class: ${elementInfo.className || '(none)'}
β’ Position: ${elementInfo.rect.left}px Γ ${elementInfo.rect.top}px
β’ Size: ${elementInfo.rect.width}px Γ ${elementInfo.rect.height}px
β’ Display: ${elementInfo.styles.display}
β’ Z-Index: ${elementInfo.styles.zIndex}
π¨ Highlight Applied:
β’ Red border (3px solid)
β’ Green outline (3px with offset)
β’ Red shadow glow
β’ Yellow background tint
π·οΈ Tracking Attributes Added:
β’ data-mcp-highlighted="true"
β’ data-mcp-highlight-id="${elementInfo.attributes['data-mcp-highlight-id']}"
β±οΈ Auto-remove in ${duration / 1000} seconds
πΈ Screenshot captured (base64 encoded, ${screenshot.length} chars)`
}],
};
}
catch (error) {
// console.error('[DevToolsElements] Highlight error:', error);
return {
content: [{
type: 'text',
text: `β Failed to highlight element: ${error.message}`
}],
};
}
}
async modifyHTML(args) {
if (!this.page || !this.client) {
return {
content: [{
type: 'text',
text: 'β DevTools not initialized. Please navigate to a page first.'
}],
};
}
try {
const result = await this.page.evaluate(({ selector, value, type, attribute }) => {
const element = document.querySelector(selector);
if (!element) {
return { success: false, error: 'Element not found' };
}
let originalValue;
let newValue;
switch (type) {
case 'innerHTML':
originalValue = element.innerHTML;
element.innerHTML = value;
newValue = element.innerHTML;
break;
case 'outerHTML':
originalValue = element.outerHTML;
element.outerHTML = value;
// Can't get new value as element is replaced
newValue = value;
break;
case 'attribute':
if (!attribute) {
return { success: false, error: 'Attribute name required for attribute modification' };
}
originalValue = element.getAttribute(attribute) || '(null)';
element.setAttribute(attribute, value);
newValue = element.getAttribute(attribute) || '(null)';
break;
default:
return { success: false, error: 'Invalid modification type' };
}
return {
success: true,
selector,
type,
attribute,
originalValue: originalValue.substring(0, 200),
newValue: newValue.substring(0, 200),
};
}, { selector: args.selector, value: args.value, type: args.type, attribute: args.attribute });
if (!result.success) {
return {
content: [{
type: 'text',
text: `β ${result.error}`
}],
};
}
return {
content: [{
type: 'text',
text: `β
HTML Modified Successfully!
π― Selector: ${args.selector}
π Type: ${args.type}${result.attribute ? ` (attribute: ${result.attribute})` : ''}
π Original: ${result.originalValue}${result.originalValue && result.originalValue.length >= 200 ? '...' : ''}
β¨ New: ${result.newValue}${result.newValue && result.newValue.length >= 200 ? '...' : ''}`
}],
};
}
catch (error) {
// console.error('[DevToolsElements] Modify HTML error:', error);
return {
content: [{
type: 'text',
text: `β Failed to modify HTML: ${error.message}`
}],
};
}
}
async createVisualElementMap(args) {
if (!this.page || !this.client) {
return {
content: [{
type: 'text',
text: 'β DevTools not initialized. Please navigate to a page first.'
}],
};
}
try {
// Apply visual labels and collect element data
const elementMap = await this.page.evaluate((includeAll) => {
// Color palette for highlighting
const colors = [
'#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FECA57',
'#DDA0DD', '#98D8C8', '#F7DC6F', '#BB8FCE', '#85C1E2',
'#F8B500', '#6C5CE7', '#A29BFE', '#FD79A8', '#FDCB6E',
'#6C5CE7', '#74B9FF', '#A29BFE', '#55A3FF', '#FD79A8'
];
// Get all interactive elements (prioritize those with data-mcp-id)
const selectors = includeAll ? '*' : 'a, button, input, textarea, select, [role="button"], [role="link"], [onclick], [data-mcp-id], img, video, iframe';
const elements = document.querySelectorAll(selectors);
const elementData = [];
let colorIndex = 0;
let labelIndex = 1;
elements.forEach((element) => {
const el = element;
const rect = el.getBoundingClientRect();
// Skip invisible or tiny elements
if (rect.width < 5 || rect.height < 5 ||
window.getComputedStyle(el).display === 'none' ||
window.getComputedStyle(el).visibility === 'hidden') {
return;
}
// Skip if already has a label
if (el.getAttribute('data-mcp-visual-map-id')) {
return;
}
const color = colors[colorIndex % colors.length];
const mapId = `element-${labelIndex}`;
// Store original styles
el.setAttribute('data-mcp-original-border', el.style.border || '');
el.setAttribute('data-mcp-original-position', el.style.position || '');
// Apply highlight border
el.style.setProperty('border', `3px solid ${color}`, 'important');
el.style.setProperty('box-sizing', 'border-box', 'important');
// Create label
const label = document.createElement('div');
label.className = 'mcp-visual-map-label';
label.textContent = labelIndex.toString();
label.style.cssText = `
position: absolute;
background: ${color};
color: white;
font-weight: bold;
font-size: 14px;
padding: 2px 6px;
border-radius: 3px;
z-index: 999999;
pointer-events: none;
font-family: monospace;
top: ${rect.top + window.scrollY}px;
left: ${rect.left + window.scrollX}px;
transform: translate(-50%, -50%);
`;
document.body.appendChild(label);
// Set tracking attributes
el.setAttribute('data-mcp-visual-map-id', mapId);
el.setAttribute('data-mcp-visual-map-color', color);
// Build CSS selector for element
let selector = el.tagName.toLowerCase();
if (el.id) {
selector = `#${el.id}`;
}
else if (el.className && typeof el.className === 'string') {
selector += `.${el.className.split(' ').filter(c => c).join('.')}`;
}
// Get existing agent page data (if available)
const mcpId = el.getAttribute('data-mcp-id');
const mcpAction = el.getAttribute('data-mcp-action');
const mcpType = el.getAttribute('data-mcp-type');
// Collect element data
elementData.push({
mapId: mapId,
label: labelIndex.toString(),
selector: selector,
dataSelector: `[data-mcp-visual-map-id="${mapId}"]`,
borderColor: color,
tagName: el.tagName.toLowerCase(),
text: (el.textContent || '').trim().substring(0, 50),
type: el.getAttribute('type') || el.getAttribute('role') || el.tagName.toLowerCase(),
position: {
top: rect.top,
left: rect.left,
width: rect.width,
height: rect.height
},
attributes: {
id: el.id,
className: el.className,
name: el.getAttribute('name'),
href: el.getAttribute('href'),
'data-mcp-id': mcpId,
'data-mcp-action': mcpAction,
'data-mcp-type': mcpType
}
});
colorIndex++;
labelIndex++;
});
// Add styles for labels
const style = document.createElement('style');
style.id = 'mcp-visual-map-styles';
style.textContent = `
.mcp-visual-map-label {
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
}
`;
document.head.appendChild(style);
return {
elementCount: elementData.length,
elements: elementData
};
}, args.includeAll || false);
// Take screenshot
await new Promise(resolve => setTimeout(resolve, 1000)); // Wait longer for all labels to render
const screenshot = await this.page.screenshot({
encoding: 'base64',
fullPage: false, // Only visible viewport
type: 'png'
});
// Validate visual element map screenshot
if (!screenshot || screenshot.length === 0) {
throw new Error('Visual element map screenshot failed: Empty or invalid screenshot buffer');
}
// Automatically open screenshot in new browser tab
try {
const browser = this.page.browser();
const newPage = await browser.newPage();
await newPage.goto(`data:image/png;base64,${screenshot}`);
// console.log('[Visual Element Map] Opened screenshot in new tab');
// Wait 2 seconds to show screenshot, then switch back to original tab
setTimeout(async () => {
try {
if (this.page) {
await this.page.bringToFront();
// console.log('[Visual Element Map] Switched back to original tab');
}
}
catch (error) {
// console.error('[Visual Element Map] Failed to switch back to original tab:', error);
}
}, 2000);
}
catch (error) {
// console.error('[Visual Element Map] Failed to open screenshot in tab:', error);
}
// Clean up after screenshot
await this.page.evaluate(() => {
// Remove labels
document.querySelectorAll('.mcp-visual-map-label').forEach(label => label.remove());
// Remove style
document.getElementById('mcp-visual-map-styles')?.remove();
// Restore original styles
document.querySelectorAll('[data-mcp-visual-map-id]').forEach(el => {
const element = el;
const originalBorder = element.getAttribute('data-mcp-original-border') || '';
element.style.border = originalBorder;
// Clean up temporary attributes
element.removeAttribute('data-mcp-visual-map-id');
element.removeAttribute('data-mcp-visual-map-color');
element.removeAttribute('data-mcp-original-border');
element.removeAttribute('data-mcp-original-position');
});
});
// Apply same size limits as regular screenshots
const MAX_BASE64_LENGTH = 11000;
const TOKEN_BUFFER = 0.8;
const SAFE_BASE64_LENGTH = Math.floor(MAX_BASE64_LENGTH * TOKEN_BUFFER);
const MAX_ELEMENTS_IN_RESPONSE = 10;
// Check if screenshot needs chunking (same logic as regular screenshots)
const screenshotTooBig = screenshot.length > SAFE_BASE64_LENGTH;
// Check if element list needs chunking
const elementsToShow = elementMap.elements.slice(0, MAX_ELEMENTS_IN_RESPONSE);
const hasMoreElements = elementMap.elements.length > MAX_ELEMENTS_IN_RESPONSE;
// For large pages (500+ elements), always use chunking mode to avoid token limits
const tooManyElements = elementMap.elementCount > 500;
if (screenshotTooBig || tooManyElements) {
// Include the image but with very limited element details to stay under token limits
return {
content: [
{
type: 'image',
data: screenshot,
mimeType: 'image/png',
},
{
type: 'text',
text: `πΊοΈ Visual Element Map Created
π Found ${elementMap.elementCount} interactive elements numbered and highlighted in the image
π― To interact with any numbered element:
1. Use agent page execute_action with existing data-mcp-id (if available)
2. Use devtools_modify_css with CSS selector [data-mcp-visual-map-id="element-{number}"]
3. Use devtools_highlight_element with CSS selector [data-mcp-visual-map-id="element-{number}"]
π‘ Examples:
β’ execute_action({actionId: "login-email", params: {value: "test@example.com"}}) - If element has data-mcp-id
β’ devtools_modify_css({selector: "[data-mcp-visual-map-id='element-1']", property: "background", value: "red"})
β’ devtools_highlight_element({selector: "[data-mcp-visual-map-id='element-1']"})
πΈ Current view shows elements in the visible area
π For full page coverage: Use screenshot_capture with fullPage: true
π± Screenshot automatically opened in new browser tab for better viewing`
}
],
};
}
return {
content: [
{
type: 'image',
data: screenshot,
mimeType: 'image/png',
},
{
type: 'text',
text: `πΊοΈ Visual Element Map Created
π Found ${elementMap.elementCount} interactive elements
πΈ Screenshot: Full page captured with all elements highlighted and labeled
π― Element Map${hasMoreElements ? ` (showing first ${MAX_ELEMENTS_IN_RESPONSE} of ${elementMap.elementCount})` : ''}:
${elementsToShow.map(el => {
const mcpId = el.attributes['data-mcp-id'];
const mcpAction = el.attributes['data-mcp-action'];
const interaction = mcpId ?
`execute_action({actionId: "${mcpId}"${mcpAction === 'fill' ? ', params: {value: "text"}' : ''}})` :
`devtools_modify_css({selector: "[data-mcp-visual-map-id='${el.mapId}']", property: "background", value: "red"})`;
return `[${el.label}] ${el.tagName}${el.attributes.id ? '#' + el.attributes.id : ''} β ${interaction} - "${el.text.substring(0, 30)}${el.text.length > 30 ? '...' : ''}" (${el.borderColor})`;
}).join('\n')}
${hasMoreElements ? `\nβ οΈ ${elementMap.elementCount - MAX_ELEMENTS_IN_RESPONSE} more elements not shown (response size limit)\n` : ''}
π― To interact with any numbered element:
1. **Agent Page Elements** (preferred): Use execute_action with data-mcp-id
2. **Visual Map Elements**: Use devtools tools with CSS selector [data-mcp-visual-map-id="element-{number}"]
π‘ Examples:
β’ execute_action({actionId: "login-email", params: {value: "test@example.com"}}) - If element has data-mcp-id
β’ devtools_modify_css({selector: "[data-mcp-visual-map-id='element-1']", property: "background", value: "red"})
β’ devtools_highlight_element({selector: "[data-mcp-visual-map-id='element-1']"})
π· Screenshot Length: ${screenshot.length} characters (base64)
π± Screenshot automatically opened in new browser tab for better viewing`
}
],
};
}
catch (error) {
// console.error('[DevToolsElements] Visual element map error:', error);
return {
content: [{
type: 'text',
text: `β Failed to create visual element map: ${error.message}`
}],
};
}
}
async getComputedStyles(args) {
if (!this.page || !this.client) {
return {
content: [{
type: 'text',
text: 'β DevTools not initialized. Please navigate to a page first.'
}],
};
}
try {
// Convert agent page ID to CSS selector if needed
let selector = args.selector;
// If it looks like an agent page ID (no CSS selector characters)
if (!selector.includes('.') && !selector.includes('#') && !selector.includes('[') && !selector.includes(' ') && !selector.includes('>')) {
// Try as data-mcp-id first
selector = `[data-mcp-id="${args.selector}"]`;
}
const styles = await this.page.evaluate((sel) => {
// Try agent page ID first if it looks like one
let element = document.querySelector(sel);
// If not found and we tried data-mcp-id, try the original selector
if (!element && sel.startsWith('[data-mcp-id=')) {
const id = sel.match(/\[data-mcp-id="(.+)"\]/)?.[1];
if (id) {
element = document.querySelector(id);
}
}
if (!element) {
return null;
}
const computed = window.getComputedStyle(element);
const allStyles = {};
// Get all computed style properties
for (let i = 0; i < computed.length; i++) {
const propName = computed[i];
allStyles[propName] = computed.getPropertyValue(propName);
}
// Also get CSS variables
const cssVars = {};
for (const prop in allStyles) {
if (prop.startsWith('--')) {
cssVars[prop] = allStyles[prop];
}
}
return {
allStyles,
cssVars,
important: {
display: computed.display,
position: computed.position,
width: computed.width,
height: computed.height,
color: computed.color,
backgroundColor: computed.backgroundColor,
fontSize: computed.fontSize,
fontFamily: computed.fontFamily,
margin: computed.margin,
padding: computed.padding,
border: computed.border,
zIndex: computed.zIndex,
opacity: computed.opacity,
visibility: computed.visibility,
}
};
}, selector);
if (!styles) {
return {
content: [{
type: 'text',
text: `β Element not found: ${args.selector}`
}],
};
}
return {
content: [{
type: 'text',
text: `π¨ Computed Styles for: ${args.selector}
π Key Properties:
${Object.entries(styles.important).map(([key, value]) => ` β’ ${key}: ${value}`).join('\n')}
π§ CSS Variables:
${Object.keys(styles.cssVars).length > 0 ?
Object.entries(styles.cssVars).map(([key, value]) => ` β’ ${key}: ${value}`).join('\n') :
' (none)'}
π‘ Total Properties: ${Object.keys(styles.allStyles).length}
Use devtools_modify_css to change any of these properties.`
}],
};
}
catch (error) {
// console.error('[DevToolsElements] Get computed styles error:', error);
return {
content: [{
type: 'text',
text: `β Failed to get computed styles: ${error.message}`
}],
};
}
}
// Cleanup method
async cleanup() {
// Note: We don't detach the CDP session here because it's managed externally
// The CDP session is passed in from index.ts and should be cleaned up there
this.page = null;
this.client = null;
}
}