Skip to main content
Glama

execute-browser-commands

Execute browser automation commands sequentially: click, type, navigate, wait, and more. Control browser behavior safely for testing or data extraction.

Instructions

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.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
commandsYesArray of commands to execute in sequence
timeoutNoOverall timeout in milliseconds (default: 30000)
contextIdNoBrowser ID to execute commands on (uses most recent browser if not provided)

Implementation Reference

  • The main tool registration and handler for 'execute-browser-commands'. Defines the schema (discriminated union of 17 command types) and the async handler that executes commands sequentially against a Playwright Page using a command handler mapping (click, type, wait, navigate, select, check, hover, focus, blur, keypress, scroll, getAttribute, getProperty, drag, refresh).
      // 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);
    
            // Define command handler type
            type CommandArgs = Record<string, unknown>;
            type CommandHandler = (page: Page, selector: string | undefined, args: CommandArgs) => Promise<string | Record<string, unknown>>;
    
            // Command handler mapping
            const commandHandlers: Record<string, CommandHandler> = {
              click: async (page: Page, selector: string | undefined, args: CommandArgs = {}) => {
                if (!selector) throw new Error('Selector is required for click command');
                await page.waitForSelector(selector, {
                  state: 'visible',
                  timeout: args.timeout as number || 5000
                });
                await page.click(selector, {
                  button: (args.button as ('left' | 'right' | 'middle')) || 'left',
                  clickCount: args.clickCount as number || 1,
                  delay: args.delay as number || 0
                });
                return `Clicked on ${selector}`;
              },
    
              type: async (page: Page, selector: string | undefined, args: CommandArgs = {}) => {
                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 as number || 5000
                });
    
                if (args.clearFirst) {
                  await page.evaluate((sel) => {
                    const element = document.querySelector(sel);
                    if (element) {
                      (element as HTMLInputElement).value = '';
                    }
                  }, selector);
                }
    
                await page.type(selector, args.text as string, {
                  delay: args.delay as number || 0
                });
    
                return `Typed "${args.text}" into ${selector}`;
              },
    
              wait: async (page: Page, selector: string | undefined, args: CommandArgs = {}) => {
                if (selector) {
                  await page.waitForSelector(selector, {
                    state: args.visible !== false ? 'visible' : 'attached',
                    timeout: args.timeout as number || 5000
                  });
                  return `Waited for element ${selector}`;
                } else if (args.time) {
                  await new Promise(resolve => setTimeout(resolve, args.time as number));
                  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 as number || 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: Page, selector: string | undefined, args: CommandArgs = {}) => {
                if (!args.url) throw new Error('URL is required for navigate command');
    
                await page.goto(args.url as string, {
                  waitUntil: args.waitUntil as ('load' | 'domcontentloaded' | 'networkidle' | 'commit') || 'networkidle0',
                  timeout: args.timeout as number || 30000
                });
    
                return `Navigated to ${args.url}`;
              },
    
              select: async (page: Page, selector: string | undefined, args: CommandArgs = {}) => {
                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 as number || 5000
                });
    
                await page.selectOption(selector, args.value as string);
    
                return `Selected value "${args.value}" in ${selector}`;
              },
    
              check: async (page: Page, selector: string | undefined, args: CommandArgs = {}) => {
                if (!selector) throw new Error('Selector is required for check command');
    
                await page.waitForSelector(selector, {
                  state: 'visible',
                  timeout: args.timeout as number || 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: Page, selector: string | undefined, args: CommandArgs = {}) => {
                if (!selector) throw new Error('Selector is required for hover command');
    
                await page.waitForSelector(selector, {
                  state: 'visible',
                  timeout: args.timeout as number || 5000
                });
    
                await page.hover(selector as string);
    
                return `Hovered over ${selector}`;
              },
    
              focus: async (page: Page, selector: string | undefined, args: CommandArgs = {}) => {
                if (!selector) throw new Error('Selector is required for focus command');
    
                await page.waitForSelector(selector, {
                  state: 'visible',
                  timeout: args.timeout as number || 5000
                });
    
                await page.focus(selector as string);
    
                return `Focused on ${selector}`;
              },
    
              blur: async (page: Page, selector: string | undefined, _args: CommandArgs = {}) => {
                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 as HTMLElement).blur();
                  }
                }, selector as string);
    
                return `Removed focus from ${selector}`;
              },
    
              keypress: async (page: Page, selector: string | undefined, args: CommandArgs = {}) => {
                if (!args.key) throw new Error('Key is required for keypress command');
    
                if (selector) {
                  await page.waitForSelector(selector, {
                    state: 'visible',
                    timeout: args.timeout as number || 5000
                  });
                  await page.focus(selector as string);
                }
    
                await page.keyboard.press(args.key as string);
    
                return `Pressed key ${args.key}${selector ? ` on ${selector}` : ''}`;
              },
    
              scroll: async (page: Page, selector: string | undefined, args: CommandArgs = {}) => {
                const x = args.x as number || 0;
                const y = args.y as number || 0;
    
                if (selector) {
                  await page.waitForSelector(selector, {
                    state: 'visible',
                    timeout: args.timeout as number || 5000
                  });
    
                  await page.evaluate(({ sel, xPos, yPos }: { sel: string; xPos: number; yPos: number }) => {
                    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 }: { xPos: number; yPos: number }) => {
                    window.scrollBy(xPos, yPos);
                  }, { xPos: x, yPos: y });
    
                  return `Scrolled window by (${x}, ${y})`;
                }
              },
    
              getAttribute: async (page: Page, selector: string | undefined, args: CommandArgs = {}) => {
                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 as number || 5000
                });
    
                const attributeValue = await page.evaluate(({ sel, attr }: { sel: string; attr: string }) => {
                  const element = document.querySelector(sel);
                  return element ? element.getAttribute(attr) : null;
                }, { sel: selector, attr: args.name as string });
    
                return {
                  selector,
                  attribute: args.name,
                  value: attributeValue
                };
              },
    
              getProperty: async (page: Page, selector: string | undefined, args: CommandArgs = {}) => {
                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 as number || 5000
                });
    
                const propertyValue = await page.evaluate(({ sel, prop }: { sel: string; prop: string }) => {
                  const element = document.querySelector(sel);
                  return element ? (element as unknown as Record<string, unknown>)[prop] : null;
                }, { sel: selector, prop: args.name as string });
    
                return {
                  selector,
                  property: args.name,
                  value: propertyValue
                };
              },
    
              refresh: async (page: Page, selector: string | undefined, args: CommandArgs = {}) => {
                await page.reload({
                  waitUntil: args.waitUntil as ('load' | 'domcontentloaded' | 'networkidle' | 'commit') || 'networkidle0',
                  timeout: args.timeout as number || 30000
                });
    
                return 'Refreshed current page';
              },
    
              drag: async (page: Page, selector: string | undefined, args: CommandArgs = {}) => {
                // Validate required arguments
                const sourceX = args.sourceX as number | undefined;
                const sourceY = args.sourceY as number | undefined;
                const offsetX = args.offsetX as number | undefined;
                const offsetY = args.offsetY as number | undefined;
    
                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 as number || 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 {
                  // Direct movement
                  await page.mouse.move(targetX, targetY);
                }
    
                // Release the mouse button
                await page.mouse.up();
    
                return `Dragged from (${sourceX}, ${sourceY}) to (${targetX}, ${targetY}) with offset (${offsetX}, ${offsetY})`;
              }
            };
    
            // Execute commands sequentially
            const startTime = Date.now();
            const results = [];
            for (const [index, cmd] of commands.entries()) {
              // Check overall timeout
              if (Date.now() - startTime > timeout) {
                results.push({
                  commandIndex: index,
                  command: cmd.command,
                  description: cmd.description,
                  status: 'error',
                  error: 'Execution timed out'
                });
                break;
              }
    
              try {
                if (!commandHandlers[cmd.command]) {
                  throw new Error(`Unknown command: ${cmd.command}`);
                }
    
                // Handle selector and args for all commands
                const selector = 'selector' in cmd ? cmd.selector : undefined;
                const args = cmd.args || {};
    
                const result = await commandHandlers[cmd.command](
                  browserStatus.page,
                  selector,
                  args
                );
    
                results.push({
                  commandIndex: index,
                  command: cmd.command,
                  description: cmd.description,
                  status: 'success',
                  result
                });
              } catch (error) {
                const errorMessage = error instanceof Error ? error.message : String(error);
                Logger.error(`Command execution failed: ${errorMessage}`);
    
                results.push({
                  commandIndex: index,
                  command: cmd.command,
                  description: cmd.description,
                  status: 'error',
                  error: errorMessage
                });
    
                // Determine whether to continue based on option
                // Check continueOnError property
                const continueOnError = cmd.args && 'continueOnError' in cmd.args ?
                  (cmd.args as Record<string, unknown>).continueOnError === true : false;
                if (!continueOnError) {
                  break;
                }
              }
            }
    
            // Return results
            const resultMessage = {
              totalCommands: commands.length,
              executedCommands: results.length,
              successCount: results.filter(r => r.status === 'success').length,
              failureCount: results.filter(r => r.status === 'error').length,
              elapsedTime: Date.now() - startTime,
              results,
              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 execute browser commands: ${errorMessage}`);
            return {
              content: [
                {
                  type: 'text',
                  text: `Failed to execute browser commands: ${errorMessage}`
                }
              ],
              isError: true
            };
          }
        }
      );
  • Input schema for execute-browser-commands: a discriminated union with 17 command variants (click, type, wait, navigate, drag, select, check, hover, focus, blur, keypress, scroll, getAttribute, getProperty, refresh) plus optional top-level timeout and contextId parameters.
    {
      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'),
  • Registration of the tool via server.tool('execute-browser-commands', ...) within the registerBrowserTools function. The tool is registered with its description, schema, and async handler.
      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);
    
          // Define command handler type
          type CommandArgs = Record<string, unknown>;
          type CommandHandler = (page: Page, selector: string | undefined, args: CommandArgs) => Promise<string | Record<string, unknown>>;
    
          // Command handler mapping
          const commandHandlers: Record<string, CommandHandler> = {
            click: async (page: Page, selector: string | undefined, args: CommandArgs = {}) => {
              if (!selector) throw new Error('Selector is required for click command');
              await page.waitForSelector(selector, {
                state: 'visible',
                timeout: args.timeout as number || 5000
              });
              await page.click(selector, {
                button: (args.button as ('left' | 'right' | 'middle')) || 'left',
                clickCount: args.clickCount as number || 1,
                delay: args.delay as number || 0
              });
              return `Clicked on ${selector}`;
            },
    
            type: async (page: Page, selector: string | undefined, args: CommandArgs = {}) => {
              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 as number || 5000
              });
    
              if (args.clearFirst) {
                await page.evaluate((sel) => {
                  const element = document.querySelector(sel);
                  if (element) {
                    (element as HTMLInputElement).value = '';
                  }
                }, selector);
              }
    
              await page.type(selector, args.text as string, {
                delay: args.delay as number || 0
              });
    
              return `Typed "${args.text}" into ${selector}`;
            },
    
            wait: async (page: Page, selector: string | undefined, args: CommandArgs = {}) => {
              if (selector) {
                await page.waitForSelector(selector, {
                  state: args.visible !== false ? 'visible' : 'attached',
                  timeout: args.timeout as number || 5000
                });
                return `Waited for element ${selector}`;
              } else if (args.time) {
                await new Promise(resolve => setTimeout(resolve, args.time as number));
                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 as number || 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: Page, selector: string | undefined, args: CommandArgs = {}) => {
              if (!args.url) throw new Error('URL is required for navigate command');
    
              await page.goto(args.url as string, {
                waitUntil: args.waitUntil as ('load' | 'domcontentloaded' | 'networkidle' | 'commit') || 'networkidle0',
                timeout: args.timeout as number || 30000
              });
    
              return `Navigated to ${args.url}`;
            },
    
            select: async (page: Page, selector: string | undefined, args: CommandArgs = {}) => {
              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 as number || 5000
              });
    
              await page.selectOption(selector, args.value as string);
    
              return `Selected value "${args.value}" in ${selector}`;
            },
    
            check: async (page: Page, selector: string | undefined, args: CommandArgs = {}) => {
              if (!selector) throw new Error('Selector is required for check command');
    
              await page.waitForSelector(selector, {
                state: 'visible',
                timeout: args.timeout as number || 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: Page, selector: string | undefined, args: CommandArgs = {}) => {
              if (!selector) throw new Error('Selector is required for hover command');
    
              await page.waitForSelector(selector, {
                state: 'visible',
                timeout: args.timeout as number || 5000
              });
    
              await page.hover(selector as string);
    
              return `Hovered over ${selector}`;
            },
    
            focus: async (page: Page, selector: string | undefined, args: CommandArgs = {}) => {
              if (!selector) throw new Error('Selector is required for focus command');
    
              await page.waitForSelector(selector, {
                state: 'visible',
                timeout: args.timeout as number || 5000
              });
    
              await page.focus(selector as string);
    
              return `Focused on ${selector}`;
            },
    
            blur: async (page: Page, selector: string | undefined, _args: CommandArgs = {}) => {
              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 as HTMLElement).blur();
                }
              }, selector as string);
    
              return `Removed focus from ${selector}`;
            },
    
            keypress: async (page: Page, selector: string | undefined, args: CommandArgs = {}) => {
              if (!args.key) throw new Error('Key is required for keypress command');
    
              if (selector) {
                await page.waitForSelector(selector, {
                  state: 'visible',
                  timeout: args.timeout as number || 5000
                });
                await page.focus(selector as string);
              }
    
              await page.keyboard.press(args.key as string);
    
              return `Pressed key ${args.key}${selector ? ` on ${selector}` : ''}`;
            },
    
            scroll: async (page: Page, selector: string | undefined, args: CommandArgs = {}) => {
              const x = args.x as number || 0;
              const y = args.y as number || 0;
    
              if (selector) {
                await page.waitForSelector(selector, {
                  state: 'visible',
                  timeout: args.timeout as number || 5000
                });
    
                await page.evaluate(({ sel, xPos, yPos }: { sel: string; xPos: number; yPos: number }) => {
                  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 }: { xPos: number; yPos: number }) => {
                  window.scrollBy(xPos, yPos);
                }, { xPos: x, yPos: y });
    
                return `Scrolled window by (${x}, ${y})`;
              }
            },
    
            getAttribute: async (page: Page, selector: string | undefined, args: CommandArgs = {}) => {
              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 as number || 5000
              });
    
              const attributeValue = await page.evaluate(({ sel, attr }: { sel: string; attr: string }) => {
                const element = document.querySelector(sel);
                return element ? element.getAttribute(attr) : null;
              }, { sel: selector, attr: args.name as string });
    
              return {
                selector,
                attribute: args.name,
                value: attributeValue
              };
            },
    
            getProperty: async (page: Page, selector: string | undefined, args: CommandArgs = {}) => {
              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 as number || 5000
              });
    
              const propertyValue = await page.evaluate(({ sel, prop }: { sel: string; prop: string }) => {
                const element = document.querySelector(sel);
                return element ? (element as unknown as Record<string, unknown>)[prop] : null;
              }, { sel: selector, prop: args.name as string });
    
              return {
                selector,
                property: args.name,
                value: propertyValue
              };
            },
    
            refresh: async (page: Page, selector: string | undefined, args: CommandArgs = {}) => {
              await page.reload({
                waitUntil: args.waitUntil as ('load' | 'domcontentloaded' | 'networkidle' | 'commit') || 'networkidle0',
                timeout: args.timeout as number || 30000
              });
    
              return 'Refreshed current page';
            },
    
            drag: async (page: Page, selector: string | undefined, args: CommandArgs = {}) => {
              // Validate required arguments
              const sourceX = args.sourceX as number | undefined;
              const sourceY = args.sourceY as number | undefined;
              const offsetX = args.offsetX as number | undefined;
              const offsetY = args.offsetY as number | undefined;
    
              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 as number || 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 {
                // Direct movement
                await page.mouse.move(targetX, targetY);
              }
    
              // Release the mouse button
              await page.mouse.up();
    
              return `Dragged from (${sourceX}, ${sourceY}) to (${targetX}, ${targetY}) with offset (${offsetX}, ${offsetY})`;
            }
          };
    
          // Execute commands sequentially
          const startTime = Date.now();
          const results = [];
          for (const [index, cmd] of commands.entries()) {
            // Check overall timeout
            if (Date.now() - startTime > timeout) {
              results.push({
                commandIndex: index,
                command: cmd.command,
                description: cmd.description,
                status: 'error',
                error: 'Execution timed out'
              });
              break;
            }
    
            try {
              if (!commandHandlers[cmd.command]) {
                throw new Error(`Unknown command: ${cmd.command}`);
              }
    
              // Handle selector and args for all commands
              const selector = 'selector' in cmd ? cmd.selector : undefined;
              const args = cmd.args || {};
    
              const result = await commandHandlers[cmd.command](
                browserStatus.page,
                selector,
                args
              );
    
              results.push({
                commandIndex: index,
                command: cmd.command,
                description: cmd.description,
                status: 'success',
                result
              });
            } catch (error) {
              const errorMessage = error instanceof Error ? error.message : String(error);
              Logger.error(`Command execution failed: ${errorMessage}`);
    
              results.push({
                commandIndex: index,
                command: cmd.command,
                description: cmd.description,
                status: 'error',
                error: errorMessage
              });
    
              // Determine whether to continue based on option
              // Check continueOnError property
              const continueOnError = cmd.args && 'continueOnError' in cmd.args ?
                (cmd.args as Record<string, unknown>).continueOnError === true : false;
              if (!continueOnError) {
                break;
              }
            }
          }
    
          // Return results
          const resultMessage = {
            totalCommands: commands.length,
            executedCommands: results.length,
            successCount: results.filter(r => r.status === 'success').length,
            failureCount: results.filter(r => r.status === 'error').length,
            elapsedTime: Date.now() - startTime,
            results,
            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 execute browser commands: ${errorMessage}`);
          return {
            content: [
              {
                type: 'text',
                text: `Failed to execute browser commands: ${errorMessage}`
              }
            ],
            isError: true
          };
        }
      }
    );
  • Command handler mapping inside execute-browser-commands: defines individual async handlers for each command type (click, type, wait, navigate, select, check, hover, focus, blur, keypress, scroll, getAttribute, getProperty, drag, refresh) that operate on a Playwright Page.
    // Command handler mapping
    const commandHandlers: Record<string, CommandHandler> = {
      click: async (page: Page, selector: string | undefined, args: CommandArgs = {}) => {
        if (!selector) throw new Error('Selector is required for click command');
        await page.waitForSelector(selector, {
          state: 'visible',
          timeout: args.timeout as number || 5000
        });
        await page.click(selector, {
          button: (args.button as ('left' | 'right' | 'middle')) || 'left',
          clickCount: args.clickCount as number || 1,
          delay: args.delay as number || 0
        });
        return `Clicked on ${selector}`;
      },
    
      type: async (page: Page, selector: string | undefined, args: CommandArgs = {}) => {
        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 as number || 5000
        });
    
        if (args.clearFirst) {
          await page.evaluate((sel) => {
            const element = document.querySelector(sel);
            if (element) {
              (element as HTMLInputElement).value = '';
            }
          }, selector);
        }
    
        await page.type(selector, args.text as string, {
          delay: args.delay as number || 0
        });
    
        return `Typed "${args.text}" into ${selector}`;
      },
    
      wait: async (page: Page, selector: string | undefined, args: CommandArgs = {}) => {
        if (selector) {
          await page.waitForSelector(selector, {
            state: args.visible !== false ? 'visible' : 'attached',
            timeout: args.timeout as number || 5000
          });
          return `Waited for element ${selector}`;
        } else if (args.time) {
          await new Promise(resolve => setTimeout(resolve, args.time as number));
          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 as number || 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: Page, selector: string | undefined, args: CommandArgs = {}) => {
        if (!args.url) throw new Error('URL is required for navigate command');
    
        await page.goto(args.url as string, {
          waitUntil: args.waitUntil as ('load' | 'domcontentloaded' | 'networkidle' | 'commit') || 'networkidle0',
          timeout: args.timeout as number || 30000
        });
    
        return `Navigated to ${args.url}`;
      },
    
      select: async (page: Page, selector: string | undefined, args: CommandArgs = {}) => {
        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 as number || 5000
        });
    
        await page.selectOption(selector, args.value as string);
    
        return `Selected value "${args.value}" in ${selector}`;
      },
    
      check: async (page: Page, selector: string | undefined, args: CommandArgs = {}) => {
        if (!selector) throw new Error('Selector is required for check command');
    
        await page.waitForSelector(selector, {
          state: 'visible',
          timeout: args.timeout as number || 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: Page, selector: string | undefined, args: CommandArgs = {}) => {
        if (!selector) throw new Error('Selector is required for hover command');
    
        await page.waitForSelector(selector, {
          state: 'visible',
          timeout: args.timeout as number || 5000
        });
    
        await page.hover(selector as string);
    
        return `Hovered over ${selector}`;
      },
    
      focus: async (page: Page, selector: string | undefined, args: CommandArgs = {}) => {
        if (!selector) throw new Error('Selector is required for focus command');
    
        await page.waitForSelector(selector, {
          state: 'visible',
          timeout: args.timeout as number || 5000
        });
    
        await page.focus(selector as string);
    
        return `Focused on ${selector}`;
      },
    
      blur: async (page: Page, selector: string | undefined, _args: CommandArgs = {}) => {
        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 as HTMLElement).blur();
          }
        }, selector as string);
    
        return `Removed focus from ${selector}`;
      },
    
      keypress: async (page: Page, selector: string | undefined, args: CommandArgs = {}) => {
        if (!args.key) throw new Error('Key is required for keypress command');
    
        if (selector) {
          await page.waitForSelector(selector, {
            state: 'visible',
            timeout: args.timeout as number || 5000
          });
          await page.focus(selector as string);
        }
    
        await page.keyboard.press(args.key as string);
    
        return `Pressed key ${args.key}${selector ? ` on ${selector}` : ''}`;
      },
    
      scroll: async (page: Page, selector: string | undefined, args: CommandArgs = {}) => {
        const x = args.x as number || 0;
        const y = args.y as number || 0;
    
        if (selector) {
          await page.waitForSelector(selector, {
            state: 'visible',
            timeout: args.timeout as number || 5000
          });
    
          await page.evaluate(({ sel, xPos, yPos }: { sel: string; xPos: number; yPos: number }) => {
            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 }: { xPos: number; yPos: number }) => {
            window.scrollBy(xPos, yPos);
          }, { xPos: x, yPos: y });
    
          return `Scrolled window by (${x}, ${y})`;
        }
      },
    
      getAttribute: async (page: Page, selector: string | undefined, args: CommandArgs = {}) => {
        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 as number || 5000
        });
    
        const attributeValue = await page.evaluate(({ sel, attr }: { sel: string; attr: string }) => {
          const element = document.querySelector(sel);
          return element ? element.getAttribute(attr) : null;
        }, { sel: selector, attr: args.name as string });
    
        return {
          selector,
          attribute: args.name,
          value: attributeValue
        };
      },
    
      getProperty: async (page: Page, selector: string | undefined, args: CommandArgs = {}) => {
        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 as number || 5000
        });
    
        const propertyValue = await page.evaluate(({ sel, prop }: { sel: string; prop: string }) => {
          const element = document.querySelector(sel);
          return element ? (element as unknown as Record<string, unknown>)[prop] : null;
        }, { sel: selector, prop: args.name as string });
    
        return {
          selector,
          property: args.name,
          value: propertyValue
        };
      },
    
      refresh: async (page: Page, selector: string | undefined, args: CommandArgs = {}) => {
        await page.reload({
          waitUntil: args.waitUntil as ('load' | 'domcontentloaded' | 'networkidle' | 'commit') || 'networkidle0',
          timeout: args.timeout as number || 30000
        });
    
        return 'Refreshed current page';
      },
    
      drag: async (page: Page, selector: string | undefined, args: CommandArgs = {}) => {
        // Validate required arguments
        const sourceX = args.sourceX as number | undefined;
        const sourceY = args.sourceY as number | undefined;
        const offsetX = args.offsetX as number | undefined;
        const offsetY = args.offsetY as number | undefined;
    
        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 as number || 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 {
          // Direct movement
          await page.mouse.move(targetX, targetY);
        }
    
        // Release the mouse button
        await page.mouse.up();
    
        return `Dragged from (${sourceX}, ${sourceY}) to (${targetX}, ${targetY}) with offset (${offsetX}, ${offsetY})`;
      }
    };
  • Reference to execute-browser-commands in the 'how-to-use' tool's HMR section - mentions users can use this tool to refresh the page.
    The HMR connection is optional.
    
    If your development environment does not support HMR, you cannot read HMR events, but you can still check the results of file modifications by using 'execute-browser-commands' to refresh the page.` }
Behavior2/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

No annotations provided, so the description must carry full behavioral disclosure. It mentions 'safely' but does not specify default error handling (e.g., stops on error unless continueOnError is set), side effects, or return value structure. The coordinate note is good, but critical execution behavior is missing, making it insufficient for an agent to anticipate outcomes.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness3/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is structured with a bullet list and a note, which is readable. However, it is somewhat lengthy and includes 'Examples are available in the schema definition' which is redundant. Some sentences are not essential; it could be more concise without losing clarity.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness2/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

No output schema exists, so the description should explain return values and behavior. It does not mention that results per command are returned, how errors propagate, or the default timeout. Given the tool's complexity (15 command types, multiple optional parameters), the description lacks sufficient context for an agent to use it reliably.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Input schema coverage is 100%, so the schema already describes all parameters. The description adds a helpful note about coordinate system (viewport-relative) and lists available commands, but this adds marginal value beyond the schema. The baseline of 3 is appropriate; the description does not significantly deepen parameter understanding.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description states 'Executes a sequence of predefined browser commands safely' and lists 15 specific commands with brief explanations. This clearly identifies the tool as a batch executor of browser actions. However, it does not explicitly differentiate from sibling tools like browser-evaluate or capture-screenshot, missing a chance to clarify when to use this instead of single-action tools.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines3/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description says the tool 'executes a sequence' and lists commands, implying it's for multi-step interactions. But it offers no explicit guidance on when to use this tool versus alternatives (e.g., use browser-evaluate for JS evaluation, capture-screenshot for screenshots). The lack of when-not-to-use or sister tool references reduces its helpfulness for agent selection.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/ESnark/blowback'

If you have feedback or need assistance with the MCP directory API, please join our Discord server