When you tell Claude Code or Cursor "open this page and read the prices of the first three products", the assistant usually fetches the raw HTML of the page. If the price is loaded with JavaScript, all it gets is an empty skeleton. Playwright MCP closes that gap: it puts a real browser behind the assistant and exposes clicking and typing as tool calls. Setup is a single line. The questions come afterwards: which IP does the browser go out from, where do the proxy username and password go, and can the agent visit any site it likes?
This post covers what Playwright MCP is and how it works, how the accessibility tree differs from a screenshot, setup in four common clients, the flags that matter, the proxy setting (including authenticated proxies), restricting origins with --allowed-origins, and why that restriction does not count as a security boundary. We checked the flags against the official README and the --help output on the day of writing. The examples were run with @playwright/mcp 0.0.82 through a local test proxy.
What is Playwright MCP?
Playwright MCP is, in the words of its official repository, a Model Context Protocol server that provides browser automation using Playwright. It has two parts. Playwright is the library that drives Chromium, Firefox and WebKit from code, and we covered it in detail in What Is Playwright and How to Use It With a Proxy. MCP is the protocol that connects AI applications to external tools in a standard way.
We do not explain the protocol itself again here. This much is enough: the assistant application (the host) starts the MCP server as a local process, the server announces the list of tools it has along with their schemas, and the model calls those tools when it needs them. The host, client and server split, the transports and the protocol-level risks are in What Is MCP (Model Context Protocol)? A Detailed Guide.
The tools Playwright MCP offers are browser actions: browser_navigate, browser_click, browser_type, browser_fill_form, browser_snapshot, browser_take_screenshot, browser_tabs and the like. In version 0.0.82 the default setup announced 25 tools. When we added --caps=vision,pdf, coordinate-based clicking and PDF generation came along and the count rose to 32. The difference from writing a browser script yourself is that the model decides the steps: you state the goal, and the model picks which link to click by looking at the page. The general workings of the agent loop are in How Do AI Agents Work? Planning, Tools and Memory.
How does Playwright MCP work?
A request goes through these steps from start to finish:
- The host application runs the command from its configuration:
npx @playwright/mcp@latest. The server connects over stdio and announces its tool list. - You write a task in natural language. The model calls the
browser_navigatetool with the address. - The server launches the browser on the first tool call. With no
--browsergiven, the installed Google Chrome opened in our test; the window is visible by default and--headlesshides it. - Once the page has loaded, the server produces the page address, the title and a snapshot of the accessibility tree. In 0.0.82 the
browser_navigateresponse saved this snapshot as a YAML file in the.playwright-mcpdirectory of the working folder and returned its path, whilebrowser_snapshotreturned the tree directly inside the response. - Every element in the tree has a reference:
link "Travel" [ref=e21]. The model names the element it wants to click by that reference:browser_click,target: e21. - The server carries out the action with Playwright and also shows the code it ran in the response:
await page.getByRole('link', { name: 'Travel' }).click();. The snapshot of the new page then comes back and the loop continues.
Thanks to that line of code, you can later turn the agent's exploration into an ordinary Playwright script. The --codegen flag picks the language of this output (typescript, python, java, csharp or none).
Why is the accessibility tree more useful than a screenshot?
The accessibility tree is the structure the browser produces for screen readers. According to MDN's definition, it carries four pieces of information for each element: name, description, role and state. Playwright exports this tree as YAML; the details of the format are in the aria snapshot documentation. The snapshot of our test page (books.toscrape.com) started like this:
- generic [active] [ref=e1]:
- banner [ref=e2]:
- generic [ref=e5]:
- link "Books to Scrape" [ref=e6] [cursor=pointer]:
- /url: index.html
- text: We love being scraped!
# ... (truncated)
- list [ref=e19]:
- listitem [ref=e20]:
- link "Travel" [ref=e21] [cursor=pointer]:
- /url: catalogue/category/books/travel_2/index.htmlIn this text the model reads directly what is a link, what is a button and what is a text box. With a screenshot it has to extract the same information from pixels and then guess the coordinates of the point to click.
Accessibility tree (browser_snapshot) | Screenshot (browser_take_screenshot) | |
|---|---|---|
| Data sent to the model | Text (YAML) | Image (PNG or JPEG) |
| Needs a vision-capable model? | No | Yes |
| How is an element targeted? | By its ref value, exactly | By coordinates, if --caps=vision is on |
| Detail that stays invisible | Colour, layout, the content of an image | The function of elements with no accessible name |
| Suited to | Navigation, forms, reading data | Visual verification, design checks |
The tool's own description says the same thing: you cannot perform actions based on the screenshot, and the snapshot is what you use for actions. Even so, do not assume the text is free. In our test the home page of the book list produced a tree of roughly 32 thousand characters, and the schemas of the 25 tools came close to 20 thousand characters. These are character counts; the token equivalent depends on the model and we did not measure it. The README is candid on the same point: for coding agents that work with a large codebase it recommends the Playwright CLI route, which does not load the tool schemas and the tree into the context, and it positions MCP for work that needs persistent browser state and step-by-step reasoning over the page. --snapshot-mode=none turns off the automatic snapshot in responses, and --mobile makes the browser open the lighter mobile pages.
How do you set up Playwright MCP?
All you need is Node.js 18 or newer and a client that supports MCP. You do not install the package by hand; the client runs it with npx every time it starts. The entry the README calls the "standard config" is the same in most clients:
{
"mcpServers": {
"playwright": {
"command": "npx",
"args": ["@playwright/mcp@latest"]
}
}
}Where that entry is written depends on the client:
| Client | Setup |
|---|---|
| Claude Code | claude mcp add playwright npx @playwright/mcp@latest |
| Claude Desktop | The standard entry is added to the claude_desktop_config.json file opened via Settings → Developer → Edit Config |
| Cursor | Cursor Settings → MCP → Add new MCP Server, type command, command npx @playwright/mcp@latest |
| VS Code | The code --add-mcp command or .vscode/mcp.json; in this file the top-level key is servers, not mcpServers |
If you are going to pass flags to the server in Claude Code, put -- in between. According to Claude Code's MCP documentation, everything after the double dash is passed to the server command untouched:
claude mcp add --scope project playwright -- npx @playwright/mcp@latest --headless --isolated--scope project writes the entry to the .mcp.json file in the project root; if you commit the file to the repository, the team uses the same setting. When we tried the command, the file ended up with the flagged version of the standard entry above. You can check the connection with claude mcp list or with /mcp inside a session. The details on the VS Code side are in the VS Code MCP documentation.
The @latest tag pulls the current version on every start. The package changes fast: the flag names in this post belong to 0.0.82. If you want the same behaviour across a team, pin the version (@playwright/mcp@0.0.82) and look at the output of npx @playwright/mcp@latest --help before upgrading.
Which flags are the most useful?
The help output of 0.0.82 lists around fifty options. These are the ones you run into in daily use:
| Flag | What it does |
|---|---|
--headless | Runs the browser without a window. The default is headed |
--browser <name> | chrome, firefox, webkit or msedge |
--isolated | Keeps the profile in memory and does not write it to disk; cookies are gone when the session closes |
--user-data-dir <path> | Directory of the persistent profile |
--storage-state <path> | Loads initial cookies and local storage into an isolated session |
--proxy-server <address> | Proxy server: http://server:3128 or socks5://server:8080 |
--proxy-bypass <domains> | Domains that skip the proxy, separated by commas |
--allowed-origins <list> | Origins the browser may request, separated by semicolons |
--blocked-origins <list> | Origins to block; evaluated before the allowlist |
--caps <list> | Extra capabilities: vision, pdf, devtools |
--config <path> | JSON configuration file |
--timeout-navigation <ms> | Navigation timeout, 60000 by default |
Every flag has an environment variable counterpart (such as PLAYWRIGHT_MCP_PROXY_SERVER and PLAYWRIGHT_MCP_ALLOWED_ORIGINS). We tried the proxy variable and it gave the same result as the flag.
How do you set a proxy in Playwright MCP?
For a proxy that asks for no credentials, meaning your outgoing IP has been added to the IP whitelist in the dashboard, a single flag is enough:
{
"mcpServers": {
"playwright": {
"command": "npx",
"args": [
"@playwright/mcp@latest",
"--headless",
"--isolated",
"--proxy-server=http://pr.proxynet.io:8000",
"--proxy-bypass=localhost,127.0.0.1"
]
}
}
}We ran this configuration with our local test proxy: every page the agent opened landed in the proxy log as a CONNECT line. The domain we added to the --proxy-bypass list never showed up in the log, so it connected directly. If you are testing your local development server, do not forget the localhost entry; otherwise the agent tries to reach the page on your own machine through the proxy.
SOCKS5 works too: --proxy-server=socks5://pr.proxynet.io:1080. In our trial the page opened and the SOCKS5 server received the domain name, so DNS resolution stayed on the proxy side. Username and password are not supported with SOCKS5; the reason is Chromium, and the details are in the Playwright proxy post mentioned above. The product side is on the SOCKS5 Proxy page.
You can confirm that the proxy is really in use by asking the agent: "Open https://httpbin.org/ip and write down the IP you see." If the address that comes back is not your own IP, the traffic is going through the proxy.
How do you define a proxy with a username and password?
The first idea that comes to mind is to embed the credentials in the address: --proxy-server=http://user:pass@pr.proxynet.io:8000. It does not work. When we tried it the server started, but the first navigation came back with this error:
Error: browserBackend.callTool: net::ERR_INVALID_AUTH_CREDENTIALS at https://httpbin.org/ipIn the proxy log we saw that the request arrived without credentials and got a 407. The error was the same when we wrote no credentials at all. So the flag carries only the scheme, the host and the port.
The fix is the configuration file. The browser.launchOptions field in the JSON passed with --config is handed over to Playwright's own launch options, and the proxy object lives there:
{
"browser": {
"isolated": true,
"launchOptions": {
"headless": true,
"proxy": {
"server": "http://pr.proxynet.io:8000",
"username": "user",
"password": "pass"
}
}
},
"network": {
"allowedOrigins": ["https://books.toscrape.com", "https://httpbin.org"]
}
}The entry on the client side only points to the file:
{
"mcpServers": {
"playwright": {
"command": "npx",
"args": ["@playwright/mcp@latest", "--config=playwright-mcp.json"]
}
}
}We tried this pair with our local proxy that requires a username and password: the browser first got a 407, sent the credentials and the page opened. Writing the file path as an absolute path is safer, because the directory in which the client starts the server varies by application.
Since the password will sit in a file as plain text, take two precautions: do not commit the file to the repository and, if possible, create a separate proxy user for this job. If you do not want to write the password anywhere, you go back to the single-flag setup from the previous section with an IP whitelist. The comparison of the two methods is in Proxy Authentication: User:Pass vs IP Whitelist.
How do you restrict the origins the agent can visit?
Giving a model a browser means every page it reads can whisper instructions to it. We explained how indirect prompt injection works and why an allowlist is sturdier than a blocklist in Safe Web Access for LLMs: Rate Limits and Permissions. Playwright MCP offers a ready-made implementation of that idea:
"args": [
"@playwright/mcp@latest",
"--allowed-origins=https://books.toscrape.com;https://httpbin.org"
]An origin consists of scheme, domain and port; the list is separated by semicolons. Its counterpart in the configuration file is the network.allowedOrigins array, which accepts a port wildcard in the form http://localhost:*. --blocked-origins does the opposite and is evaluated first; when it is used without an allowlist, every address not on the list stays open.
In our test, an agent that tried to go to an address outside the list got this response: net::ERR_BLOCKED_BY_CLIENT. An off-list image we placed inside an allowed page did not load either, so the rule covers subresources as well as navigation.
Three observations show what this warning means in practice:
- Redirects get past the list. When a local address on the allowlist redirected with a
302to a site outside the list, the page opened and the agent read its content. - A connection to a blocked address can still be opened. We saw a
CONNECTline in the proxy log for the blocked domain. The page did not load, but the browser did connect to that server. - The browser's own background traffic is not subject to the rule. Chrome's requests to its update and account services went through the proxy regardless of the list. If you pay for traffic by the GB, those requests count against the meter too.
The default tools also include browser_evaluate and browser_run_code_unsafe. The description of the second one is explicit: it executes arbitrary JavaScript in the server process and is equivalent to remote code execution. File system access is limited to the working folder by default and file:// addresses are blocked; --allow-unrestricted-file-access removes that limit, so do not use it unless you have to.
You build the real boundary outside: run the agent under a separate user account or in a container, keep it apart from your personal sessions with --isolated, do not switch off the approval step for tool calls and, if possible, run a second domain check on the outgoing proxy. The flags do not replace these layers, they add to them.
Profile and session: persistent or isolated?
The default mode is a persistent profile: cookies and login data are stored on disk, and in the next session the agent picks up where it left off. The profile directory is derived from the client's working folder, so different projects get separate profiles. The README carries a warning: a persistent profile can be used by only one browser at a time. If you are going to open two clients in the same project, give the second one --isolated or a separate --user-data-dir.
--isolated starts every session clean and deletes everything when the browser closes. For data-reading jobs this is the right default. If you need to test your own application while logged in, you export the cookies once and load them with --storage-state; the format is in Playwright's authentication documentation. That file carries your session keys, so protect it like a password. The third mode is connecting to your already open Chrome with --extension. The agent can then reach all of your logged-in tabs, so pick this route only when you know exactly what you are doing.
Use cases
- Localization checks: having the agent walk through how your site looks from a given country. You point the proxy at that country's exit node and ask the agent to report on items such as language, currency and the cookie notice. Details are on the localization solution page; for checks that need the look of a real home connection, Residential Proxy is used.
- Exploratory testing: having the agent run through a flow in your own application and turning the Playwright code it produces into a permanent test. Test setups from different countries are on the app testing page.
- One-off data reads from a dynamic page: getting a few values from a public page that loads with JavaScript. For regular, high-volume work an agent is expensive; the permanent setup is on the data scraping solution page, and whether a browser is needed at all is covered in Static vs Dynamic Pages: Do You Need a Headless Browser?.
- Debugging: with the
browser_console_messagesandbrowser_network_requeststools the agent reads the page's console errors and network requests and summarises them for you.
Agent or not, the thing doing the browsing is a browser and the same rules apply: follow the robots.txt file and the site's terms of use, prefer the official API where one exists, and keep the request rate low. We do not recommend detection-evasion plugins. Why sites try to tell automated visitors apart is covered in Why Are AI Shopping Agents Blocked on Websites?.
Common mistakes
| What you see | Why | What to do |
|---|---|---|
net::ERR_INVALID_AUTH_CREDENTIALS | The proxy wants credentials; none were given or they were embedded in the address | Write username and password in the launchOptions.proxy field of the --config file |
net::ERR_PROXY_CONNECTION_FAILED | The proxy address or port is wrong, or the outgoing connection is stopped by a firewall | Try the same address with cURL |
net::ERR_BLOCKED_BY_CLIENT | The address is outside --allowed-origins or inside --blocked-origins | Add the origin to the list with its scheme and port |
| The browser does not open in a second client | The persistent profile is locked by another browser | --isolated or a separate --user-data-dir |
| The agent cannot reach your local server | localhost traffic also goes to the proxy | --proxy-bypass=localhost,127.0.0.1 |
There are also habit mistakes that did not make it into the table:
- Treating the allowlist as a security measure. A redirect gets past the list; build isolation at the process and network level.
- Opening your personal Chrome profile to the agent. The profile that holds your email and banking sessions should not be in the hands of a model that reads outside content.
- Asking for a screenshot for every job. Actions already run on the snapshot; the image is only for visual verification.
- Team work with
@latest. Flag names can change from one version to the next; pin the version. - Leaving regular crawling to the agent. Every step costs a model call. Explore with the agent, turn the generated code into a script and run that.
Decision guide
| Need | Recommendation |
|---|---|
| The assistant has to read a page loaded with JavaScript | Playwright MCP, --headless --isolated |
| The agent has to go out from a specific country | That country's exit node with --proxy-server |
| Proxy with username and password | launchOptions.proxy in the --config file |
| You do not want the password in a file | IP whitelist and --proxy-server on its own |
| Limiting the agent to a few sites | --allowed-origins, plus process and network isolation |
| Coding agent working in a large codebase | The Playwright CLI the README recommends |
| A crawl of hundreds of pages that runs every day | Not an agent, but a script written with the Playwright library |
| Learning the protocol and its risks | Our MCP guide |
Frequently asked questions
Is Playwright MCP free?
Yes. The package is published under the Apache 2.0 licence and runs with npx at no charge. The cost comes from two places: the tool schemas and page snapshots the model processes, and the proxy traffic you use.
What is the difference between Playwright MCP and the Playwright library?
With the library you code the steps, and the script follows the same path every time it runs. With the MCP server the model decides the steps; you only state the goal. The first is cheap and predictable for repeated work, the second is quick for exploration and one-off jobs. The proxy details on the library side (proxy per context, rotation, the error table) are in our Playwright proxy post.
Which browser does it use, and does Chrome have to be installed?
With no --browser given, the Google Chrome on the system opened in our test. You can change it with --browser firefox, webkit or msedge, or point to a specific browser binary with --executable-path.
Is it the same thing as Browser Use?
The goal is the same: letting a model use a browser. Browser Use is a standalone Python agent library and runs the loop itself. Playwright MCP only offers the tools; the loop is run by the assistant you already use (Claude Code, Cursor, VS Code).
Can it be used with a rotating proxy?
It can, but a browser opens many connections for a single page, and on a gateway that changes IP on every connection those connections may go out from different addresses. That is no problem when reading independent pages. If you do not want the IP to change in the middle of a multi-step flow, pick Sticky Proxy. How rotation works is covered in our post on IP rotation.
Does using a proxy make verification screens go away?
No. A proxy only changes which IP the request goes out from. The signals left by a browser under automation and the request rate stay the same. The lasting route is a reasonable rate, permitted pages and the official API where one exists.
Summary
Playwright MCP gives your assistant a real browser and has it read the page as an accessibility tree. Setup is a single line: npx @playwright/mcp@latest. For a proxy, --proxy-server is enough; if a username and password are needed, embedding them in the address ends in ERR_INVALID_AUTH_CREDENTIALS, and the right place is the launchOptions.proxy object in the configuration file. --allowed-origins narrows the agent's territory, but it does not cover redirects and, in the documentation's own words, it is not a security boundary; build isolation at the process and network level. Pin the version, explore with the agent and hand the repeated work over to a script. You can find suitable proxy types for your agent's exit node in our proxy services.




