UNPKG

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