Your Playwright suite already runs in CI. The specs went through review, your team chose the locators, and a red run blocks the merge. Now an agent on the team drives a browser through Playwright MCP, which lets an LLM interact with web pages through structured accessibility snapshots. Nobody doubts the agent can click through your app. The open question is whether anything it writes belongs in your repo.
Getting that wrong has a concrete cost. A snapshot holds every element in the accessibility tree, and that can include UI a user can't reach. In issue #39955, users report closed drawers and collapsed sections in their snapshots, and agents writing test steps against them. A test like that can look plausible in review and still exercise nothing a user does. And we'd put little weight on a spec that passed once on the agent's run: --repeat-each <N> runs each test N times, and its default is 1.
Playwright now also bundles the MCP server and playwright-cli, run with npx playwright mcp and npx playwright cli. For coding agents, the Playwright CLI is a second option to weigh.
The rule we land on is short: MCP explores and drafts, your suite runs the tests, and nothing an agent writes merges until a person has reviewed its locators and it has passed repeat runs.
What Playwright MCP is, and what it is not
Playwright MCP lets a model drive a browser through structured accessibility snapshots, with no vision model required (Playwright MCP introduction). The model reads text and picks elements. The browser does the rest.
The setup involves an MCP client, the Playwright MCP server and the browser the server opens. Your agent runs inside an MCP client such as Claude Code, Cursor, VS Code or Claude Desktop. The client connects to the Playwright MCP server and calls its tools, and the server drives a real browser and returns what the page looks like after each step.
Use the server your project already has
The docs' standard client configuration runs the standalone package, npx @playwright/mcp@latest (getting started with Playwright MCP). That package ships with its own Playwright build, so @latest can drift from the version your project pins. Since Playwright 1.62, Playwright bundles the MCP server, runnable via npx playwright mcp (release notes). On 1.62 or later, point the client at the bundled server instead:
{
"mcpServers": {
"playwright": {
"command": "npx",
"args": ["playwright", "mcp"]
}
}
}
That entry uses your project's Playwright only when the client starts the server from your project's directory. Run elsewhere, npx fetches the latest playwright package instead. For a client that doesn't run inside a project, keep the standalone entry from the docs.
What it is not
Playwright MCP is a Model Context Protocol server that provides browser automation capabilities using Playwright. It drives a browser on a model's behalf, and your specs still run under npx playwright test, in the pipeline you already have. It is also not the community project with a similar name: @executeautomation/playwright-mcp-server, which installs through npm, Smithery or mcp-get, is a separate server.
How the snapshot-and-ref loop drives the browser
The model reads the accessibility snapshot, finds element refs such as e5, and passes them as the target of the next tool call. Playwright MCP works on the page's accessibility tree rather than pixels, and each tool returns a structured snapshot of the page's elements, their roles and their text (getting started with Playwright MCP). The model never has to guess coordinates from a screenshot.
Each element in that snapshot carries a ref. The model reads a ref such as e5 and passes it as the target of its next tool call, whether that call fills a form, clicks a button or checks a checkbox. One turn of the loop runs like this:
- The client calls a tool, such as
browser_navigateto open a page. - The server acts in the browser and returns the accessibility tree with element refs.
- The model finds the element it needs in that tree and takes its ref.
- The client calls the next tool with that ref as
target, and the server returns a fresh snapshot.
Anti-pattern: a full snapshot before every action
On a small page, we'd expect an extra snapshot to cost little. On a large page, browser_find returns only the matching subtree instead of the entire snapshot, and the pattern below never uses it:
# Anti-pattern: a full browser_snapshot before every action on a large page
browser_snapshot -> the whole accessibility tree
browser_type target: "e5" -> types the todo, returns a snapshot
browser_snapshot -> the whole tree again
browser_type target: "e9" -> types the next todo, returns a snapshot
Most tools already return a fresh snapshot after each action, so an explicit browser_snapshot before the next action repeats a page the model has just read.
Correct pattern: refs, locator strings and browser_find
Read the ref from the snapshot the last call returned, and switch to the cheaper lookups when the page is large. target also accepts any Playwright selector or locator string, which skips a snapshot round-trip when the element is already known. On a large page, browser_find costs far less than a full snapshot when the agent needs only one element and its ref.
# The snapshot the last call returned: the model reads the ref
- textbox "What needs to be done?" [ref=e5]
# Next call: the ref goes in as target, with the text to type
browser_type target: "e5", text: "Buy groceries"
# Element already known: a locator string as target skips the round-trip
browser_type target: "getByRole('textbox', { name: 'What needs to be done?' })", text: "Buy groceries"
# Large page, one element needed: search for it instead of reading everything
browser_find text: "What needs to be done?" -> the matching nodes and their refs, not the whole tree
The locator form matters beyond the saved round-trip: a committed spec needs locators, not refs.
The 70+ tools, grouped by job, and how capabilities gate them
More than 70 tools is a reason to turn most of them off. Playwright MCP's tools cover navigation, forms, network mocking, storage, tracing, video and more (Playwright MCP introduction). Every tool the model doesn't need is one more wrong choice it can make.
Capabilities control which tools the server exposes to the LLM, and only the core tools are on by default. The docs group the opt-in tools into network, storage, testing, devtools, PDF, vision and config capabilities. Sorted by the job they do for a test engineer, navigation and form filling drive the page through the snapshot-and-ref loop. Network tools mock the requests the app makes, and storage keeps login state for later sessions. Testing adds assertions the agent can make about the page. Tracing and video keep a record of what the agent did, and the --caps=devtools option turns on the tracing tools.
For testing work, the docs suggest core plus the testing assertions, with storage for auth persistence. Add devtools only when you need a trace:
npx playwright mcp --caps=devtools
The fewer tools the model sees, the better. Tool schemas are one of the reasons Playwright's comparison gives for MCP's higher token cost, so even a tool the model never calls has a price. Start from core, and add a capability when a task fails for lack of it, not up front.
Enabling everything so the agent never hits a missing tool widens the list the model has to choose from. Capabilities don't limit what the agent can do, though. browser_run_code_unsafe is a core tool, and core tools are always available and cannot be disabled (Playwright MCP capabilities). The tool runs arbitrary JavaScript in the Playwright server process, and the docs call it RCE-equivalent (code execution). Any client connected to the server can do anything the server process can do, on a developer laptop or on a CI runner. Since no flag turns that tool off, control which clients can reach the server instead.
Running MCP headless, in Docker and over HTTP
The browser opens in headed mode by default so you can watch (Playwright MCP options). That default suits a developer at a desk. In a container or on a host without a display, run the browser headless.
The other half is reaching the server from outside the container. The server binds to localhost by default, which nothing outside the container can reach. Inside the container, start the server headless, serve it over HTTP, and bind it to all interfaces with --host 0.0.0.0:
# Headless browser, HTTP server on port 8931, bound to all interfaces in the container
npx playwright mcp --headless --port 8931 --host 0.0.0.0
Then point the MCP client at the server's URL instead of having the client start a local process:
{
"mcpServers": {
"playwright": {
"url": "http://localhost:8931/mcp"
}
}
}
The server answers only requests addressed to localhost, a loopback IP or the --host value. If the client reaches the container by another name, such as a Compose service name, add that name with --allowed-hosts.
Keep the port off public networks
Inside the container, --host 0.0.0.0 is what you want. Published from the container, the same port is open to anyone who can reach it. The server has no authentication option, and browser_run_code_unsafe is always on, so anyone who can reach the port can run code in the container. The project's README says outright that Playwright MCP is not a security boundary (Playwright MCP security).
If the client runs on the Docker host, publish the port on the host's loopback address. Only clients on that host can connect:
docker run -p 127.0.0.1:8931:8931 <your-mcp-image>
If the client runs on another machine, the loopback mapping refuses its connection. Publish the port on the host's address on a private network you control instead, here 10.0.0.12, and add that address to the server's --allowed-hosts:
docker run -p 10.0.0.12:8931:8931 <your-mcp-image>
Never publish the port on a public address.
Idle browsers close, sometimes
A headless browser the server launched closes after an hour without tool calls. Headed browsers, and browsers attached over --cdp-endpoint or --extension, never close on their own. The next tool call after that launches a new browser, so the agent has to navigate again.
Anti-pattern: treating --secrets as protection
Production credentials handed to an agent still reach the page it drives, whatever --secrets hides from the transcript. --secrets redacts matching plain text in tool responses, and when the agent types a placeholder, the server substitutes the real value in the page. The docs are explicit that the redaction is a convenience, not a security boundary. Give agents test accounts whose credentials you would accept seeing in a transcript.
Persistent profiles, isolated sessions and saved login state
A profile can only be used by one browser at a time, so a second server pointed at a locked profile fails to start (Playwright MCP user profile). Point two agents on one machine at the same persistent profile, and the second server never starts while the first holds the lock.
Sharing one logged-in browser is a real need, and the Playwright browser extension doesn't handle it well yet. With more than one agent connected at a time, the extension's connect flow brings Chrome to the front while the person is typing in another app (#42973). The team that reported it runs Claude Code, Codex and Cursor on macOS against the browser they're already logged into, and asked for a background mode. The issue is open and labelled v1.65, and a maintainer wrote on a related pull request (#43500) that the team is working on a fix that needs upstream changes in Chromium first.
Anti-pattern: an isolated session on a profile directory
The tempting fix is to make each session isolated and still point it at a profile directory. The docs rule that out: --isolated cannot be combined with --user-data-dir.
# Anti-pattern: --isolated cannot be combined with --user-data-dir
npx playwright mcp --isolated --user-data-dir ./.mcp-profile
Correct pattern: isolated sessions with saved state
Load login state into an isolated session with --storage-state instead. --storage-state takes the path of an initial storage state file for isolated sessions:
# One isolated session per agent, seeded with saved login state
npx playwright mcp --isolated --storage-state ./playwright/.auth/user.json
When the agent needs to save authenticated state for reuse across sessions, the storage capability covers that, and the docs' suggested testing workflow combines core, the testing assertions and storage for auth persistence.
Prefer isolated sessions with saved storage state for anything repeatable, one session per agent, all reading the same state file. Keep persistent profiles for one person exploring by hand, where a single browser holding the lock is exactly what you want.
Where MCP fits next to a Playwright suite in CI
MCP belongs before CI, not inside it. Use it to explore the app and to draft specs. Those specs land in the repo as plain Playwright tests, and your existing pipeline runs them with the rest of the suite. MCP does not replace the runner, and your pipeline doesn't change for it.
Playwright's own test agents point the same way. Under the hood, their agent definitions are collections of instructions and MCP tools (Playwright Test Agents).
Anti-pattern: the agent session as the CI run
When every push asks a model to click through the app and report back, every run pays MCP's token cost, the one covered under limits below. The bigger problem is the merge gate: it becomes the agent's account of what happened instead of assertions a reviewer read before merging. Commit the specs the agent drafts, and let CI run those.
Record the exploration, then review it
An agent's summary of what it tested is a claim, not evidence. When the agent also records a trace, screenshots and page snapshots, you can check what it did in the browser instead of trusting its summary. Playwright MCP's tracing tools, browser_start_tracing and browser_stop_tracing, belong to the developer tools capability, which the --caps=devtools option turns on. Add it to the client's server entry for exploration sessions:
{
"mcpServers": {
"playwright": {
"command": "npx",
"args": ["playwright", "mcp", "--caps=devtools"]
}
}
}
A page snapshot in that recording is the page's accessibility tree with its text, the same kind of view the model worked from. Currents Agent evidence saves the recording as a session behind one link, and the trace opens in a trace player on the session page.
Getting MCP-drafted tests ready to merge
Review a drafted spec the way you would review a pull request from someone who first saw your app this morning. Two checks come before the merge, the locators and repeat runs, and our full pre-merge checklist for AI-generated tests goes beyond both.
Review every locator
Refs like e5 come from a snapshot, and refs are invalidated when the page changes. The spec needs Playwright locators. Because target accepts any Playwright selector or locator string too, the agent can explore with the same locators the spec will use. Check each one before merging. Prefer a role and accessible name, such as getByRole('button', { name: 'Submit' }), over an id like #submit that the agent happened to find, and confirm a user can reach the element at all.
Anti-pattern: merging after one green run
A single green run proves little. --repeat-each <N> runs each test N times, and its default is 1, so a plain run checks a new spec exactly once. Merging on the strength of a run like the one below is the anti-pattern:
# Anti-pattern: one green run of an agent-drafted spec, then merge
npx playwright test tests/todo-filters.spec.ts
Correct pattern: repeat before merging
Run the new spec many times on its own before it merges:
# Run only the new spec, 25 times, before it merges
npx playwright test tests/todo-filters.spec.ts --repeat-each 25
One failure in the repeats means the spec isn't ready. Fix the wait or the locator that caused it (the usual causes are in how to fix a flaky test), and repeat again.
Some flakiness only shows up later, across many CI runs. Currents marks a test that has retries enabled and does not pass on its first attempt as flaky, and Test Explorer shows the tests with the highest flakiness rate.
Limits: token cost and snapshots of UI the user can't reach
Most tools return a fresh snapshot automatically after each action, so the LLM always has up-to-date page state. Playwright's own comparison rates MCP's token cost as higher than the CLI's, because tool schemas and snapshots stay in context (Playwright MCP introduction). We'd expect that cost to grow with the size of the pages the agent works on and the number of steps in a task.
Off-screen elements look like everything else
The second limit is breadth. Refs go on every node the accessibility tree exposes, not only on interactive elements, and the snapshot includes elements outside the viewport. Issue #39955 asks for a viewport-only mode. A Playwright maintainer replied that including off-screen elements is by design, so the agent can understand the whole page without scrolling, and that Playwright scrolls elements into view automatically. A footer link below the fold is a fair target for a test.
The trouble is UI a user can't reach at all that still sits in the accessibility tree. Commenters on the issue report a closed drawer moved off-screen with transform: translateX(100%), and elements inside collapsed accordions and hidden tabs. The maintainer's answer is that a click on a hidden element fails when the test runs, and for clicks that holds. An assertion is different. toHaveText() doesn't check visibility, so a step that only reads text from a closed drawer can pass. The issue is open and labelled P3-collecting-feedback, and no change has been committed.
Don't assume every element in a snapshot is something a user can reach. browser_snapshot { boxes: true } adds a [box=x,y,width,height] bounding box to every node, viewport-relative in CSS pixels. A box below the viewport is usually content the user scrolls to. A box to the left or right of the viewport, on a page that doesn't scroll sideways, is usually something hidden, such as a closed drawer. Have the agent take snapshots with boxes when it drafts tests. Check every step aimed at an element beside the viewport, or inside a collapsed section, before the spec merges.
MCP or the Playwright CLI for coding agents
For a coding agent working in your repo, MCP is often the expensive way to get a browser. Playwright now ships both entry points:
# Coding agent working in the repo: concise CLI output, skills loaded on demand
npx playwright cli
# Specialized agentic loops, exploratory automation
npx playwright mcp
The difference is what stays in context. The CLI's concise output avoids loading large tool schemas into the model context (Playwright CLI), and its skills load on demand. MCP keeps tool schemas and snapshots in context, the cost the previous section described.
Use the CLI when a coding agent works inside your repo: writing a spec, checking a fix, reading output it can act on. Choose MCP for what Playwright's comparison rates it best for, specialized agentic loops and exploratory automation, such as an exploration session that works through the app across many steps. To weigh them on your own suite, this side-by-side of MCP, the CLI, test agents and prompts shows how to score the tests each one writes.
Don't default to MCP for every coding agent because it was set up first.
What to change in your workflow
MCP is good at the part of test writing that used to mean clicking through the app with a notebook open. Keep it there, before CI, and let your pipeline run what comes out of it.
Treat everything an agent writes as a draft. Nothing reaches the main branch until someone has reviewed its locators, confirmed a user can reach every element it touches, and seen it pass a --repeat-each run, however confident the agent's summary sounds.
The setup follows from that rule. Enable only the capabilities a task needs. browser_run_code_unsafe is always on, so let only clients you trust reach the server. Give each agent its own isolated session seeded with saved state, not a shared profile.
And MCP is not always the right browser for an agent. When a coding agent only needs to check its work inside the repo, or when token cost starts to dominate an agent task, revisit the choice between MCP and the CLI.




