Playwright MCP & CLI

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

Playwright can be driven by a human through a test file, and it can be driven by an AI agent through a tool call. This page covers the second way. Microsoft ships two agent-facing front ends for the same Playwright engine: Playwright MCP (@playwright/mcp), a Model Context Protocol server that exposes browser tools to any MCP client, and Playwright CLI (@playwright/cli, binary playwright-cli), a shell command that an agent runs the way a developer would, with a generated skill file that teaches the agent its commands. Since Playwright 1.62 both are also bundled in the main package as npx playwright mcp and npx playwright cli. Playwright 1.64 adds WebMCP support (the tools a page registers for agents), and Playwright 1.59 and later let an agent attach to a browser that a script or a paused test is already driving.

The page is written for Playwright 1.64.1, @playwright/mcp 0.0.83 (its server reports itself as Playwright 1.64.0-alpha) and @playwright/cli 0.1.22, and was checked on 25 October 2026. Cypress 16.2 and Selenium 4.51 have their own AI features, covered in AI Features in Cypress & Selenium. Every command, tool call and output excerpt below was run against the Bookshelf example application on macOS with headless Google Chrome 155 (and Playwright’s Chromium 156 build where stated). Each Bookshelf instance used its own port, so the port in an excerpt differs from one run to the next.

How the agent runs were driven matters, so it is stated once, plainly. A headless claude -p --mcp-config session could not authenticate in the sandbox that produced this page (the CLI reported an expired OAuth session), so no Claude Code session produced the transcripts below. Instead, a small program started the real Playwright MCP server over stdio and a language model (the assistant that wrote this page) issued the tool calls one at a time, reading each result before choosing the next: exactly the observe-act loop that an MCP client runs, with the real server, the real tools and the real output. The claude, codex, copilot and code command lines shown for installation come from the products' documentation and were not executed. For the ideas behind the loop itself see How AI Agents Drive Browsers, MCP architecture and Agent loop and architectures.

Which front end to use

Playwright MCP, Playwright CLI and the Playwright library are three doors into one engine. The difference that matters for an agent is what ends up in its context window.

Playwright MCP Playwright CLI Playwright library (a test file)

Interface

MCP tools with JSON schemas, called by the model

Shell commands (playwright-cli click e5) run by the model

TypeScript, Python, Java or .NET code you or the agent write

What the agent loads

Every tool schema (25 core tools, 72 with every capability group) plus a snapshot after each action

A skill file that is read on demand and short command output; the snapshot is written to a file

Nothing about the tool surface; the agent writes and runs code

Browser state

Held by the server for the whole session

Held by a background daemon, one browser per named session

Held by the test process for one run

Default mode

Headed (you watch it)

Headless

Headless

Best for

Exploratory loops, self-healing, agents that need persistent state and rich introspection

Coding agents (Claude Code, Copilot, Cursor) working in a large repository where context is scarce

Repeatable, reviewed tests that run in CI without a model

The Playwright README states the trade-off directly: modern coding agents increasingly prefer CLI workflows exposed as skills because a CLI invocation avoids loading large tool schemas and verbose accessibility trees, while MCP remains the better fit for specialised loops that benefit from persistent state and iterative reasoning over page structure. Neither replaces the test file. A verified flow is worth turning into a normal Playwright test (see Playwright Test Agents and Script-less Testing with AI Agents).

flowchart LR A["Agent (Claude Code, Cursor, Copilot, Codex)"] -->|"MCP tool calls"| M["Playwright MCP server"] A -->|"shell commands + skill file"| C["playwright-cli client"] C -->|"local socket"| D["CLI daemon: one browser per session"] M --> E["Playwright engine"] D --> E T["Playwright test run or script"] -->|"browser.bind() or --debug=cli"| E E --> B["Chromium, Firefox, WebKit, Chrome, Edge"] B --> P["Page: DOM, accessibility tree, WebMCP tools"]

Playwright MCP

How it works

The server launches (or connects to) a browser and exposes tools that act on it. Its central idea is the accessibility snapshot: after a navigation or an action the server returns a YAML outline of the page in which every actionable element carries a ref such as e14. The model reads the outline, picks a ref and passes it as the target of the next call. No screenshot and no vision model are needed, the outline is far smaller than the DOM, and the same ref always means the same element until the page changes. Snapshots are written to files in the output directory and linked from the tool result; --snapshot-mode none stops the server from attaching them to every response and leaves the agent to call browser_snapshot when it needs one.

Two details learned from the runs are worth knowing before the first call. First, element arguments are a human-readable element description plus a target that is a ref or a selector (older write-ups say ref, and a call that uses it is rejected). Second, refs can go stale: after a navigation the refs in a page that contains frames get a frame prefix, so e14 becomes f1e14.

>>> browser_click {"element":"Sign in button","ref":"e14"}
### Error
Invalid arguments for tool "browser_click":
✖ Invalid input: expected string, received undefined
  → at target

>>> browser_click {"element":"Sign in button","target":"e14"}
### Ran Playwright code
await page.getByTestId('signin-submit').click();

Most tool results also contain a Ran Playwright code section with the equivalent Playwright call. That makes an MCP session a recorder: the exact locators the agent used (getByTestId('signin-submit')) can be pasted into a test, and they are real locators, not guesses, because Playwright derived them from the live element.

Installing it in an MCP client

All clients take the same server definition, which Playwright calls the standard config:

{
  "mcpServers": {
    "playwright": {
      "command": "npx",
      "args": ["@playwright/mcp@latest"]
    }
  }
}

The server needs Node.js 18 or newer (the playwright.dev pages state 20 or newer, which is the safer choice). Client by client, the documented commands are:

# Claude Code: adds the server to the local scope; add --scope project to write .mcp.json and share it with the team
claude mcp add playwright npx @playwright/mcp@latest

# VS Code (GitHub Copilot agent mode): registers the server through the VS Code CLI
code --add-mcp '{"name":"playwright","command":"npx","args":["@playwright/mcp@latest"]}'

# Codex CLI
codex mcp add playwright npx "@playwright/mcp@latest"

# Copilot CLI: interactive form, or the subcommand (writes ~/.copilot/mcp-config.json)
copilot mcp add playwright -- npx @playwright/mcp@latest

The README lists Copilot CLI’s file form as /mcp add inside the CLI, or an entry in ~/.copilot/mcp-config.json:

{
  "mcpServers": {
    "playwright": {
      "type": "local",
      "command": "npx",
      "tools": ["*"],
      "args": ["@playwright/mcp@latest"]
    }
  }
}

Codex also reads TOML, and Cursor takes the same command through its settings screen:

# ~/.codex/config.toml
[mcp_servers.playwright]
command = "npx"
args = ["@playwright/mcp@latest"]

In Cursor, open Cursor Settings, then MCP, then Add new MCP Server, name it playwright, choose the command type and enter npx @playwright/mcp@latest; the Edit button lets you append arguments such as --headless. The README has one-click install links for VS Code and Cursor and short instructions for roughly twenty more clients (Claude Desktop, Windsurf, Gemini CLI, Goose, Junie, Kiro, opencode and others), all of which accept the standard config. Claude Code, VS Code, Cursor, Codex and Copilot CLI are the clients this page covers; Claude Code’s own MCP options are explained in Connecting MCP hosts.

Once the server is added, nothing else is configured by hand: the browser downloads on first use, and the agent finds the tools through the normal MCP tool listing. Arguments go in the args array, so a team that wants a visible, project-local configuration writes, for example, "args": ["@playwright/mcp@latest", "--headless", "--isolated", "--output-dir", ".playwright-mcp"].

Pin the version in anything that runs unattended. @latest is convenient interactively, but a server whose tool names and arguments changed between releases (the ref to target rename above is one) breaks a prompt that worked yesterday.

A first-run pitfall: the browser the server expects

The server looks for the browser build that matches its own Playwright revision. On the machine that produced this page the Playwright package used elsewhere had Chromium installed, but the server wanted a different headless-shell revision, and every tool call failed until a browser was chosen:

>>> browser_navigate {"url":"http://localhost:3920/books"}
### Error
Error: Browser "chromium" is not installed; expected executable at
~/Library/Caches/ms-playwright/chromium_headless_shell-1247/chrome-headless-shell-mac-arm64/chrome-headless-shell.
Run `npx @playwright/mcp install-browser chromium` to install

The fix is either the command in the message or --browser chrome (or "launchOptions": { "channel": "chrome" } in the config file below) to use the installed Google Chrome. The earlier transcripts in this page used the Chrome channel for that reason. A headed run needs a display; on a machine without one use --headless, or start the server where the display exists and connect over HTTP (see Standalone HTTP server and Docker).

The core tools

The core tools are always on and cannot be disabled. In 0.0.83 there are 25:

Tool What it does

browser_navigate, browser_navigate_back

Go to a URL, go back in history

browser_snapshot

Return the accessibility snapshot (preferred over a screenshot for acting)

browser_find

Search the current snapshot for text or a regular expression and return the matching nodes with context, which is cheaper than a full snapshot on a big page

browser_click, browser_hover, browser_drag, browser_drop

Click, hover, drag between two elements, drop files or MIME-typed data on an element

browser_type, browser_fill_form, browser_select_option, browser_press_key

Type into a field (with submit: true to press Enter), fill several fields in one call, choose a dropdown option, press a key

browser_file_upload, browser_handle_dialog

Answer a file chooser, accept or dismiss an alert, confirm or prompt dialog

browser_take_screenshot

Screenshot of the viewport, full page or one element (an image for the model; it cannot act on it)

browser_console_messages

Console messages at or above the configured level

browser_network_requests, browser_network_request

Numbered list of requests since the page loaded (static assets hidden unless asked for), and full headers and body of one request

browser_evaluate

Run a JavaScript function in the page (or on one element)

browser_run_code_unsafe

Run a Playwright snippet async (page) => { ... } in the server process, with the whole Playwright API; the tool’s own description says it is equivalent to remote code execution

browser_emulate_media

Switch the colour scheme and other CSS media features

browser_resize, browser_tabs, browser_wait_for, browser_close

Resize the window, list, open, close or select tabs, wait for text to appear or disappear or for a time to pass, close the page

Source: Playwright MCP: Introduction and the tool listing returned by the server (tools/list) at version 0.0.83

browser_run_code_unsafe is the escape hatch for anything the individual tools do not cover (geolocation, clipboard, iframes, custom waits, a script loaded with its filename argument). It runs in the Node.js server process, not in the page, so it deserves the same suspicion as any other command execution; see Security notes.

Example: verifying a feature from acceptance criteria

The first run asked the agent to verify the add-review feature of Bookshelf against three acceptance criteria without writing any code. This is the prompt, with the password replaced by the environment variable that holds it:

Using only the Playwright MCP browser tools, verify the "add review" feature of the Bookshelf app running at
http://localhost:3971 against these acceptance criteria. Do not write any code or test files.
1. Signed in as the reader (username reader, password in BOOKSHELF_READER_PASSWORD), a reader can open a book detail
   page and submit a review with rating 4 and the text "Clear and practical, with a great index." and it appears in the
   review list.
2. A review shorter than 10 characters is rejected with a visible validation message and is not added.
3. A review whose text contains FAIL shows an error and the optimistic entry is rolled back.
Report PASS or FAIL per criterion with the evidence you observed (visible text, refs, console or network facts).
Keep the report under 200 words.

The server was started headless and isolated (--headless --isolated). The first criterion in the transcript, trimmed:

>>> browser_navigate {"url":"http://localhost:3971/signin"}
await page.goto('http://localhost:3971/signin');
- Page Title: Sign in - Bookshelf

>>> browser_fill_form {"fields":[{"name":"Username","type":"textbox","target":"e12","value":"reader"},
                                 {"name":"Password","type":"textbox","target":"e13","value":"$BOOKSHELF_READER_PASSWORD"}]}
await page.getByTestId('username').fill('reader');
await page.getByTestId('password').fill('$BOOKSHELF_READER_PASSWORD');

>>> browser_click {"element":"Sign in button","target":"e14"}
await page.getByTestId('signin-submit').click();
- Page URL: http://localhost:3971/account

>>> browser_navigate {"url":"http://localhost:3971/books/1"}
>>> browser_select_option {"element":"Rating","target":"f2e31","values":["4"]}
await page.getByTestId('review-rating-input').selectOption('4');
>>> browser_type {"element":"Your review","target":"f2e32","text":"Clear and practical, with a great index."}
await page.getByTestId('review-text-input').fill('Clear and practical, with a great index.');
>>> browser_click {"element":"Submit review","target":"f2e33"}
await page.getByTestId('review-submit').click();

>>> browser_find {"text":"Clear and practical"}
Found 1 match for "Clear and practical":
        - listitem [ref=f2e35]:
          - strong [ref=f2e36]: reader
          - text: ★★★★
          - paragraph [ref=f2e37]: Clear and practical, with a great index.

>>> browser_network_requests {}
5. [POST] http://localhost:3971/api/reviews => [201] Created

The agent then used the same page to check the other two criteria. The evidence it collected is the point of the exercise:

>>> browser_type {"element":"Your review","target":"f2e32","text":"Too short"}
>>> browser_click {"element":"Submit review","target":"f2e33"}
>>> browser_find {"regex":"at least|characters|error"}
        - alert [ref=f2e38]: Write at least 10 characters
>>> browser_network_requests {}
5. [POST] http://localhost:3971/api/reviews => [201] Created        <- unchanged: no second POST

>>> browser_type {"element":"Your review","target":"f2e32","text":"This text will FAIL on save."}
>>> browser_click {"element":"Submit review","target":"f2e33"}
>>> browser_find {"regex":"Could not|error|failed"}
        - alert [ref=f2e38]: Could not save the review
>>> browser_network_requests {}
6. [POST] http://localhost:3971/api/reviews => [500] Internal Server Error
>>> browser_console_messages {}
[ERROR] Failed to load resource: the server responded with a status of 500 (Internal Server Error) @ http://localhost:3971/api/reviews:0

Criterion 1 passes with a 201, criterion 2 with a visible alert and no new request, criterion 3 with a 500, the visible error and the optimistic entry gone from the list. Three facts make this evidence rather than assertion: the agent read the network log (so it knows no request was sent for the short text), it read the console, and it observed the page after the failure. A report that says only "the error message appeared" would be weaker. The same session also shows the cost of short waits: browser_wait_for with time: 1 was used because Bookshelf’s review request has a deliberate delay, and a text/textGone wait is the better habit.

Example: a visual and responsive check

A second run asked for a design check: at least four catalogue columns on a desktop, two on a phone, no horizontal scroll and a 32-pixel page title. The agent resized the window, took screenshots and measured the layout in the page instead of judging pixels:

>>> browser_resize {"width":1280,"height":800}
>>> browser_navigate {"url":"http://localhost:3971/books"}
>>> browser_take_screenshot {"type":"png","filename":"catalogue-desktop.png"}
>>> browser_evaluate {"function":"() => { const g=document.querySelector(\"[data-testid=book-list]\")||document.querySelector(\"main ul\"); const cs=getComputedStyle(g); return {tag:g.tagName, display:cs.display, columns:cs.gridTemplateColumns, cardCount:g.children.length, font:getComputedStyle(document.querySelector(\"h1\")).fontSize}; }"}
{ "tag": "UL", "display": "grid", "columns": "185.594px 185.594px 185.609px 185.594px 185.609px", "cardCount": 6, "font": "32px" }

>>> browser_resize {"width":375,"height":812}
>>> browser_evaluate {"function":"() => { const g=document.querySelector(\"[data-testid=book-list]\")||document.querySelector(\"main ul\"); const cs=getComputedStyle(g); return {columns:cs.gridTemplateColumns, overflowX: document.documentElement.scrollWidth > innerWidth}; }"}
{ "columns": "163.5px 163.5px", "overflowX": false }

Five columns of about 185 pixels at 1280 pixels wide, two columns of 163.5 pixels at 375 pixels, no overflow and a 32-pixel heading: all four targets met. A screenshot is still useful as a record or for a vision-capable model, but a number from browser_evaluate is a check the agent can repeat and a reviewer can read. For pixel-level regressions use Visual Regression Testing, which compares against a stored baseline. Passing --mobile to the server (a generic Pixel 10 on Chromium, an iPhone 17 on WebKit) or --device "iPhone 15" starts the browser already emulating a phone; the docs point out that mobile pages are usually lighter, so their snapshots also cost fewer tokens.

Capability groups

The server starts with the core tools. Other tool groups are opt-in and enabled with --caps, the PLAYWRIGHT_MCP_CAPS environment variable, or capabilities in the config file. Keeping groups off matters: every enabled tool adds its schema to the model’s context on every turn, and a smaller surface gives the model fewer ways to do something you did not intend.

{
  "mcpServers": {
    "playwright": {
      "command": "npx",
      "args": ["@playwright/mcp@latest", "--caps=testing,network"]
    }
  }
}

A reality check: --help for 0.0.83 and the README option table list only vision, pdf and devtools as possible --caps values, but the capabilities page documents seven groups, and the server accepts all of them. Starting it with --caps vision,pdf,devtools,testing,network,storage,config exposed 72 tools in total, which matches the documentation’s "70+". The groups and their tools at 0.0.83:

Group Tools Typical use

core (always)

The 25 tools of the previous section

Navigate, act, observe

vision

browser_mouse_move_xy, browser_mouse_click_xy, browser_mouse_drag_xy, browser_mouse_down, browser_mouse_up, browser_mouse_wheel

Pages whose controls are not in the accessibility tree (canvas, custom widgets), with a vision-capable model

pdf

browser_pdf_save

Export the page to a PDF

devtools

browser_start_tracing, browser_stop_tracing, browser_start_video, browser_stop_video, browser_video_chapter, browser_video_show_actions, browser_video_hide_actions, browser_start_recording, browser_stop_recording, browser_highlight, browser_hide_highlight, browser_annotate, browser_resume

Traces, video evidence, recording a user’s manual actions as code, highlighting elements, pausing for feedback in the dashboard

testing

browser_generate_locator, browser_verify_element_visible, browser_verify_text_visible, browser_verify_list_visible, browser_verify_value

Assertions that return the equivalent expect() code, and locators for a test file

network

browser_route, browser_route_list, browser_unroute, browser_network_state_set

Mock requests, go offline

storage

browser_cookie_list/get/set/delete/clear, browser_localstorage_*, browser_sessionstorage_*, browser_storage_state, browser_set_storage_state

Inspect or seed cookies and storage; save and restore a signed-in state

config

browser_get_config

Print the final resolved configuration

Source: Playwright MCP: Capabilities and the tool listing returned by the server at version 0.0.83

Example: the testing group turns exploration into assertions

With --caps testing the agent can assert instead of merely look, and every assertion answers with the Playwright code that expresses it:

>>> browser_verify_element_visible {"role":"heading","accessibleName":"Catalogue"}
Done
await expect(page.getByRole('heading', { name: 'Catalogue' })).toBeVisible();

>>> browser_verify_list_visible {"element":"book list","target":"e16","items":["The Quiet Compiler","Rivers of Static"]}
Done
await expect(page.locator('body')).toMatchAriaSnapshot(`
  - list:
    - listitem: "The Quiet Compiler"
    - listitem: "Rivers of Static"
  `);

>>> browser_verify_text_visible {"text":"99 books found"}
### Error
Text not found

>>> browser_generate_locator {"element":"Search button","target":"e14"}
### Error
Error: Ref e14 not found in the current page snapshot. Try capturing new snapshot.
>>> browser_generate_locator {"element":"Search button","target":"f1e14"}
getByTestId('search-submit')

Two things deserve a reviewer’s eye. The failing check returns an error, so the agent cannot silently claim a pass. And the code that browser_verify_text_visible returned for the passing text check was expect(page.getByTestId('result-count')).toBeVisible(): it asserts that the element is visible, not that it shows the text, so pasting it into a test would weaken the check. Treat generated assertions as a draft and tighten them (toHaveText('1 book found')) before they enter a suite. The stale-ref error is the one from earlier: refs are only valid for the snapshot they came from.

Server options

Options go in the client’s args array, are read from PLAYWRIGHT_MCP_* environment variables (the option name in upper case with underscores, for example PLAYWRIGHT_MCP_HEADLESS and PLAYWRIGHT_MCP_CAPS), or come from the config file. npx @playwright/mcp@latest --help prints the list. This is the 0.0.83 list, grouped by purpose.

Browser and launch

Option Meaning

--browser <browser>

chrome, firefox, webkit or msedge (a Chrome channel). Default is Chromium

--headless

Run without a window. The default is headed

--executable-path <path>

Path to a browser executable

--device <device>

Emulate a named device from Playwright’s registry, for example "iPhone 15"

--mobile

Emulate a generic phone (Pixel 10 on Chromium, iPhone 17 on WebKit); cannot be combined with --device

--viewport-size <size>

Viewport, for example 1280x720

--user-agent <ua string>

Override the user agent

--no-sandbox / --sandbox

Disable the browser sandbox for all process types (needed in some containers) / enable it for the ones that are normally not sandboxed

--ignore-https-errors

Ignore certificate errors

--proxy-server <proxy>, --proxy-bypass <bypass>

Proxy, for example http://myproxy:3128 or socks5://myproxy:8080, and comma-separated domains that skip it

--grant-permissions <permissions...>

Permissions for the context, for example geolocation, clipboard-read

--block-service-workers

Block service workers

--test-id-attribute <attribute>

Attribute that getByTestId uses (default data-testid)

Profile, state and page setup

Option Meaning

--user-data-dir <path>

Persistent profile directory; if omitted, a per-workspace profile is used

--isolated

Keep the profile in memory; nothing is written to disk. Cannot be combined with --user-data-dir

--storage-state <path>

Load cookies and local storage from a storage-state file into an isolated session

--init-page <path...>

A TypeScript file whose default export async ({ page }) => { ... } runs on every new page

--init-script <path...>

A JavaScript file added as an init script, evaluated before any page script

Connecting to something that already exists

Option Meaning

--extension

Connect to a running Chrome or Edge through the Playwright Extension (see below)

--profile-dir-name <name>

With --extension, the profile to attach to, for example "Profile 1"

--cdp-endpoint <endpoint>

Chrome DevTools Protocol endpoint, or a channel name such as chrome

--cdp-header <headers...>, --cdp-timeout <ms>

Headers for the CDP connect request, connect timeout (default 30000)

--endpoint <endpoint>

A browser bound with browser.bind() or a Playwright server WebSocket

Output, snapshots and responses

Option Meaning

--output-dir <path>

Where automatically named files (snapshots, screenshots without a file name, console logs) go

--output-max-size <bytes>

Threshold for evicting old output files

--file-paths <mode>

Render file paths in results as relative (default) or absolute

--snapshot-mode <mode>

full (default) or none

--snapshot-boxes

Add [box=x,y,width,height] to each element in snapshots (viewport-relative CSS pixels)

--image-responses <mode>

allow (default), omit, or only (a response that carries an image contains only the image parts)

--console-level <level>

error, warning, info or debug; each level includes the more severe ones

--codegen <lang>

Language of the Ran Playwright code section: typescript (default), python, java, csharp or none

--save-session

Save the session (tool calls and results) into the output directory

Timeouts

Option Meaning

--timeout-action <ms>

Default action timeout (5000)

--timeout-navigation <ms>

Default navigation timeout (60000)

--timeout-settle <ms>

How long to wait after each action for triggered work to settle (500)

--idle-timeout <ms>

Close the browser after this long without a completed tool call; the next call relaunches it. One hour for headless browsers, never for headed ones, 0 disables

Security and access

Option Meaning

--allowed-origins <origins>

Semicolon-separated list of origins the browser may request. The help text stresses that it is not a security boundary and does not affect redirects

--blocked-origins <origins>

Semicolon-separated origins to block; evaluated before the allowlist

--secrets <path>

A dotenv file; matching plain-text values are replaced in tool responses so the model does not see them. A convenience, not a security feature

--allow-unrestricted-file-access

Allow file access outside the workspace roots and file:// navigation (both blocked by default)

--allowed-hosts <hosts...>

Hosts the server may be reached as (DNS-rebinding protection); * disables the check

Transport and sharing

Option Meaning

--port <port>

Listen on this port for HTTP/SSE transport instead of stdio

--host <host>

Bind address (default localhost; 0.0.0.0 for all interfaces)

--shared-browser-context

Reuse one browser context for all connected HTTP clients

--caps <caps>

Extra capability groups, see above

--config <path>

JSON configuration file

--no-webmcp

Do not collect or expose tools that pages register through WebMCP (see below)

Source: Playwright MCP README: Configuration and the output of playwright-mcp --help at version 0.0.83

Three of these options deserve a closer reading. --isolated is the safe default for testing: every session starts clean and browser_close discards all state. --storage-state is how an isolated session starts already signed in (see Playwright: Authentication & Isolation for how to produce that file). --allowed-origins is useful but weaker than it looks: the help text says it does not stop redirects, so it keeps an agent from wandering by accident, not from a determined or injected one.

Profiles and initial state

Without options the server uses a persistent profile in a per-workspace directory (under ~/Library/Caches/ms-playwright-mcp/ on macOS and ~/.cache/ms-playwright-mcp/ on Linux, named mcp-{channel}-{workspace-hash}), so logins survive between sessions and different projects get different profiles. A profile can serve only one browser at a time: a second client in the same workspace fails to start unless it uses --isolated. For test work prefer --isolated and seed state explicitly:

{
  "mcpServers": {
    "playwright": {
      "command": "npx",
      "args": ["@playwright/mcp@latest", "--isolated", "--storage-state=./auth-state.json"]
    }
  }
}

--init-page and --init-script set up the page before the agent touches it, for example geolocation and a fixed viewport, or a flag the application can read to switch off animations:

// init-page.ts
export default async ({ page }) => {
  await page.context().grantPermissions(['geolocation']);
  await page.context().setGeolocation({ latitude: 37.7749, longitude: -122.4194 });
  await page.setViewportSize({ width: 1280, height: 720 });
};

The JSON configuration file

Everything above can live in a JSON file passed with --config. The file keeps long argument lists out of the client config, can be committed next to the project, and exposes options (contextOptions, launchOptions) that have no command line flag. Its top-level keys are: browser (with browserName, isolated, userDataDir, launchOptions, contextOptions, cdpEndpoint, cdpHeaders, cdpTimeout, remoteEndpoint, initPage and initScript), extension, server (port, host, allowedHosts), capabilities, saveSession, webmcp, sharedBrowserContext, secrets, outputDir, outputMaxSize, console.level, network.allowedOrigins and network.blockedOrigins, testIdAttribute, timeouts (action, navigation, expect, settle, idle), imageResponses, filePaths, snapshot (mode, boxes), allowUnrestrictedFileAccess and codegen. launchOptions and contextOptions take the same values as Playwright’s launch and newContext.

This is the file used for the next run (the Chrome channel is there for the reason given earlier):

{
  "browser": {
    "browserName": "chromium",
    "isolated": true,
    "launchOptions": { "channel": "chrome", "headless": true },
    "contextOptions": { "viewport": { "width": 1280, "height": 800 }, "colorScheme": "light" }
  },
  "capabilities": ["testing", "network", "config"],
  "testIdAttribute": "data-testid",
  "network": { "allowedOrigins": ["http://localhost:3920"] },
  "timeouts": { "action": 5000, "navigation": 30000 },
  "snapshot": { "mode": "full" },
  "outputDir": "./out",
  "console": { "level": "warning" }
}

The server was started with playwright-mcp --config playwright-mcp.config.json. With the config capability enabled, the agent can ask the server what it actually resolved, which is the quickest way to debug a configuration that does not behave:

>>> browser_get_config {}
{
  "browser": {
    "launchOptions": { "channel": "chrome", "headless": true, "chromiumSandbox": true,
                       "args": ["--disable-blink-features=AutomationControlled"] },
    "contextOptions": { "viewport": { "width": 1280, "height": 800 }, "colorScheme": "light" },
    "browserName": "chromium",
    "isolated": true
  },
  "codegen": "typescript",
  "timeouts": { "action": 5000, "navigation": 30000, "expect": 5000, "settle": 500 },
  "capabilities": ["testing", "network", "config"],
  "testIdAttribute": "data-testid",
  "network": { "allowedOrigins": ["http://localhost:3920"] },
  "snapshot": { "mode": "full" },
  "outputDir": "./out",
  "console": { "level": "warning" },
  "server": {},
  "configFile": "<workspace>/playwright-mcp.config.json"
}

The resolved configuration shows the defaults filled in (expect, settle, codegen) and one that the file did not ask for: the server adds --disable-blink-features=AutomationControlled to the launch arguments. The origin allowlist also worked as described. Navigating to Bookshelf (an allowed origin) succeeded, and navigating to another local port was refused by the browser:

>>> browser_navigate {"url":"http://localhost:3921/"}
### Error
Error: browserBackend.callTool: net::ERR_BLOCKED_BY_CLIENT at http://localhost:3921/

Connecting to a browser that is already running

By default the server starts its own browser. Four options attach to an existing one instead, which matters when the page sits behind a login that is painful to automate:

{ "args": ["@playwright/mcp@latest", "--cdp-endpoint=chrome"] }
{ "args": ["@playwright/mcp@latest", "--cdp-endpoint=http://localhost:9222"] }
{ "args": ["@playwright/mcp@latest", "--endpoint=ws://localhost:3000/"] }
{ "args": ["@playwright/mcp@latest", "--extension", "--profile-dir-name=Profile 1"] }
  • --cdp-endpoint=chrome (or msedge, chrome-beta, chrome-canary and the other channels) attaches by channel name. In the target browser open chrome://inspect/#remote-debugging and enable "Allow remote debugging for this browser instance"; no special launch flags or port are needed.

  • --cdp-endpoint=http://localhost:9222 attaches to any Chromium-family browser started with --remote-debugging-port, including Electron apps and cloud browser services.

  • --endpoint connects to a Playwright server or to a browser bound with browser.bind() (see below).

  • --extension connects through the Playwright Extension, installed in Chrome or Edge. The agent then works in your existing tabs with your logged-in sessions, cookies and installed browser extensions, which is the way to get past SSO and two-factor authentication. --profile-dir-name picks the profile; by default it is the last used profile that has the extension.

These modes were not run for this page: the extension must be installed in a real Chrome or Edge profile and approved by a person, and no such profile was available to the sandbox. They are also where the risk is highest, because the agent now acts with your real accounts. Use a dedicated browser profile, not your everyday one, and read Securing Browser Agents first.

Standalone HTTP server and Docker

When the browser must be headed on a machine with no display for the MCP client (an IDE worker process, a remote box), start the server where the display exists and give it a port; clients then use a URL instead of a command:

npx @playwright/mcp@latest --port 8931
{ "mcpServers": { "playwright": { "url": "http://localhost:8931/mcp" } } }

--host 0.0.0.0 exposes the server on every interface and --allowed-hosts controls which host names it will answer to; exposing browser control to a network without authentication deserves the same care as exposing a shell. The README also documents a Docker image, which supports headless Chromium only:

{
  "mcpServers": {
    "playwright": {
      "command": "docker",
      "args": ["run", "-i", "--rm", "--init", "--pull=always", "mcr.microsoft.com/playwright/mcp"]
    }
  }
}
docker run -d -i --rm --init --pull=always \
  --entrypoint node \
  --name playwright \
  -p 8931:8931 \
  mcr.microsoft.com/playwright/mcp \
  /app/cli.js --headless --browser chromium --no-sandbox --port 8931 --host 0.0.0.0

The standalone server and the Docker image were not run for this page, which used the stdio transport throughout. A containerised browser cannot reach localhost on the host, so a Bookshelf test from inside the container would use host.docker.internal as in the Docker examples of Running in CI/CD.

The bundled commands: npx playwright mcp and cli

Playwright 1.62 started bundling both tools into the main package, so a project that already depends on @playwright/test or playwright needs nothing else:

$ npx playwright --version
Version 1.64.1
$ npx playwright mcp --help | head -4
Usage: npx playwright mcp [options]

run the Playwright MCP server
$ npx playwright cli --help | head -5
playwright-cli - run playwright mcp commands from terminal

Agent skill: ../../playwright/node_modules/playwright-core/lib/tools/skills/playwright-cli/SKILL.md

Usage: playwright-cli <command> [args] [options]

npx playwright mcp takes the same options as the standalone server, and an MCP client can launch it with "command": "npx", "args": ["playwright", "mcp", "--headless"]. The advantage is a single version: the server, the CLI and your tests then use the same Playwright and the same browser build, which also avoids the browser mismatch described earlier. The standalone packages remain the faster-moving releases (@playwright/mcp 0.0.83 and @playwright/cli 0.1.22 at the check date, both Apache-2.0), so choose by whether you want the newest tools or a pinned, consistent toolchain.

Playwright CLI for coding agents

Why a CLI at all

An MCP server puts its entire tool surface and a snapshot of every page into the model’s context. A coding agent that is already holding a large codebase may not want that. playwright-cli flips the arrangement: the agent calls an ordinary shell command, the browser lives in a background daemon, each command prints a few lines, and the page snapshot goes to a file the agent reads only if it needs to. Knowledge of the commands comes from a skill, a small bundle of instructions that the agent loads on demand, instead of from schemas that sit in context permanently. The docs summarise the intent: token-efficient output, skills instead of verbose help, a persistent browser with no start-up cost per command, ref-based interaction on accessibility snapshots, and multiple isolated sessions.

Installing it

npm install -g @playwright/cli@latest     # Node.js 20 or newer
playwright-cli --help
npx playwright cli --help                 # the same CLI from a project that already has Playwright (1.62+)

playwright-cli install                    # create .playwright/, add it to .gitignore, download the browser if missing
playwright-cli install --skills           # install the skill for Claude Code (default, same as --skills=claude)
playwright-cli install --skills=agents    # .agents/skills layout
playwright-cli install --skills -g        # into the home directory, for every project
playwright-cli install-browser firefox    # one specific browser; also --with-deps, --dry-run, --list, --force

playwright-cli install --skills was run in an empty directory:

$ playwright-cli install --skills
✅ Workspace initialized at `<workspace>`.
✅ Skill installed to `.claude/skills/playwright-cli`.
✅ Found chrome, will use it as the default browser.
./.claude/skills/playwright-cli/SKILL.md
./.claude/skills/playwright-cli/references/test-generation.md
./.claude/skills/playwright-cli/references/element-attributes.md
./.claude/skills/playwright-cli/references/video-recording.md
./.claude/skills/playwright-cli/references/playwright-tests.md
./.claude/skills/playwright-cli/references/running-code.md
./.claude/skills/playwright-cli/references/storage-state.md
./.claude/skills/playwright-cli/references/tracing.md
./.claude/skills/playwright-cli/references/pr-attachments.md
./.claude/skills/playwright-cli/references/request-mocking.md
./.claude/skills/playwright-cli/references/session-management.md

SKILL.md describes the command surface (interactions, snapshots and refs, sessions, raw output, the open and attach parameters) and links to the reference files, which the agent opens only when the task needs them: running and debugging Playwright tests, request mocking, running Playwright code, session management, storage state, test generation, tracing, video recording and inspecting element attributes. The skill works with Claude Code, GitHub Copilot, Cursor and any agent that loads locally installed skills (see Agent mode). Skills are optional: you can also tell an agent "test the add-todo flow using playwright-cli, check playwright-cli --help for the commands" and let it discover them, at the cost of reading the help text.

Daemon and sessions

The first open starts a daemon that owns the browser; later commands connect to it, so each one returns in milliseconds. -s=<name> selects a named session, and every session has its own browser, cookies, storage, history and tabs. The default profile is in memory (state survives between commands but not after close); --persistent saves it to disk and --profile=<dir> chooses the directory.

playwright-cli open https://example.com                    # session "default", in-memory profile
playwright-cli -s=admin open https://example.com --persistent
playwright-cli list                                        # sessions in this workspace; list --all for every workspace
playwright-cli -s=admin close                              # stop one browser
playwright-cli close-all                                   # stop them all
playwright-cli kill-all                                    # force kill unresponsive browsers
playwright-cli -s=admin delete-data                        # delete the stored profile
PLAYWRIGHT_CLI_SESSION=todo-app claude .                   # every command in that agent session uses the todo-app browser

The command set

playwright-cli --help lists the commands; playwright-cli --help <command> shows one command. Every action prints the Playwright code it ran, as MCP does. Global options are --json, --raw and --version.

Area Commands

Core

open [url], attach [name], close, detach, goto <url>, type <text>, click <target> [button], dblclick, fill <target> <text>, drag, drop, hover, select <target> <val>, upload <files...>, check, uncheck, snapshot [target], find [text], eval <func> [target], dialog-accept [prompt], dialog-dismiss, resize <w> <h>, delete-data

Navigation

go-back, go-forward, reload

Keyboard and mouse

press <key>, keydown, keyup, mousemove <x> <y>, mousedown, mouseup, mousewheel <dx> <dy>

Save as

screenshot [target] (--filename), pdf

Tabs

tab-list, tab-new [url], tab-close [index], tab-select <index>

Storage

state-save [file], state-load <file>, cookie-*, localstorage-*, sessionstorage-*

Network

requests, request <index>, request-headers, request-body, response-headers, response-body, route <pattern>, route-list, unroute, network-state-set <state>

DevTools

console [min-level], run-code [code], tracing-start, tracing-stop, recording-start, recording-stop, video-start [file], video-stop, video-chapter <title>, video-show-actions, video-hide-actions, show, pause-at <location>, resume, step-over, generate-locator <target>, highlight [target]

WebMCP (1.64)

webmcp-list, webmcp-call <tool> --params '{...}'

Install and sessions

install, install-browser [browser], -s=<name> <cmd>, list, close-all, kill-all, config-print

Example: the same search, driven from the shell

This is the run recorded for the CLI, with a named session bk. Compare the volume of output with the MCP excerpts above: the snapshot is a file, and fill returns the page URL, the page title and the code it ran.

$ playwright-cli -s=bk open http://localhost:3971/books
### Browser `bk` opened with pid 49293.
await page.goto('http://localhost:3971/books');
- Page Title: Catalogue - Bookshelf
- [Snapshot](.playwright-cli/page-2026-10-25T21-47-04-498Z.yml)

$ playwright-cli -s=bk fill e13 compiler --submit
await page.getByTestId('search-input').fill('compiler');
await page.getByTestId('search-input').press('Enter');
- Page URL: http://localhost:3971/books?q=compiler

$ playwright-cli -s=bk snapshot
- paragraph [ref=f1e15]: 1 book found
  ...
$ playwright-cli -s=bk generate-locator e14
### Error
Error: Ref e14 not found in the current page snapshot. Try capturing new snapshot.

$ playwright-cli -s=bk requests
Note: 13 static requests not shown, run with --static option to see them.

$ playwright-cli -s=bk eval document.title
"Catalogue - Bookshelf"

$ playwright-cli -s=bk set-color-scheme dark
await page.emulateMedia({ colorScheme: 'dark' });

$ playwright-cli -s=bk screenshot --filename=dark.png
- [Screenshot of viewport](./dark.png)

$ playwright-cli list
### Browsers
- bk:
  - status: open
  - browser-type: chrome
  - user-data-dir: <in-memory>
  - headed: false

$ playwright-cli -s=bk close
Browser 'bk' closed

(The generate-locator error is the same stale-ref rule as in MCP: the snapshot printed just before it numbers the search button f1e14, not e14, so the agent has to use the ref from the latest snapshot.)

The CLI also records traces. tracing-start prints the paths immediately, so the viewer can be opened while the automation is still going:

$ playwright-cli -s=tr open http://localhost:3971/books/1
$ playwright-cli -s=tr tracing-start
Trace recording started
- [Action log](.playwright-cli/traces/trace-1792964832954.trace)
- [Network log](.playwright-cli/traces/trace-1792964832954.network)
- [Resources](.playwright-cli/traces/resources)
$ playwright-cli -s=tr click getByTestId('theme-toggle')
$ playwright-cli -s=tr tracing-stop
Trace recording stopped.
$ npx playwright show-trace .playwright-cli/traces/trace-1792964832954.trace

The trace opens in the ordinary Trace Viewer described in Playwright: Debugging, Tracing & Codegen. (The last command, show-trace, needs a display and was not run here.)

Configuration and profiles

The CLI runs headless; --headed shows the window. open accepts --browser=chrome|firefox|webkit|msedge, --mobile, --device="iPhone 15", --persistent, --profile=<dir> and --config=<file>. A JSON config file uses the same schema as the MCP file above and is merged from four sources, each overriding the previous: ~/.playwright/cli.config.json, .playwright/cli.config.json in the workspace, PLAYWRIGHT_MCP_* environment variables and the command-line options. playwright-cli config-print prints the merged result. The run above also used set-color-scheme dark, which emulates the dark colour scheme and is not in the documentation’s command listing; use --help on your version to see what it offers. The CLI enables all capability groups, which is why generate-locator, tracing-start and route are always available.

Attaching to a running browser

attach creates a session that points at an existing browser: attach --cdp=chrome (a running Chrome or Edge with remote debugging enabled), attach --cdp=http://localhost:9222, attach --endpoint=ws://localhost:3000, attach --extension (through the Playwright Extension) or attach <name> for a browser that a script or a test has bound by name. detach ends the session and leaves that browser running; close is only for browsers the CLI launched. These work the same as the MCP connection modes, and are likewise not run here for the extension and remote-debugging variants. The named variant is run in Binding a browser and debugging a test from the CLI.

The dashboard

playwright-cli show opens a dashboard that lists every session with a live screencast. Click a session to see its viewport, tab bar and navigation controls, and to type and click in the page yourself: the intended use is watching a background agent, taking over when it hits a CAPTCHA or a two-factor prompt, and closing stale sessions. show --annotate turns the dashboard into a feedback tool: you draw boxes on the live page and write comments, and the command returns the annotated screenshot, the snapshot of the marked region and your notes, blocking until you finish. That is the supported way to ask an agent’s browser for a human design review mid-run. show --port=0, --host and --kill control the dashboard daemon. The window is graphical, so the dashboard was documented but not opened in the headless environment of these runs.

WebMCP: tools that the page offers to the agent

Everything so far has the agent working out what a page can do by reading its accessibility tree. WebMCP is a proposed browser API that inverts this: the page registers tools (a name, a description, a JSON Schema for input, and an execute function) on navigator.modelContext, and an agent calls them directly instead of clicking through the interface. A catalogue page could register search_catalog and a shop could register add_to_cart. The API is experimental.

Playwright 1.64 added page.webmcp and frame.webmcp (class WebMCP) so that your own tests can exercise those tools like any other part of the application:

import { chromium } from 'playwright';

const browser = await chromium.launch({ args: ['--enable-features=WebMCP'] });
const page = await browser.newPage();
await page.goto('https://example.com');
for (const tool of await page.webmcp.tools())
  console.log(tool.name, tool.description);
const result = await page.webmcp.callTool('add', { a: 2, b: 40 });
console.log(result.content[0].text); // "42"

webmcp.tools() returns each tool’s name, description, optional inputSchema and optional annotations (readOnly, untrustedContent, consequential: hints the page provides about the tool). webmcp.callTool(name, input, options) calls a tool and returns what its execute function resolved to, typically an object with a content array; the method throws when the tool does not exist. page.webmcp covers the main frame only; a child frame lists its own tools through its own frame.webmcp. WebMCP needs browser support: Chromium enables it with --enable-features=WebMCP, Firefox with the dom.modelcontext.enabled and dom.modelcontext.testing.enabled preferences, and WebKit does not implement it. tools() throws if the browser was launched without WebMCP support.

The agent-facing side:

  • Playwright MCP collects page-registered tools by default and offers them to the model as extra tools named webmcp_<tool>; --no-webmcp (or "webmcp": false in the config file, or PLAYWRIGHT_MCP_WEBMCP=false) switches that off.

  • playwright-cli has webmcp-list and webmcp-call <tool> --params '{"query":"cats"}'.

playwright-cli webmcp-list
playwright-cli webmcp-call search_catalog --params '{"query":"cats"}'

What happened when this was tried on Bookshelf is limited, and worth stating exactly. Bookshelf does not register any WebMCP tools, and the CLI says so:

$ playwright-cli -s=tr webmcp-list
### Result
No WebMCP tools registered on the page.

To go further, a small page was written that registers a search_catalog tool through navigator.modelContext.registerTool and calls Bookshelf’s /api/books endpoint:

<script>
if (navigator.modelContext) {
  navigator.modelContext.registerTool({
    name: 'search_catalog',
    description: 'Search the Bookshelf catalogue by title',
    inputSchema: { type: 'object', properties: { query: { type: 'string' } }, required: ['query'] },
    annotations: { readOnlyHint: true },
    async execute({ query }) {
      const r = await fetch('http://localhost:3920/api/books?q=' + encodeURIComponent(query));
      const data = await r.json();
      const titles = data.items.map((b) => b.title);
      document.getElementById('out').textContent = 'Found: ' + titles.join(', ');
      return { content: [{ type: 'text', text: JSON.stringify(titles) }] };
    }
  });
} else { document.getElementById('out').textContent = 'navigator.modelContext is not available'; }
</script>

The page was served locally and opened through Playwright MCP (with the installed Google Chrome 155, once with --enable-features=WebMCP passed in launchOptions.args of the config file) and through the library with the script below on Playwright’s Chromium 156 build, with the extra --enable-blink-features and --enable-experimental-web-platform-features switches, and with Chrome 155. In every case the page printed its fallback text, typeof navigator.modelContext was undefined inside the page, page.webmcp.tools() returned an empty list, page.webmcp.callTool('search_catalog', ...) threw No WebMCP tool named "search_catalog". The frame does not register any WebMCP tools., and Playwright MCP exposed no webmcp_* tool (25 core tools before and after navigation).

import { chromium } from 'playwright';

const browser = await chromium.launch({ args: ['--enable-features=WebMCP'] });
const page = await browser.newPage();
await page.goto('http://localhost:3921/');
for (const tool of await page.webmcp.tools())
  console.log(tool.name, '-', tool.description, JSON.stringify(tool.annotations));
const result = await page.webmcp.callTool('search_catalog', { query: 'compiler' });
console.log(result.content[0].text);
await browser.close();
webMCP.callTool: No WebMCP tool named "search_catalog". The frame does not register any WebMCP tools.

So on this macOS host the documented flag was not enough to turn the API on in these browser builds, and no webmcp_<tool> call was observed. The Playwright API, the MCP exposure and the CLI commands above are therefore documented from the official reference and release notes and were not demonstrated end to end; check your own browser version before depending on it.

Whatever the browser support, treat page-provided tools as untrusted input. The Playwright documentation says tool names, descriptions, schemas and results all come from the page, and the consequential and untrustedContent annotations are the page’s own claims, not guarantees. A malicious or compromised page can describe a harmful tool innocently, or return text meant to redirect the agent; see Prompt injection and Excessive agency and tool security. Use --no-webmcp when an agent visits pages you do not control, and require a human to approve any consequential call (Human in the loop).

Binding a browser and debugging a test from the CLI

Playwright 1.59 added browser.bind(title, options), which makes a browser that a script launched available to other clients: playwright-cli, @playwright/mcp (through --endpoint) and other Playwright clients. By default it listens on a named pipe; passing host and port uses a WebSocket (port 0 lets the operating system choose) and the returned endpoint is then a ws:// URL. browser.unbind() stops accepting new connections. The CLI binds every browser it starts, so they all appear in the dashboard, and PLAYWRIGHT_DASHBOARD=1 shows the browsers of @playwright/test there too.

import { chromium } from 'playwright';

const browser = await chromium.launch({ headless: true });
const page = await browser.newPage();
await page.goto('http://localhost:3920/books');
const { endpoint } = await browser.bind('bookshelf-demo', { workspaceDir: process.cwd() });
console.log('bound', endpoint);
await new Promise((r) => setTimeout(r, 45000));   // keep the browser alive while a client attaches
await browser.unbind();
await browser.close();

While that script ran, the CLI listed the bound browser, attached to it, worked with the page the script had opened, and detached without closing it:

$ playwright-cli list --all
### Browser servers available for attach
- browser "bookshelf-demo":
  - browser: chromium
  - version: v1.64.1
  - data-dir: <in-memory>
  - run `playwright-cli attach "bookshelf-demo"` to attach

$ playwright-cli attach bookshelf-demo
### Session `bookshelf-demo` created, attached to `bookshelf-demo`.
Run commands with: playwright-cli -s=bookshelf-demo <command>
- Page URL: http://localhost:3920/books
- Page Title: Catalogue - Bookshelf

$ playwright-cli -s=bookshelf-demo eval document.title
"Catalogue - Bookshelf"
$ playwright-cli -s=bookshelf-demo goto http://localhost:3920/books/1
- Page Title: The Quiet Compiler - Bookshelf
$ playwright-cli list
- bookshelf-demo:
  - status: open
  - browser-type: chromium (attached)
$ playwright-cli -s=bookshelf-demo detach
Browser 'bookshelf-demo' detached

The same mechanism lets an agent debug a test that is already running. npx playwright test --debug=cli runs the test paused at its first action, binds its browser under a generated name such as tw-eb03a5, and prints how to attach. The agent then steps through the test, looking at the page between steps. Run the test in the background so it stays paused while the agent works. This is the Bookshelf test that was debugged:

import { test, expect } from '@playwright/test';

test('search finds a book', async ({ page }) => {
  await page.goto('/books');
  await page.getByTestId('search-input').fill('compiler');
  await page.getByTestId('search-submit').click();
  await expect(page.getByTestId('result-count')).toHaveText('1 book found');
});
$ PLAYWRIGHT_HTML_OPEN=never npx playwright test tests/search.spec.ts --debug=cli
Running 1 test using 1 worker

### The test is currently paused at the start

### Debugging Instructions
- Run "playwright-cli attach tw-eb03a5" to attach to this test

$ playwright-cli attach tw-eb03a5
### Session `tw-eb03a5` created, attached to `tw-eb03a5`.
- Page URL: about:blank
### Paused
- Navigate /books at tests/search.spec.ts:4

$ playwright-cli --session=tw-eb03a5 step-over
- Page URL: http://localhost:3920/books
### Paused
- Fill "compiler" getByTestId('search-input') at tests/search.spec.ts:5

$ playwright-cli --session=tw-eb03a5 step-over
### Paused
- Click getByTestId('search-submit') at tests/search.spec.ts:6

$ playwright-cli --session=tw-eb03a5 pause-at tests/search.spec.ts:7
- Page URL: http://localhost:3920/books?q=compiler
### Paused
- Expect "toHaveText" getByTestId('result-count') at tests/search.spec.ts:7

$ playwright-cli --session=tw-eb03a5 find "1 book"
- paragraph [ref=f1e15]: 1 book found

$ playwright-cli --session=tw-eb03a5 resume
...
  ✓  1 tests/search.spec.ts:3:5 › search finds a book (121ms)
  1 passed (13.1s)

PLAYWRIGHT_HTML_OPEN=never stops the HTML report from opening when the run ends. Because the test is paused at the very start, pause-at <file>:<line> is usually the fastest way to the interesting step. Every CLI action prints the equivalent Playwright TypeScript, so the fix for a failing locator can be pasted straight back into the test. For a test that already failed, Playwright 1.59 also added npx playwright trace (open, actions, action <n>) to read a trace from the command line, which an agent can use instead of the viewer. A test-fixing loop built on these tools, with a planner, generator and healer, is the subject of Playwright Test Agents.

Choosing and combining them

A workable division of labour, supported by what the runs showed:

  • Authoring a test from a requirement. Use the CLI with the skill (or MCP with --caps testing) to explore the live app. The Ran Playwright code sections give real locators; copy them into a test and tighten the assertions, as the browser_verify_text_visible example showed.

  • Verifying a change without writing code. MCP in isolated mode, with testing and network if the agent must assert or mock. Ask for evidence (network log, console, visible text), not for a verdict.

  • Debugging a failing test. --debug=cli, then the CLI, then npx playwright trace.

  • Watching or supervising an agent. The dashboard, and --headed while developing.

  • Anything that must run on every commit. A normal test in CI. An agent session is non-deterministic and costs tokens; see Running in CI/CD and Flaky Tests & Reliability.

For the other browser-control options, including Chrome DevTools MCP for performance work, see Chrome DevTools for Agents and Agents That Control the Browser. To check an MCP server you have configured, MCP testing and debugging shows the MCP Inspector.

Security notes

A browser agent reads untrusted pages and can act with whatever the browser can reach, so the configuration is part of the security design. Full guidance is in Securing Browser Agents, MCP security and the OWASP Top 10 for Agentic Applications. The points specific to Playwright MCP and CLI:

  • Playwright MCP is not a security boundary, in the README’s own words. Rely on the MCP client’s permission prompts and on the operating system for real isolation.

  • Prefer --isolated with a minimal --storage-state for a test account. Never point an agent at your everyday browser profile (--extension, --cdp-endpoint=chrome, attach --cdp=chrome) unless you accept that it can use every session in it.

  • Leave capability groups off until needed. storage hands the model your cookies; browser_run_code_unsafe, which is core, runs arbitrary code in the server process, so some clients should deny it by default.

  • --allowed-origins and --blocked-origins do not follow redirects and are not a boundary. --secrets hides values from responses but is a convenience. --allow-unrestricted-file-access removes the workspace restriction on files and file:// URLs.

  • Do not bind the HTTP server to 0.0.0.0 on a shared network, and keep the DNS-rebinding check (--allowed-hosts) on.

  • Pass credentials by environment variable or secrets file, never inside a prompt or a committed config; the transcripts here used $BOOKSHELF_READER_PASSWORD.

  • Page content, including WebMCP tools, snapshots and console text, can contain instructions aimed at the model. Review actions that change data or spend money before they run.

What changed recently

  • Playwright 1.64. WebMCP support: page.webmcp and frame.webmcp with tools() and callTool(); Playwright MCP exposes page tools as webmcp_<tool> by default and takes --no-webmcp; playwright-cli gains webmcp-list and webmcp-call. Video options (frame rate, action styling) are also available in MCP and the CLI.

  • Playwright 1.62. The MCP server and the CLI are bundled: npx playwright mcp and npx playwright cli.

  • Playwright 1.59 and after. browser.bind() and browser.unbind(), playwright-cli show (the dashboard), attach by name, PLAYWRIGHT_DASHBOARD=1, npx playwright test --debug=cli and npx playwright trace for agents.

  • @playwright/mcp 0.0.83. Tool arguments are element plus target (not ref); browser_find, browser_drop, browser_network_request and browser_run_code_unsafe are core tools; capability groups testing, network, storage and config exist beyond the three that --help lists; new options include --mobile, --endpoint, --snapshot-mode, --idle-timeout and --image-responses.

  • @playwright/cli 0.1.22. Installs skills with install --skills (Claude Code or .agents layout, optionally global), records traces and videos, and attaches to bound browsers, CDP endpoints and the extension.

See What’s Changed and Upgrading for the wider picture across Playwright, Cypress and Selenium.