Skip to main content
Glama
SJMakin

even-better-playwright-mcp

by SJMakin

browser_execute

Execute Playwright code to automate browser interactions, inspect page accessibility, and manage web automation tasks within a controlled environment.

Instructions

Execute Playwright code with these in scope:

  • page - Current Playwright page

  • context - Browser context, access all pages via context.pages()

  • state - Persistent object across calls (e.g., state.myPage = await context.newPage())

  • $('e5') - Shorthand for page.locator('aria-ref=e5')

  • accessibilitySnapshot() - Get current page snapshot

  • require - Load Node.js modules (path, url, crypto, buffer, util, assert, os, fs)

  • Node.js globals: setTimeout, setInterval, fetch, URL, Buffer, crypto, etc.

Rules

  • Multiple calls: Use multiple execute calls for complex logic - helps understand intermediate state and isolate failures

  • Never close: Never call browser.close() or context.close(). Only close pages you created or if user asks

  • No bringToFront: Never call unless user asks - it's disruptive and unnecessary

  • Check state after actions: Always verify page state after clicking/submitting (see next section)

  • Clean up listeners: Call page.removeAllListeners() at end to prevent leaks

  • Wait for load: Use page.waitForLoadState('domcontentloaded') not page.waitForEvent('load') - waitForEvent times out if already loaded

  • Avoid timeouts: Prefer proper waits over page.waitForTimeout() - there are better ways

Checking Page State

After any action (click, submit, navigate), verify what happened:

console.log('url:', page.url()); console.log(await accessibilitySnapshot().then(x => x.split('\n').slice(0, 30).join('\n')));

For visually complex pages (grids, galleries, dashboards), use screenshotWithAccessibilityLabels({ page }) instead.

Accessibility Snapshots

await accessibilitySnapshot()  // Full snapshot
await accessibilitySnapshot({ search: /button|submit/i })  // Filter results
await accessibilitySnapshot({ showDiffSinceLastCall: true })  // Show changes

Example output:

- banner [ref=e3]:
    - link "Home" [ref=e5] [cursor=pointer]:
        - /url: /
    - navigation [ref=e12]:
        - link "Docs" [ref=e13] [cursor=pointer]

Use aria-ref to interact - NO quotes around the ref value:

await page.locator('aria-ref=e13').click()  // or: await $('e13').click()

For pagination: (await accessibilitySnapshot()).split('\n').slice(0, 50).join('\n')

Choosing snapshot method:

  • Use accessibilitySnapshot for simple pages, text search, token efficiency

  • Use screenshotWithAccessibilityLabels for complex visual layouts, spatial position matters

Selector Best Practices

For unknown sites: use accessibilitySnapshot() with aria-ref For development (with source access), prefer:

  1. [data-testid="submit"] - explicit test attributes

  2. getByRole('button', { name: 'Save' }) - semantic

  3. getByText('Sign in'), getByLabel('Email') - user-facing

  4. input[name="email"] - semantic HTML

  5. Avoid: classes/IDs that change frequently

If locator matches multiple elements (strict mode violation), use .first(), .last(), or .nth(n):

await page.locator('button').first().click()
await page.locator('li').nth(3).click()  // 4th item (0-indexed)

Working with Pages

const pages = context.pages().filter(x => x.url().includes('localhost'));
state.newPage = await context.newPage(); await state.newPage.goto('https://example.com');

Navigation

await page.goto('https://example.com', { waitUntil: 'domcontentloaded' });
await waitForPageLoad({ page, timeout: 5000 });

Common Patterns

Popups: const [popup] = await Promise.all([page.waitForEvent('popup'), page.click('a[target=_blank]')]); await popup.waitForLoadState(); Downloads: const [download] = await Promise.all([page.waitForEvent('download'), page.click('button.download')]); await download.saveAs('/tmp/' + download.suggestedFilename()); iFrames: const frame = page.frameLocator('#my-iframe'); await frame.locator('button').click(); Dialogs: page.on('dialog', async d => { await d.accept(); }); await page.click('button'); Load files: const fs = require('fs'); const content = fs.readFileSync('./data.txt', 'utf-8'); await page.locator('textarea').fill(content);

page.evaluate

Code inside page.evaluate() runs in the browser - use plain JavaScript only. console.log inside evaluate runs in browser, not visible here:

const title = await page.evaluate(() => document.title);
console.log('Title:', title);  // Log outside evaluate

Utility Functions

  • getLatestLogs({ page?, count?, search? }) - Get browser console logs

  • getCleanHTML({ locator, search?, showDiffSinceLastCall?, includeStyles? }) - Get cleaned HTML

  • waitForPageLoad({ page, timeout? }) - Smart load detection (ignores analytics/ads)

  • getCDPSession() - Get CDP session for raw Chrome DevTools Protocol commands

  • getLocatorStringForElement(locator) - Get stable selector from ephemeral aria-ref

  • getReactSource({ locator }) - Get React component source location (dev mode only)

  • getStylesForLocator({ locator, cdp }) - Inspect CSS styles (read styles-api resource first)

  • createDebugger({ cdp }) - Set breakpoints, step through code (read debugger-api resource first)

  • createEditor({ cdp }) - View/edit page scripts and CSS (read editor-api resource first)

  • screenshotWithAccessibilityLabels({ page }) - Screenshot with Vimium-style visual labels (yellow=links, orange=buttons, coral=inputs)

Network Interception

For scraping/reverse-engineering APIs, intercept network instead of scrolling DOM:

state.requests = []; state.responses = [];
page.on('request', req => { if (req.url().includes('/api/')) state.requests.push({ url: req.url(), method: req.method(), headers: req.headers() }); });
page.on('response', async res => { if (res.url().includes('/api/')) { try { state.responses.push({ url: res.url(), status: res.status(), body: await res.json() }); } catch {} } });

Then trigger actions and analyze: console.log('Captured', state.responses.length, 'API calls'); Clean up when done: page.removeAllListeners('request'); page.removeAllListeners('response');

IMPORTANT: After navigation, refs are stale - call snapshot tool again.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
codeYesPlaywright code with {page, context, state, $} in scope. Should be concise - use ; for multiple statements.
timeoutNoTimeout in milliseconds (default: 30000)

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/SJMakin/even-better-playwright-mcp'

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