writing-skills
writing-skills
Use when creating new skills, editing existing skills, or verifying skills work before deployment
playwright-mcpPlaywright MCP is a Model Context Protocol server that lets an AI agent drive a real browser: it reads each page as a structured accessibility snapshot and clicks, types, and fills forms by element reference instead of guessing at pixels. Use when someone asks to "let the agent use a browser", "add the Playwright MCP server", "open the site and check it", "…
Independent GitHub snapshot · Automated classification · Original documentation governs
Playwright MCP runs a browser and exposes it to an MCP client as tools such as browser_navigate, browser_click and browser_type. Every action returns a text snapshot of the page's accessibility tree in which each element carries a short reference (ref=e5); the agent passes that reference back as the target of the next call. No vision model is needed, and the same page structure always produces the same interaction.
The server is an npm package started with npx; nothing is installed globally. The browser is downloaded on first use.
# Claude Code
claude mcp add playwright npx @playwright/mcp@latest
# Claude Code, with server flags (everything after -- goes to the server)
claude mcp add playwright -- npx @playwright/mcp@latest --headless --caps=testing,storage
# OpenAI Codex
codex mcp add playwright npx "@playwright/mcp@latest"
# VS Code
code --add-mcp '{"name":"playwright","command":"npx","args":["@playwright/mcp@latest"]}'
Cursor, Gemini CLI, Claude Desktop, Windsurf and most other clients take the standard JSON block in their MCP settings file:
{
"mcpServers": {
"playwright": {
"command": "npx",
"args": ["@playwright/mcp@latest"]
}
}
}
Server flags go into the same args array, one string per flag, for example "--caps=testing,storage".
browser_navigate opens a URL and returns a snapshot.[ref=...].target.→ browser_navigate { url: "https://demo.playwright.dev/todomvc" }
- heading "todos" [level=1] [ref=e3]
- textbox "What needs to be done?" [ref=e5]
→ browser_type { target: "e5", text: "Renew TLS certificate", submit: true }
- textbox "What needs to be done?" [ref=e5]
- list [ref=e8]:
- listitem [ref=e9]:
- checkbox "Toggle Todo" [ref=e10]
- text: Renew TLS certificate
target also accepts a Playwright selector or locator string such as #submit or getByRole('button', { name: 'Save' }). Elements inside an iframe get a frame prefix, for example f1e12. A stale ref fails with Ref e10 not found in the current page snapshot; take a new snapshot and retry.
| Tool | Key parameters | Use |
|---|---|---|
browser_navigate | url | Open a page |
browser_snapshot | target, depth, boxes, filename | Re-read the page, or only one subtree |
browser_find | text or regex | Locate one element on a large page without a full snapshot |
browser_click | target, doubleClick, button, modifiers | Click |
browser_type | target, text, submit, slowly | Fill one field, optionally press Enter |
browser_fill_form | fields[] of name, target, type, value | Fill several fields in one call |
browser_select_option | target, values | Choose options in a dropdown |
browser_wait_for | text, textGone, time (seconds, max 30) | Wait for async work |
browser_take_screenshot | target, fullPage, filename, type | Visual record; cannot be acted on |
browser_console_messages | level, all | Read page console output |
browser_network_requests | filter, static | List requests; browser_network_request shows one in full |
browser_evaluate | function, target | Run JavaScript inside the page |
browser_tabs | action, index, url | List, open, select, close tabs |
browser_handle_dialog | accept, promptText | Answer alert, confirm, prompt |
browser_file_upload | paths | Answer a file chooser |
Field type in browser_fill_form is one of textbox, checkbox, radio, combobox, slider; checkboxes and radios take "true" or "false" as value.
Extra tool groups are enabled with --caps, comma-separated. Enable only what the task needs: every group adds tool schemas to the model context.
| Capability | Adds |
|---|---|
storage | Cookies, localStorage, sessionStorage, browser_storage_state, browser_set_storage_state |
testing | browser_verify_element_visible, browser_verify_text_visible, browser_verify_list_visible, browser_verify_value, browser_generate_locator |
network | browser_route, browser_route_list, browser_unroute, browser_network_state_set |
vision | Coordinate mouse tools such as browser_mouse_click_xy for canvas and unlabeled widgets |
pdf | browser_pdf_save |
devtools | Tracing, video recording, action recording, highlighting |
config | browser_get_config to print the resolved configuration |
| Flag | Effect |
|---|---|
--headless | Run without a window. The default is headed. |
--browser=firefox | One of chrome (default), firefox, webkit, msedge |
--device="iPhone 15" or --mobile | Device emulation |
--viewport-size=1280x720 | Viewport in pixels |
--isolated | In-memory profile, nothing saved to disk |
--user-data-dir=./profiles/staging | Explicit persistent profile directory |
--storage-state=./auth-state.json | Load cookies and localStorage into an isolated session |
--extension | Attach to a running Chrome or Edge through the Playwright extension |
--cdp-endpoint=http://localhost:9222 | Attach to a running Chromium-based browser over CDP |
--output-dir=./playwright-output | Where unnamed screenshots, PDFs and traces are written |
--snapshot-mode=none | Stop attaching a snapshot to every response |
--timeout-action=8000, --timeout-navigation=45000 | Milliseconds; defaults are 5000 and 60000 |
--codegen=python | Language of the generated code: typescript (default), python, java, csharp, none |
--secrets=./.secrets | dotenv file with values the model must not see |
Most flags have an environment variable with the PLAYWRIGHT_MCP_ prefix, for example PLAYWRIGHT_MCP_HEADLESS and PLAYWRIGHT_MCP_CAPS. Precedence, lowest to highest: config file, environment, command line.
--isolated starts clean every time. Seed it with --storage-state=./auth-state.json. It cannot be combined with --user-data-dir.--extension reuses tabs and sessions of a running Chrome or Edge, which avoids SSO and 2FA flows.With the storage capability the agent saves and restores a login itself:
→ browser_storage_state { filename: "auth-state.json" }
- [Storage state](auth-state.json)
→ browser_set_storage_state { filename: "auth-state.json" }
Storage state restored from auth-state.json
--config takes a server configuration file. It is not the client's mcpServers file.
{
"browser": {
"browserName": "chromium",
"isolated": true,
"launchOptions": { "headless": true },
"contextOptions": { "viewport": { "width": 1280, "height": 720 } }
},
"capabilities": ["testing", "storage"],
"network": { "blockedOrigins": ["https://www.google-analytics.com"] },
"timeouts": { "action": 8000, "navigation": 45000 },
"outputDir": "./playwright-output"
}
npx @playwright/mcp@latest --config ./playwright-mcp.config.json
Start the server yourself when the client cannot spawn a headed browser (remote IDE workers, machines where only one shell has a display):
npx @playwright/mcp@latest --port 8931
{
"mcpServers": {
"playwright": { "url": "http://localhost:8931/mcp" }
}
}
The official image supports headless Chromium only:
{
"mcpServers": {
"playwright": {
"command": "docker",
"args": ["run", "-i", "--rm", "--init", "--pull=always", "mcr.microsoft.com/playwright/mcp"]
}
}
}
Request: "Open the todo demo, add 'Renew TLS certificate' and 'Rotate API keys', tick the first one, and tell me if the counter is right and whether the console shows errors."
Each call returns a snapshot; only the last one is shown.
→ browser_navigate { url: "https://demo.playwright.dev/todomvc" }
→ browser_type { target: "e5", text: "Renew TLS certificate", submit: true }
→ browser_type { target: "e5", text: "Rotate API keys", submit: true }
→ browser_click { target: "e10" }
- list [ref=e8]:
- listitem [ref=e9]:
- checkbox "Toggle Todo" [checked] [ref=e10]
- text: Renew TLS certificate
- listitem [ref=e13]:
- checkbox "Toggle Todo" [ref=e14]
- text: Rotate API keys
- contentinfo [ref=e18]:
- text: 1 item left
→ browser_console_messages { level: "error" }
Total messages: 0 (Errors: 0, Warnings: 0)
Result: the agent reports that both items were added, the first is checked, the counter reads "1 item left", and the console is clean. Ref numbers differ between page loads; they are always read from the latest snapshot.
Request: "Log in to the admin panel on localhost:3000 with the staging account and save the session so you don't have to log in again."
The password stays out of the conversation. The user exports ADMIN_PASSWORD in their own shell (the staging account's password from the team password manager), writes it to a .secrets file that is listed in .gitignore, and registers the server:
printf 'ADMIN_PASSWORD=%s\n' "$ADMIN_PASSWORD" > .secrets
claude mcp add playwright -- npx @playwright/mcp@latest --caps=storage --secrets ./.secrets
The agent types the key name; the server substitutes the value and masks it in responses:
→ browser_navigate { url: "http://localhost:3000/login" }
- textbox "Email" [ref=e4]
- textbox "Password" [ref=e6]
- button "Sign in" [ref=e8]
→ browser_fill_form { fields: [
{ name: "Email", target: "e4", type: "textbox", value: "ops@harborline.internal" },
{ name: "Password", target: "e6", type: "textbox", value: "ADMIN_PASSWORD" } ] }
→ browser_click { target: "e8" }
- heading "Orders" [level=1] [ref=e2]
→ browser_storage_state { filename: "auth-state.json" }
- [Storage state](auth-state.json)
Result: auth-state.json is written to the workspace root. Later sessions start with --isolated --storage-state=./auth-state.json and open /orders without the login form.
--headless turns the window off. CI, containers and SSH sessions need --headless or the Docker image.browser_find, or browser_snapshot with target or depth, over full snapshots. Screenshots cost far more tokens than snapshots and cannot be acted on.--isolated or its own --user-data-dir.browser_wait_for with text or textGone only for slow async work, not fixed sleeps.--allowed-origins, --blocked-origins and the workspace file restriction catch accidents; they do not stop a determined page or redirect. Isolation must come from the client's permissions, a container, or a throwaway profile.browser_run_code_unsafe runs arbitrary JavaScript in the server process. Use it only with a trusted client and only when no dedicated tool fits (custom waits, reload, iframes)..secrets or auth-state.json.0.0.0.0 with --host exposes browser control to the network. Keep the default localhost unless the server runs inside a container.playwright-testing skill; use this server to explore the app and collect the generated locators first. For high-volume scraping, a scripted crawler is cheaper. For coding agents with shell access, the Playwright team recommends the separate Playwright CLI (@playwright/cli) as the more token-efficient interface; MCP fits long exploratory sessions that need persistent browser state.