@joekannan/playwright-framework-mcp
v1.0.2
Published
Custom MCP server for Playwright QA automation framework
Maintainers
Readme
playwright-framework-mcp
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
- What This Server Does
- Two MCP Servers: Roles Explained
- Prerequisites
- Installation
- Usage in Your Test Framework
- MCP Server Registration
- All 11 Tools — Full Reference
- Workflow: UI End-to-End Tests
- Workflow: API Tests
- Workflow: Debug a Failing Test
- Workflow: CI/CD Pipeline
- Quality Gate Rules
- Limitations
- 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 testand 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 proofTypical combined pattern:
create_page_from_url(this server) scans the page and generates the POM filebrowser_navigate+browser_snapshot(official server) verifies the locators are correctcreate_spec_from_page(this server) generates the specrun_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-mcpUse 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 passingOption 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.tsUsage 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
└── .gitignoreAPI 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-testattribute 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
urlrequires a login and you do not provideloginUrl+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. Thegoto()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 passwordThis 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 addedSample 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 addedSample 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 failsnpm cifor reproducible installs- HTML report uploaded as artifact for 30 days
workflow_dispatchfor 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_workflowStep-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 usesbrowser_navigate+browser_snapshotto verify the locators are still valid on the live page before writing the spec - When a test fails with
selectorclassification — agent usesbrowser_navigate+browser_snapshotto inspect the current DOM state and see whatdata-testvalues are actually present - Filling in TODO stubs — agent uses
browser_navigate→browser_fill→browser_click→browser_snapshotto 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_workflowStep-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 greenWorkflow: 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/developand 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-chromiumetc., 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 refreshRed 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 Version → Use 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.comOr 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:
- Ask developers to add
data-testattributes to interactive elements - The page requires authentication — provide
loginUrl,username,password - The page is dynamically rendered — check with
extract_page_locatorsfirst
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 --noEmitCommon causes:
- Missing
node_modules— runnpm install - Wrong import extension — all local imports must use
.jsnot.ts playwrightpackage missing — runnpm install playwright
Playwright browsers not installed in generated project
cd "C:/Projects/MyProject"
npx playwright install --with-depsscaffold_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 fileDevelopment 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)