blowback-context
Version:
MCP server that integrates with FE development server for Cursor
1,043 lines (1,039 loc) • 62.4 kB
JavaScript
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 {