跳到主要内容

Playwright MCP: The Deep Practical Guide (2026)

进阶

Microsoft's playwright-mcp sits at ~35k GitHub stars and — per the community-maintained MCP registries — is currently the most-installed MCP server on the planet, ranked ahead of even the official GitHub MCP and Figma MCP servers. If you use Claude Code, Cursor, Codex, Windsurf, or Claude Desktop and you have ever asked your agent to "check the site" or "grab data from that dashboard," this is the tool doing the actual work.

Almost every guide to it stops at the two-line install. This page is the part that matters after: what the tool is actually doing under the hood, the modes people don't realize exist, and the sharp edges — token cost, security, browser-profile locking — that show up around week two.

What you'll learn
  • Understand why accessibility-snapshot mode (default) is not just faster than vision — it is a different automation paradigm the LLM treats deterministically
  • Know the three profile modes — persistent, isolated, browser-extension — and when each is the right choice
  • Turn on the opt-in capability packs (network, storage, devtools, vision, pdf, testing) with --caps and understand why they are off by default
  • See the real token cost of Playwright MCP in a Claude Code session and know when Playwright-as-a-Skill wins instead
  • Deploy Playwright MCP as a standalone HTTP/SSE server, in Docker, and safely for autonomous runs (secrets masking is a convenience, not a boundary)

Why this server ate the ecosystem

Playwright MCP is the reference implementation of "give an LLM a browser," and it made two design bets that turned out to be correct:

  1. Structured accessibility snapshots as the primary interface — not screenshots. The model gets a compact, deterministic tree of elements (roles, names, refs). No vision model required, no coordinate hallucination, tokens spent on structure instead of pixels.
  2. Playwright's real automation engine underneath — same waits, same auto-actionability checks, same locator system that has hardened over years of production QA. Nothing bespoke.

Result: on a fresh install you get roughly 50+ tools across navigation, form filling, tabs, snapshots, screenshots, console access, network inspection, and a few opt-in categories. That is a lot of surface — which brings us straight to the first thing that surprises people.

The mode you are actually running

By default the MCP server runs in accessibility-snapshot mode. When your agent calls browser_snapshot, it does not get a screenshot — it gets a YAML-like tree:

- Page URL: https://example.com/login
- role: main
- role: form
- role: textbox, name: "Email", ref: e12
- role: textbox, name: "Password", ref: e13
- role: button, name: "Sign in", ref: e14

The agent then calls browser_click({ ref: "e14" }) — no CSS selectors it invented, no coordinates it guessed. ref is a handle the server minted from the underlying Playwright locator, so the click is as reliable as a hand-written page.getByRole('button', { name: 'Sign in' }).click().

This is why people who come from Selenium/Puppeteer scripts written by an LLM think "browser use by AI is broken" — they were feeding the model raw HTML or screenshots. Snapshot mode does not have that failure mode, because the model never sees a selector it could get wrong.

Pro tip
  • If your agent is guessing CSS selectors, you are almost certainly using a different browser MCP — or you disabled snapshots.
  • Snapshots are page-scoped. For iframes, use browser_snapshot inside the iframe explicitly; the tool exposes iframe navigation.
  • Refs are ephemeral. They are only valid until the next page mutation — treat them like React fiber IDs.

Opt-in capability packs (--caps)

Only the boring, safe tools ship enabled. Powerful ones live behind the --caps flag and you turn them on per-server:

{
"mcpServers": {
"playwright": {
"command": "npx",
"args": ["@playwright/mcp@latest", "--caps=network,storage,pdf"]
}
}
}
CapWhat it unlocksWhy it is off by default
networkMock requests, set offline, route interceptionCan silently rewrite traffic; needs intent
storageRead/write cookies, localStorage, sessionStorageSame-origin data theft is trivial once on
devtoolsTracing, video recording, element highlightingVery large artifacts, disk cost
visionPixel-coordinate mouse actionsBypasses the deterministic model — see below
pdfSave current page as PDFFine, just noise for most sessions
testingElement/text/value verification, locator generationTest-authoring niche, adds ~a dozen tools
configRead resolved server config backDebug-only

The non-obvious one is vision. Turning it on gives the model browser_mouse_move_at_coordinates and friends. It also silently changes the failure profile of the whole session, because the agent will fall back to coordinate clicking when snapshots are inconvenient — and now you have a browser being driven by a language model doing pixel math. Enable it only when a canvas element or a broken-a11y widget forces your hand.

The three profile modes

This is where the interesting design lives.

Guided walkthrough1 of 3
  1. The server launches Chromium against a per-workspace user-data directory (path derived from a hash of your working folder). Cookies, localStorage, saved passwords, and history persist across sessions. Great for authenticated dashboards. Sharp edge: only one instance can hold the profile at a time — a second Claude Code window against the same folder will error out. Point --user-data-dir at a shared location and you can share state across projects.

Install: the four configs you actually want

Standard local (Claude Desktop / Claude Code / Cursor)

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

Isolated + preloaded auth (CI-ish, reproducible)

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

Attach to my real Chrome tab (browser extension)

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

Standalone HTTP server (share one browser across many agents)

# One-time on your workstation:
npx @playwright/mcp@latest --port 8931

# Then every client points at it:
{
"mcpServers": {
  "playwright": { "url": "http://localhost:8931/mcp" }
}
}

The HTTP mode is the one people miss. If you run four coding agents on the same machine, four separate npx installations of Playwright MCP each launch their own Chromium. One shared HTTP server keeps a single browser pool, which is both cheaper and easier to observe.

Docker: the only supported "headless server" recipe

# One-shot (stdio):
docker run -i --rm --init --pull=always mcr.microsoft.com/playwright/mcp

# Long-lived HTTP server on port 8931:
docker run -d -i --rm --init --pull=always \
--entrypoint node \
-p 8931:8931 \
mcr.microsoft.com/playwright/mcp \
/app/cli.js --headless --browser chromium --no-sandbox --port 8931 --host 0.0.0.0

Two things the docs bury: the Docker image is headless Chromium only (no Firefox, no WebKit, no headed mode), and --no-sandbox is required inside the container. If you need Firefox or a real GPU, run the server on the host.

The token-cost fight: MCP vs Skill vs raw CLI

The other thing nobody warns you about: Playwright MCP is the heaviest single server you can attach to Claude Code, at roughly 3,500 tokens of tool-schema overhead per session — before you make a single call. That number lives in your context window for the whole conversation.

Reported measurements from the community (link below) put a typical "test this site" task at:

ApproachTokens for the taskSonnet cost (approx)
Playwright MCP (default caps)~114k~$0.34
Playwright CLI + a Skill file~27k~$0.08

The Skill approach ships a small SKILL.md that documents Playwright's CLI and lets the agent shell out with bash. The tool schema stays out of the model's context until it is needed, and the skill file itself is read once. Recent Playwright MCP versions have narrowed this gap by no longer streaming full page state on every call, but the schema overhead is still the schema overhead.

Rule of thumb: MCP for interactive/exploratory work where the model needs conversational back-and-forth with the DOM. Skill/CLI for repeatable jobs — screenshotting a list of URLs, running a test suite, or anything you would put in a cron.

See the Claude Code page on MCP token cost for how to measure this in your own session.

Secrets masking is a convenience, not a boundary

Playwright MCP supports a secrets map in its config:

{
"secrets": {
"OPENAI_API_KEY": "sk-real-key-here",
"GITHUB_TOKEN": "ghp_real"
}
}

When the server sees those exact strings inside a tool response, it substitutes the key name back in before forwarding the result to the model. This is genuinely useful — page content that echoes your API key will no longer leak it into the LLM transcript.

But the project README is explicit and repeats it several times: "Playwright MCP is not a security boundary." Concretely:

  • A page can render your secret in a title attribute or as base64 and the masker will not catch it.
  • Anything the model asks the browser to do — including document.cookie reads via a form field trick — still executes with your real profile's authority.
  • The extension mode attaches to your everyday Chrome. Every logged-in tab becomes reachable in principle.

Treat an agent with Playwright MCP + persistent profile the same way you would treat a fresh employee with your admin cookies: fine for narrow, supervised tasks, catastrophic on --dangerously-skip-permissions. See Agentic browsers & same-origin trust and What your agent uploads for the wider threat model.

What changed recently

Recent releases (v0.0.79 line) are worth knowing about because they change defaults:

  • --timeout-settle — the server now waits a configurable number of ms (default 500) after each action for triggered work to settle before returning. Raise it for slow SPAs, lower it for perf tests.
  • WebP screenshotsbrowser_take_screenshot accepts type: "png" | "jpeg" | "webp" and infers from filename. WebP is roughly 30–50% smaller than PNG for the same quality, worth changing if you screenshot a lot.
  • Codegen output for Python / Java / C# — the testing cap can now emit test skeletons in more than TypeScript.
  • Download event detection — replaces the previous error-based inference; downloads now trigger a real event so agents can wait on them.
  • Browser-extension CDP relay — hardened with header validation on the WebSocket upgrade.

Debugging playbook

Guided walkthrough1 of 5
  1. The snapshot is stale. Have the agent call browser_snapshot again after every navigation or form submit — refs from the previous snapshot are dead. If it still cannot find the element, the button may be inside an iframe or Shadow DOM; expand the snapshot scope.
还没有卡片 — 添加一些开始学习吧。🃏

Quick check

Check yourself

0/5
  1. By default, when your agent calls browser_click, what identifies the target element?
  2. You want two Claude Code windows against the same repo, both using Playwright MCP with your logged-in profile. What is the correct move?
  3. Which --caps value most changes the safety profile of your session?
  4. For a repeatable batch job (screenshot 200 URLs nightly), which is usually the right tool?
  5. You configure the secrets map with GITHUB_TOKEN=ghp_xxx. A page includes the token base64-encoded in a hidden field. What happens?

When Playwright MCP is the wrong tool

  • You need shared login with an ongoing human browsing session. Consider a shared-login agent browser like the one covered in Browser agents that inherit your logins — Playwright MCP's extension mode gets close, but the UX around approvals and Spaces is different.
  • You are testing production code paths and want the actual Playwright test runner. Use @playwright/test directly; the MCP is optimized for agentic exploration, not for CI test authoring.
  • The workload is 100% headless scraping of static HTML. A plain fetch + parser is orders of magnitude cheaper. Save the browser for pages that actually need JavaScript execution.
  • You cannot risk any browser state leakage. Use --isolated with a fresh --storage-state snapshot per run. Do not use the extension mode against your daily Chrome.

Sources & further reading