Automating a real storefront with Playwright MCP: a step-by-step guide
TL;DR
Point an LLM at a public storefront through Playwright MCP. Drive six flows. Put a checkable assertion in every prompt. Convert one flow into a Playwright spec and run it without the agent. MCP is excellent at exploration and a poor substitute for a regression suite — the agent is the hands, not the judgement.
The target is demo.evershop.io/kids, a public EverShop demo store. Fourteen products, a price filter, a colour facet, a sort control, add-to-cart, a cart page, and a checkout. Enough surface area to hit every interesting case.
By the end you will have driven six flows through an LLM, measured what they cost, and converted one of them into a Playwright spec that runs without an agent.
Prerequisites
- Node.js 20 or newer. The Playwright docs specify 20+. Older guides say 18; that is stale.
- An MCP client: Claude Code, VS Code, Cursor, Windsurf, Codex, Cline, or similar.
- A model with tool use. Any frontier model works. Cost varies a lot between them.
If you want the conceptual background first — MCP as a reasoning layer, Playwright as the execution layer — that is Playwright + MCP. This post is the opposite: a session you can actually run.
Part 1: Install
The standard config, which works across most clients:
{
"mcpServers": {
"playwright": {
"command": "npx",
"args": ["@playwright/mcp@latest"]
}
}
}
Claude Code:
claude mcp add playwright npx @playwright/mcp@latest
VS Code:
code --add-mcp '{"name":"playwright","command":"npx","args":["@playwright/mcp@latest"]}'
Cursor: Settings → MCP → Add new MCP Server → command type → npx @playwright/mcp@latest
Part 2: Verify the connection
Ask your client to list tools. You should see roughly thirty, including:
| Category | Tools |
|---|---|
| Navigation | browser_navigate, browser_navigate_back, browser_close |
| Interaction | browser_click, browser_type, browser_fill_form, browser_select_option, browser_hover, browser_drag, browser_press_key |
| Inspection | browser_snapshot, browser_take_screenshot, browser_console_messages, browser_network_requests |
| Execution | browser_evaluate, browser_run_code_unsafe |
| Control | browser_wait_for, browser_resize, browser_handle_dialog, browser_install |
If you get an error about the browser executable missing, the agent should call browser_install itself. If it does not, run npx playwright install chromium manually.
The loop you are about to run
Every interaction follows the same cycle:
The snapshot looks like this:

No pixels, no coordinates, no vision model. Roles, accessible names, and refs. This is why Playwright MCP is deterministic in a way screenshot-driven agents are not: ref=f1e135 either resolves or fails loudly. It never clicks the wrong thing because the layout shifted four pixels.
browser_click takes a human-readable element description plus the snapshot ref as target (older server versions named that field ref). The description is not decorative — it is what shows up in logs and what makes a failed call debuggable. Prompts that produce good descriptions produce readable traces.
Part 3: Flow 1 — Read the category page
The simplest possible flow.
Prompt:
Navigate to https://demo.evershop.io/kids and tell me:
- how many products are listed
- the full price range
- what colour options the filter offers
Answer only from the accessibility snapshot. Do not guess.
Expected tool calls: browser_navigate → (snapshot returns automatically)
That is it. One call. The navigate result already includes the page state, so an efficient agent does not need a separate browser_snapshot.
What you should get back: 14 products, $12.00–$35.00, and colours White, Black, Yellow, Green.
Why this step matters: it establishes that the agent reads from the tree rather than from its priors. That last line in the prompt — answer only from the snapshot — is doing real work. Without it, models will happily fill gaps from training data about what an e-commerce page usually contains.

First observation
The Kids category contains stainless steel thermoses, ceramic vases, candy bowls, desk pen holders, and coffee cups. Nothing kid-specific.
Note that and move on. It becomes the point of this whole guide in Part 11.
Part 4: Flow 2 — Filter, then verify the filter
Prompt:
On https://demo.evershop.io/kids, apply the Black colour filter.
Then list every product still visible and confirm each one is a Black variant.
If any non-Black product remains, say so explicitly and do not call this a pass.
Expected tool calls:
browser_navigate→{ "url": "https://demo.evershop.io/kids" }browser_click→{ "element": "Black colour filter", "target": "e17" }browser_wait_for→{ "text": "products" }(or a short time value; the grid re-renders client-side)browser_snapshot→ read the filtered grid
What you should see: the grid narrows to the - Black variants. Because EverShop's demo names products with the colour suffix baked in (Ceramic Candy Bowl - Black), the agent can verify the filter from the accessibility tree alone — no screenshot, no DOM query, no colour analysis.
This is the best case for Playwright MCP and it is worth understanding why. The assertion is verifiable from accessible names. When product names do not encode the filtered attribute — the normal case in a real store — the agent has nothing to check against and will report "the filter was clicked" as though that were the same as "the filter worked." It is not.
If the grid re-renders asynchronously and the agent snapshots too early, it reads the pre-filter tree and confidently reports the wrong result. This is the single most common source of false passes. Force the wait:
After clicking the filter, wait for the product count text to change
before taking a snapshot.
Part 5: Flow 3 — The sort dropdown
Prompt:
On https://demo.evershop.io/kids, change the sort control from Default
to price ascending. Report the first three product names and prices in
the new order.
Expected tool calls: browser_navigate → browser_snapshot → browser_select_option → browser_snapshot
browser_select_option is the right tool for a native <select>. If EverShop renders a custom dropdown component instead, the agent has to browser_click to open it and browser_click again to pick an option. Let the agent discover which — do not specify the mechanism in your prompt. Watch which tool it reaches for; that tells you what the component actually is, which is genuinely useful information about the app.
Assertion: the pen holders ($12.00) should surface first, thermoses ($35.00) last. If prices come back unsorted, you have found either a bug or a race — check by re-running with an explicit wait.
Part 6: Flow 4 — Add to cart and read the mini-cart
Here is where multi-step state starts to bite.
Prompt:
On https://demo.evershop.io/kids, add the Ceramic Coffee Cup - Black
($15.00) to the cart. Then open the cart and tell me the item count
and the cart total.
Take a screenshot of the cart before answering.
Expected tool calls:
browser_navigatebrowser_click→ the Add to Cart button on that specific product cardbrowser_wait_forbrowser_snapshotbrowser_click→ the cart control in the headerbrowser_take_screenshot
Assertion: 1 item, $15.00. If you get a different total, someone else's session or a stale persistent profile is involved — this is exactly what --isolated prevents.
Part 7: Flow 5 — Cart manipulation
Prompt:
Open the cart. Increase the quantity of the coffee cup to 3 and confirm
the line total updates to $45.00. Then remove the item and confirm the
cart reports empty.
Expected tool calls: browser_navigate → browser_snapshot → browser_type or browser_select_option (qty field) → browser_wait_for → browser_snapshot → browser_click (Remove) → browser_snapshot
Why this flow is worth running: it is the first one with a computed assertion. $15.00 × 3 = $45.00 is a check the agent can actually get wrong, unlike "did the page change." Arithmetic assertions are the cheapest way to find out whether your agent is verifying or narrating.
Expected empty state: EverShop's CartItems component renders "Your cart is empty" with a Continue Shopping link. That exact string is your assertion target.
Part 8: Flow 6 — Login and storage state
Logging in on every run is the biggest avoidable cost in agentic browser testing. Do it once, save the state, reuse it. Enable storage tools with --caps=storage.
Step 1 — log in and save state.
Navigate to https://demo.evershop.io/account/login
Fill the login form with <email> and <password>.
Then save the browser storage state to ./.auth/evershop.json
Expected tool calls: browser_navigate → browser_fill_form → browser_click → browser_wait_for → browser_storage_state with a file path.
browser_fill_form batches multiple fields in one call instead of one browser_type per field. On a six-field form that is five fewer round trips and five fewer snapshots in context. Prefer it wherever the form has more than one input — it is the single highest-leverage token optimisation in this whole guide.
Step 2 — reuse it. Restart the server with:
"args": [
"@playwright/mcp@latest",
"--isolated",
"--storage-state=./.auth/evershop.json"
]
Every subsequent session starts logged in. Zero login steps, zero credentials in prompts.
.auth/evershop.json contains live session cookies. Gitignore it, do not paste it into a chat, and rotate it like you would rotate a token.
Part 9: Forcing error states with network mocking
Real bugs live in the unhappy paths, and you cannot reliably reach them by clicking. Enable route mocking with --caps=network.
Prompt:
Mock the cart API endpoint to return a 500, then attempt to add any
product to the cart. Tell me exactly what the user sees. Include the
console output.
Expected tool calls: browser_route → browser_navigate → browser_click → browser_snapshot → browser_console_messages
What you are testing: whether the failure is visible. Plenty of storefronts swallow a failed add-to-cart silently — the button spins, nothing happens, no error appears. That is a bug you will almost never find by happy-path clicking, and it is a genuinely excellent use of an agent.
Pair with browser_network_requests to confirm the mock intercepted what you thought it did. Agents routinely mock a URL pattern that does not match the real request and then report a clean pass on a mock that never fired.
Part 10: Debugging with console and network
Not a flow, a habit. Any time something looks wrong:
browser_console_messages → JS errors, warnings, framework noise
browser_network_requests → every request since page load
Prompt pattern that works well:
Reproduce the issue, then show me any console errors and any network
requests that returned 4xx or 5xx. Correlate them with what's on screen.
This is Playwright MCP at its genuine best — the agent holds the UI symptom, the console error, and the failed request in one context and connects them. Doing that by hand means three DevTools panels and a lot of tab switching.
Part 11: The part that determines whether any of this is useful
Go back to Flow 1. The Kids category is full of thermoses and vases. And check the footer: New arrivals, Coffee, Tea, Equipment, Brew guides, Our sourcing, The journal, Shipping & returns, Track an order, Contact us — every one of those links resolves to the homepage.
Now run this:
Verify that the site navigation works correctly.
The agent will click Track an order, land on a page that renders perfectly, find no errors, and report success.
It is not wrong. It did what you asked. "Works correctly" was never defined, so it defaulted to "something happened and nothing exploded."
This is the central limitation, and it is not a Playwright bug. The agent has no oracle. It can verify that a transition occurred; it cannot verify the transition was correct, because correctness lives in your product requirements, which are not in the accessibility tree.
Every prompt in this guide that produces a trustworthy result contains an explicit, checkable assertion:
- "confirm each one is a Black variant"
- "confirm the line total updates to $45.00"
- "confirm the cart reports empty"
Every prompt that does not will produce a confident, useless pass. The assertion is your job. The agent is the hands, not the judgement.
This is the same structural problem as asking a coding agent to test its own PR: without an independent definition of correct, the loop grades itself on whether it finished, not whether it was right.
Part 12: Turning exploration into a test
Everything so far is exploration. It is not a test, because it is not repeatable — refs change per session, the agent's click sequence varies run to run, and there is no pass/fail signal a CI system can consume.
The conversion step is the payoff:
Write that as a Playwright test using role-based locators.
Every tool result includes a "Ran Playwright code" block showing the actual Playwright call the server executed. The agent has been generating valid Playwright the whole time — this step just collects it into a file.

This is what it wrote to tests/kids-black-cheapest-cart.spec.ts:
import { test, expect } from '@playwright/test';
test('add cheapest Black item to cart; cart subtotal matches listed price', async ({ page }) => {
await page.goto('https://demo.evershop.io/kids');
// Apply the Black colour filter
await page.getByRole('checkbox', { name: 'Black' }).click();
await expect(page.getByText('5 products').first()).toBeVisible();
// Product card links embed the price in their accessible text; nav links do not,
// so filtering by price pattern isolates only product cards.
const productLinks = page
.getByRole('main')
.getByRole('link')
.filter({ hasText: /\$\d+\.\d{2}/ });
const count = await productLinks.count();
let minPrice = Infinity;
let cheapestIdx = 0;
let listedPriceText = '';
for (let i = 0; i < count; i++) {
const text = (await productLinks.nth(i).textContent()) ?? '';
const match = text.match(/\$(\d+\.\d{2})/);
if (match) {
const price = parseFloat(match[1]);
if (price < minPrice) {
minPrice = price;
cheapestIdx = i;
listedPriceText = match[0];
}
}
}
expect(listedPriceText).toBeTruthy();
// Navigate to the cheapest product's detail page
await productLinks.nth(cheapestIdx).click();
// The PDP requires an explicit variant selection before add-to-cart is accepted
await page.getByRole('button', { name: 'Black' }).click();
await page.getByRole('button', { name: 'Add to cart' }).click();
// Cart drawer must open
const cart = page.getByRole('dialog', { name: 'Your Cart' });
await expect(cart).toBeVisible();
// The subtotal label and its value share a parent element; assert both appear together
await expect(cart.getByText('Subtotal:').locator('..')).toContainText(listedPriceText);
});
Three decisions in this spec that are worth keeping:
- Price-pattern filter on links.
.filter({ hasText: /\$\d+\.\d{2}/ })isolates product cards from nav and footer links. No CSS class selectors. - Cheapest item is computed at runtime. The loop parses each card's accessible text and tracks the minimum, so a catalogue change does not hardcode a product name.
- Variant is selected explicitly. EverShop's PDP rejects Add to cart until a colour is chosen. The spec clicks Black because that is what a user has to do — the agent learned it from the session, not from a guess.
Then verify it independently:
npx playwright test tests/kids-black-cheapest-cart.spec.ts
Run it three times. If it passes three times, you have a test. If it is flaky, you have a race the agent papered over with retries — and you have just learned something real about your app.
That spec is now a CI citizen. How you run it at scale is a different problem — Playwright on GitHub Actions if you want to own the grid.
Part 13: What it costs
No licence fee. Playwright MCP is open source. The cost is tokens, and the shape surprises people.
The mechanism
Each step appends a fresh snapshot to your context. Step 15 carries fourteen previous snapshots of a page that mostly did not change. Cost grows worse than linearly in step count, and answer quality degrades before you hit any hard context limit.
This is one of the bills in what it actually costs to use your coding agent as your testing agent — the visible one. The compounding bills are engineer time spent re-prompting, and the retry factor below.
Measuring it properly
Do not trust anyone's published figure, including any I could give you:
- Run one flow end to end, real model, real prompt.
- Pull input/output token counts for that session from your provider's usage dashboard.
- Divide by step count → cost per step.
- Multiply by (flows × runs per day × retry factor).
- Get per-million rates from your provider's current pricing page. I am not quoting rates — they change, and mine would be stale.
On the retry factor: budgets do not die on the first clean run. They die on the fifth retry of a flaky checkout, or a nightly job across forty flows.
Cutting it
| Technique | Why it helps |
|---|---|
browser_fill_form over repeated browser_type | one call, one snapshot, instead of N |
| Storage state instead of logging in | removes 3–5 steps from every session |
| One flow per session | stops context from compounding |
| Explicit assertions in the prompt | fewer exploratory dead-end calls |
| Convert to a spec after one run | the repeatable version costs zero tokens |
That last row is the real answer. The cheapest agentic test is the one you only ran through an agent once.
The CLI alternative
Microsoft now also ships Playwright CLI, explicitly positioned for coding agents and explicitly pitched on token efficiency. The mechanism: it writes each snapshot to a file and returns a path, so the tree enters context only when the agent actually needs it, and it avoids loading large tool schemas up front. It also runs a persistent daemon, so there is no browser startup cost per command.
I have seen third-party benchmarks claiming a several-fold token reduction versus MCP. I have not verified those and would treat the specific multiplier as unconfirmed — but the direction is well supported by the architecture. If token spend is your binding constraint, spend an evening on it.
Part 14: Honest scorecard
Good at:
- Exploring an unfamiliar app — twenty minutes of clicking becomes one prompt
- Reproducing and diagnosing a specific bug, with console and network correlation in one context
- Forcing error states via route mocking
- Generating a first draft of a Playwright spec
- Deterministic element targeting — refs resolve or fail loudly, no coordinate guessing
Bad at:
- Knowing what "correct" means (Part 11)
- Running the same check identically twice — refs and click sequences vary by session
- Long flows, where context bloat degrades both cost and quality
- Anything unattended:
browser_run_code_unsafeexecutes arbitrary JS in the Playwright server process and the docs describe it as RCE-equivalent - Being a test suite — no retry policy, no reporting, no parallelism, no CI signal
Part 15: Where to draw the line
Playwright MCP is an exploration and debugging tool that is frequently mistaken for a regression testing tool. Those are different jobs.
Reach for it when: mapping an unfamiliar flow, reproducing a bug, prototyping what a test should assert, doing a one-off UI data pull.
Don't when: you need identical execution on every PR, a pass/fail signal a build system can act on, or the ability to explain a failure from three weeks ago.
For the CI case you need stable, versioned test definitions and a deterministic runner. That is a different category — Playwright's own test runner if you want to own the code, or one of the PR-native agentic tools (DevAssure O2 and a few others) if you would rather keep test intent in plain English and skip selector maintenance. Whichever way you go, the useful principle holds: the agent that writes the test and the agent that grades it should not be the same loop.
Disclosure: I work on DevAssure O2, which sits in that second category. Weight the CI paragraph accordingly. Parts 1 through 13 are what I would tell you regardless — Playwright MCP is a good tool that gets asked to do a job it was not built for.
Quick reference
# Install
claude mcp add playwright npx @playwright/mcp@latest
# First flow
"Navigate to https://demo.evershop.io/kids, filter to Black, add the
cheapest item to the cart, and confirm the cart total equals that
item's listed price."
# Convert to a spec
"Write that as a Playwright test using role-based locators."
# Verify independently
npx playwright test
Count the snapshots in your first run. That number is your bill.
Frequently asked questions
Playwright MCP is Microsoft’s Model Context Protocol server for Playwright. An LLM client calls tools such as browser_navigate and browser_click; the server drives a real browser and returns an accessibility snapshot with element refs. The model never clicks pixels or guesses coordinates.
Links
External
- Playwright MCP: https://github.com/microsoft/playwright-mcp
- Playwright MCP configuration: https://playwright.dev/mcp/configuration/options
- Playwright CLI for coding agents: https://playwright.dev/agent-cli/introduction
- EverShop demo: https://demo.evershop.io/kids
Related
- Playwright + MCP (conceptual): https://www.devassure.io/blog/playwright-mcp/
- Why coding agents can't test: https://www.devassure.io/blog/why-coding-agents-cant-test/
- The hidden bill of coding-agent testing: https://www.devassure.io/blog/hidden-bill-coding-agent-vs-testing-agent/
- Playwright GitHub Actions grid: https://www.devassure.io/blog/playwright-github-actions-in-house-grid/
- Why developers should stop writing Playwright tests: https://www.devassure.io/blog/why-devs-should-stop-playwright/
- DevAssure O2: https://www.devassure.io/o2-testing-agent
