npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@joekannan/playwright-framework-mcp

v1.0.2

Published

Custom MCP server for Playwright QA automation framework

Readme

playwright-framework-mcp

npm version GitHub

A custom Model Context Protocol (MCP) server that gives AI coding agents (GitHub Copilot, Claude, Cursor) the ability to scaffold, generate, run, and maintain a production-grade Playwright test automation framework — fully automatically.


Table of Contents

  1. What This Server Does
  2. Two MCP Servers: Roles Explained
  3. Prerequisites
  4. Installation
  5. Usage in Your Test Framework
  6. MCP Server Registration
  7. All 11 Tools — Full Reference
  8. Workflow: UI End-to-End Tests
  9. Workflow: API Tests
  10. Workflow: Debug a Failing Test
  11. Workflow: CI/CD Pipeline
  12. Quality Gate Rules
  13. Limitations
  14. Troubleshooting

What This Server Does

This MCP server is purpose-built for QA automation engineers. It exposes 11 tools that cover the complete lifecycle of a Playwright test project:

| Category | What the tools do | |---|---| | Scaffolding | Create a fully structured project with config, fixtures, utilities | | Page Objects | Scan live pages, extract locators, generate POM classes automatically | | Fixtures | Wire all page objects into base.fixture.ts automatically | | Test Generation | Generate UI spec files and API test suites from page objects or OpenAPI specs | | Test Execution | Run tests and get AI-ready failure reports with root cause classification | | CI/CD | Generate GitHub Actions workflow with matrix browser strategy | | Quality Gate | Enforce naming conventions, selector rules, and import standards |


Two MCP Servers: Roles Explained

This project uses two MCP servers simultaneously. They have completely different roles.

playwright-framework-mcp (this server)

Role: Code generator and analyser

  • Creates files on disk (page objects, specs, fixtures, configs)
  • Runs tsc, npm test, playwright test and parses results
  • Understands your project structure
  • Never controls a browser directly for UI interaction

@playwright/mcp (official Microsoft server)

Role: Live browser controller

  • Navigates pages: browser_navigate
  • Clicks elements: browser_click
  • Fills forms: browser_fill
  • Takes screenshots: browser_screenshot
  • Asserts visible text: browser_snapshot
  • Used when an AI agent needs to verify that a generated test works before writing it to disk

How They Work Together

AI Agent
   │
   ├─► playwright-framework-mcp     ← scaffold, generate, run, analyse
   │     scaffold_project
   │     create_page_from_url        ← scans DOM headlessly, writes .page.ts
   │     sync_fixtures               ← wires imports into base.fixture.ts
   │     create_spec_from_page       ← writes .spec.ts
   │     run_and_diagnose_tests      ← runs playwright, returns failures
   │     diagnose_test_failure       ← classifies errors + suggests fix
   │     generate_ci_workflow        ← writes GitHub Actions YAML
   │     validate_qa_standards       ← checks naming + selector rules
   │
   └─► @playwright/mcp (official)   ← live browser for verification
         browser_navigate            ← go to URL
         browser_click               ← click a button
         browser_fill                ← type into an input
         browser_snapshot            ← read DOM for assertions
         browser_screenshot          ← visual proof

Typical combined pattern:

  1. create_page_from_url (this server) scans the page and generates the POM file
  2. browser_navigate + browser_snapshot (official server) verifies the locators are correct
  3. create_spec_from_page (this server) generates the spec
  4. run_and_diagnose_tests (this server) runs it and returns any failures

Prerequisites

| Requirement | Version | Notes | |---|---|---| | Node.js | v18+ (v25 recommended) | node --version to check | | npm | v9+ | comes with Node | | Git | any | for CI/CD workflow | | VS Code | 1.100+ | for MCP integration | | GitHub Copilot | latest | or any MCP-capable AI agent |


Installation

Choose the approach that fits your workflow:

Option A — npm package (recommended, zero setup)

No installation required — npx downloads and caches the package on first use.

# Verify the server starts
npx -y @joekannan/playwright-framework-mcp

Use this in .vscode/mcp.json (see Usage in Your Test Framework).

Option B — Local clone (faster startup, works offline)

# 1. Clone the repository
git clone https://github.com/Joekannan/playwright-framework-mcp.git
cd playwright-framework-mcp

# 2. Install dependencies and build
npm install
npm run build

# 3. Verify the server starts
node dist/index.js
# Expected output on stderr: playwright-framework-mcp v1.0.0 running on stdio

# 4. Run unit tests
npm test
# Expected: 12/12 passing

Option C — Source / development

# Clone and run directly from TypeScript source (no build step needed)
git clone https://github.com/Joekannan/playwright-framework-mcp.git
cd playwright-framework-mcp
npm install
npx tsx src/index.ts

Usage in Your Test Framework

Add the MCP server to the .vscode/mcp.json file inside your test project (create the file if it doesn't exist). Three config options — pick one:

Option A — npm package (recommended)

Always uses the latest published version. No local setup needed.

{
  "servers": {
    "playwright-framework-mcp": {
      "type": "stdio",
      "command": "npx",
      "args": ["-y", "@joekannan/playwright-framework-mcp"]
    }
  }
}

Option B — Local clone (faster cold start)

Use this after cloning and building locally (see Installation → Option B).

{
  "servers": {
    "playwright-framework-mcp": {
      "type": "stdio",
      "command": "node",
      "args": ["dist/index.js"],
      "cwd": "C:\\Users\\YourName\\playwright-framework-mcp"
    }
  }
}

Replace C:\\Users\\YourName\\playwright-framework-mcp with the actual path where you cloned the repo.

Option C — Source (development / contribution)

{
  "servers": {
    "playwright-framework-mcp": {
      "type": "stdio",
      "command": "npx",
      "args": ["tsx", "src/index.ts"],
      "cwd": "/path/to/cloned/playwright-framework-mcp"
    }
  }
}

Adding the official Playwright browser server (optional but recommended)

For AI-assisted verification of generated tests, add both servers together:

{
  "servers": {
    "playwright-mcp": {
      "type": "stdio",
      "command": "npx",
      "args": ["@playwright/mcp@latest", "--browser", "chromium", "--headless"]
    },
    "playwright-framework-mcp": {
      "type": "stdio",
      "command": "npx",
      "args": ["-y", "@joekannan/playwright-framework-mcp"]
    }
  }
}

After saving mcp.json, open GitHub Copilot Chat → Agent mode — the 11 tools will appear automatically.


MCP Server Registration

Create or update .vscode/mcp.json in your workspace root (the folder you opened in VS Code, one level above playwright-framework-mcp/):

{
  "servers": {
    "playwright-mcp": {
      "type": "stdio",
      "command": "npx",
      "args": ["@playwright/mcp@latest", "--browser", "chromium", "--headless"]
    },
    "playwright-framework-mcp": {
      "type": "stdio",
      "command": "npx",
      "args": ["-y", "@joekannan/playwright-framework-mcp"]
    }
  }
}

Also create .vscode/settings.json in the workspace root to prevent VS Code red-underline false positives:

{
  "typescript.tsdk": "playwright-framework-mcp/node_modules/typescript/lib",
  "typescript.enablePromptUseWorkspaceTsdk": true
}

After saving, VS Code will prompt "Use workspace TypeScript version?" — click Allow.


All 11 Tools — Full Reference


1. scaffold_project

What it does: Creates a complete Playwright test project from scratch. Runs npm install and npx playwright install --with-deps automatically.

Required inputs:

| Parameter | Type | Example | |---|---|---| | projectName | string | "Playwright_SwagLab" | | baseUrl | string (URL) | "https://www.saucedemo.com" |

Optional inputs:

| Parameter | Type | Default | Example | |---|---|---|---| | targetDir | string (path) | current directory | "C:/Projects" | | browsers | array | ["chromium"] | ["chromium","firefox","webkit"] |

Output: Summary listing every directory and file created, plus npm install result.

Generated structure:

ProjectName/
├── api/
│   ├── client/
│   │   └── api-client.ts          ← base HTTP wrapper (GET/POST/PUT/PATCH/DELETE + Bearer auth)
│   ├── schemas/
│   │   ├── common.schemas.ts      ← ApiResponse<T>, PaginatedResponse<T>, ErrorResponse
│   │   ├── auth.schemas.ts        ← LoginRequest, RegisterRequest, LoginResponse
│   │   └── user.schemas.ts        ← UserResponse, CreateUserRequest, UpdateUserRequest
│   └── services/
│       ├── auth.service.ts        ← getToken(), login(), register()
│       └── user.service.ts        ← getAll(), getById(), create(), update(), remove()
├── pages/
│   └── base.page.ts
├── tests/
│   ├── e2e/
│   └── api/
├── fixtures/
│   ├── base.fixture.ts            ← UI page object fixtures (auto-generated by sync_fixtures)
│   └── api.fixture.ts             ← apiClient, authenticatedApiClient, authService, userService
├── helpers/
├── utils/
│   └── env.utils.ts               ← ENV constants (BASE_URL, API_BASE_URL, etc.)
├── data/test-data/
│   ├── users.json
│   └── api-request-bodies.json    ← sample bodies for login, createUser, updateItem, etc.
├── config/
├── .github/
│   ├── agents/
│   └── workflows/
├── playwright.config.ts
├── package.json
├── tsconfig.json
├── .env.qa
└── .gitignore

API layer design (SDET pattern):

| Layer | Location | Responsibility | |---|---|---| | Client | api/client/api-client.ts | Raw HTTP — one method per verb, injects Bearer token | | Schemas | api/schemas/*.schemas.ts | TypeScript interfaces split by domain | | Services | api/services/*.service.ts | Typed resource operations — one class per REST resource | | Fixtures | fixtures/api.fixture.ts | Injects ready-to-use services into every API test |

Tests use the service layer directly — no raw URLs, no manual Authorization headers:

// tests/api/users.api.spec.ts
import { test, expect } from '../../fixtures/api.fixture';

test('GET /users returns paginated list @smoke', async ({ userService }) => {
  const result = await userService.getAll();
  expect(result.data.length).toBeGreaterThan(0);
});

api.fixture.ts resolves auth automatically — it first tries API_TOKEN from .env.qa, then falls back to logging in with API_USERNAME + API_PASSWORD. Add new resources by creating a new *.service.ts alongside user.service.ts and extending ApiFixtures in api.fixture.ts.


2. extract_page_locators

What it does: Opens a headless browser, navigates to a URL, and returns every [data-test] element as a structured table. Does NOT write any files. Use this to preview locators before generating a page object, or to debug a failing test.

Required inputs:

| Parameter | Type | Example | |---|---|---| | url | string (URL) | "https://www.saucedemo.com" |

Optional inputs:

| Parameter | Type | Example | |---|---|---| | loginUrl | string (URL) | "https://www.saucedemo.com" | | username | string | "standard_user" | | password | string | "secret_sauce" |

Sample output:

Found 5 [data-test] element(s) on: https://www.saucedemo.com

locatorName                  selector                                 tag        type       visibility
─────────────────────────── ──────────────────────────────────────── ────────── ────────── ─────────
username                     [data-test="username"]                   input      text       private
password                     [data-test="password"]                   input      password   private
loginBtn                     [data-test="login-btn"]                  input      submit     private
loginErrorButton             [data-test="login-error-button"]         button     —          private
errorMessage                 [data-test="error-message"]              h3         —          readonly

✅ Pass this list to create_page_from_url to generate the Page Object file.

When to use it:

  • Before create_page_from_url — preview what will be generated
  • When a test fails with "locator not found" — check if the data-test attribute was renamed or removed
  • Exploring a new page before writing tests

3. create_page_from_url

What it does: Combines browser scan + page object generation into one step. Opens headless Chromium, scans for [data-test] elements, generates a ClassName.page.ts file that extends BasePage, and writes it to disk.

Required inputs:

| Parameter | Type | Example | |---|---|---| | url | string (URL) | "https://www.saucedemo.com/inventory.html" | | pageName | string (PascalCase, no "Page" suffix) | "Inventory" | | pagesDir | string (absolute path) | "C:/Projects/MyApp/pages" |

Optional inputs:

| Parameter | Type | Example | |---|---|---| | loginUrl | string (URL) | "https://www.saucedemo.com" | | username | string | "standard_user" | | password | string | "secret_sauce" |

Output: Path to the written file, count of locators, private vs readonly breakdown.


⚠️ Critical: always pass login credentials for pages behind authentication

The scanner opens a plain headless browser with no session. If the target url requires a login and you do not provide loginUrl + username + password, the browser will be redirected to the login page. The scanner will then capture login page locators instead of the locators of the page you asked for. The goto() URL in the generated file will still point to your target page, but every single locator and method will belong to the login page — making the page object completely wrong.

What goes wrong without credentials (example):

You call:

url:      "https://www.saucedemo.com/inventory.html"
pageName: "Inventory"

The browser opens inventory.html, gets redirected to the login page, and produces:

export class InventoryPage extends BasePage {
  // ❌ ALL of these belong to the login page — wrong page was scanned
  private readonly username: Locator;
  private readonly password: Locator;
  private readonly loginButton: Locator;
  readonly loginContainer: Locator;

  async goto(): Promise<void> {
    await this.navigate('https://www.saucedemo.com/inventory.html'); // ← URL is correct
  }

  async fillCredentials(...) { ... } // ← login method on an inventory page object!
  async login(...) { ... }           // ← completely wrong
}

The correct call (with credentials):

url:      "https://www.saucedemo.com/inventory.html"
pageName: "Inventory"
pagesDir: "C:/Projects/MyApp/pages"
loginUrl: "https://www.saucedemo.com"   ← always the login page URL
username: "standard_user"               ← test account username
password: "secret_sauce"               ← test account password

This produces the correct page object with inventory-specific locators only.

Rule: when to pass login credentials

| Page | Pass loginUrl + credentials? | |---|---| | Login page itself | No — it is publicly accessible | | Every other page in the app | Yes — always |

The loginUrl is always the same login page URL for all calls. Only url and pageName change per page:

# Login page — no credentials needed
url: "https://www.saucedemo.com"   pageName: "Login"

# All authenticated pages — always include loginUrl + username + password
url: "https://www.saucedemo.com/inventory.html"          pageName: "Inventory"   loginUrl: "https://www.saucedemo.com"   username: "standard_user"   password: "secret_sauce"
url: "https://www.saucedemo.com/cart.html"               pageName: "Cart"        loginUrl: "https://www.saucedemo.com"   username: "standard_user"   password: "secret_sauce"
url: "https://www.saucedemo.com/checkout-step-one.html" pageName: "Checkout"    loginUrl: "https://www.saucedemo.com"   username: "standard_user"   password: "secret_sauce"

Generated file example (inventory.page.ts) — with credentials:

// pages/inventory.page.ts
import { Page, Locator } from '@playwright/test';
import { BasePage } from './base.page';

export class InventoryPage extends BasePage {
  private readonly shoppingCartLink: Locator;
  private readonly productSortContainer: Locator;
  readonly shoppingCartBadge: Locator;
  readonly inventoryList: Locator;
  readonly inventoryItem: Locator;
  readonly inventoryItemName: Locator;
  readonly inventoryItemPrice: Locator;

  constructor(page: Page) {
    super(page);
    this.shoppingCartLink = page.locator('[data-test="shopping-cart-link"]');
    this.productSortContainer = page.locator('[data-test="product-sort-container"]');
    this.shoppingCartBadge = page.locator('[data-test="shopping-cart-badge"]');
    this.inventoryList = page.locator('[data-test="inventory-list"]');
    this.inventoryItem = page.locator('[data-test="inventory-item"]');
    this.inventoryItemName = page.locator('[data-test="inventory-item-name"]');
    this.inventoryItemPrice = page.locator('[data-test="inventory-item-price"]');
  }

  async goto(): Promise<void> {
    await this.navigate('https://www.saucedemo.com/inventory.html');
  }

  async sortBy(option: string): Promise<void> {
    await this.productSortContainer.selectOption(option);
  }

  async openCart(): Promise<void> {
    await this.shoppingCartLink.click();
  }

  async addToCart(itemSlug: string): Promise<void> {
    await this.page.locator(`[data-test="add-to-cart-${itemSlug}"]`).click();
  }

  async getItemNames(): Promise<string[]> {
    return this.inventoryItemName.allInnerTexts();
  }

  async getItemPrices(): Promise<number[]> {
    const texts = await this.inventoryItemPrice.allInnerTexts();
    return texts.map((t) => parseFloat(t.replace(/[^0-9.]/g, '')));
  }
}

Locator visibility rules:

  • private readonly — interactive elements (input, button, select, textarea, a, submit)
  • readonly — display elements (h1, span, div, p, etc.) used in assertions

4. sync_fixtures

What it does: Scans the pages/ directory, finds all *.page.ts files (excluding base.page.ts), derives class names, and writes a complete fixtures/base.fixture.ts that wires every page object into Playwright's test fixture system.

Required inputs:

| Parameter | Type | Example | |---|---|---| | pagesDir | string (absolute path) | "C:/Projects/MyApp/pages" | | fixturesDir | string (absolute path) | "C:/Projects/MyApp/fixtures" |

Output: Path to written file, list of page objects wired.

Generated file example:

// fixtures/base.fixture.ts
// Auto-generated by sync_fixtures — do not edit manually.
import { test as base } from '@playwright/test';
import { LoginPage } from '../pages/login.page.js';
import { InventoryPage } from '../pages/inventory.page.js';

export type TestFixtures = {
  loginPage: LoginPage;
  inventoryPage: InventoryPage;
};

export const test = base.extend<TestFixtures>({
  loginPage: async ({ page }, use) => {
    await use(new LoginPage(page));
  },
  inventoryPage: async ({ page }, use) => {
    await use(new InventoryPage(page));
  },
});

export { expect } from '@playwright/test';

Re-run after every new page object — it is safe to overwrite.


5. create_spec_from_page

What it does: Reads a .page.ts file, extracts the class name and all async methods, auto-generates test scenarios, and writes a .spec.ts file.

Required inputs:

| Parameter | Type | Example | |---|---|---| | pageFilePath | string (absolute path) | "C:/Projects/MyApp/pages/login.page.ts" | | specsDir | string (absolute path) | "C:/Projects/MyApp/tests/e2e/login" | | baseUrl | string (URL) | "https://www.saucedemo.com" |

Optional inputs:

| Parameter | Type | Default | Example | |---|---|---|---| | tags | array | ["@smoke"] | ["@smoke","@regression"] |

Output: Path to written file, list of generated test titles.

Generated file example:

// tests/e2e/login/login.spec.ts
import { test, expect } from '../../fixtures/base.fixture';
import { LoginPage } from '../../pages/login.page';

test.describe('LoginPage', () => {
  test('should load Login page successfully @smoke', async ({ page }) => {
    const loginPage = new LoginPage(page);
    await loginPage.goto();
    expect(page.url()).toContain('https://www.saucedemo.com');
  });

  test('should login successfully @smoke', async ({ page }) => {
    const loginPage = new LoginPage(page);
    await loginPage.goto();
    // TODO: set up test data
    await loginPage.login();
    // TODO: assert expected outcome
  });
});

6. extract_api_scenarios

What it does: Reads an OpenAPI 3.x spec and extracts all HTTP operations as a structured list of scenarios ready to pass directly into generate_api_test.

Supports three input modes — use whichever is most convenient:

| Mode | When to use | |---|---| | specFilePath.json file | You have the spec saved as JSON on disk | | specFilePath.yaml / .yml file | You have the spec saved as YAML on disk | | specContent → raw string | You want to paste the YAML/JSON content directly — no file needed |

Inputs (provide one of the two):

| Parameter | Type | Required | Example | |---|---|---|---| | specFilePath | string (absolute path) | one of these | "C:/Projects/MyApp/data/openapi.json" | | specContent | string (raw YAML or JSON) | one of these | paste the full YAML/JSON content |

Output: JSON array of scenario objects — paste directly into generate_api_test.scenarios.


Example 1 — using a file path (JSON):

specFilePath: "C:/Projects/MyApp/data/openapi.json"

Example 2 — using a file path (YAML):

specFilePath: "C:/Projects/MyApp/data/openapi.yaml"

No conversion needed — YAML is parsed automatically.

Example 3 — pasting raw YAML content directly (no file needed):

specContent: |
  openapi: 3.1.0
  info:
    title: E-commerce API
    version: 1.0.0
  paths:
    /auth/login:
      post:
        summary: Login and get access token
        requestBody:
          required: true
        responses:
          '200':
            description: Authenticated successfully
          '401':
            description: Unauthorized
    /products:
      get:
        summary: List all products
        responses:
          '200':
            description: List of products
    /cart/items:
      post:
        summary: Add item to cart
        requestBody:
          required: true
        responses:
          '200':
            description: Item added

Sample output for the above:

[
  {
    "method": "POST",
    "path": "/auth/login",
    "operationId": "post_auth_login",
    "summary": "Login and get access token",
    "expectedStatuses": [201, 400],
    "hasBody": true,
    "requiredParams": []
  },
  {
    "method": "GET",
    "path": "/products",
    "operationId": "get_products",
    "summary": "List all products",
    "expectedStatuses": [200],
    "hasBody": false,
    "requiredParams": []
  },
  {
    "method": "POST",
    "path": "/cart/items",
    "operationId": "post_cart_items",
    "summary": "Add item to cart",
    "expectedStatuses": [201, 400],
    "hasBody": true,
    "requiredParams": []
  }
]

Copy the entire JSON array and paste it into generate_api_test.scenarios.


7. generate_api_test

What it does: Reads an OpenAPI 3.x spec and extracts all HTTP operations as a structured list of scenarios ready to pass directly into generate_api_test.

Supports three input modes — use whichever is most convenient:

| Mode | When to use | |---|---| | specFilePath.json file | You have the spec saved as JSON on disk | | specFilePath.yaml / .yml file | You have the spec saved as YAML on disk | | specContent → raw string | You want to paste the YAML/JSON content directly — no file needed |

Inputs (provide one of the two):

| Parameter | Type | Required | Example | |---|---|---|---| | specFilePath | string (absolute path) | one of these | "C:/Projects/MyApp/data/openapi.json" | | specContent | string (raw YAML or JSON) | one of these | paste the full YAML/JSON content |

Output: JSON array of scenario objects — paste directly into generate_api_test.scenarios.


Example 1 — using a file path (JSON):

specFilePath: "C:/Projects/MyApp/data/openapi.json"

Example 2 — using a file path (YAML):

specFilePath: "C:/Projects/MyApp/data/openapi.yaml"

No conversion needed — YAML is parsed automatically.

Example 3 — pasting raw YAML content directly (no file needed):

specContent: |
  openapi: 3.1.0
  info:
    title: E-commerce API
    version: 1.0.0
  paths:
    /auth/login:
      post:
        summary: Login and get access token
        requestBody:
          required: true
        responses:
          '200':
            description: Authenticated successfully
          '401':
            description: Unauthorized
    /products:
      get:
        summary: List all products
        responses:
          '200':
            description: List of products
    /cart/items:
      post:
        summary: Add item to cart
        requestBody:
          required: true
        responses:
          '200':
            description: Item added

Sample output for the above:

[
  {
    "method": "POST",
    "path": "/auth/login",
    "operationId": "post_auth_login",
    "summary": "Login and get access token",
    "expectedStatuses": [201, 400],
    "hasBody": true,
    "requiredParams": []
  },
  {
    "method": "GET",
    "path": "/products",
    "operationId": "get_products",
    "summary": "List all products",
    "expectedStatuses": [200],
    "hasBody": false,
    "requiredParams": []
  },
  {
    "method": "POST",
    "path": "/cart/items",
    "operationId": "post_cart_items",
    "summary": "Add item to cart",
    "expectedStatuses": [201, 400],
    "hasBody": true,
    "requiredParams": []
  }
]

Copy the entire JSON array and paste it into generate_api_test.scenarios.


8. run_and_diagnose_tests

What it does: Runs npx playwright test in the target project, captures JSON results, classifies every failure by root cause, and returns an AI-friendly diagnosis report.

Required inputs:

| Parameter | Type | Example | |---|---|---| | projectDir | string (absolute path) | "C:/Projects/MyApp" |

Optional inputs:

| Parameter | Type | Example | |---|---|---| | grep | string | "@smoke" "login" | | project | string | "chromium" "firefox" |

Failure classifications:

| Type | Meaning | Common fix | |---|---|---| | selector | [data-test] not found | Re-run extract_page_locators to check current DOM | | assertion | expect() value mismatch | Check expected value in test | | timeout | Page/element load exceeded | Add waitForLoadState or increase timeout | | navigation | Page did not load | Check URL and network | | import | Module not found | Run sync_fixtures then npm install |


9. diagnose_test_failure

What it does: Deep analysis of a single test failure. Takes the raw error message and the test file path, classifies the root cause, and suggests a precise fix.

Required inputs:

| Parameter | Type | Example | |---|---|---| | errorMessage | string | "Error: locator('[data-test=\"login-btn\"]') not found" | | testFilePath | string (absolute path) | "C:/Projects/MyApp/tests/e2e/login/login.spec.ts" |


10. generate_ci_workflow

What it does: Writes a .github/workflows/playwright.yml GitHub Actions workflow with parallel browser matrix, npm ci, npx playwright install, artifact upload for HTML reports.

Required inputs:

| Parameter | Type | Example | |---|---|---| | projectDir | string (absolute path) | "C:/Projects/MyApp" |

Optional inputs:

| Parameter | Type | Default | Example | |---|---|---|---| | browsers | array | ["chromium"] | ["chromium","firefox","webkit"] | | nodeVersion | string | "lts/*" | "20" |

Generated workflow features:

  • Matrix strategy per browser (parallel jobs)
  • fail-fast: false — other browsers keep running if one fails
  • npm ci for reproducible installs
  • HTML report uploaded as artifact for 30 days
  • workflow_dispatch for manual runs
  • Secrets-ready env vars commented in

11. validate_qa_standards

What it does: Lints a spec or page object file against the team's QA standards. Returns a list of violations with line-level detail.

Required inputs:

| Parameter | Type | Example | |---|---|---| | content | string | paste the file content | | fileType | enum | "spec" "page-object" "api-spec" "fixture" |

Rules enforced:

| Rule | Applies to | What is checked | |---|---|---| | No page.locator() in specs | spec | Use page objects instead | | No waitForTimeout | spec, page-object | Use proper waiting strategies | | No test.only | spec | Would skip all other tests in CI | | Import from fixture, not @playwright/test | spec | Must use base.fixture.ts | | Tags required | spec | Every test must have @smoke or @regression | | Must extend BasePage | page-object | Enforce inheritance | | No axios/fetch | api-spec | Use Playwright's request fixture only | | Has request.* call | api-spec | Must have an actual HTTP call |


Workflow: UI End-to-End Tests

This is the complete tool call order for building and running UI tests from zero.

Step 1  scaffold_project
Step 2  extract_page_locators     ← optional preview / verify
Step 3  create_page_from_url      ← repeat for each page
Step 4  sync_fixtures             ← run once after all pages done
Step 5  validate_qa_standards     ← check page-object quality
Step 6  create_spec_from_page     ← repeat for each page
Step 7  validate_qa_standards     ← check spec quality
Step 8  run_and_diagnose_tests
Step 9  diagnose_test_failure     ← only if failures in step 8
Step 8  run_and_diagnose_tests    ← re-run after fixing
Step 10 generate_ci_workflow

Step-by-step with sample inputs (SauceDemo example)

Step 1 — scaffold_project

projectName: "Playwright_SwagLab"
targetDir:   "C:/Projects"
baseUrl:     "https://www.saucedemo.com"
browsers:    ["chromium"]

Step 2 — extract_page_locators (preview login page)

url: "https://www.saucedemo.com"

→ Review the table. Confirms username, password, loginBtn exist with correct selectors.

Step 3a — create_page_from_url (Login page)

url:      "https://www.saucedemo.com"
pageName: "Login"
pagesDir: "C:/Projects/Playwright_SwagLab/pages"

Step 3b — create_page_from_url (Inventory page — login required)

url:       "https://www.saucedemo.com/inventory.html"
pageName:  "Inventory"
pagesDir:  "C:/Projects/Playwright_SwagLab/pages"
loginUrl:  "https://www.saucedemo.com"
username:  "standard_user"
password:  "secret_sauce"

Step 3c — create_page_from_url (Cart page — login required)

url:       "https://www.saucedemo.com/cart.html"
pageName:  "Cart"
pagesDir:  "C:/Projects/Playwright_SwagLab/pages"
loginUrl:  "https://www.saucedemo.com"
username:  "standard_user"
password:  "secret_sauce"

Step 4 — sync_fixtures

pagesDir:    "C:/Projects/Playwright_SwagLab/pages"
fixturesDir: "C:/Projects/Playwright_SwagLab/fixtures"

→ Writes base.fixture.ts wiring loginPage, inventoryPage, cartPage.

Step 5 — validate_qa_standards (page object)

content:  <paste contents of login.page.ts>
fileType: "page-object"

Step 6a — create_spec_from_page (Login)

pageFilePath: "C:/Projects/Playwright_SwagLab/pages/login.page.ts"
specsDir:     "C:/Projects/Playwright_SwagLab/tests/e2e/login"
baseUrl:      "https://www.saucedemo.com"
tags:         ["@smoke"]

Step 7 — validate_qa_standards (spec)

content:  <paste contents of login.spec.ts>
fileType: "spec"

Step 8 — run_and_diagnose_tests

projectDir: "C:/Projects/Playwright_SwagLab"
grep:       "@smoke"

Step 9 — diagnose_test_failure (if failures)

errorMessage: "<paste the error>"
testFilePath: "C:/Projects/Playwright_SwagLab/tests/e2e/login/login.spec.ts"

→ Fix the TODO stubs in the spec, then repeat Step 8.

Step 10 — generate_ci_workflow

projectDir: "C:/Projects/Playwright_SwagLab"
browsers:   ["chromium", "firefox", "webkit"]

Where the official @playwright/mcp server is used in this workflow

The official @playwright/mcp server is invoked by the AI agent (not by you manually) at these points:

  • After create_page_from_url — agent uses browser_navigate + browser_snapshot to verify the locators are still valid on the live page before writing the spec
  • When a test fails with selector classification — agent uses browser_navigate + browser_snapshot to inspect the current DOM state and see what data-test values are actually present
  • Filling in TODO stubs — agent uses browser_navigatebrowser_fillbrowser_clickbrowser_snapshot to discover the exact steps needed for each action method, then writes them into the spec

You do not call these tools yourself — the AI agent calls them automatically as part of reasoning about your app.


Workflow: API Tests

Step 1  scaffold_project               ← only if project doesn't exist yet
Step 2  extract_api_scenarios          ← if you have an OpenAPI spec
   OR   (manually write scenarios)     ← if no spec available
Step 3  generate_api_test              ← writes .api.spec.ts + updates .env.qa
Step 4  validate_qa_standards
Step 5  run_and_diagnose_tests
Step 6  diagnose_test_failure          ← only if failures
Step 7  generate_ci_workflow

Step-by-step with sample inputs (reqres.in example)

Step 2 — extract_api_scenarios (Option A: you have a YAML file on disk)

specFilePath: "C:/Projects/Playwright_SwagLab/data/openapi.yaml"

Step 2 — extract_api_scenarios (Option B: you have a JSON file on disk)

specFilePath: "C:/Projects/Playwright_SwagLab/data/openapi.json"

Step 2 — extract_api_scenarios (Option C: paste raw YAML directly — no file needed)

specContent: <paste the full YAML or JSON content here>

→ In all three cases, the output is a JSON array. Copy it and paste into generate_api_test.scenarios.

Step 3 — generate_api_test (manual scenarios for reqres.in)

apiName:    "ReqresUsers"
baseUrl:    "https://reqres.in"
outputDir:  "C:/Projects/Playwright_SwagLab/tests/api"
projectDir: "C:/Projects/Playwright_SwagLab"
scenarios:
[
  {
    "method": "GET",
    "path": "/api/users",
    "operationId": "getUsers",
    "summary": "Get paginated list of users",
    "expectedStatuses": [200],
    "hasBody": false,
    "requiredParams": []
  },
  {
    "method": "POST",
    "path": "/api/users",
    "operationId": "createUser",
    "summary": "Create a new user",
    "expectedStatuses": [201],
    "hasBody": true,
    "requiredParams": []
  },
  {
    "method": "DELETE",
    "path": "/api/users/2",
    "operationId": "deleteUser",
    "summary": "Delete a user returns 204",
    "expectedStatuses": [204],
    "hasBody": false,
    "requiredParams": []
  }
]

Step 5 — run_and_diagnose_tests (API only)

projectDir: "C:/Projects/Playwright_SwagLab"
grep:       "@smoke"
project:    "chromium"

Workflow: Debug a Failing Test

1. run_and_diagnose_tests          ← get the failure report
2. Read the "classification" field in the output
3. If "selector"  → extract_page_locators on the failing page URL
4. If "assertion" → inspect the expected vs actual values in the report
5. If "timeout"   → add waitForLoadState / increase timeout in the spec
6. If "import"    → run sync_fixtures, then npm install in projectDir
7. diagnose_test_failure           ← for complex multi-line errors
8. Fix the code
9. run_and_diagnose_tests          ← re-run to confirm green

Workflow: CI/CD Pipeline

Step 1 — generate_ci_workflow

projectDir:  "C:/Projects/Playwright_SwagLab"
browsers:    ["chromium", "firefox", "webkit"]
nodeVersion: "lts/*"

Generated workflow behaviour:

  • Triggers on push to main/master/develop and on pull requests
  • Runs 3 parallel jobs (one per browser)
  • Each job: checkout → node setup → npm ci → playwright install → run tests → upload HTML report
  • Reports artifact named playwright-report-chromium etc., retained 30 days

To add secrets (e.g. test user credentials):

env:
  STANDARD_USER: ${{ secrets.STANDARD_USER }}
  STANDARD_PASSWORD: ${{ secrets.STANDARD_PASSWORD }}

Add them in: GitHub repo → Settings → Secrets and variables → Actions.


Quality Gate Rules

Run validate_qa_standards before every commit. The following will block if violated:

Spec files (fileType: "spec")

| Rule | Why | |---|---| | No raw page.locator() calls | Forces use of page objects | | No waitForTimeout() | Flaky — use waitForLoadState or toBeVisible | | No test.only | Would silently skip all other tests in CI | | Import test/expect from ../../fixtures/base.fixture | Not from @playwright/test directly | | Every test title contains @smoke or @regression | Enables tag-based test filtering in CI |

Page object files (fileType: "page-object")

| Rule | Why | |---|---| | Class must extend BasePage | Inherit navigate(), waitForNetworkIdle() etc. |

API spec files (fileType: "api-spec")

| Rule | Why | |---|---| | No axios or fetch | Use Playwright's built-in request fixture | | Every test must call request.get/post/put/patch/delete | Catches empty test stubs | | Every test title contains @smoke or @api | Enables tag-based filtering |


Limitations

This server works best with standard web apps that follow common patterns. Some app types will produce incomplete output or require manual customisation.

Won't work — hard blockers

| Situation | Why it fails | What to do | |---|---|---| | No data-test attributes | Every locator relies on [data-test="..."]. Zero elements found = zero locators generated. | Ask developers to add data-test attributes to interactive elements. Example: <button data-test="login-btn"> | | MFA / TOTP / SMS 2FA | The login helper does: fill username → fill password → click submit → wait. It cannot enter a one-time code. | Pre-create a session token and inject it via localStorage/cookie manually before running. | | SSO / OAuth2 / SAML (Okta, Azure AD, Google Sign-In) | Login redirects to an external identity provider. The helper cannot follow that flow. | Use a direct API token or bypass SSO for test accounts. | | iframes and Shadow DOM | document.querySelectorAll('[data-test]') only scans the top-level document. Elements inside <iframe> or Shadow DOM hosts are invisible. | Use extract_page_locators to confirm, then add the iframe/shadow selectors manually to the generated page object. | | Corporate VPN / proxy required | discoverRuntimeValues launches a plain chromium.launch() with no proxy config. | Add { proxy: { server: '...' } } to the chromium.launch() calls in page-objects.ts and test-generator.ts. | | CAPTCHA on login | The login helper cannot solve CAPTCHAs. | Disable CAPTCHA for test environments, or use a test-only bypass key. |


Works partially — generates TODO placeholders instead of real values

| Situation | What is missing | Fix | |---|---|---| | Multi-step login (username → next page → password, e.g. Gmail-style) | Post-login path and item slugs not discovered. The helper fills the username then cannot find the password field on the next screen. | Provide credentials and let the AI agent fill in the TODO stubs manually using the official @playwright/mcp browser tools. | | Infinite scroll / lazy-loaded items | Only the first batch of items visible at networkidle are scanned. Slug list is incomplete. | Scroll the page before scanning, or add extra slugs to the spec manually. | | Custom dropdowns (not <select>) | Sort/filter options use React/Angular custom components. identifySortOptions only reads <select> elements. selectOptions stays empty. | Add sort scenario values manually after generation. | | Error toast that auto-dismisses | Bad-credential probe fires and reads the DOM. If the error disappears before the read, errorText stays null. | Inspect the app manually and add the error string to the spec. | | Non-English UI | identifySortOptions pattern-matches English phrases ("az", "NAME_ASC", "A-Z", "price-low", etc.). Non-English option labels fall through to generic option_0, option_1 names. | Replace the generic names with the correct translated option values. | | Hash routing (/#/dashboard) | Post-login URL assertion captures the pathname. Hash-only changes won't be captured correctly. | Update the toHaveURL assertion in the generated spec to match the actual hash path. |


Needs manual customisation — wrong assumptions for some apps

| Situation | Default assumption | How to customise | |---|---|---| | App stays at / after login | postLoginPath captures the URL pathname. If it's /, the assertion toHaveURL(/) is too broad. | Replace the assertion with a more specific one (element visible, title, etc.). | | WebSocket / long-polling / streaming pages | waitUntil: "networkidle" is used everywhere. Apps that keep a persistent connection will never reach networkidle (30 s timeout). | Change waitUntil to "domcontentloaded" in the relevant page.goto() calls in page-objects.ts. | | Rate-limited or short-lived login sessions | Every test run performs a full UI login. Apps with strict rate limits or very short token TTLs will cause flaky tests. | Implement a storageState session cache — log in once, save state, reuse across tests. | | Multi-tenant / subdomain-per-org apps | The scanner runs against one URL and hardcodes it in the generated page object. | Parameterise BASE_URL using .env.qa per environment, or generate separate page objects per tenant. | | Dynamic data-test IDs (e.g. data-test="item-123" where 123 is a DB ID) | Generic prefix grouping detects the pattern and generates a parameterised method. But if IDs change between environments, the discovered slugs won't match. | Replace the hardcoded slugs in the spec with values read from an API call or test data file. |


Troubleshooting

MCP Inspector: "Error Connecting to MCP Inspector Proxy"

When you refresh the browser, the SSE connection drops and the proxy crashes. Always restart:

# Ctrl+C to stop, then:
npm run inspect
# Click Connect in the browser — do not refresh

Red underlines in index.ts / VS Code

VS Code is using its bundled TypeScript instead of the workspace version. Ensure .vscode/settings.json exists at the workspace root:

{
  "typescript.tsdk": "playwright-framework-mcp/node_modules/typescript/lib",
  "typescript.enablePromptUseWorkspaceTsdk": true
}

Then: Command Palette → TypeScript: Select TypeScript VersionUse Workspace Version.

generate_api_test error: "Expected object, received array"

You double-wrapped the scenarios array. The scenarios field value must start with [{ not [[{. In the MCP Inspector, paste the entire JSON array directly into the scenarios field — do not add it as a child item.

API tests fail immediately with connection error or empty URL

BASE_URL is empty — API_BASE_URL is not set in .env.qa. The generated spec has no hardcoded fallback.

Fix: open .env.qa in your project root and add:

API_BASE_URL=https://your-api.com

Or re-run generate_api_test with projectDir pointing to your project — it sets this automatically.

API tests fail with 401 Unauthorized on every request

The API requires authentication. Add an Authorization header to each request:

const AUTH_HEADERS = { Authorization: `Bearer ${process.env['API_TOKEN'] ?? ''}` };
// then in each test:
const response = await request.get(`${BASE_URL}/endpoint`, { headers: AUTH_HEADERS });

Store the token in .env.qa as API_TOKEN=your-token-here.

extract_api_scenarios treats pasted YAML as a file path

You put the YAML content into the specFilePath field. Leave specFilePath blank and paste the YAML into specContent instead.

| Field | What to put in it | |---|---| | specFilePath | Absolute path to a .json, .yaml, or .yml file on disk | | specContent | Raw YAML or JSON pasted directly as a string |

create_page_from_url generates login locators for a non-login page

You called it without loginUrl, username, and password for a page that requires authentication. The browser was redirected to the login page and scanned that instead. The symptom is a page object containing fillCredentials, submit, loginButton etc. for a page like InventoryPage or CartPage.

Fix: Re-run with all three login parameters:

url:      "https://yourapp.com/dashboard"
pageName: "Dashboard"
pagesDir: "C:/Projects/MyApp/pages"
loginUrl: "https://yourapp.com/login"   ← required for any authenticated page
username: "test_user"
password: "test_pass"

The loginUrl is always the same across all calls — only url and pageName change per page.

create_page_from_url finds 0 locators

The page has no [data-test] attributes. Options:

  1. Ask developers to add data-test attributes to interactive elements
  2. The page requires authentication — provide loginUrl, username, password
  3. The page is dynamically rendered — check with extract_page_locators first

sync_fixtures reports 0 page objects

No *.page.ts files exist in pagesDir, or you pointed to the wrong directory. Run create_page_from_url first.

tsc --noEmit reports errors

cd playwright-framework-mcp
npx tsc --noEmit

Common causes:

  • Missing node_modules — run npm install
  • Wrong import extension — all local imports must use .js not .ts
  • playwright package missing — run npm install playwright

Playwright browsers not installed in generated project

cd "C:/Projects/MyProject"
npx playwright install --with-deps

scaffold_project runs this automatically, but if it timed out or the network was unavailable, run it manually.


Project Structure (this MCP server)

playwright-framework-mcp/
├── src/
│   ├── index.ts                    ← Server entry point
│   └── tools/
│       ├── scaffold.ts             ← scaffold_project
│       ├── page-objects.ts         ← extract_page_locators, create_page_from_url
│       ├── fixture-manager.ts      ← sync_fixtures
│       ├── test-generator.ts       ← create_spec_from_page, extract_api_scenarios, generate_api_test
│       ├── test-runner.ts          ← run_and_diagnose_tests, diagnose_test_failure
│       ├── ci-cd.ts                ← generate_ci_workflow
│       ├── quality-gate.ts         ← validate_qa_standards
│       └── quality-gate.test.ts    ← 12 Vitest unit tests
├── .vscode/
│   └── mcp.json                    ← MCP server registration
├── package.json
├── tsconfig.json
└── README.md                       ← this file

Development Scripts

npm run dev       # Run server with tsx (for development)
npm run build     # Compile TypeScript to dist/
npm run inspect   # Launch MCP Inspector in browser
npm test          # Run Vitest unit tests (12 tests)