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.` }

Schema Changelog

Changes observed during successful MCP inspections.

  1. Changed1 schema field changedv1.0.0
    • addedInput schema / properties / contextId
      Added value: +{
      +  "description": "Browser ID to execute commands on (uses most recent browser if not provided)",
      +  "type": "string"
      +}
  2. First observed

TDQS

B3.3/5.0
Behavior2/5

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

No annotations exist, so the description must disclose behavior on its own. It only says 'safely' and lists commands, without noting side effects, sequential execution semantics, error/failure behavior, or return value shape. The coordinate reference note is helpful, but the description does not disclose that commands can navigate, click, mutate state, or that continueOnError controls failure handling.

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

Conciseness4/5

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

The description is moderately sized and well-structured: a one-line summary, a scannable command list, and a focused coordinate note. There is no fluff, and the pointer to examples in the schema avoids unnecessary duplication.

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?

The tool is highly complex with 15 command types and per-command args, and the schema handles that well. However, the description does not explain what the tool returns, whether commands execute in order, what happens on failure, or how timeout and contextId interact. For an agent deciding whether to use this plus how to interpret results, these are material gaps.

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

Parameters4/5

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

Schema description coverage is 100%, so the baseline is 3. The description adds value with the coordinate-system note: for mouse commands coordinates are viewport-relative with (0,0) at top-left, which is not fully explicit in the individual parameter descriptions. The command list also clarifies the allowed command vocabulary.

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

Purpose5/5

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

The description states a clear verb and resource: 'Executes a sequence of predefined browser commands safely.' The appended command list makes the scope concrete and distinct from single-action sibling tools like capture-screenshot or get-element-properties. It is specific, not tautological.

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

Usage Guidelines2/5

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

No guidance is given on when to prefer this tool over individual browser tools, when a sequence is needed, or when a more targeted sibling like browser-evaluate or get-element-properties would be better. The word 'sequence' implies multi-step use, but that is left to inference rather than stated.

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