Skip to main content
Glama
muhammadraheel-vd

Playwright MCP Server

README.md
# Playwright MCP Server — POC with 100+ Test Cases

**VentureDive QA Engineering**
Prepared by: Raheel Imran
Date: May 2026

---

## Overview

This Proof of Concept (POC) demonstrates the **Playwright MCP Server** (`@playwright/mcp`) used as the browser automation engine for a complete test suite. It includes:

- A fully functional demo web application — **TaskMaster Pro**
- **166 Playwright test cases** across 8 feature areas using reusable MCP-style commands
- A `cmd` fixture that maps directly to `@playwright/mcp` tool names
- A live MCP server demo that connects via `@modelcontextprotocol/sdk` and drives the browser using real MCP protocol — no external API calls
- MCP server configuration for Claude Desktop and Claude Code
- An interactive HTML test report

---

## Architecture

```
┌──────────────────────────────────────────────────────────────┐
│                   Playwright MCP POC                         │
│                                                              │
│  ┌───────────────────┐   MCP Protocol    ┌────────────────┐  │
│  │  MCP Client       │◄─────────────────►│ @playwright/mcp│  │
│  │  (run-demo.ts)    │   stdio transport │  Server        │  │
│  └───────────────────┘                   └───────┬────────┘  │
│                                                  │           │
│                                         Playwright Browser   │
│                                                  │           │
│                                         ┌────────▼─────────┐ │
│                                         │ TaskMaster Pro   │ │
│                                         │ localhost:3000   │ │
│                                         └──────────────────┘ │
│                                                              │
│  Playwright Test Runner (166 tests)                          │
│  └── tests/fixtures/browser-commands.ts  ← reusable cmd      │
│      ├── auth.spec.ts          (25 tests)                    │
│      ├── navigation.spec.ts    (20 tests)                    │
│      ├── dashboard.spec.ts     (12 tests)                    │
│      ├── tasks.spec.ts         (45 tests)                    │
│      ├── categories.spec.ts    (14 tests)                    │
│      ├── profile.spec.ts       (17 tests)                    │
│      ├── settings.spec.ts      (17 tests)                    │
│      └── ui-components.spec.ts (16 tests)                    │
└──────────────────────────────────────────────────────────────┘
```

---

## Quick Start

### 1. Install dependencies
```bash
npm install
npx playwright install chromium
```

### 2. Start the demo app
```bash
npm run dev
# → http://localhost:3000
```

### 3. Run all tests and generate HTML report
```bash
npm test
npm run test:report    # opens playwright-report/index.html
```

### 4. Run the MCP server demo (browser driven via MCP protocol)
```bash
npm run demo
# Starts @playwright/mcp server, connects as MCP client, runs 10 scenarios
```

### 5. Interactive test runner
```bash
npm run test:ui
```

---

## npm Scripts

| Script | Description |
|--------|-------------|
| `npm run dev` | Start the TaskMaster Pro demo app on port 3000 |
| `npm test` | Run all 100+ tests (Chromium + Firefox) |
| `npm run test:ui` | Open Playwright interactive UI mode |
| `npm run test:report` | Open generated HTML report |
| `npm run test:headed` | Run tests in headed browser |
| `npm run test:debug` | Run tests with Playwright debugger |
| `npm run demo` | Run the MCP server demo (real MCP protocol) |
| `npm run mcp:start` | Start `@playwright/mcp` standalone server |

---

## Reusable Browser Commands

All tests import from `tests/fixtures/browser-commands.ts` instead of `@playwright/test` directly.
Command names match `@playwright/mcp` tool names exactly, making the test code portable to any MCP client.

```typescript
import { test, expect } from './fixtures/browser-commands';

test('example', async ({ cmd }) => {
  await cmd.browser_navigate('/login.html');
  await cmd.browser_type('email-input', 'testuser@test.com');
  await cmd.browser_type('password-input', 'Test@123');
  await cmd.browser_click('login-btn');
  await cmd.browser_expect_url(/dashboard/);
  await cmd.browser_take_screenshot('login-success.png');
});
```

### Full command reference

| Command | MCP Tool | Description |
|---------|----------|-------------|
| `cmd.browser_navigate(url)` | `browser_navigate` | Navigate to URL |
| `cmd.browser_navigate_back()` | `browser_navigate_back` | Go back in history |
| `cmd.browser_click(testId)` | `browser_click` | Click element by data-testid |
| `cmd.browser_type(testId, text)` | `browser_type` | Fill input field |
| `cmd.browser_clear_and_type(testId, text)` | `browser_type` | Clear then fill |
| `cmd.browser_select_option(testId, value)` | `browser_select_option` | Select dropdown option |
| `cmd.browser_check(testId)` | `browser_click` | Check a checkbox |
| `cmd.browser_uncheck(testId)` | `browser_click` | Uncheck a checkbox |
| `cmd.browser_take_screenshot(filename?)` | `browser_take_screenshot` | Capture screenshot |
| `cmd.browser_get_url()` | — | Get current URL |
| `cmd.browser_get_text(testId)` | — | Get element text |
| `cmd.browser_get_value(testId)` | — | Get input value |
| `cmd.browser_is_visible(testId)` | `browser_snapshot` | Returns boolean |
| `cmd.browser_is_checked(testId)` | `browser_snapshot` | Returns boolean |
| `cmd.browser_count(testId)` | `browser_snapshot` | Count elements |
| `cmd.browser_wait_for(testId, state?)` | `browser_wait_for` | Wait for element |
| `cmd.browser_wait_for_url(pattern)` | — | Wait for URL |
| `cmd.browser_set_storage(key, value)` | — (fixture-only) | Set localStorage via page.evaluate |
| `cmd.browser_evaluate(fn)` | — (fixture-only) | Run JS via page.evaluate |
| `cmd.browser_expect_url(pattern)` | — | Assert current URL |
| `cmd.browser_expect_visible(testId)` | — | Assert visible |
| `cmd.browser_expect_hidden(testId)` | — | Assert hidden |
| `cmd.browser_expect_text(testId, text)` | — | Assert text content |
| `cmd.browser_expect_count(testId, n)` | — | Assert element count |
| `cmd.browser_expect_value(testId, val)` | — | Assert input value |
| `cmd.browser_expect_checked(testId)` | — | Assert checkbox state |
| `cmd.browser_expect_disabled(testId)` | — | Assert disabled |
| `cmd.browser_expect_attr(testId, attr, val)` | — | Assert attribute |

---

## Demo App — TaskMaster Pro

A fully self-contained web app using `localStorage` for all data persistence.

| Page | URL | Features |
|------|-----|----------|
| Login | `/login.html` | Form validation, password toggle, remember-me |
| Register | `/register.html` | Password strength, validation |
| Dashboard | `/dashboard.html` | Stats, recent tasks, quick-add |
| Tasks | `/tasks.html` | CRUD, filter, search, sort, bulk actions |
| Categories | `/tasks.html#categories` | Create, edit, delete categories |
| Profile | `/profile.html` | Edit name, avatar, change password |
| Settings | `/settings.html` | Theme, notifications, language, export |
| 404 | `/404.html` | Error page |

**Default test credentials:**

| Email | Password | Role |
|-------|----------|------|
| `testuser@test.com` | `Test@123` | Test User |
| `admin@test.com` | `Admin@123` | Admin User |

Pre-seeded data: 5 tasks (3 active, 2 completed) and 4 categories.

### MCP Tools provided by @playwright/mcp

See the full categorized reference in [MCP Server Setup → Available MCP tools](#available-mcp-tools) below. Key tools used in this demo app:

| Tool | Description |
|------|-------------|
| `browser_navigate` | Navigate to a URL |
| `browser_snapshot` | Get accessibility tree — returns ARIA refs for click/type targets |
| `browser_click` | Click an element by ARIA ref |
| `browser_type` | Type text into a focused field |
| `browser_fill_form` | Fill multiple form fields in one call |
| `browser_select_option` | Select a dropdown value |
| `browser_take_screenshot` | Capture the current page as an image |
| `browser_localstorage_set` | Write a localStorage key (used for fast auth seeding) |
| `browser_evaluate` | Evaluate JavaScript on the page |
| `browser_wait_for` | Wait for an element or condition |
| `browser_network_requests` | Inspect captured network traffic |
| `browser_route` | Mock API responses for isolated testing |
| `browser_tabs` | List, open, close, or switch tabs |
| `browser_close` | Close the browser |

---

## MCP Server Setup

`@playwright/mcp` exposes browser automation as MCP tools so any MCP-compatible client (Claude Code, Claude Desktop, VS Code, Cursor, Windsurf) can drive a real browser with no screenshots or vision models — just accessibility snapshots.

### Claude Code (project-level)

A `.mcp.json` file is included at the project root. Claude Code picks it up automatically when you open this directory:

```json
{
  "mcpServers": {
    "playwright": {
      "command": "npx",
      "args": ["@playwright/mcp@latest"]
    }
  }
}
```

Or install it manually via the CLI:

```bash
claude mcp add playwright npx @playwright/mcp@latest
```

### Claude Desktop

Add the following to `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or `%APPDATA%\Claude\claude_desktop_config.json` (Windows):

```json
{
  "mcpServers": {
    "playwright": {
      "command": "npx",
      "args": ["@playwright/mcp@latest"]
    }
  }
}
```

### VS Code

One-liner via the VS Code CLI:

```bash
code --add-mcp '{"name":"playwright","command":"npx","args":["@playwright/mcp@latest"]}'
```

Or use the included `.vscode/mcp.json` — VS Code with the Copilot extension picks it up automatically:

```json
{
  "servers": {
    "playwright": {
      "type": "stdio",
      "command": "npx",
      "args": ["@playwright/mcp@latest"]
    }
  }
}
```

### Cursor / Windsurf

Add the same block under `mcpServers` in their respective settings files.

### Configuration options

Append flags to the `args` array to customise the server:

| Flag | Effect |
|------|--------|
| `--headless` | Run browser without a visible window (default: headed) |
| `--browser <browser>` | Browser to use: `chrome`, `firefox`, `webkit`, `msedge` (default: Chromium) |
| `--isolated` | Keep browser profile in memory; do not persist to disk |
| `--port <n>` | Expose the server over HTTP/SSE instead of stdio |
| `--host <host>` | Host to bind to (default: `localhost`; use `0.0.0.0` for all interfaces) |
| `--vision` | Use screenshots instead of accessibility snapshots (requires vision model) |
| `--caps <caps>` | Comma-separated capability subset: `tabs,pdf,history,wait,files,install` |
| `--device <device>` | Emulate a device, e.g. `"iPhone 15"` |
| `--viewport-size <size>` | Set viewport, e.g. `"1280, 720"` |
| `--user-agent <ua>` | Override the browser user-agent string |
| `--storage-state <path>` | Load saved auth state (cookies/localStorage) from a JSON file |
| `--save-trace` | Save a Playwright Trace of the session to `--output-dir` |
| `--output-dir <path>` | Directory for screenshots, PDFs, and traces |
| `--proxy-server <proxy>` | Route traffic through a proxy, e.g. `http://myproxy:3128` |
| `--no-sandbox` | Disable the browser sandbox (useful in some CI environments) |
| `--extension` | Connect to an existing browser tab instead of launching a new browser |
| `--config <path>` | Load advanced settings from a JSON config file |

Example — headless Firefox in isolated mode:

```json
{
  "mcpServers": {
    "playwright": {
      "command": "npx",
      "args": ["@playwright/mcp@latest", "--headless", "--browser=firefox", "--isolated"]
    }
  }
}
```

### Standalone server (HTTP transport)

For headless/CI environments where stdio is not suitable:

```bash
npx @playwright/mcp@latest --port 8931
```

Then point your MCP client at `http://localhost:8931/mcp` using the `url` form:

```json
{
  "mcpServers": {
    "playwright": {
      "url": "http://localhost:8931/mcp"
    }
  }
}
```

### Available MCP tools

**Navigation**

| Tool | Description |
|------|-------------|
| `browser_navigate` | Navigate to a URL |
| `browser_navigate_back` / `browser_navigate_forward` | Browser history navigation |
| `browser_reload` | Reload the current page |

**Interaction**

| Tool | Description |
|------|-------------|
| `browser_click` | Click an element by ARIA ref |
| `browser_check` / `browser_uncheck` | Check or uncheck a checkbox |
| `browser_type` | Type text into a focused field |
| `browser_press_sequentially` | Type text one character at a time (for masked fields) |
| `browser_fill_form` | Fill multiple form fields by their labels in one call |
| `browser_select_option` | Select a dropdown value |
| `browser_hover` | Hover over an element |
| `browser_drag` / `browser_drop` | Drag-and-drop between elements |
| `browser_press_key` / `browser_keydown` / `browser_keyup` | Keyboard key events |
| `browser_handle_dialog` | Accept or dismiss browser dialogs |
| `browser_file_upload` | Upload a file to a file input |
| `browser_mouse_click_xy` / `browser_mouse_move_xy` / `browser_mouse_drag_xy` | Pixel-precise mouse control |
| `browser_mouse_down` / `browser_mouse_up` / `browser_mouse_wheel` | Low-level mouse events |

**Inspection & Assertions**

| Tool | Description |
|------|-------------|
| `browser_snapshot` | Get the page accessibility tree — primary way to read elements and obtain ARIA refs |
| `browser_take_screenshot` | Capture the current page as an image |
| `browser_annotate` | Capture a screenshot with annotated highlights |
| `browser_highlight` / `browser_hide_highlight` | Visually highlight an element on the page |
| `browser_generate_locator` | Generate a Playwright locator for a given element |
| `browser_verify_element_visible` | Assert an element is visible |
| `browser_verify_text_visible` | Assert specific text is visible on the page |
| `browser_verify_list_visible` | Assert a list of items is visible |
| `browser_verify_value` | Assert an element's value |
| `browser_wait_for` | Wait for an element or network condition |

**Storage**

| Tool | Description |
|------|-------------|
| `browser_evaluate` | Evaluate a JavaScript expression on the page |
| `browser_localstorage_get` / `browser_localstorage_set` / `browser_localstorage_list` | Read/write localStorage |
| `browser_localstorage_clear` / `browser_localstorage_delete` | Remove localStorage items |
| `browser_sessionstorage_get` / `browser_sessionstorage_set` / `browser_sessionstorage_list` | sessionStorage CRUD |
| `browser_sessionstorage_clear` / `browser_sessionstorage_delete` | Remove sessionStorage items |
| `browser_cookie_get` / `browser_cookie_set` / `browser_cookie_list` | Read/write cookies |
| `browser_cookie_clear` / `browser_cookie_delete` | Remove cookies |
| `browser_storage_state` | Export current cookies and storage to a file |
| `browser_set_storage_state` | Import previously saved storage state |

**Network**

| Tool | Description |
|------|-------------|
| `browser_network_requests` | List all captured network requests |
| `browser_network_request` | Get details for a single network request |
| `browser_network_clear` | Clear the captured request log |
| `browser_network_state_set` | Simulate online/offline/throttled network conditions |
| `browser_route` | Mock requests matching a URL pattern with a custom response |
| `browser_route_list` | List active route mocks |
| `browser_unroute` | Remove a route mock |

**Tabs**

| Tool | Description |
|------|-------------|
| `browser_tabs` | List, create (`new`), close, or switch to a tab by index |

**Console**

| Tool | Description |
|------|-------------|
| `browser_console_messages` | Get browser console output |
| `browser_console_clear` | Clear the console message log |

**Recording & Tracing**

| Tool | Description |
|------|-------------|
| `browser_start_tracing` / `browser_stop_tracing` | Record a Playwright Trace |
| `browser_start_video` / `browser_stop_video` / `browser_video_chapter` | Record session video |
| `browser_pdf_save` | Save the current page as a PDF |

**Misc**

| Tool | Description |
|------|-------------|
| `browser_resize` | Resize the browser window |
| `browser_get_config` | Get the current server configuration |
| `browser_run_code_unsafe` | Execute an arbitrary Playwright script (RCE-equivalent; trusted clients only) |
| `browser_close` | Close the browser |

> **How accessibility snapshots work**: Instead of returning a screenshot, `browser_snapshot` returns a structured text tree of all visible elements with their roles, labels, and unique `[ref=…]` identifiers. LLMs reference these ids when calling `browser_click` or `browser_type` — no vision model required.

### Verify the server is working

```bash
# Start the demo app first
npm run dev

# Then run the MCP demo (connects via stdio and runs 10 browser scenarios)
npm run demo
```

---

## MCP Demo (`demo/`)

`demo/run-demo.ts` connects to `@playwright/mcp` via real MCP stdio protocol using `@modelcontextprotocol/sdk`. It runs 10 browser scenarios with no external API dependencies.

```
npm run demo
```

```
╔══════════════════════════════════════════════════════════╗
║  Playwright MCP Server — Direct MCP Protocol Demo       ║
╚══════════════════════════════════════════════════════════╝

Target: http://localhost:3000
Transport: stdio (@playwright/mcp subprocess)

Connecting to @playwright/mcp server…
Connected.

Available MCP tools (14):
  • browser_navigate
  • browser_snapshot
  • browser_click
  …

S01  ✓ PASS  Login page loads via MCP browser_navigate
S02  ✓ PASS  Fill login form using browser_type
S03  ✓ PASS  Capture login page screenshot
…
```

---

## Test Coverage

| Area | Tests | TC Range |
|------|-------|----------|
| Authentication | 25 | TC001–TC025 |
| Navigation | 20 | TC026–TC045 |
| Dashboard | 12 | TC046–TC057 |
| Task Management | 45 | TC058–TC102 |
| Categories | 14 | TC103–TC116 |
| Profile | 17 | TC117–TC133 |
| Settings | 17 | TC134–TC150 |
| UI Components | 16 | TC151–TC166 |
| **Total** | **166** | |

---

## Project Structure

```
.
├── demo-app/
│   ├── server.js                  Express static file server (port 3000)
│   └── public/
│       ├── index.html             Entry point (redirect)
│       ├── login.html
│       ├── register.html
│       ├── dashboard.html
│       ├── tasks.html             Tasks + Categories
│       ├── profile.html
│       ├── settings.html
│       ├── 404.html
│       ├── css/styles.css
│       └── js/utils.js            Shared helpers + seed data
├── tests/
│   ├── fixtures/
│   │   └── browser-commands.ts    Reusable MCP-style command fixture
│   ├── helpers/
│   │   └── auth.helper.ts         Fast localStorage-based auth setup
│   ├── auth.spec.ts               TC001–TC025
│   ├── navigation.spec.ts         TC026–TC045
│   ├── dashboard.spec.ts          TC046–TC057
│   ├── tasks.spec.ts              TC058–TC102
│   ├── categories.spec.ts         TC103–TC116
│   ├── profile.spec.ts            TC117–TC133
│   ├── settings.spec.ts           TC134–TC150
│   └── ui-components.spec.ts      TC151–TC166
├── demo/
│   ├── scenarios.ts               10 MCP test scenario definitions
│   └── run-demo.ts                MCP client demo (real MCP protocol)
├── playwright-report/             Generated HTML report (after npm test)
├── playwright.config.ts
├── package.json
└── tsconfig.json
```

---

## Key Design Decisions

| Decision | Rationale |
|----------|-----------|
| `data-testid` attributes on all elements | Tests don't break when CSS or text changes |
| Reusable `browser-commands.ts` fixture | Command names match `@playwright/mcp` tools — tests are portable |
| `loginViaStorage()` helper | Sets session via `localStorage.setItem` directly — 10× faster than UI login |
| Sequential workers (`workers: 1`) | Prevents `localStorage` conflicts between parallel tests |
| `retries: 1` locally, `retries: 2` on CI | Handles transient timing issues without masking real failures |
| No external API dependencies | MCP demo uses `@playwright/mcp` directly via stdio protocol |

---