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 ( |
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).
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();
Source: Playwright MCP: Snapshots
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
Source: Playwright MCP: Claude Code, Playwright MCP: VS Code, Playwright MCP README and GitHub Docs: Adding MCP servers for Copilot CLI
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
Source: Playwright MCP: Profile & State
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 |
|---|---|
|
Go to a URL, go back in history |
|
Return the accessibility snapshot (preferred over a screenshot for acting) |
|
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 |
|
Click, hover, drag between two elements, drop files or MIME-typed data on an element |
|
Type into a field (with |
|
Answer a file chooser, accept or dismiss an alert, confirm or prompt dialog |
|
Screenshot of the viewport, full page or one element (an image for the model; it cannot act on it) |
|
Console messages at or above the configured level |
|
Numbered list of requests since the page loaded (static assets hidden unless asked for), and full headers and body of one request |
|
Run a JavaScript function in the page (or on one element) |
|
Run a Playwright snippet |
|
Switch the colour scheme and other CSS media features |
|
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.
Source: Playwright MCP: Introduction
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
Source: Playwright MCP: Forms and Playwright MCP: Network
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
Source: Playwright MCP: Console and Playwright MCP: Network
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"]
}
}
}
Source: Playwright MCP: Capabilities
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 |
|
|
Pages whose controls are not in the accessibility tree (canvas, custom widgets), with a vision-capable model |
|
|
Export the page to a PDF |
|
|
Traces, video evidence, recording a user’s manual actions as code, highlighting elements, pausing for feedback in the dashboard |
|
|
Assertions that return the equivalent |
|
|
Mock requests, go offline |
|
|
Inspect or seed cookies and storage; save and restore a signed-in state |
|
|
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 |
|---|---|
|
|
|
Run without a window. The default is headed |
|
Path to a browser executable |
|
Emulate a named device from Playwright’s registry, for example |
|
Emulate a generic phone (Pixel 10 on Chromium, iPhone 17 on WebKit); cannot be combined with |
|
Viewport, for example |
|
Override the user agent |
|
Disable the browser sandbox for all process types (needed in some containers) / enable it for the ones that are normally not sandboxed |
|
Ignore certificate errors |
|
Proxy, for example |
|
Permissions for the context, for example |
|
Block service workers |
|
Attribute that |
Profile, state and page setup
| Option | Meaning |
|---|---|
|
Persistent profile directory; if omitted, a per-workspace profile is used |
|
Keep the profile in memory; nothing is written to disk. Cannot be combined with |
|
Load cookies and local storage from a storage-state file into an isolated session |
|
A TypeScript file whose default export |
|
A JavaScript file added as an init script, evaluated before any page script |
Connecting to something that already exists
| Option | Meaning |
|---|---|
|
Connect to a running Chrome or Edge through the Playwright Extension (see below) |
|
With |
|
Chrome DevTools Protocol endpoint, or a channel name such as |
|
Headers for the CDP connect request, connect timeout (default 30000) |
|
A browser bound with |
Output, snapshots and responses
| Option | Meaning |
|---|---|
|
Where automatically named files (snapshots, screenshots without a file name, console logs) go |
|
Threshold for evicting old output files |
|
Render file paths in results as |
|
|
|
Add |
|
|
|
|
|
Language of the |
|
Save the session (tool calls and results) into the output directory |
Timeouts
| Option | Meaning |
|---|---|
|
Default action timeout (5000) |
|
Default navigation timeout (60000) |
|
How long to wait after each action for triggered work to settle (500) |
|
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, |
Security and access
| Option | Meaning |
|---|---|
|
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 |
|
Semicolon-separated origins to block; evaluated before the allowlist |
|
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 file access outside the workspace roots and |
|
Hosts the server may be reached as (DNS-rebinding protection); |
Transport and sharing
| Option | Meaning |
|---|---|
|
Listen on this port for HTTP/SSE transport instead of stdio |
|
Bind address (default |
|
Reuse one browser context for all connected HTTP clients |
|
Extra capability groups, see above |
|
JSON configuration file |
|
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"]
}
}
}
Source: Playwright MCP: Profile & State
--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"
}
Source: Playwright MCP: Capabilities
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(ormsedge,chrome-beta,chrome-canaryand the other channels) attaches by channel name. In the target browser openchrome://inspect/#remote-debuggingand enable "Allow remote debugging for this browser instance"; no special launch flags or port are needed. -
--cdp-endpoint=http://localhost:9222attaches to any Chromium-family browser started with--remote-debugging-port, including Electron apps and cloud browser services. -
--endpointconnects to a Playwright server or to a browser bound withbrowser.bind()(see below). -
--extensionconnects 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-namepicks 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
Source: Playwright CLI: Installation
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
Source: Playwright CLI: Skills
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 |
|
Navigation |
|
Keyboard and mouse |
|
Save as |
|
Tabs |
|
Storage |
|
Network |
|
DevTools |
|
WebMCP (1.64) |
|
Install and sessions |
|
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
Source: Playwright CLI: Quick Start
(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
Source: Playwright CLI: Tracing
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"
Source: Playwright: WebMCP
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": falsein the config file, orPLAYWRIGHT_MCP_WEBMCP=false) switches that off. -
playwright-cli has
webmcp-listandwebmcp-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.
Source: Playwright CLI: Introduction
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>
Source: Playwright: WebMCP
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.
Source: Playwright: WebMCP
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();
Source: Playwright: Browser.bind()
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
Source: Playwright CLI: Attach & Detach
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)
Source: Playwright CLI: Test Debugging
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. TheRan Playwright codesections give real locators; copy them into a test and tighten the assertions, as thebrowser_verify_text_visibleexample showed. -
Verifying a change without writing code. MCP in isolated mode, with
testingandnetworkif 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, thennpx playwright trace. -
Watching or supervising an agent. The dashboard, and
--headedwhile 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
--isolatedwith a minimal--storage-statefor 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.
storagehands 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-originsand--blocked-originsdo not follow redirects and are not a boundary.--secretshides values from responses but is a convenience.--allow-unrestricted-file-accessremoves the workspace restriction on files andfile://URLs. -
Do not bind the HTTP server to
0.0.0.0on 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.webmcpandframe.webmcpwithtools()andcallTool(); Playwright MCP exposes page tools aswebmcp_<tool>by default and takes--no-webmcp;playwright-cligainswebmcp-listandwebmcp-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 mcpandnpx playwright cli. -
Playwright 1.59 and after.
browser.bind()andbrowser.unbind(),playwright-cli show(the dashboard),attachby name,PLAYWRIGHT_DASHBOARD=1,npx playwright test --debug=cliandnpx playwright tracefor agents. -
@playwright/mcp0.0.83. Tool arguments areelementplustarget(notref);browser_find,browser_drop,browser_network_requestandbrowser_run_code_unsafeare core tools; capability groupstesting,network,storageandconfigexist beyond the three that--helplists; new options include--mobile,--endpoint,--snapshot-mode,--idle-timeoutand--image-responses. -
@playwright/cli0.1.22. Installs skills withinstall --skills(Claude Code or.agentslayout, 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.