How the Playwright MCP server gives your agent a real browser
The Playwright MCP server is installed by adding one mcpServers entry — command npx, args @playwright/mcp@latest — to your AI client’s config, then restarting the client completely. From there your agent drives Chrome, Firefox, WebKit or Edge through accessibility snapshots: it reads a ref such as e5, calls a tool on that ref, and repeats. The setup below pins a version instead of trusting a moving tag, picks a profile mode, and keeps logins alive between runs.

📌 TL;DR Executive Summary
- Core Takeaway: One config entry — command
npx, args@playwright/mcp@latest— and the agent gets 70+ tools over Chrome, Firefox, WebKit and Edge. It works from a structured accessibility snapshot, where each element carries a ref likee5that the model passes back as the target of the next call. No vision model is involved. - Key Risk/Challenge: Each snapshot costs roughly 200–400 tokens, a complex flow can run 30+ snapshot-reasoning-toolcall turns, and
browser_run_code_unsaferuns arbitrary JavaScript inside the server process. Page-registered WebMCP tools arrive from the page itself and must be treated as untrusted. - Recommended Solution: Pin a release you have tested instead of
@latest, keep the default persistent profile so logins survive, enable only the capability groups the task needs, and point the agent at an isolated profile with its own proxy exit when the accounts matter.
Prerequisites: three things to settle before you touch any JSON
The server is a Node process, so the first failure most people hit is a version mismatch. Playwright’s documentation asks for Node.js 20 or newer, while Microsoft’s README and Learn articles still say 18 or higher. Install 20 or newer and you satisfy both sets of instructions. Beyond that you need an MCP-capable client and the browser binaries for the channel you plan to drive.
- Node.js 20 or newer plus npm, which brings
npxwith it. - An MCP client that reads an
mcpServersblock — VS Code, Cursor, Windsurf, Claude Code or Claude Desktop. - Browser binaries for your channel: Chrome, Edge, Firefox or WebKit.
- Optional: the Playwright CLI at
npm install -g @playwright/cli, so you can measure its token cost against MCP later. - Optional: an existing managed browser profile with a local automation endpoint, if the agent has to work inside accounts you already run.
node --version # expect v20.x or newer
npx --version
npx playwright install chrome firefox # channel binaries the server can launch
Run those three commands first. They separate a Node problem from a config problem, and config problems are the ones that eat an afternoon because the process exits before it logs anything useful.
Step 1 — Add the server to your client’s mcpServers block
The standard install is a stdio server: the client launches npx and talks to it over standard input and output. These are the documented command and args from the official getting-started guide.
{
"mcpServers": {
"playwright": {
"command": "npx",
"args": ["@playwright/mcp@latest"]
}
}
}
Where that file lives depends on the client. Cursor reads ~/.cursor/mcp.json globally and .cursor/mcp.json for a single project, and it supports stdio, SSE and Streamable HTTP transports. Cursor also understands ${env:NAME} interpolation, but its envFile option is stdio-only — if you switch to HTTP, those variables have to reach the process another way.
Save, then restart the client fully rather than reopening the MCP panel. A half-restarted client is the most common reason a correct config appears to do nothing at all.
Step 2 — Pin the version and choose stdio or HTTP
@latest resolves a new tag every time the client starts, so an unreviewed release can land in your environment mid-project and run with the privileges of your editor. A configuration audit of 110 MCP servers, drawn from 244 collected config files, found 91.8% of them unpinned; treat that as one auditor’s sample rather than an industry figure, but the risk it points at is real. Check the release list, run a version locally, then pin it.
# List published versions
npm view @playwright/mcp versions --json
# Pinned stdio launch — replace 0.0.83 with a version you have actually run
npx @playwright/[email protected] --headless
# Standalone HTTP server on a fixed port
npx @playwright/mcp@latest --port 8931
With HTTP transport, clients point at http://localhost:8931/mcp instead of spawning a process. That suits containers, dev containers and setups where several clients share one browser. The catch is session hygiene: HTTP sessions use a five-second heartbeat timeout, adjustable through the PLAYWRIGHT_MCP_PING_TIMEOUT_MS environment variable, and setting it to 0 disables the heartbeat. If your agent pauses to reason past five seconds and the session dies, that variable is the fix — not a reinstall.
Worth knowing while you pin: the MCP specification has stable revisions dated 2025-06-18 and 2025-11-25, with 2024-11-05 now legacy. Older servers still work, but structured tool output and elicitation behave differently across revisions.
browser_run_code_unsafe executes arbitrary JavaScript inside the Playwright server process, and the documentation describes it as remote-code-execution equivalent. Enable it only for trusted MCP clients, never in the same session where the agent is reading pages you do not control. Page-registered WebMCP tools — surfaced as webmcp_<tool> — carry the same category of risk, because the page supplies the tool name, schema and results. Treat all of it as untrusted input, and opt out with --no-webmcp if you do not want it.
Step 3 — Headed or headless, which channel, which profile mode
The server runs the browser in headed mode by default; add --headless to hide the window. While you are building, headed is the better choice — you catch a wrong selector in seconds instead of reading a snapshot diff.
| Flag | What it changes | When to use it |
|---|---|---|
--headless |
Hides the browser window (headed is the default) | CI runs, servers, unattended agents |
--browser=chrome|firefox|webkit|msedge |
Picks the engine to launch | Match the engine your customers actually run |
--isolated |
In-memory profile, nothing persisted | One-off reads where no login is needed |
--extension |
Connects through a companion browser extension | Driving a browser window you already have open and signed in |
--profile-dir-name |
Selects which Chrome profile extension mode attaches to | Several Chrome profiles on one machine |
--idle-timeout |
Closes headless browsers after inactivity; defaults to one hour, 0 disables it |
Long-running agents that pause between tasks |
--caps=network, --caps=storage, --caps=testing, --caps=devtools |
Unlocks extra tool groups, one flag per group | Only when the task genuinely needs that group |
Profile behaviour confuses people most. The default mode is persistent, and profiles land in ms-playwright/mcp-{channel}-{workspace-hash}. Cookies, local storage and sessions survive between runs — what you want for a signed-in agent account, and also why two projects on one machine end up with separate profiles. Choose --isolated for a clean room and lose the logins with it.
If you plan to run several signed-in identities side by side, read the walkthrough on multiple browser profiles before duplicating configs — profile directories, cookie stores and lock files all interact when two processes point at the same path.
Step 4 — Capability flags and the token bill you are actually paying
The server covers 70+ tools across navigation, forms, network mocking, storage, tracing and video on Chrome, Firefox, WebKit and Edge. Only the core set loads by default; the rest sit behind capability flags such as --caps=network, --caps=storage, --caps=testing and --caps=devtools.
That gating exists because of tokens. A single accessibility-tree snapshot consumes roughly 200–400 tokens, and a complex multi-step flow can reach 30+ snapshot-reasoning-toolcall turns. Full agent test runs have been measured around 114K tokens, against roughly 27K for a CLI-skill workflow doing comparable work. Every group you enable adds tool definitions the model reads before it reasons about your actual task, so more tools is rarely better.
| Playwright MCP | Playwright CLI | |
|---|---|---|
| Typical token cost | Higher — snapshots enter context on every turn | Lower — code is written and executed, not narrated |
| Default browser mode | Headed | Headless |
| Install | JSON entry in the client config | npm install -g @playwright/cli |
| Best fit | Persistent, exploratory loops where the agent decides the next step | High-throughput coding agents that already know the goal |
Microsoft’s own README recommends the CLI plus SKILLs over MCP for high-throughput coding agents, and reserves MCP for the cases where the agent really does need to look around. That split is fair. “Generate a test for this page” is cheaper on the CLI. “Log in, find the invoice, check whether the total matches the order” is the shape MCP was built for.
Two newer features change what you can do without a screenshot loop. Recent releases added 60 fps video capture through browser_start_video with an fps option and cursor: true, plus browser_emulate_media for colour scheme, reduced motion, contrast and print emulation. If your pages render differently in dark mode or in print stylesheets, that tool is a cleaner check than a visual diff.
Step 5 — Keep logins between agent sessions
When the agent has to reach an authenticated page repeatedly, you have three levers. The persistent profile is the default and keeps cookies on disk. Storage state lets you export cookies and local storage from one context and load them into another. The third lever — attaching Playwright to a profile that already exists outside the MCP server — is the one most tutorials skip.
The persistent profile handles the common case with no code at all, as long as the workspace hash stays the same and you avoid --isolated. Storage state is for moving a session between contexts, or for seeding a clean context with a login you established elsewhere.
"""Sign in once, export the session, then reuse it without signing in again."""
from playwright.sync_api import sync_playwright
STATE = "mcp-session-state.json"
with sync_playwright() as p:
browser = p.chromium.launch(headless=False) # headed, same as the MCP default
# 1. Sign in once and export cookies + local storage
context = browser.new_context()
page = context.new_page()
page.goto("https://example.com/login")
page.get_by_role("textbox", name="Email").fill("[email protected]")
page.get_by_role("button", name="Continue").click()
page.wait_for_load_state("networkidle")
context.storage_state(path=STATE)
context.close()
# 2. Reuse the saved state in a later run
reused = browser.new_context(storage_state=STATE)
check = reused.new_page()
check.goto("https://example.com/account")
print("signed in:", check.get_by_role("heading", name="Account").is_visible())
reused.close()
browser.close()
The third lever matters when your profiles carry their own fingerprint, timezone and residential proxy exit. Send.win exposes a local automation endpoint for Selenium, Puppeteer and Playwright on the Team plan, and you point your Playwright client at it instead of launching a stock Chromium. Copy the URL from the profile’s automation settings; if that endpoint is a CDP endpoint, connect_over_cdp is the API you use. If you would rather keep nothing installed locally, a cloud browser server runs the same profiles on remote nodes and keeps sessions in sync across devices.
Common errors and what actually fixes them
| Symptom | Cause | Fix |
|---|---|---|
| Server never appears in the client | Stale client cache, wrong config path, or invalid JSON | Fully restart the client, validate the JSON, confirm the file is where that client looks |
| Command fails immediately on launch | Node older than 20 | Install Node 20 or newer, then reopen the terminal so PATH refreshes |
| Browser launch error, executable missing | Channel binary not installed | npx playwright install chrome (or firefox, webkit, msedge) |
EADDRINUSE on port 8931 |
An HTTP server instance is already running | Stop the old process or pick another port and update the client URL |
| HTTP session drops mid-task | Five-second heartbeat timeout | Raise PLAYWRIGHT_MCP_PING_TIMEOUT_MS, or set it to 0 |
| Browser disappears after about an hour | --idle-timeout defaults to one hour for headless browsers |
Set --idle-timeout 0 or a larger value |
browser_wait_for stops at 30 seconds |
Capping behaviour fixed in v0.0.83 | Upgrade past that release |
| Dialogs break navigation | Dialog handling bug fixed in v0.0.83 | Upgrade, and keep an explicit dialog handler in scripts you own |
| Unexpected tools in the tool list | The page registered WebMCP tools | Launch with --no-webmcp and treat page-supplied tools as untrusted |
Two rows deserve emphasis. A Node mismatch is not subtle — the process dies before it can log anything useful — so check the version first, not last. And community servers behave differently from the reference one: the executeautomation server installs missing browsers on first use and can run HTTP/SSE bound to 127.0.0.1 with a /health endpoint, Bearer token and custom header auth. Weigh that convenience against supply-chain discipline, since community packages often take credentials through environment variables.
If sessions are being challenged rather than failing outright, the MCP plumbing is usually fine and the exit IP is the problem. Sticky residential exits hold one address long enough to finish a multi-step flow; rotating exits break the session halfway through a form and force a fresh challenge. The comparison of residential proxy services covers what differs between providers, including how per-GB pricing lands once an agent starts browsing instead of requesting.
Putting it together: the config to paste and the pre-flight check
Here is the config and the verification script together. The config pins a release, enables only two capability groups, disables page-registered tools, and keeps the persistent profile so logins survive.
{
"mcpServers": {
"playwright": {
"command": "npx",
"args": [
"@playwright/[email protected]",
"--browser=chrome",
"--caps=network",
"--caps=storage",
"--no-webmcp",
"--idle-timeout=0"
]
}
}
}
Replace 0.0.83 with the version you tested. If you run this over HTTP instead of stdio, set PLAYWRIGHT_MCP_PING_TIMEOUT_MS in the environment of that process — that is the transport the heartbeat applies to.
"""Pre-flight check: attach to a managed profile and confirm what the agent will see."""
from playwright.sync_api import sync_playwright
CDP_URL = "http://127.0.0.1:PORT" # copy it from the profile's automation settings
TARGET = "https://example.com/login"
def probe(page, label):
print(f"[{label}] UA: {page.evaluate('navigator.userAgent')}")
print(f"[{label}] TZ: {page.evaluate('Intl.DateTimeFormat().resolvedOptions().timeZone')}")
print(f"[{label}] LANG: {page.evaluate('navigator.language')}")
print(f"[{label}] CORES: {page.evaluate('navigator.hardwareConcurrency')}")
with sync_playwright() as p:
browser = p.chromium.connect_over_cdp(CDP_URL)
context = browser.contexts[0] # the profile's own context
page = context.pages[0] if context.pages else context.new_page()
page.set_default_timeout(30_000)
page.goto(TARGET, wait_until="domcontentloaded")
probe(page, "target")
email = page.get_by_role("textbox", name="Email") # example selector — use yours
if email.count():
email.fill("[email protected]")
page.get_by_role("button", name="Continue").click()
page.wait_for_load_state("networkidle")
print("post-login URL:", page.url)
page.screenshot(path="profile-check.png")
# Do not call browser.close() — that shuts down the profile you attached to.
Read the printed values before you let the agent loose. The timezone, locale, user agent and core count should be consistent with the proxy exit the profile uses. If the timezone reports one country and the exit IP sits in another, anti-fraud systems see the mismatch immediately, and no amount of snapshot reasoning will fix it. The Playwright stealth setup guide walks through what gets checked and in what order.
Then the loop runs the way the server intends: the agent receives a snapshot, every interactive element comes back with a ref such as e5, and the model passes that ref as the target of its next tool call. What you manage is the cost of that loop — snapshot size, turn count and the capability groups you left switched on — not the cleverness of the prompt.
🏆 Send.win Verdict
An MCP agent is only as safe as the browser it drives, and by default it drives a stock Chromium that reads as stock Chromium to every anti-fraud system watching. Send.win gives the agent a profile with its own coherent fingerprint — canvas, WebGL, audio, fonts and hardware spoofed at the engine level — plus a timezone, locale and WebRTC profile that follow the proxy exit IP. On the Team plan the local automation API for Selenium, Puppeteer and Playwright lets you attach the agent to that profile instead of launching your own machine’s browser.
Try Send.win free today — 30 days at $0 with 10 isolated profiles and built-in residential proxies, cancel anytime, and the local automation API when you move to Team.
Frequently Asked Questions
Does the Playwright MCP server need a vision model or screenshots?
No. It returns a structured accessibility snapshot, and each element carries a ref like e5 that the model passes as the target of the next tool call. The loop stays text-only, which is cheaper than pixel reasoning and works with models that have no image input. Screenshot and vision tooling sits behind its own capability flag if you specifically want it.
How do I install the Playwright MCP server in Cursor or VS Code?
Add an mcpServers entry with command: "npx" and args: ["@playwright/mcp@latest"], then restart the client completely. Cursor reads ~/.cursor/mcp.json globally or .cursor/mcp.json per project, and supports stdio, SSE and Streamable HTTP transports. If the entry does not appear, the config path or the JSON syntax is wrong far more often than the server is.
Why does my Playwright MCP server fail to connect?
Check three things in order: Node 20 or newer, the config file location your client actually reads, and whether a stale client cache still holds the old settings. After that, look for a port conflict if you run HTTP transport, and confirm the browser channel binary is installed. A full restart of the editor, not just the MCP panel, clears most of these.
How do I keep login state across Playwright MCP sessions?
Do nothing — persistent is the default profile mode, and profiles are stored under ms-playwright/mcp-{channel}-{workspace-hash}, so cookies survive between runs in the same workspace. Avoid --isolated if you need those logins, and use storage state when you want to move a session between contexts. For accounts you already manage, attach over CDP to that profile’s local automation endpoint instead.
Should I use Playwright MCP or the Playwright CLI?
Pick by workflow, not preference. MCP costs more tokens because snapshots enter context on every turn — one measurement puts full agent runs near 114K tokens against roughly 27K for a comparable CLI-skill workflow — but it handles exploratory loops where the agent decides the next step. Microsoft’s README recommends the CLI plus SKILLs for high-throughput coding agents. The CLI installs with npm install -g @playwright/cli.
Is browser_run_code_unsafe safe to enable?
Only narrowly. It runs arbitrary JavaScript inside the Playwright server process, and the documentation describes it as remote-code-execution equivalent. Enable it for trusted MCP clients on a machine you are willing to rebuild, and never in a session where the agent is reading a page you do not control. Page-registered WebMCP tools carry a similar risk because the page supplies the tool names, schemas and results.
How much context do accessibility snapshots consume?
Roughly 200–400 tokens per snapshot, which adds up quickly across a multi-step task. Complex flows routinely reach 30+ snapshot-reasoning-toolcall turns. Trimming capability flags, working on shorter pages and avoiding redundant navigations are the levers that keep a run inside your context window.
Why does my headless browser close on its own after about an hour?
That is the default --idle-timeout of one hour, which closes headless browsers after a stretch without tool calls. Pass --idle-timeout 0 to disable it, or set a larger value if you want the safety net without the interruption. Headed sessions are not subject to that same default.
Automate Playwright Mcp Server With Send.win
Send.win pairs isolated, fingerprint-managed browser profiles with a full Automation API, so your scripts run in profiles that look and behave like real, separate users:
- Selenium, Puppeteer & Playwright support – drive any profile programmatically (Team plan)
- Isolated profiles – each with its own fingerprint, cookies, and storage
- Built-in residential proxies – with automatic timezone, locale, and WebRTC matching
- Desktop app for Windows, macOS & Linux – plus cloud sessions when you don’t want a local install
Try the instant cloud browser demo — no install, straight from your browser. Then compare plans: a 30-day free trial with no credit card, and paid plans from $6.99/month billed annually.