·

Playwright MCP With Cursor

Set up Playwright MCP in Cursor so your AI agent can drive a real browser to test and inspect live web pages right from your editor.

Cursor's Agent mode connects to MCP servers through a settings panel or .cursor/mcp.json, and its main advantage for E2E work is proximity: the agent runs in the same window as your app code, so you can go from "generate a test," to "read the failure," to "fix the component," to "re-run the test," without ever leaving the editor. This topic covers the connection, generation workflow, debugging loop, and the specific rough edges Cursor has with Playwright MCP today.

Connecting Playwright MCP to Cursor Agent Mode

Add the server via .cursor/mcp.json at the project root (or ~/.cursor/mcp.json for a global config available across all projects):

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

Alternatively, use Cursor's Settings → MCP panel, which writes the same file but gives you a toggle to enable/disable individual servers without deleting the config — useful when you want Playwright MCP off for non-test-related sessions to reduce tool-schema noise in the context window.

Confirm the connection in the MCP settings panel — a green dot next to playwright with a tool count. If it's red, Cursor's error surface here is one of the weaker parts of the tool: you often just see "failed to start" with no further detail. Drop to a terminal and run the raw command to get an actual error:

npx -y @playwright/mcp@latest --isolated

Common failure in Cursor specifically: if you have a workspace-level Node version manager (.nvmrc) that Cursor's integrated terminal respects but its MCP process spawner doesn't, npx can resolve to a different, older Node version than the one you're developing with, sometimes too old for the MCP server's Node engine requirement. Pin an absolute path to a known-good Node/npx if this bites you:

{
  "mcpServers": {
    "playwright": {
      "command": "/usr/local/bin/npx",
      "args": ["-y", "@playwright/mcp@latest", "--isolated"]
    }
  }
}

Once connected, invoke it in Agent mode by referencing it naturally — Cursor auto-attaches relevant tools based on context, but you can also be explicit:

@playwright Navigate to localhost:3000 and take a snapshot of the homepage.

Tips
- Use the Settings → MCP panel toggle to disable Playwright MCP when working on non-UI tasks — fewer tool schemas in context means faster, cheaper turns.
- If connection fails with no useful error, always fall back to running the raw npx command in a terminal first — Cursor's MCP error surface is genuinely under-informative.
- Pin an absolute Node/npx path in the config if your project uses an .nvmrc version Cursor's MCP spawner doesn't pick up correctly.


Generating Playwright Test Specs from Code and User Stories in Cursor

Cursor's edge here is that the agent can read your actual component source alongside driving the live browser — it doesn't have to guess prop names or state shape from the rendered DOM alone, it can open the file.

Look at src/components/CheckoutForm.tsx to understand the validation rules,
then navigate to localhost:3000/checkout via Playwright MCP and verify each
validation rule fires correctly in the live UI (required fields, card number
format, zip code format). Write the resulting test suite to
tests/e2e/checkout-validation.spec.ts.

Because it read the component first, expect the agent to catch validation rules that aren't obviously visible from the DOM alone — for instance, a debounced validator that only fires after a field loses focus, which it can see directly in the source (onBlur handler) rather than having to accidentally trigger it during a live click sequence.

import { test, expect } from '@playwright/test';

test.describe('checkout form validation', () => {
  test('shows required-field errors after blur, not on every keystroke', async ({ page }) => {
    await page.goto('/checkout');
    const nameField = page.getByLabel('Full name');
    await nameField.click();
    await nameField.blur();
    await expect(page.getByText('Full name is required')).toBeVisible();
  });

  test('rejects a malformed card number on blur', async ({ page }) => {
    await page.goto('/checkout');
    const cardField = page.getByLabel('Card number');
    await cardField.fill('1234');
    await cardField.blur();
    await expect(page.getByText('Invalid card number')).toBeVisible();
  });

  test('rejects a zip code that does not match the 5-digit pattern', async ({ page }) => {
    await page.goto('/checkout');
    const zipField = page.getByLabel('Zip code');
    await zipField.fill('AB1');
    await zipField.blur();
    await expect(page.getByText('Zip code must be 5 digits')).toBeVisible();
  });
});

The blur-based test structure directly reflects onBlur validation logic the agent found in the component source — a live-browser-only agent without file access might miss this timing detail and write a flakier fill() → immediate assertion that only sometimes catches the debounced error message.

Tips
- Point the agent at the relevant component file explicitly before the live-browser step — reading validation logic in source beats inferring it purely from DOM behavior, especially for debounced or async validators.
- When a form uses onBlur validation, make sure generated tests call .blur() (or equivalent, like clicking elsewhere) rather than asserting immediately after .fill().
- Cross-reference generated assertion text against the actual error strings in source — a model reading a live DOM snapshot can occasionally get punctuation or wording slightly wrong on error messages if it wasn't shown the source string directly.


Running and Debugging Playwright Tests via Cursor Agent Mode

The debug loop that makes Cursor genuinely useful for this: run the test, feed the raw failure back to Agent mode, and let it use Playwright MCP to re-investigate the live page rather than guessing from the stack trace alone.

npx playwright test tests/e2e/checkout-validation.spec.ts --reporter=list
1) checkout form validation › rejects a malformed card number on blur

   Error: expect(locator).toBeVisible() failed
   Locator: getByText('Invalid card number')
   Expected: visible
   Received: <element(s) not found>

Feed it straight to the agent:

This test failed:
[paste the failure output above]

Use Playwright MCP to navigate to /checkout, fill an invalid card number,
blur the field, and take a snapshot to see what error text (if any) actually
renders. Then tell me if the test's expected text is wrong or if there's a
real bug in the validation.

A useful outcome here is the agent distinguishing "the app's actual error text is 'Card number is invalid' not 'Invalid card number'" (a copy mismatch, fix the test) from "no error rendered at all" (a real bug, fix the app) — because it re-checks the live page rather than pattern-matching the stack trace against what it originally wrote.

Agent: I navigated and checked. The live page shows the text
"Card number is invalid" — the validation logic is working correctly, but
the test's expected string doesn't match. I'll update the assertion.
await expect(page.getByText('Card number is invalid')).toBeVisible();

This re-verify-against-reality step is the core value proposition of MCP-backed debugging over an agent working from static code and stack traces alone — it can close the loop by actually looking, the same instinct you'd want from a human debugging a flaky assertion.

Tips
- Always ask the agent to re-navigate and re-check the live page on a failure, rather than accepting a fix based purely on reading the stack trace and the original test code.
- When a fix turns out to be "the test's expected string was wrong," double check it against the actual component source too — sometimes the live DOM text is inconsistent across states (e.g., a loading vs. settled error message) and the agent caught one but not the general case.
- Keep --reporter=list (or --reporter=line) handy for pasting compact failure output into the agent — the default HTML reporter output isn't practical to paste into a chat prompt.


Known Limitations and Workarounds for Playwright MCP in Cursor

Weak MCP connection diagnostics, as noted above — budget for occasionally dropping to a raw terminal command to get real error output rather than trusting the Settings panel's status indicator alone.

Tool call visibility is less granular than Claude Code's VS Code sidebar. Cursor shows tool calls inline in the chat as collapsible blocks, but doesn't provide the same structured, persistently browsable timeline. For a long debugging session, it's easy to lose track of exactly which snapshot informed which decision if you don't scroll back carefully.

Context window management on long sessions. Cursor's Agent mode, depending on the underlying model selected, can start trimming or summarizing earlier tool call results in long sessions, which occasionally causes it to "forget" an earlier snapshot detail and re-navigate to re-check something it already established. Not necessarily wrong, but it burns tool calls and time. Breaking a long generation+debug session into two focused conversations mitigates this.

Model selection affects Playwright MCP tool-use quality noticeably. Cursor lets you pick the underlying model per session; a faster/cheaper model selected for quick edits will produce noticeably worse tool-call sequencing (extra redundant snapshots, less precise locator choices) than a stronger model. For E2E test generation specifically, it's worth explicitly switching to your strongest available model rather than leaving whatever default was set for general coding.

No native trace-viewer integration. Unlike the VS Code Claude Code extension pairing nicely with playwright test --ui in a second pane, Cursor doesn't have anything bundled — you're opening npx playwright show-trace in a separate terminal/browser tab exactly as you would without any AI tooling at all.

npx playwright show-trace test-results/checkout-validation-*/trace.zip

Tips
- Explicitly select your strongest available model for Playwright MCP test-generation sessions in Cursor — the model-quality effect on tool-call sequencing is larger than it is for plain code editing.
- Split long generate-then-debug sessions into two conversations if you notice the agent re-confirming things it already established — it's a sign context is getting trimmed.
- Keep npx playwright show-trace as a manual fallback; don't expect Cursor to surface trace-viewer-equivalent detail inside the chat itself.


Tips

Tips
- Cursor's biggest advantage here is same-window access to both source code and the live browser — lean into prompts that explicitly ask it to read a component before testing behavior driven by that component's logic.
- Treat the Settings MCP status indicator as a rough signal only; a raw terminal command is still the fastest way to get an actual, useful error.
- For serious E2E generation work, use your strongest available model in Cursor — the gap in Playwright MCP tool-use quality between models is larger than most people expect.