UNPKG

blowback-context

Version:

MCP server that integrates with FE development server for Cursor

1,043 lines (1,039 loc) 62.4 kB
import { z } from 'zod'; import { ENABLE_BASE64 } from '../constants.js'; import { Logger } from '../utils/logger.js'; import { LogManager } from './log-manager.js'; export function registerBrowserTools(server, contextManager, lastHMREvents, screenshotHelpers) { // Get log manager instance const logManager = LogManager.getInstance(); // Function to record logs to file (simplified - will be updated when needed) async function appendLogToFile(type, text) { try { const logEntry = JSON.stringify({ type, text, timestamp: new Date().toISOString(), url: 'unknown', checkpointId: null }) + '\n'; // Record log await logManager.appendLog(logEntry); } catch (error) { Logger.error(`Failed to write console log to file: ${error}`); } } // Utility function: Get browser for operation const getContextForOperation = (contextId) => { let contextInstance; if (contextId) { contextInstance = contextManager.getContext(contextId); if (!contextInstance) { return { isStarted: false, error: { content: [ { type: 'text', text: `Browser '${contextId}' not found. Use 'list-browsers' to see available browsers or 'start-browser' to create one.` } ], isError: true } }; } } else { contextInstance = contextManager.getMostRecentContext(); if (!contextInstance) { return { isStarted: false, error: { content: [ { type: 'text', text: 'No active browsers found. Use \'start-browser\' to create a browser first.' } ], isError: true } }; } } // Note: contextInstance.page is now always defined (never null) return { isStarted: true, page: contextInstance.page }; }; // Utility function: Get current checkpoint ID const getCurrentCheckpointId = async (page) => { const checkpointId = await page.evaluate(() => { const metaTag = document.querySelector('meta[name="__mcp_checkpoint"]'); return metaTag ? metaTag.getAttribute('data-id') : null; }); return checkpointId; }; /** * Serializes evaluation result based on the specified return type * @param result The raw result from JavaScript evaluation * @param returnType The desired return type * @returns Serialized result */ const serializeResult = (result, returnType) => { try { switch (returnType) { case 'string': return String(result); case 'number': return Number(result); case 'boolean': return Boolean(result); case 'json': return JSON.stringify(result, null, 2); case 'auto': default: // Auto-detect and return serializable result return JSON.parse(JSON.stringify(result)); } } catch (error) { Logger.warn('Result serialization failed, returning string representation'); return String(result); } }; // Screenshot capture tool server.tool('capture-screenshot', `Captures a screenshot of the current page or a specific element. Stores the screenshot in the MCP resource system and returns a resource URI. If ENABLE_BASE64 environment variable is set to 'true', also includes base64 encoded image in the response.`, { selector: z.string().optional().describe('CSS selector to capture (captures full page if not provided)'), url: z.string().optional().describe('URL to navigate to before capturing screenshot. Do not provide if you want to capture the current page.'), contextId: z.string().optional().describe('Browser ID to capture from (uses most recent browser if not provided)') }, async ({ selector, url, contextId }) => { try { // Get browser for operation const browserStatus = getContextForOperation(contextId); if (!browserStatus.isStarted) { return browserStatus.error; } // Get current URL const currentUrl = browserStatus.page.url(); // If URL is provided and different from current URL, navigate to it if (url && url !== currentUrl) { Logger.info(`Navigating to ${url} before capturing screenshot`); await browserStatus.page.goto(url, { waitUntil: 'networkidle' }); } // Get current checkpoint ID const checkpointId = await getCurrentCheckpointId(browserStatus.page); let screenshot; if (selector) { // Wait for element to appear await browserStatus.page.waitForSelector(selector, { state: 'visible', timeout: 5000 }); const element = await browserStatus.page.locator(selector).first(); if (!element) { return { content: [ { type: 'text', text: `Element with selector "${selector}" not found` } ], isError: true }; } screenshot = await element.screenshot(); } else { // Capture full page screenshot = await browserStatus.page.screenshot({ fullPage: true }); } // Get final URL (may be different after navigation) const finalUrl = browserStatus.page.url(); // Use screenshot helpers if available if (!screenshotHelpers) { return { content: [ { type: 'text', text: 'Screenshot helpers not available. Cannot save screenshot.' } ], isError: true }; } // Add screenshot using the resource system const description = selector ? `Screenshot of element ${selector} at ${finalUrl}` : `Screenshot of full page at ${finalUrl}`; // Get browser context from the actual browser instance let browserContext = {}; if (contextId) { const contextInstance = contextManager.getContext(contextId); if (contextInstance) { browserContext = { browser_id: contextInstance.id, browser_type: contextInstance.type, session_id: `${contextInstance.id}-${contextInstance.createdAt.getTime()}` }; } } else { const contextInstance = contextManager.getMostRecentContext(); if (contextInstance) { browserContext = { browser_id: contextInstance.id, browser_type: contextInstance.type, session_id: `${contextInstance.id}-${contextInstance.createdAt.getTime()}` }; } } const screenshotResult = await screenshotHelpers.addScreenshot(screenshot, description, checkpointId, finalUrl.replace(/^http(s)?:\/\//, ''), browserContext); Logger.info(`Screenshot saved with ID: ${screenshotResult.id}, URI: ${screenshotResult.resourceUri}`); // Result message construction const resultMessage = { message: 'Screenshot captured successfully', id: screenshotResult.id, resourceUri: screenshotResult.resourceUri, checkpointId, url: finalUrl, }; // Build content array const content = [ { type: 'text', text: JSON.stringify(resultMessage, null, 2) } ]; // Add base64 image only if ENABLE_BASE64 is true if (ENABLE_BASE64) { content.push({ type: 'image', data: screenshot.toString('base64'), mimeType: 'image/png' }); } return { content }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); Logger.error(`Failed to capture screenshot: ${errorMessage}`); return { content: [ { type: 'text', text: `Failed to capture screenshot: ${errorMessage}` } ], isError: true }; } }); // Element properties retrieval tool server.tool('get-element-properties', 'Retrieves properties and state information of a specific element', { selector: z.string().describe('CSS selector of the element to inspect'), properties: z.array(z.string()).describe("Array of property names to retrieve (e.g., ['value', 'checked', 'textContent'])") }, async ({ selector, properties }) => { try { // Check browser status const browserStatus = getContextForOperation(); if (!browserStatus.isStarted) { return browserStatus.error; } // Get current checkpoint ID const checkpointId = await getCurrentCheckpointId(browserStatus.page); // Check if element exists await browserStatus.page.waitForSelector(selector, { state: 'visible', timeout: 5000 }); // Retrieve element properties const elementProperties = await browserStatus.page.evaluate(({ selector, propertiesToGet }) => { const element = document.querySelector(selector); if (!element) return null; const result = {}; propertiesToGet.forEach((prop) => { result[prop] = element[prop]; }); return result; }, { selector, propertiesToGet: properties }); if (!elementProperties) { return { content: [ { type: 'text', text: `Element with selector "${selector}" not found` } ], isError: true }; } // Result message construction const resultMessage = { selector, properties: elementProperties, checkpointId }; return { content: [ { type: 'text', text: JSON.stringify(resultMessage, null, 2) } ] }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); Logger.error(`Failed to get element properties: ${errorMessage}`); return { content: [ { type: 'text', text: `Failed to get element properties: ${errorMessage}` } ], isError: true }; } }); // Element styles retrieval tool server.tool('get-element-styles', 'Retrieves style information of a specific element', { selector: z.string().describe('CSS selector of the element to inspect'), styleProperties: z.array(z.string()).describe("Array of style property names to retrieve (e.g., ['color', 'fontSize', 'backgroundColor'])") }, async ({ selector, styleProperties }) => { try { // Check browser status const browserStatus = getContextForOperation(); if (!browserStatus.isStarted) { return browserStatus.error; } // Get current checkpoint ID const checkpointId = await getCurrentCheckpointId(browserStatus.page); // Retrieve element styles const styles = await browserStatus.page.evaluate(({ selector, stylePropsToGet }) => { const element = document.querySelector(selector); if (!element) return null; const computedStyle = window.getComputedStyle(element); const result = {}; stylePropsToGet.forEach((prop) => { result[prop] = computedStyle.getPropertyValue(prop); }); return result; }, { selector, stylePropsToGet: styleProperties }); if (!styles) { return { content: [ { type: 'text', text: `Element with selector "${selector}" not found` } ], isError: true }; } // Result message construction const resultMessage = { selector, styles, checkpointId }; return { content: [ { type: 'text', text: JSON.stringify(resultMessage, null, 2) } ] }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); Logger.error(`Failed to get element styles: ${errorMessage}`); return { content: [ { type: 'text', text: `Failed to get element styles: ${errorMessage}` } ], isError: true }; } }); // Element dimensions and position retrieval tool server.tool('get-element-dimensions', 'Retrieves dimension and position information of a specific element', { selector: z.string().describe('CSS selector of the element to inspect') }, async ({ selector }) => { try { // Check browser status const browserStatus = getContextForOperation(); if (!browserStatus.isStarted) { return browserStatus.error; } // Get current checkpoint ID const checkpointId = await getCurrentCheckpointId(browserStatus.page); // Retrieve element dimensions and position information const dimensions = await browserStatus.page.evaluate((selector) => { const element = document.querySelector(selector); if (!element) return null; const rect = element.getBoundingClientRect(); return { width: rect.width, height: rect.height, top: rect.top, left: rect.left, bottom: rect.bottom, right: rect.right, x: rect.x, y: rect.y, isVisible: !!(rect.width && rect.height && window.getComputedStyle(element).display !== 'none' && window.getComputedStyle(element).visibility !== 'hidden') }; }, selector); if (!dimensions) { return { content: [ { type: 'text', text: `Element with selector "${selector}" not found` } ], isError: true }; } // Result message construction const resultMessage = { selector, dimensions, checkpointId }; return { content: [ { type: 'text', text: JSON.stringify(resultMessage, null, 2) } ] }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); Logger.error(`Failed to get element dimensions: ${errorMessage}`); return { content: [ { type: 'text', text: `Failed to get element dimensions: ${errorMessage}` } ], isError: true }; } }); // Network request monitoring tool server.tool('monitor-network', 'Monitors network requests in the browser for a specified duration', { urlPattern: z.string().optional().describe('URL pattern to filter (regex string)'), duration: z.number().optional().describe('Duration in milliseconds to monitor (default: 5000)') }, async ({ urlPattern, duration = 5000 }) => { try { // Check browser status const browserStatus = getContextForOperation(); if (!browserStatus.isStarted) { return browserStatus.error; } const requests = []; const pattern = urlPattern ? new RegExp(urlPattern) : null; // Start network request monitoring const requestHandler = (request) => { const url = request.url(); if (!pattern || pattern.test(url)) { requests.push({ url, method: request.method(), resourceType: request.resourceType(), timestamp: Date.now() }); } }; browserStatus.page.on('request', requestHandler); // Wait for specified duration await new Promise(resolve => setTimeout(resolve, duration)); // Stop monitoring browserStatus.page.off('request', requestHandler); return { content: [ { type: 'text', text: requests.length > 0 ? `Captured ${requests.length} network requests:\n${JSON.stringify(requests, null, 2)}` : 'No network requests matching the criteria were captured during the monitoring period.' } ] }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); Logger.error(`Failed to monitor network: ${errorMessage}`); return { content: [ { type: 'text', text: `Failed to monitor network: ${errorMessage}` } ], isError: true }; } }); // Element HTML content retrieval tool server.tool('get-element-html', 'Retrieves the HTML content of a specific element and its children with optional depth control', { selector: z.string().describe('CSS selector of the element to inspect'), includeOuter: z.boolean().optional().describe("If true, includes the selected element's outer HTML; otherwise returns only inner HTML (default: false)"), depth: z.number().int().min(-1).optional().describe('Control HTML depth limit: -1 = unlimited (default), 0 = text only, 1+ = limited depth with deeper elements shown as <!-- omitted -->') }, async ({ selector, includeOuter = false, depth = -1 }) => { try { // Check browser status const browserStatus = getContextForOperation(); if (!browserStatus.isStarted) { return browserStatus.error; } // Check if element exists await browserStatus.page.waitForSelector(selector, { state: 'visible', timeout: 5000 }); // Get element's HTML content with depth control const htmlContent = await browserStatus.page.evaluate(({ selector, includeOuter, depth }) => { const element = document.querySelector(selector); if (!element) return null; // Handle unlimited depth (backward compatibility) if (depth === -1) { return includeOuter ? element.outerHTML : element.innerHTML; } // Handle text-only mode if (depth === 0) { return element.textContent || ''; } // Handle depth-limited mode with DOM cloning const cloned = element.cloneNode(true); function trimDepth(node, currentDepth) { if (currentDepth >= depth) { // Replace content with omitted marker node.innerHTML = '<!-- omitted -->'; return; } // Process child elements Array.from(node.children).forEach(child => { trimDepth(child, currentDepth + 1); }); } // Start depth counting from appropriate level trimDepth(cloned, includeOuter ? 0 : 1); return includeOuter ? cloned.outerHTML : cloned.innerHTML; }, { selector, includeOuter, depth }); if (htmlContent === null) { return { content: [ { type: 'text', text: `Element with selector "${selector}" not found` } ], isError: true }; } // Result message construction const resultMessage = { selector, htmlType: depth === 0 ? 'textContent' : (includeOuter ? 'outerHTML' : 'innerHTML'), depth, depthLimited: depth !== -1, length: htmlContent.length, checkpointId: await getCurrentCheckpointId(browserStatus.page) }; return { content: [ { type: 'text', text: JSON.stringify(resultMessage, null, 2) }, { type: 'text', text: htmlContent } ] }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); Logger.error(`Failed to get element HTML: ${errorMessage}`); return { content: [ { type: 'text', text: `Failed to get element HTML: ${errorMessage}` } ], isError: true }; } }); // Console logs retrieval tool server.tool('get-console-logs', 'Retrieves console logs from the development server', { checkpoint: z.string().optional().describe('If specified, returns only logs recorded at this checkpoint'), limit: z.number().optional().describe('Number of logs to return, starting from the most recent log') }, async ({ checkpoint, limit = 100 }) => { try { // Read logs (always provide limit value) const result = await logManager.readLogs(limit, checkpoint); // Parse logs const parsedLogs = result.logs.map((log) => { try { return JSON.parse(log); } catch (error) { return { type: 'unknown', text: log, timestamp: new Date().toISOString() }; } }); return { content: [ { type: 'text', text: JSON.stringify({ logs: parsedLogs, writePosition: result.writePosition, totalLogs: result.totalLogs }, null, 2) } ] }; } catch (error) { Logger.error(`Failed to read console logs: ${error}`); return { content: [ { type: 'text', text: `Failed to read console logs: ${error}` } ], isError: true }; } }); // Browser command execution tool server.tool('execute-browser-commands', `Executes a sequence of predefined browser commands safely. Available commands: - click: Clicks on an element matching the selector or at specified coordinates - type: Types text into an input element - wait: Waits for an element, a specified time period, or a condition - navigate: Navigates to a specified URL - select: Selects an option in a dropdown - check: Checks or unchecks a checkbox - hover: Hovers over an element - focus: Focuses an element - blur: Removes focus from an element - keypress: Simulates pressing a keyboard key - scroll: Scrolls the page or an element - getAttribute: Gets an attribute value from an element - getProperty: Gets a property value from an element - drag: Performs a drag operation from one position to another - refresh: Refreshes the current page Note on coordinates: For all mouse-related commands (click, drag, etc.), coordinates are relative to the browser viewport where (0,0) is the top-left corner. X increases to the right, Y increases downward. Examples are available in the schema definition.`, { commands: z.array(z.discriminatedUnion('command', [ z.object({ command: z.literal('click'), selector: z.string().optional().describe('CSS selector of element to click (required unless x,y coordinates are provided)'), description: z.string().optional().describe('Description of this command step'), args: z.object({ button: z.enum(['left', 'right', 'middle']).optional().describe('Mouse button to use (default: left)'), clickCount: z.number().optional().describe('Number of clicks (default: 1)'), delay: z.number().optional().describe('Delay between mousedown and mouseup in ms (default: 0)'), x: z.number().optional().describe('X coordinate to click (used instead of selector)'), y: z.number().describe('Y coordinate to click (used instead of selector)'), continueOnError: z.boolean().optional().describe('Whether to continue executing commands if this command fails') }).optional() }), z.object({ command: z.literal('type'), selector: z.string().describe('CSS selector of input element to type into'), description: z.string().optional().describe('Description of this command step'), args: z.object({ text: z.string().describe('Text to type into the element'), delay: z.number().optional().describe('Delay between keystrokes in ms (default: 0)'), clearFirst: z.boolean().optional().describe('Whether to clear the input field before typing (default: false)'), continueOnError: z.boolean().optional().describe('Whether to continue executing commands if this command fails') }).optional() }), z.object({ command: z.literal('wait'), selector: z.string().optional().describe('CSS selector to wait for'), description: z.string().optional().describe('Description of this command step'), args: z.object({ time: z.number().optional().describe('Time to wait in milliseconds (use this or selector)'), visible: z.boolean().optional().describe('Wait for element to be visible (default: true)'), timeout: z.number().optional().describe('Maximum time to wait in ms (default: 5000)'), continueOnError: z.boolean().optional().describe('Whether to continue executing commands if this command fails') }).optional() }), z.object({ command: z.literal('navigate'), description: z.string().optional().describe('Description of this command step'), args: z.object({ url: z.string().describe('URL to navigate to'), waitUntil: z.enum(['load', 'domcontentloaded', 'networkidle0', 'networkidle2']).optional() .describe('Navigation wait condition (default: networkidle0)'), continueOnError: z.boolean().optional().describe('Whether to continue executing commands if this command fails') }) }), z.object({ command: z.literal('drag'), description: z.string().optional().describe('Description of this command step'), args: z.object({ sourceX: z.number().describe('X coordinate to start the drag from (distance from left edge of viewport)'), sourceY: z.number().describe('Y coordinate to start the drag from (distance from top edge of viewport)'), offsetX: z.number().describe('Horizontal distance to drag (positive for right, negative for left)'), offsetY: z.number().describe('Vertical distance to drag (positive for down, negative for up)'), smoothDrag: z.boolean().optional().describe('Whether to perform a smooth, gradual drag movement (default: false)'), steps: z.number().optional().describe('Number of intermediate steps for smooth drag (default: 10)'), continueOnError: z.boolean().optional().describe('Whether to continue executing commands if this command fails') }) }), z.object({ command: z.literal('select'), selector: z.string().describe('CSS selector of select element'), description: z.string().optional().describe('Description of this command step'), args: z.object({ value: z.string().describe('Value of the option to select'), continueOnError: z.boolean().optional().describe('Whether to continue executing commands if this command fails') }) }), z.object({ command: z.literal('check'), selector: z.string().describe('CSS selector of checkbox element'), description: z.string().optional().describe('Description of this command step'), args: z.object({ checked: z.boolean().optional().describe('Whether to check or uncheck the box (default: true)'), continueOnError: z.boolean().optional().describe('Whether to continue executing commands if this command fails') }).optional() }), z.object({ command: z.literal('hover'), selector: z.string().describe('CSS selector of element to hover over'), description: z.string().optional().describe('Description of this command step'), args: z.object({ continueOnError: z.boolean().optional().describe('Whether to continue executing commands if this command fails') }).optional() }), z.object({ command: z.literal('focus'), selector: z.string().describe('CSS selector of element to focus'), description: z.string().optional().describe('Description of this command step'), args: z.object({ continueOnError: z.boolean().optional().describe('Whether to continue executing commands if this command fails') }).optional() }), z.object({ command: z.literal('blur'), selector: z.string().describe('CSS selector of element to blur'), description: z.string().optional().describe('Description of this command step'), args: z.object({ continueOnError: z.boolean().optional().describe('Whether to continue executing commands if this command fails') }).optional() }), z.object({ command: z.literal('keypress'), selector: z.string().optional().describe('CSS selector of element to target (optional)'), description: z.string().optional().describe('Description of this command step'), args: z.object({ key: z.string().describe("Key to press (e.g., 'Enter', 'Tab', 'ArrowDown')"), continueOnError: z.boolean().optional().describe('Whether to continue executing commands if this command fails') }) }), z.object({ command: z.literal('scroll'), selector: z.string().optional().describe('CSS selector of element to scroll (scrolls page if not provided)'), description: z.string().optional().describe('Description of this command step'), args: z.object({ x: z.number().optional().describe('Horizontal scroll amount in pixels (default: 0)'), y: z.number().optional().describe('Vertical scroll amount in pixels (default: 0)'), continueOnError: z.boolean().optional().describe('Whether to continue executing commands if this command fails') }).optional() }), z.object({ command: z.literal('getAttribute'), selector: z.string().describe('CSS selector of element'), description: z.string().optional().describe('Description of this command step'), args: z.object({ name: z.string().describe('Name of the attribute to retrieve'), continueOnError: z.boolean().optional().describe('Whether to continue executing commands if this command fails') }) }), z.object({ command: z.literal('getProperty'), selector: z.string().describe('CSS selector of element'), description: z.string().optional().describe('Description of this command step'), args: z.object({ name: z.string().describe('Name of the property to retrieve'), continueOnError: z.boolean().optional().describe('Whether to continue executing commands if this command fails') }) }), z.object({ command: z.literal('refresh'), description: z.string().optional().describe('Description of this command step'), args: z.object({ waitUntil: z.enum(['load', 'domcontentloaded', 'networkidle0', 'networkidle2']).optional() .describe('Navigation wait condition (default: networkidle0)'), continueOnError: z.boolean().optional().describe('Whether to continue executing commands if this command fails') }).optional() }) ])).describe('Array of commands to execute in sequence'), timeout: z.number().optional().describe('Overall timeout in milliseconds (default: 30000)'), contextId: z.string().optional().describe('Browser ID to execute commands on (uses most recent browser if not provided)') }, async ({ commands, timeout = 30000, contextId }) => { try { // Check browser status const browserStatus = getContextForOperation(contextId); if (!browserStatus.isStarted) { return browserStatus.error; } // Get current checkpoint ID const checkpointId = await getCurrentCheckpointId(browserStatus.page); // Command handler mapping const commandHandlers = { click: async (page, selector, args = {}) => { if (!selector) throw new Error('Selector is required for click command'); await page.waitForSelector(selector, { state: 'visible', timeout: args.timeout || 5000 }); await page.click(selector, { button: args.button || 'left', clickCount: args.clickCount || 1, delay: args.delay || 0 }); return `Clicked on ${selector}`; }, type: async (page, selector, args = {}) => { if (!selector) throw new Error('Selector is required for type command'); if (!args.text) throw new Error('Text is required for type command'); await page.waitForSelector(selector, { state: 'visible', timeout: args.timeout || 5000 }); if (args.clearFirst) { await page.evaluate((sel) => { const element = document.querySelector(sel); if (element) { element.value = ''; } }, selector); } await page.type(selector, args.text, { delay: args.delay || 0 }); return `Typed "${args.text}" into ${selector}`; }, wait: async (page, selector, args = {}) => { if (selector) { await page.waitForSelector(selector, { state: args.visible !== false ? 'visible' : 'attached', timeout: args.timeout || 5000 }); return `Waited for element ${selector}`; } else if (args.time) { await new Promise(resolve => setTimeout(resolve, args.time)); return `Waited for ${args.time}ms`; } else if (args.function) { // Only allow limited wait conditions await page.waitForFunction(`document.querySelectorAll('${args.functionSelector}').length ${args.functionOperator || '>'} ${args.functionValue || 0}`, { timeout: args.timeout || 5000 }); return `Waited for function condition on ${args.functionSelector}`; } else { throw new Error('Either selector, time, or function parameters are required for wait command'); } }, navigate: async (page, selector, args = {}) => { if (!args.url) throw new Error('URL is required for navigate command'); await page.goto(args.url, { waitUntil: args.waitUntil || 'networkidle0', timeout: args.timeout || 30000 }); return `Navigated to ${args.url}`; }, select: async (page, selector, args = {}) => { if (!selector) throw new Error('Selector is required for select command'); if (!args.value) throw new Error('Value is required for select command'); await page.waitForSelector(selector, { state: 'visible', timeout: args.timeout || 5000 }); await page.selectOption(selector, args.value); return `Selected value "${args.value}" in ${selector}`; }, check: async (page, selector, args = {}) => { if (!selector) throw new Error('Selector is required for check command'); await page.waitForSelector(selector, { state: 'visible', timeout: args.timeout || 5000 }); const checked = args.checked !== false; if (checked) { await page.check(selector); } else { await page.uncheck(selector); } return `${checked ? 'Checked' : 'Unchecked'} checkbox ${selector}`; }, hover: async (page, selector, args = {}) => { if (!selector) throw new Error('Selector is required for hover command'); await page.waitForSelector(selector, { state: 'visible', timeout: args.timeout || 5000 }); await page.hover(selector); return `Hovered over ${selector}`; }, focus: async (page, selector, args = {}) => { if (!selector) throw new Error('Selector is required for focus command'); await page.waitForSelector(selector, { state: 'visible', timeout: args.timeout || 5000 }); await page.focus(selector); return `Focused on ${selector}`; }, blur: async (page, selector, _args = {}) => { if (!selector) throw new Error('Selector is required for blur command'); await page.evaluate((sel) => { const element = document.querySelector(sel); if (element && 'blur' in element) { element.blur(); } }, selector); return `Removed focus from ${selector}`; }, keypress: async (page, selector, args = {}) => { if (!args.key) throw new Error('Key is required for keypress command'); if (selector) { await page.waitForSelector(selector, { state: 'visible', timeout: args.timeout || 5000 }); await page.focus(selector); } await page.keyboard.press(args.key); return `Pressed key ${args.key}${selector ? ` on ${selector}` : ''}`; }, scroll: async (page, selector, args = {}) => { const x = args.x || 0; const y = args.y || 0; if (selector) { await page.waitForSelector(selector, { state: 'visible', timeout: args.timeout || 5000 }); await page.evaluate(({ sel, xPos, yPos }) => { const element = document.querySelector(sel); if (element) { element.scrollBy(xPos, yPos); } }, { sel: selector, xPos: x, yPos: y }); return `Scrolled element ${selector} by (${x}, ${y})`; } else { await page.evaluate(({ xPos, yPos }) => { window.scrollBy(xPos, yPos); }, { xPos: x, yPos: y }); return `Scrolled window by (${x}, ${y})`; } }, getAttribute: async (page, selector, args = {}) => { if (!selector) throw new Error('Selector is required for getAttribute command'); if (!args.name) throw new Error('Attribute name is required for getAttribute command'); await page.waitForSelector(selector, { state: args.visible !== false ? 'visible' : 'attached', timeout: args.timeout || 5000 }); const attributeValue = await page.evaluate(({ sel, attr }) => { const element = document.querySelector(sel); return element ? element.getAttribute(attr) : null; }, { sel: selector, attr: args.name }); return { selector, attribute: args.name, value: attributeValue }; }, getProperty: async (page, selector, args = {}) => { if (!selector) throw new Error('Selector is required for getProperty command'); if (!args.name) throw new Error('Property name is required for getProperty command'); await page.waitForSelector(selector, { state: args.visible !== false ? 'visible' : 'attached', timeout: args.timeout || 5000 }); const propertyValue = await page.evaluate(({ sel, prop }) => { const element = document.querySelector(sel); return element ? element[prop] : null; }, { sel: selector, prop: args.name }); return { selector, property: args.name, value: propertyValue }; }, refresh: async (page, selector, args = {}) => { await page.reload({ waitUntil: args.waitUntil || 'networkidle0', timeout: args.timeout || 30000 }); return 'Refreshed current page'; }, drag: async (page, selector, args = {}) => { // Validate required arguments const sourceX = args.sourceX; const sourceY = args.sourceY; const offsetX = args.offsetX; const offsetY = args.offsetY; if (sourceX === undefined || sourceY === undefined) { throw new Error('sourceX and sourceY are required for drag command'); } if (offsetX === undefined || offsetY === undefined) { throw new Error('offsetX and offsetY are required for drag command'); } const smoothDrag = args.smoothDrag === true; const steps = args.steps || 10; // Calculate target coordinates const targetX = sourceX + offsetX; const targetY = sourceY + offsetY; // Perform the drag operation await page.mouse.move(sourceX, sourceY); await page.mouse.down(); // Optional: Implement a gradual movement for more realistic drag if (smoothDrag) { const stepX = offsetX / steps; const stepY = offsetY / steps; for (let i = 1; i <= steps; i++) { await page.mouse.move(sourceX + stepX * i, sourceY + stepY * i, { steps: 1 }); // Small delay between steps for more natural movement await new Promise(resolve => setTimeout(resolve, 10)); } } else {