Claude Code has good first-class MCP support: it can add a server via a CLI subcommand, respects project-level .mcp.json, and — in the VS Code extension — surfaces tool calls inline in the chat panel so you can watch the browser being driven step by step. This topic covers wiring Playwright MCP into both surfaces and the workflow differences between them.
Installing and Connecting Playwright MCP to Claude Code
The fastest path is the claude mcp add command, which writes the server entry for you rather than hand-editing JSON:
claude mcp add playwright -- npx -y @playwright/mcp@latest
This registers the server at user scope by default. For a project-specific setup that your whole team shares via version control, write .mcp.json at the repo root instead:
{
"mcpServers": {
"playwright": {
"command": "npx",
"args": ["-y", "@playwright/mcp@latest", "--isolated"]
}
}
}
Commit this file. Claude Code prompts each teammate to approve project-scoped MCP servers the first time they open the repo — a deliberate friction point so nobody silently inherits a server that can execute JS in a browser and touch the network without noticing.
Verify the connection:
claude mcp list
playwright: npx -y @playwright/mcp@latest --isolated - ✓ Connected
If it shows "Failed to connect," the most common cause is a missing browser binary — run npx playwright install chromium once in the project, since @playwright/mcp doesn't bundle browser binaries itself.
Inside a Claude Code session, confirm the tools are visible:
/mcp
This lists connected servers and lets you inspect or disable individual tools (useful if you want to exclude browser_evaluate for a locked-down session — see the security section in the previous topic).
Tips
- Project-scoped.mcp.jsonplus--isolatedis the right default for a shared repo — nobody accidentally inherits another dev's logged-in browser profile.
- Runclaude mcp addfrom the repo root so the config lands in the right.mcp.json, not your global user config —claude mcp list --scope projectvs--scope userwill show you which is which.
- If Chromium fails to launch headless in a Docker-based dev container, add--no-sandboxto the server args as a stopgap, but don't ship that flag to a real CI runner without understanding why it's normally required.
Generating and Running E2E Tests from Natural Language Descriptions
Once connected, describe the flow directly in the CLI session. Claude Code will interleave MCP tool calls (visible in the terminal as they happen) with its own file-writing tools.
> Using Playwright MCP, navigate to http://localhost:5173/signup, fill in the
> registration form with a random test email and password "TestPass123!",
> submit it, and confirm the welcome page loads. Then write a Playwright test
> at tests/e2e/signup.spec.ts that automates this, using a dynamically
> generated email per run so re-running the test doesn't collide with an
> existing account.
A realistic generated file handles the "don't collide on re-run" requirement with a timestamp-based email, which is the kind of detail that separates a usable generated test from a recorder transcript:
import { test, expect } from '@playwright/test';
test('registers a new user and lands on welcome page', async ({ page }) => {
const email = `test.user.${Date.now()}@example.com`;
await page.goto('/signup');
await page.getByLabel('Email').fill(email);
await page.getByLabel('Password', { exact: true }).fill('TestPass123!');
await page.getByLabel('Confirm password').fill('TestPass123!');
await page.getByRole('button', { name: 'Create account' }).click();
await expect(page).toHaveURL(/\/welcome/);
await expect(page.getByRole('heading', { name: /welcome/i })).toBeVisible();
});
Run it the normal way — Claude Code doesn't intercept your test runner, the generated file is a plain Playwright spec:
npx playwright test tests/e2e/signup.spec.ts --project=chromium
If it fails, paste the failure back into the same session rather than starting fresh — Claude Code retains the conversation context, including what it saw during the live walkthrough, which helps it distinguish "the app changed since I looked" from "I wrote a bad assertion."
> That test failed with: expect(locator).toBeVisible() failed,
> element not found: getByRole('heading', { name: /welcome/i }).
> Re-check the welcome page structure with Playwright MCP and fix the test.
Tips
- Give the agent a concrete uniqueness strategy up front (timestamp, UUID, random suffix) for any signup/creation flow — otherwise it'll hardcode a fixed test email that breaks on the second run.
- When a generated test fails, ask the agent to re-navigate and re-snapshot rather than guessing a fix from memory — the live app may genuinely have changed.
- Keep prompts scoped to one flow at a time; a single prompt asking for five unrelated test files produces shallower coverage on each than five focused prompts.
Playwright MCP in the Claude Code VS Code Extension for Test Workflows
The VS Code extension runs the same underlying agent but adds a visible tool-call timeline in the sidebar — each browser_navigate, browser_click, browser_snapshot call appears as a collapsible entry you can expand to see the raw accessibility snapshot the model received. This matters for debugging why the agent chose a particular locator: you can see exactly what it "saw."
Setup mirrors the CLI: open the Claude Code panel in VS Code, and it picks up the same .mcp.json from the workspace root automatically — no separate configuration needed. If you want a VS Code-only server (e.g., pointed at a different local port for a frontend dev server), use the extension's MCP settings UI instead of editing JSON directly, which writes to .vscode/mcp.json scoped to that workspace.
The practical advantage of the extension for test work: you can have the integrated terminal running npx playwright test --ui (Playwright's own UI mode, showing a live trace of each test step) in one pane while the Claude Code sidebar drives Playwright MCP in another. When a generated test fails, you're looking at Playwright's trace viewer and the agent's reasoning side by side, rather than tabbing between windows.
npx playwright test --ui
One friction point specific to the extension: if your VS Code workspace has multiple folders open (a monorepo with frontend/ and backend/ as separate roots), .mcp.json resolution can pick up the wrong root's config. Keep the Playwright MCP config at the workspace root that also contains playwright.config.ts, not a sibling package.
Tips
- Expand the tool-call entries in the sidebar when a generated locator looks wrong — the raw snapshot usually shows why the model picked that role/name pair.
- Runnpx playwright test --uialongside the extension for failures; the visual trace surfaces DOM state issues faster than re-reading the agent's text explanation.
- In multi-root workspaces, double-check which.mcp.jsonis active with the extension's MCP status indicator before assuming the server config you edited is the one in effect.
Best Practices for AI-Generated Playwright Test Suites
Treat AI-generated specs the same way you'd treat a junior engineer's first draft: useful, fast, and needing a real review pass before merging.
Enforce a locator convention explicitly. Left unguided, models will happily mix page.locator('.css-class'), page.getByText(), and getByRole() in the same file depending on what the live snapshot made easiest at that moment. Put a short style note in CLAUDE.md at the project root:
## E2E Test Conventions
- Prefer `getByRole` and `getByLabel` over CSS selectors.
- Use `getByTestId` only when no accessible role/label exists.
- Every new spec file goes in `tests/e2e/`, named `{feature}.spec.ts`.
- Async test data (emails, usernames) must be uniquely generated per run.
Claude Code reads this file automatically and will follow it without you repeating the rules in every prompt.
Don't let generated tests silently duplicate fixtures. Ask explicitly for shared setup to go into playwright.config.ts or a fixtures.ts file rather than each generated spec re-implementing its own login helper:
// tests/e2e/fixtures.ts
import { test as base } from '@playwright/test';
export const test = base.extend<{ loggedInPage: void }>({
loggedInPage: async ({ page }, use) => {
await page.goto('/login');
await page.getByLabel('Email').fill(process.env.TEST_USER_EMAIL!);
await page.getByLabel('Password').fill(process.env.TEST_USER_PASSWORD!);
await page.getByRole('button', { name: 'Sign in' }).click();
await use();
},
});
Review for flakiness patterns before merging. The most common issue in agent-generated specs is a missing await expect(...).toBeVisible() before an interaction — the model's live session had real network latency to mask timing issues that a fast CI runner won't have. Run new specs at least 3 times locally (npx playwright test --repeat-each=3) before trusting them in CI.
Version-pin the MCP server for the whole team. A silent @playwright/mcp minor bump can change tool names or snapshot format, which changes what the agent generates without anyone changing a prompt. Pin it in .mcp.json:
{
"mcpServers": {
"playwright": {
"command": "npx",
"args": ["-y", "@playwright/mcp@0.0.29", "--isolated"]
}
}
}
Tips
- Put locator and file-naming conventions inCLAUDE.mdonce — it saves re-explaining them in every generation prompt and produces consistent output across teammates.
- Run--repeat-each=3on any newly generated spec before merging; flaky waits show up fast under repetition.
- Pin the MCP server version in committed config — an unpinned@latestis a source of silent, hard-to-diagnose drift in what the agent generates over time.
Tips
Tips
-claude mcp listand/mcpinside a session are your two fastest sanity checks when something feels off — most "the agent isn't seeing my page" issues are actually a disconnected server.
- Keep atests/e2e/fixtures.tsconvention from day one; retrofitting shared setup into a pile of independently-generated specs is more work than establishing the pattern up front.
- The VS Code extension's tool-call timeline is worth learning to read closely — it's the fastest way to understand why a generated locator doesn't match what you expected.