This is page 1 of 5. Use http://codebase.md/pvinis/mcp-playwright-stealth?page={x} to view the full context.
# Directory Structure
```
├── .gitattributes
├── .github
│ └── workflows
│ ├── docusaurus-gh-pages.yml
│ ├── node.js.yml
│ └── test.yml
├── .gitignore
├── Dockerfile
├── docs
│ ├── docs
│ │ ├── ai-courses
│ │ │ ├── _category_.json
│ │ │ ├── AIAgents.mdx
│ │ │ ├── GenAICourse.mdx
│ │ │ ├── img
│ │ │ │ └── GenAI.png
│ │ │ └── MachineLearning.mdx
│ │ ├── img
│ │ │ └── mcp-server.png
│ │ ├── intro.mdx
│ │ ├── local-setup
│ │ │ ├── _category_.json
│ │ │ ├── Debugging.mdx
│ │ │ ├── img
│ │ │ │ └── mcp-server.png
│ │ │ └── Installation.mdx
│ │ ├── playwright-api
│ │ │ ├── _category_.json
│ │ │ ├── Examples.md
│ │ │ ├── img
│ │ │ │ ├── api-response.png
│ │ │ │ └── playwright-api.png
│ │ │ └── Supported-Tools.mdx
│ │ ├── playwright-web
│ │ │ ├── _category_.json
│ │ │ ├── Console-Logging.mdx
│ │ │ ├── Examples.md
│ │ │ ├── img
│ │ │ │ ├── console-log.gif
│ │ │ │ ├── mcp-execution.png
│ │ │ │ └── mcp-result.png
│ │ │ ├── Recording-Actions.mdx
│ │ │ ├── Support-of-Cline-Cursor.mdx
│ │ │ └── Supported-Tools.mdx
│ │ ├── release.mdx
│ │ └── testing-videos
│ │ ├── _category_.json
│ │ ├── AIAgents.mdx
│ │ └── Bdd.mdx
│ ├── docusaurus.config.ts
│ ├── package-lock.json
│ ├── package.json
│ ├── sidebars.ts
│ ├── src
│ │ ├── components
│ │ │ └── HomepageFeatures
│ │ │ ├── index.tsx
│ │ │ ├── styles.module.css
│ │ │ └── YouTubeVideoEmbed.tsx
│ │ ├── css
│ │ │ └── custom.css
│ │ └── pages
│ │ ├── index.module.css
│ │ ├── index.tsx
│ │ └── markdown-page.md
│ ├── static
│ │ ├── .nojekyll
│ │ └── img
│ │ ├── docusaurus-social-card.jpg
│ │ ├── docusaurus.png
│ │ ├── EA-Icon.jpg
│ │ ├── EA-Icon.svg
│ │ ├── easy-to-use.svg
│ │ ├── favicon.ico
│ │ ├── logo.svg
│ │ ├── node.svg
│ │ ├── playwright.svg
│ │ ├── undraw_docusaurus_mountain.svg
│ │ ├── undraw_docusaurus_react.svg
│ │ └── undraw_docusaurus_tree.svg
│ └── tsconfig.json
├── image
│ └── playwright_claude.png
├── jest.config.cjs
├── LICENSE
├── package-lock.json
├── package.json
├── README.md
├── run-tests.cjs
├── run-tests.js
├── smithery.yaml
├── src
│ ├── __tests__
│ │ ├── codegen.test.ts
│ │ ├── toolHandler.test.ts
│ │ ├── tools
│ │ │ ├── api
│ │ │ │ └── requests.test.ts
│ │ │ └── browser
│ │ │ ├── advancedInteraction.test.ts
│ │ │ ├── console.test.ts
│ │ │ ├── goNavigation.test.ts
│ │ │ ├── interaction.test.ts
│ │ │ ├── navigation.test.ts
│ │ │ ├── output.test.ts
│ │ │ ├── screenshot.test.ts
│ │ │ └── visiblePage.test.ts
│ │ └── tools.test.ts
│ ├── index.ts
│ ├── requestHandler.ts
│ ├── toolHandler.ts
│ ├── tools
│ │ ├── api
│ │ │ ├── base.ts
│ │ │ ├── index.ts
│ │ │ └── requests.ts
│ │ ├── browser
│ │ │ ├── base.ts
│ │ │ ├── console.ts
│ │ │ ├── index.ts
│ │ │ ├── interaction.ts
│ │ │ ├── navigation.ts
│ │ │ ├── output.ts
│ │ │ ├── response.ts
│ │ │ ├── screenshot.ts
│ │ │ ├── useragent.ts
│ │ │ └── visiblePage.ts
│ │ ├── codegen
│ │ │ ├── generator.ts
│ │ │ ├── index.ts
│ │ │ ├── recorder.ts
│ │ │ └── types.ts
│ │ ├── common
│ │ │ └── types.ts
│ │ └── index.ts
│ ├── tools.ts
│ └── types.ts
├── test-import.js
├── tsconfig.json
└── tsconfig.test.json
```
# Files
--------------------------------------------------------------------------------
/docs/static/.nojekyll:
--------------------------------------------------------------------------------
```
```
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
```
package-lock.json linguist-generated=true
```
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
.cache
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
.DS_Store
.DS_Store?
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
# Playwright MCP Server 🎭
[](https://smithery.ai/server/@executeautomation/playwright-mcp-server)
A Model Context Protocol server that provides browser automation capabilities using Playwright. This server enables LLMs to interact with web pages, take screenshots, generate test code, web scraps the page and execute JavaScript in a real browser environment.
<a href="https://glama.ai/mcp/servers/yh4lgtwgbe"><img width="380" height="200" src="https://glama.ai/mcp/servers/yh4lgtwgbe/badge" alt="mcp-playwright MCP server" /></a>
## Screenshot

## [Documentation](https://executeautomation.github.io/mcp-playwright/) | [API reference](https://executeautomation.github.io/mcp-playwright/docs/playwright-web/Supported-Tools)
## Installation
You can install the package using either npm, mcp-get, or Smithery:
Using npm:
```bash
npm install -g @executeautomation/playwright-mcp-server
```
Using mcp-get:
```bash
npx @michaellatman/mcp-get@latest install @executeautomation/playwright-mcp-server
```
Using Smithery
To install Playwright MCP for Claude Desktop automatically via [Smithery](https://smithery.ai/server/@executeautomation/playwright-mcp-server):
```bash
npx -y @smithery/cli install @executeautomation/playwright-mcp-server --client claude
```
#### Installation in VS Code
Install the Playwright MCP server in VS Code using one of these buttons:
<!--
// Generate using?:
const config = JSON.stringify({ name: 'playwright', command: 'npx', args: ["-y", "@executeautomation/playwright-mcp-server"] });
const urlForWebsites = `vscode:mcp/install?${encodeURIComponent(config)}`;
// Github markdown does not allow linking to `vscode:` directly, so you can use our redirect:
const urlForGithub = `https://insiders.vscode.dev/redirect?url=${encodeURIComponent(urlForWebsites)}`;
-->
[<img alt="Install in VS Code Insiders" src="https://img.shields.io/badge/VS_Code_Insiders-VS_Code_Insiders?style=flat-square&label=Install%20Server&color=24bfa5">](https://insiders.vscode.dev/redirect?url=vscode-insiders%3Amcp%2Finstall%3F%257B%2522name%2522%253A%2522playwright%2522%252C%2522command%2522%253A%2522npx%2522%252C%2522args%2522%253A%255B%2522-y%2522%252C%2522%2540executeautomation%252Fplaywright-mcp-server%2522%255D%257D)
Alternatively, you can install the Playwright MCP server using the VS Code CLI:
```bash
# For VS Code
code --add-mcp '{"name":"playwright","command":"npx","args":["@executeautomation/playwright-mcp-server"]}'
```
```bash
# For VS Code Insiders
code-insiders --add-mcp '{"name":"playwright","command":"npx","args":["@executeautomation/playwright-mcp-server"]}'
```
After installation, the ExecuteAutomation Playwright MCP server will be available for use with your GitHub Copilot agent in VS Code.
## Configuration to use Playwright Server
Here's the Claude Desktop configuration to use the Playwright server:
```json
{
"mcpServers": {
"playwright": {
"command": "npx",
"args": ["-y", "@executeautomation/playwright-mcp-server"]
}
}
}
```
## Testing
This project uses Jest for testing. The tests are located in the `src/__tests__` directory.
### Running Tests
You can run the tests using one of the following commands:
```bash
# Run tests using the custom script (with coverage)
node run-tests.cjs
# Run tests using npm scripts
npm test # Run tests without coverage
npm run test:coverage # Run tests with coverage
npm run test:custom # Run tests with custom script (same as node run-tests.cjs)
```
The test coverage report will be generated in the `coverage` directory.
## Star History
[](https://star-history.com/#executeautomation/mcp-playwright&Date)
```
--------------------------------------------------------------------------------
/docs/src/pages/markdown-page.md:
--------------------------------------------------------------------------------
```markdown
---
title: Markdown page example
---
# Markdown page example
You don't need React to write simple standalone pages.
```
--------------------------------------------------------------------------------
/src/tools/api/index.ts:
--------------------------------------------------------------------------------
```typescript
export * from './base.js';
export * from './requests.js';
// TODO: Add exports for other API tools as they are implemented
```
--------------------------------------------------------------------------------
/docs/src/components/HomepageFeatures/styles.module.css:
--------------------------------------------------------------------------------
```css
.features {
display: flex;
align-items: center;
padding: 2rem 0;
width: 100%;
}
.featureSvg {
height: 200px;
width: 200px;
}
```
--------------------------------------------------------------------------------
/docs/docs/playwright-api/_category_.json:
--------------------------------------------------------------------------------
```json
{
"label": "Playwright API Features",
"position": 5,
"collapsed": false,
"link": {
"type": "generated-index",
"description": "Supported features in Playwright API Testing."
}
}
```
--------------------------------------------------------------------------------
/docs/docs/playwright-web/_category_.json:
--------------------------------------------------------------------------------
```json
{
"label": "Playwright Web Features",
"position": 4,
"collapsed": false,
"link": {
"type": "generated-index",
"description": "Supported features in Playwright Web browser automation."
}
}
```
--------------------------------------------------------------------------------
/docs/docs/local-setup/_category_.json:
--------------------------------------------------------------------------------
```json
{
"label": "Local Development",
"position": 3,
"collapsed": false,
"link": {
"type": "generated-index",
"description": "Understand how to setup Playwright MCP Server to run in your local machine."
}
}
```
--------------------------------------------------------------------------------
/tsconfig.test.json:
--------------------------------------------------------------------------------
```json
{
"extends": "./tsconfig.json",
"compilerOptions": {
"noImplicitAny": false,
"strictNullChecks": false,
"types": ["jest", "node"]
},
"include": ["src/**/*", "test/**/*", "src/__tests__/**/*"]
}
```
--------------------------------------------------------------------------------
/docs/tsconfig.json:
--------------------------------------------------------------------------------
```json
{
// This file is not used in compilation. It is here just for a nice editor experience.
"extends": "@docusaurus/tsconfig",
"compilerOptions": {
"baseUrl": "."
},
"exclude": [".docusaurus", "build"]
}
```
--------------------------------------------------------------------------------
/docs/docs/testing-videos/_category_.json:
--------------------------------------------------------------------------------
```json
{
"label": "MCP Testing Videos",
"position": 7,
"collapsed": false,
"link": {
"type": "generated-index",
"description": "AI Testing Videos helps you learn using MCP for Testing and Development"
}
}
```
--------------------------------------------------------------------------------
/docs/docs/ai-courses/_category_.json:
--------------------------------------------------------------------------------
```json
{
"label": "AI Courses to Learn",
"position": 6,
"collapsed": false,
"link": {
"type": "generated-index",
"description": "AI Courses which helps you learn more on Using it for Testing and Development"
}
}
```
--------------------------------------------------------------------------------
/test-import.js:
--------------------------------------------------------------------------------
```javascript
// test-import.js
import { setupRequestHandlers } from './dist/requestHandler.js';
import { handleToolCall } from './dist/toolHandler.js';
console.log('Imports successful!');
console.log('setupRequestHandlers:', typeof setupRequestHandlers);
console.log('handleToolCall:', typeof handleToolCall);
```
--------------------------------------------------------------------------------
/run-tests.js:
--------------------------------------------------------------------------------
```javascript
const { execSync } = require('child_process');
try {
console.log('Running tests...');
const output = execSync('npx jest --no-cache --coverage', { encoding: 'utf8' });
console.log(output);
} catch (error) {
console.error('Error running tests:', error.message);
console.log(error.stdout);
}
```
--------------------------------------------------------------------------------
/src/tools/browser/index.ts:
--------------------------------------------------------------------------------
```typescript
export * from './base.js';
export * from './screenshot.js';
export * from './navigation.js';
export * from './console.js';
export * from './interaction.js';
export * from './response.js';
export * from './useragent.js';
// TODO: Add exports for other browser tools as they are implemented
// export * from './interaction.js';
```
--------------------------------------------------------------------------------
/docs/src/pages/index.module.css:
--------------------------------------------------------------------------------
```css
/**
* CSS files with the .module.css suffix will be treated as CSS modules
* and scoped locally.
*/
.heroBanner {
padding: 4rem 0;
text-align: center;
position: relative;
overflow: hidden;
}
@media screen and (max-width: 996px) {
.heroBanner {
padding: 2rem;
}
}
.buttons {
display: flex;
align-items: center;
justify-content: center;
}
```
--------------------------------------------------------------------------------
/docs/src/components/HomepageFeatures/YouTubeVideoEmbed.tsx:
--------------------------------------------------------------------------------
```typescript
import React from 'react';
export default function YouTubeVideoEmbed({ videoId }) {
return (
<iframe
width="700"
height="400"
src={`https://www.youtube.com/embed/${videoId}`}
title="ExecuteAutomation YouTube"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
></iframe>
);
}
```
--------------------------------------------------------------------------------
/smithery.yaml:
--------------------------------------------------------------------------------
```yaml
# Smithery configuration file: https://smithery.ai/docs/config#smitheryyaml
startCommand:
type: stdio
configSchema:
# JSON Schema defining the configuration options for the MCP.
type: object
properties: {}
commandFunction:
# A function that produces the CLI command to start the MCP on stdio.
|-
(config) => ({command: 'node', args: ['dist/index.js'], env: {}})
```
--------------------------------------------------------------------------------
/src/types.ts:
--------------------------------------------------------------------------------
```typescript
import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
export interface Tool {
name: string;
description: string;
parameters: {
type: string;
properties: Record<string, unknown>;
required?: string[];
};
handler: (args: any) => Promise<any>;
}
export interface ToolCall {
name: string;
parameters: Record<string, unknown>;
result?: CallToolResult;
}
```
--------------------------------------------------------------------------------
/run-tests.cjs:
--------------------------------------------------------------------------------
```
const { execSync } = require('child_process');
try {
console.log("Running tests with coverage...");
execSync('npx jest --no-cache --coverage --testMatch="<rootDir>/src/__tests__/**/*.test.ts"', { stdio: 'inherit' });
console.log("Tests completed successfully!");
} catch (error) {
console.error("Error running tests:", error.message);
console.log(error.stdout?.toString() || "No output");
process.exit(1);
}
```
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
```json
{
"compilerOptions": {
"target": "ES2020",
"module": "ES2022",
"moduleResolution": "bundler",
"outDir": "./dist",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"declaration": true,
"noImplicitAny": false,
"strictNullChecks": false,
"allowJs": true,
"resolveJsonModule": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "src/__tests__"]
}
```
--------------------------------------------------------------------------------
/src/tools/index.ts:
--------------------------------------------------------------------------------
```typescript
export * from './common/types.js';
export * from './browser/index.js';
export * from './api/index.js';
// Tool type constants
export const BROWSER_TOOLS = [
"playwright_navigate",
"playwright_screenshot",
"playwright_click",
"playwright_iframe_click",
"playwright_fill",
"playwright_select",
"playwright_hover",
"playwright_evaluate",
"playwright_console_logs",
"playwright_close",
"playwright_get_visible_text",
"playwright_get_visible_html"
];
export const API_TOOLS = [
"playwright_get",
"playwright_post",
"playwright_put",
"playwright_patch",
"playwright_delete"
];
```
--------------------------------------------------------------------------------
/src/tools/codegen/types.ts:
--------------------------------------------------------------------------------
```typescript
import { ToolCall } from '../../types.js';
export interface CodegenAction {
toolName: string;
parameters: Record<string, unknown>;
timestamp: number;
result?: unknown;
}
export interface CodegenSession {
id: string;
actions: CodegenAction[];
startTime: number;
endTime?: number;
options?: CodegenOptions;
}
export interface PlaywrightTestCase {
name: string;
steps: string[];
imports: Set<string>;
}
export interface CodegenOptions {
outputPath?: string;
testNamePrefix?: string;
includeComments?: boolean;
}
export interface CodegenResult {
testCode: string;
filePath: string;
sessionId: string;
}
```
--------------------------------------------------------------------------------
/docs/docs/local-setup/Debugging.mdx:
--------------------------------------------------------------------------------
```markdown
---
sidebar_position: 2
---
import YouTubeVideoEmbed from '@site/src/components/HomepageFeatures/YouTubeVideoEmbed';
## Debugging MCP Server 🚀
Efficiently debug your **MCP Server** with **MCP Inspector** from Claude! This powerful tool helps you **speed up debugging** and testing for the tools you build for the **Playwright MCP Server**.
## Step 1 : Install the MCP Inspector
To get started, run the following command:
```bash
npx @modelcontextprotocol/inspector node dist/index.js
```
## Step 2: Navigate to MCP Inspector
```bash
http://localhost:5173 🚀
```
## Here is the video demonstration
<YouTubeVideoEmbed videoId="98l_k0XYXKs" />
```
--------------------------------------------------------------------------------
/jest.config.cjs:
--------------------------------------------------------------------------------
```
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
collectCoverage: true,
coverageDirectory: 'coverage',
coverageReporters: ['text', 'lcov'],
collectCoverageFrom: [
'src/**/*.ts',
'!src/index.ts', // exclude index.ts
],
testMatch: [
'<rootDir>/src/__tests__/**/*.test.ts'
],
modulePathIgnorePatterns: [
"<rootDir>/docs/",
"<rootDir>/dist/"
],
moduleNameMapper: {
"^(.*)\\.js$": "$1"
},
transform: {
'^.+\\.tsx?$': ['ts-jest', {
useESM: true,
tsconfig: 'tsconfig.test.json'
}],
},
extensionsToTreatAsEsm: ['.ts'],
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
};
```
--------------------------------------------------------------------------------
/docs/docs/playwright-web/Support-of-Cline-Cursor.mdx:
--------------------------------------------------------------------------------
```markdown
---
sidebar_position: 3
---
import YouTubeVideoEmbed from '@site/src/components/HomepageFeatures/YouTubeVideoEmbed';
# 💻 Support of Cline and Cursor
Playwright MCP Server now fully supports **Cline** and **Cursor**.
Below, you will find video demonstrations showing how **Cline** and **Cursor** work with the Playwright MCP Server.
---
### 🎥 Support of **Cursor**
Check out the demonstration of how **Cursor** integrates seamlessly with Playwright MCP Server:
<YouTubeVideoEmbed videoId="Fp7D7RuzElo" />
---
### 🎥 Support of **Cline**
Watch the demonstration of **Cline** in action with Playwright MCP Server:
<YouTubeVideoEmbed videoId="bUD10uvW6lw" />
---
```
--------------------------------------------------------------------------------
/docs/docs/playwright-web/Console-Logging.mdx:
--------------------------------------------------------------------------------
```markdown
---
sidebar_position: 2
---
# 📁 Support of Console Logs
Playwright MCP Server now supports Console logging of browsers with the power of Playwright.
This feature is especially useful when you want to capture the logs of the browser console while performing any action during development and testing.
Following logs types are supported
- `log`
- `info`
- `warn`
- `error`
- `debug`
- `all`
:::tip Usage Example
To invoke `Playwright_console_logs` via MCP Playwright, use the following prompt:
```plaintext
Get the console log from the browser whenever you perform any action.
:::
---
:::info

Demo of how the console logs are captured in Playwright MCP Server
:::
```
--------------------------------------------------------------------------------
/.github/workflows/node.js.yml:
--------------------------------------------------------------------------------
```yaml
# This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs
name: Node.js CI
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [18.x, 20.x, 22.x]
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
steps:
- uses: actions/checkout@v4
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- run: npm ci
- run: npm run build --if-present
```
--------------------------------------------------------------------------------
/docs/sidebars.ts:
--------------------------------------------------------------------------------
```typescript
import type {SidebarsConfig} from '@docusaurus/plugin-content-docs';
// This runs in Node.js - Don't use client-side code here (browser APIs, JSX...)
/**
* Creating a sidebar enables you to:
- create an ordered group of docs
- render a sidebar for each doc of that group
- provide next/previous navigation
The sidebars can be generated from the filesystem, or explicitly defined here.
Create as many sidebars as you want.
*/
const sidebars: SidebarsConfig = {
// By default, Docusaurus generates a sidebar from the docs folder structure
tutorialSidebar: [{type: 'autogenerated', dirName: '.'}],
// But you can create a sidebar manually
/*
tutorialSidebar: [
'intro',
'hello',
{
type: 'category',
label: 'Tutorial',
items: ['tutorial-basics/create-a-document'],
},
],
*/
};
export default sidebars;
```
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
```typescript
#!/usr/bin/env node
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { createToolDefinitions } from "./tools.js";
import { setupRequestHandlers } from "./requestHandler.js";
async function runServer() {
const server = new Server(
{
name: "executeautomation/playwright-mcp-server",
version: "1.0.3",
},
{
capabilities: {
resources: {},
tools: {},
},
}
);
// Create tool definitions
const TOOLS = createToolDefinitions();
// Setup request handlers
setupRequestHandlers(server, TOOLS);
// Create transport and connect
const transport = new StdioServerTransport();
await server.connect(transport);
}
runServer().catch((error) => {
console.error("Fatal error in main():", error);
process.exit(1);
});
```
--------------------------------------------------------------------------------
/src/tools/browser/output.ts:
--------------------------------------------------------------------------------
```typescript
import { BrowserToolBase } from './base.js';
import { ToolContext, ToolResponse, createSuccessResponse, createErrorResponse } from '../common/types.js';
import * as path from 'path';
/**
* Tool for saving page as PDF
*/
export class SaveAsPdfTool extends BrowserToolBase {
/**
* Execute the save as PDF tool
*/
async execute(args: any, context: ToolContext): Promise<ToolResponse> {
return this.safeExecute(context, async (page) => {
const filename = args.filename || 'page.pdf';
const options = {
path: path.resolve(args.outputPath || '.', filename),
format: args.format || 'A4',
printBackground: args.printBackground !== false,
margin: args.margin || {
top: '1cm',
right: '1cm',
bottom: '1cm',
left: '1cm'
}
};
await page.pdf(options);
return createSuccessResponse(`Saved page as PDF: ${options.path}`);
});
}
}
```
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
```yaml
name: Test
on:
push:
pull_request:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Node.js
uses: actions/setup-node@v3
with:
node-version: 22 # Use current LTS version
cache: 'npm'
- name: Install dependencies
run: npm install
- name: Build project
run: npm run build
- name: Run tests with coverage (custom script)
run: node run-tests.cjs
continue-on-error: true
id: custom-test
- name: Run tests with coverage (npm script)
if: steps.custom-test.outcome == 'failure'
run: npm run test:coverage -- --testMatch="<rootDir>/src/__tests__/**/*.test.ts"
- name: Upload coverage report
uses: actions/upload-artifact@v4
with:
name: coverage-report
path: coverage/
if-no-files-found: warn
```
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
```dockerfile
# Generated by https://smithery.ai. See: https://smithery.ai/docs/config#dockerfile
# Use Node.js image for building the project
FROM node:20-alpine AS builder
# Set the working directory
WORKDIR /app
# Copy package.json and package-lock.json
COPY package.json package-lock.json ./
# Install dependencies without running scripts to prevent automatic build
RUN npm install --ignore-scripts
# Copy the entire source directory
COPY src ./src
COPY tsconfig.json ./
# Build the project
RUN npm run build
# Use a minimal Node.js image for running the project
FROM node:20-alpine AS release
# Set the working directory
WORKDIR /app
# Copy the built files from the builder stage
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/package.json ./package.json
COPY --from=builder /app/package-lock.json ./package-lock.json
# Install production dependencies
RUN npm ci --ignore-scripts --omit=dev
# Set the command to run the server
ENTRYPOINT ["node", "dist/index.js"]
```
--------------------------------------------------------------------------------
/docs/src/css/custom.css:
--------------------------------------------------------------------------------
```css
/**
* Any CSS included here will be global. The classic template
* bundles Infima by default. Infima is a CSS framework designed to
* work well for content-centric websites.
*/
/* You can override the default Infima variables here. */
:root {
--ifm-color-primary: #2e8555;
--ifm-color-primary-dark: #29784c;
--ifm-color-primary-darker: #277148;
--ifm-color-primary-darkest: #205d3b;
--ifm-color-primary-light: #33925d;
--ifm-color-primary-lighter: #359962;
--ifm-color-primary-lightest: #3cad6e;
--ifm-code-font-size: 95%;
--docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.1);
}
/* For readability concerns, you should choose a lighter palette in dark mode. */
[data-theme='dark'] {
--ifm-color-primary: #25c2a0;
--ifm-color-primary-dark: #21af90;
--ifm-color-primary-darker: #1fa588;
--ifm-color-primary-darkest: #1a8870;
--ifm-color-primary-light: #29d5b0;
--ifm-color-primary-lighter: #32d8b4;
--ifm-color-primary-lightest: #4fddbf;
--docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.3);
}
```
--------------------------------------------------------------------------------
/src/tools/common/types.ts:
--------------------------------------------------------------------------------
```typescript
import type {
CallToolResult,
TextContent,
ImageContent,
} from "@modelcontextprotocol/sdk/types.js";
import type { Page, Browser, APIRequestContext } from "rebrowser-playwright";
// Context for tool execution
export interface ToolContext {
page?: Page;
browser?: Browser;
apiContext?: APIRequestContext;
server?: any;
}
// Standard response format for all tools
export interface ToolResponse extends CallToolResult {
content: (TextContent | ImageContent)[];
isError: boolean;
}
// Interface that all tool implementations must follow
export interface ToolHandler {
execute(args: any, context: ToolContext): Promise<ToolResponse>;
}
// Helper functions for creating responses
export function createErrorResponse(message: string): ToolResponse {
return {
content: [
{
type: "text",
text: message,
},
],
isError: true,
};
}
export function createSuccessResponse(
message: string | string[]
): ToolResponse {
const messages = Array.isArray(message) ? message : [message];
return {
content: messages.map((msg) => ({
type: "text",
text: msg,
})),
isError: false,
};
}
```
--------------------------------------------------------------------------------
/docs/package.json:
--------------------------------------------------------------------------------
```json
{
"name": "playwright-mcp-server",
"version": "0.0.0",
"private": true,
"scripts": {
"docusaurus": "docusaurus",
"start": "docusaurus start",
"build": "docusaurus build",
"swizzle": "docusaurus swizzle",
"deploy": "docusaurus deploy",
"clear": "docusaurus clear",
"serve": "docusaurus serve",
"write-translations": "docusaurus write-translations",
"write-heading-ids": "docusaurus write-heading-ids",
"typecheck": "tsc"
},
"dependencies": {
"@docusaurus/core": "3.6.3",
"@docusaurus/preset-classic": "3.6.3",
"@mdx-js/react": "^3.0.0",
"clsx": "^2.0.0",
"gh-pages": "^6.2.0",
"prism-react-renderer": "^2.3.0",
"react": "^18.0.0",
"react-dom": "^18.0.0"
},
"devDependencies": {
"@docusaurus/module-type-aliases": "3.6.3",
"@docusaurus/tsconfig": "3.6.3",
"@docusaurus/types": "3.6.3",
"typescript": "~5.6.2"
},
"browserslist": {
"production": [
">0.5%",
"not dead",
"not op_mini all"
],
"development": [
"last 3 chrome version",
"last 3 firefox version",
"last 5 safari version"
]
},
"engines": {
"node": ">=18.0"
}
}
```
--------------------------------------------------------------------------------
/docs/src/pages/index.tsx:
--------------------------------------------------------------------------------
```typescript
import clsx from 'clsx';
import Link from '@docusaurus/Link';
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
import Layout from '@theme/Layout';
import HomepageFeatures from '@site/src/components/HomepageFeatures';
import Heading from '@theme/Heading';
import styles from './index.module.css';
function HomepageHeader() {
const {siteConfig} = useDocusaurusContext();
return (
<header className={clsx('hero hero--primary', styles.heroBanner)}>
<div className="container">
<Heading as="h1" className="hero__title">
{siteConfig.title}
</Heading>
<p className="hero__subtitle">{siteConfig.tagline}</p>
<div className={styles.buttons}>
<Link
className="button button--secondary button--lg"
to="/docs/intro">
Playwright MCP Server Tutorial - 5min ⏱️
</Link>
</div>
</div>
</header>
);
}
export default function Home(): JSX.Element {
const {siteConfig} = useDocusaurusContext();
return (
<Layout
title={`${siteConfig.title}`}
description="Description will go into a meta tag in <head />">
<HomepageHeader />
<main>
<HomepageFeatures />
</main>
</Layout>
);
}
```
--------------------------------------------------------------------------------
/docs/docs/local-setup/Installation.mdx:
--------------------------------------------------------------------------------
```markdown
---
sidebar_position: 1
---
## Build and Run Playwright MCP Server locally
To build/run Playwright MCP Server in your local machine follow the below steps
### Step 1 : Clone Repository
```bash
git clone https://github.com/executeautomation/mcp-playwright.git
```
## Step 2: Install Dependencies
```bash
npm install
```
## Step 3: Build Code
```bash
npm run build
npm link
```
## Step 4: Configuring Playwright MCP in Claude Desktop
Modify your `claude-desktop-config.json` file as shown below to work with local playwright mcp server
```json
{
"mcpServers": {
"playwright": {
"command": "npx",
"args": [
"--directory",
"/your-playwright-mcp-server-clone-directory",
"run",
"@executeautomation/playwright-mcp-server"
]
}
}
}
```
:::warning Important
After modifying the `claude-desktop-config.json` file, you **must** completely close Claude Desktop and **manually terminate** any running processes from **Task Manager** (Windows 10/11).
⚠️ If you skip this step, the configuration changes **may not take effect**.
:::
## Reward
If your setup is all correct, you should see Playwright MCP Server pointing your local machine source code

```
--------------------------------------------------------------------------------
/src/tools/browser/useragent.ts:
--------------------------------------------------------------------------------
```typescript
import { BrowserToolBase } from './base.js';
import type { ToolContext, ToolResponse } from '../common/types.js';
import { createSuccessResponse, createErrorResponse } from '../common/types.js';
interface CustomUserAgentArgs {
userAgent: string;
}
/**
* Tool for validating custom User Agent settings
*/
export class CustomUserAgentTool extends BrowserToolBase {
/**
* Execute the custom user agent tool
*/
async execute(args: CustomUserAgentArgs, context: ToolContext): Promise<ToolResponse> {
return this.safeExecute(context, async (page) => {
if (!args.userAgent) {
return createErrorResponse("Missing required parameter: userAgent must be provided");
}
try {
const currentUserAgent = await page.evaluate(() => navigator.userAgent);
if (currentUserAgent !== args.userAgent) {
const messages = [
"Page was already initialized with a different User Agent.",
`Requested: ${args.userAgent}`,
`Current: ${currentUserAgent}`
];
return createErrorResponse(messages.join('\n'));
}
return createSuccessResponse("User Agent validation successful");
} catch (error) {
return createErrorResponse(`Failed to validate User Agent: ${(error as Error).message}`);
}
});
}
}
```
--------------------------------------------------------------------------------
/.github/workflows/docusaurus-gh-pages.yml:
--------------------------------------------------------------------------------
```yaml
name: Deploy Docusaurus documentation to GH Pages
on:
# Runs on pushes targeting the default branch
push:
branches: ["main"]
pull_request:
branches: [ "main" ]
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
permissions:
contents: read
pages: write
id-token: write
# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
concurrency:
group: "pages"
cancel-in-progress: false
jobs:
# Build job
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install dependencies
working-directory: docs
run: npm install
- name: Build site
working-directory: docs
run: npm run build
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
path: docs/build
# Deployment job
deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
needs: build
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
```
--------------------------------------------------------------------------------
/docs/docs/testing-videos/Bdd.mdx:
--------------------------------------------------------------------------------
```markdown
---
sidebar_position: 2
---
import YouTubeVideoEmbed from '@site/src/components/HomepageFeatures/YouTubeVideoEmbed';
# 🎭 BDD Testing with Playwright MCP Server 🥒
In this video, we explore **Behavior Driven Development (BDD) testing** using **Playwright MCP Server**, without writing a single line of code—just seamless automation!
Watch how **Cursor + MCP Servers** transform testing into a fully automated, hands-free process! 🔥
<div align="center">
<YouTubeVideoEmbed videoId="5knmhGep2o4" />
</div>
---
:::info 💡 **Note**
We will be using the following **MCP Server Tools**:
1. **Playwright MCP Server** (This is the tool referenced in the documentation)
:::
---
## 📚 **BDD Scenario for Login**
```gherkin
Feature: Login
Scenario: To Perform login operation in EAApp website
Given I navigate to "https://eaapp.somee.com/"
When I click on "Login" button
And I enter "admin" in "UserName" field
And I enter "password" in "Password" field
And I click on "Login" button
Then I should see the Employee Details menu
```
---
## 📚 **BDD Scenario for Console Logs**
```gherkin
Feature: Network Console
Scenario: To Perform Network Console operation
Given I navigate to amazon.com
And I handle if there is any popup
And I click the All menu from the Amazon website
And I read the network console logs
When I see any error in the network console logs
Then I print them out
And I close the browser
```
---
This structured format enhances readability and ensures clarity for developers and testers. 🚀
```
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
```json
{
"name": "@pvinis/playwright-stealth-mcp-server",
"version": "1.0.4",
"description": "Model Context Protocol servers for Playwright (with stealth)",
"license": "MIT",
"author": "ExecuteAutomation, Ltd (https://executeautomation.com)",
"homepage": "https://executeautomation.github.io/mcp-playwright/",
"bugs": "https://github.com/executeautomation/mcp-playwright/issues",
"types": "dist/index.d.ts",
"type": "module",
"bin": {
"playwright-mcp-server": "dist/index.js"
},
"files": [
"dist"
],
"scripts": {
"build": "tsc && shx chmod +x dist/*.js",
"prepare": "npm run build",
"watch": "tsc --watch",
"test": "jest --testMatch=\"<rootDir>/src/__tests__/**/*.test.ts\"",
"test:coverage": "jest --coverage --testMatch=\"<rootDir>/src/__tests__/**/*.test.ts\"",
"test:custom": "node run-tests.cjs"
},
"repository": {
"type": "git",
"url": "https://github.com/executeautomation/mcp-playwright.git"
},
"dependencies": {
"@modelcontextprotocol/sdk": "1.6.1",
"@playwright/browser-chromium": "1.51.1",
"@playwright/browser-firefox": "1.51.1",
"@playwright/browser-webkit": "1.51.1",
"@playwright/test": "^1.51.1",
"rebrowser-playwright": "1.49.1",
"uuid": "^9.0.1"
},
"keywords": [
"playwright",
"automation",
"AI",
"Claude MCP"
],
"devDependencies": {
"@types/jest": "^29.5.14",
"@types/node": "^20.10.5",
"jest": "^29.7.0",
"jest-playwright-preset": "^4.0.0",
"shx": "^0.3.4",
"ts-jest": "^29.2.6",
"typescript": "^5.8.2"
}
}
```
--------------------------------------------------------------------------------
/docs/src/components/HomepageFeatures/index.tsx:
--------------------------------------------------------------------------------
```typescript
import clsx from 'clsx';
import Heading from '@theme/Heading';
import styles from './styles.module.css';
type FeatureItem = {
title: string;
Svg: React.ComponentType<React.ComponentProps<'svg'>>;
description: JSX.Element;
};
const FeatureList: FeatureItem[] = [
{
title: 'Easy to Use',
Svg: require('@site/static/img/easy-to-use.svg').default,
description: (
<>
Playwright MCP Server is easy to use, just change the Claude config file and you are done.
</>
),
},
{
title: 'Test UI and APIs',
Svg: require('@site/static/img/playwright.svg').default,
description: (
<>
Test both UI and API of your application with plain English text. No <code>code</code> required.
</>
),
},
{
title: 'Powered by NodeJS',
Svg: require('@site/static/img/node.svg').default,
description: (
<>
Playwright MCP Server is built on top of NodeJS, making it fast and efficient.
</>
),
},
];
function Feature({title, Svg, description}: FeatureItem) {
return (
<div className={clsx('col col--4')}>
<div className="text--center">
<Svg className={styles.featureSvg} role="img" />
</div>
<div className="text--center padding-horiz--md">
<Heading as="h3">{title}</Heading>
<p>{description}</p>
</div>
</div>
);
}
export default function HomepageFeatures(): JSX.Element {
return (
<section className={styles.features}>
<div className="container">
<div className="row">
{FeatureList.map((props, idx) => (
<Feature key={idx} {...props} />
))}
</div>
</div>
</section>
);
}
```
--------------------------------------------------------------------------------
/docs/docs/testing-videos/AIAgents.mdx:
--------------------------------------------------------------------------------
```markdown
---
sidebar_position: 1
---
import YouTubeVideoEmbed from '@site/src/components/HomepageFeatures/YouTubeVideoEmbed';
# 🎭 UI + Database Testing 💽
In this video, we explore API, UI, and Database testing using Playwright MCP Server + SQLite MCP Server in Cursor—where AI agents handle everything for you! No coding required, just seamless automation.
Watch how Cursor + MCP Servers transform testing into a fully automated, hands-free process! 🔥
<div align="center">
<YouTubeVideoEmbed videoId="FZpUw1p370o" />
</div>
---
:::info 💡 **Note**
We will be using following MCP Server Tools
1. Playwright MCP server (Which is this tool that you are referring in the documentation)
2. [SQLite MCP Server](https://github.com/modelcontextprotocol/servers/tree/main/src/sqlite)
:::
---
## ⌨ **Cursor Rules**
We will be using Cursor Rules in this demonstration, Cursor Rules help define how AI agents interact with the
code, automate tasks, and follow specific workflows. These rules guide how agents should generate, modify, or test code within the
development environment.
Here is the Cursor Rules we are using in this demonstration
```js title="api-ui-test.mdc"
When I say /test, then perform following
Navigate to http://localhost:8000/Product/List.
Create product by clicking Create link .
Then create a product with some realistic data for Name, Price and Select ProductType as CPU and click create input type with id as Create.
Check the Database for the created record
Use the Schema: http://localhost:8001/swagger/v1/swagger.json
Here is the baseURL of API Base URL: http://localhost:8001/
Also check the API which performs the GET operation if the values are correctly retreived for the created product from the above schema
```
--------------------------------------------------------------------------------
/src/requestHandler.ts:
--------------------------------------------------------------------------------
```typescript
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import {
ListResourcesRequestSchema,
ReadResourceRequestSchema,
ListToolsRequestSchema,
CallToolRequestSchema,
Tool
} from "@modelcontextprotocol/sdk/types.js";
import { handleToolCall, getConsoleLogs, getScreenshots } from "./toolHandler.js";
export function setupRequestHandlers(server: Server, tools: Tool[]) {
// List resources handler
server.setRequestHandler(ListResourcesRequestSchema, async () => ({
resources: [
{
uri: "console://logs",
mimeType: "text/plain",
name: "Browser console logs",
},
...Array.from(getScreenshots().keys()).map(name => ({
uri: `screenshot://${name}`,
mimeType: "image/png",
name: `Screenshot: ${name}`,
})),
],
}));
// Read resource handler
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
const uri = request.params.uri.toString();
if (uri === "console://logs") {
return {
contents: [{
uri,
mimeType: "text/plain",
text: getConsoleLogs().join("\n"),
}],
};
}
if (uri.startsWith("screenshot://")) {
const name = uri.split("://")[1];
const screenshot = getScreenshots().get(name);
if (screenshot) {
return {
contents: [{
uri,
mimeType: "image/png",
blob: screenshot,
}],
};
}
}
throw new Error(`Resource not found: ${uri}`);
});
// List tools handler
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: tools,
}));
// Call tool handler
server.setRequestHandler(CallToolRequestSchema, async (request) =>
handleToolCall(request.params.name, request.params.arguments ?? {}, server)
);
}
```
--------------------------------------------------------------------------------
/src/tools/browser/console.ts:
--------------------------------------------------------------------------------
```typescript
import { BrowserToolBase } from './base.js';
import { ToolContext, ToolResponse, createSuccessResponse } from '../common/types.js';
/**
* Tool for retrieving and filtering console logs from the browser
*/
export class ConsoleLogsTool extends BrowserToolBase {
private consoleLogs: string[] = [];
/**
* Register a console message
* @param type The type of console message
* @param text The text content of the message
*/
registerConsoleMessage(type: string, text: string): void {
this.consoleLogs.push(`[${type}] ${text}`);
}
/**
* Execute the console logs tool
*/
async execute(args: any, context: ToolContext): Promise<ToolResponse> {
// No need to use safeExecute here as we don't need to interact with the page
// We're just filtering and returning logs that are already stored
let logs = [...this.consoleLogs];
// Filter by type if specified
if (args.type && args.type !== 'all') {
logs = logs.filter(log => log.startsWith(`[${args.type}]`));
}
// Filter by search text if specified
if (args.search) {
logs = logs.filter(log => log.includes(args.search));
}
// Limit the number of logs if specified
if (args.limit && args.limit > 0) {
logs = logs.slice(-args.limit);
}
// Clear logs if requested
if (args.clear) {
this.consoleLogs = [];
}
// Format the response
if (logs.length === 0) {
return createSuccessResponse("No console logs matching the criteria");
} else {
return createSuccessResponse([
`Retrieved ${logs.length} console log(s):`,
...logs
]);
}
}
/**
* Get all console logs
*/
getConsoleLogs(): string[] {
return this.consoleLogs;
}
/**
* Clear all console logs
*/
clearConsoleLogs(): void {
this.consoleLogs = [];
}
}
```
--------------------------------------------------------------------------------
/src/tools/codegen/recorder.ts:
--------------------------------------------------------------------------------
```typescript
import { v4 as uuidv4 } from 'uuid';
import { CodegenAction, CodegenSession } from './types';
export class ActionRecorder {
private static instance: ActionRecorder;
private sessions: Map<string, CodegenSession>;
private activeSession: string | null;
private constructor() {
this.sessions = new Map();
this.activeSession = null;
}
static getInstance(): ActionRecorder {
if (!ActionRecorder.instance) {
ActionRecorder.instance = new ActionRecorder();
}
return ActionRecorder.instance;
}
startSession(): string {
const sessionId = uuidv4();
this.sessions.set(sessionId, {
id: sessionId,
actions: [],
startTime: Date.now(),
});
this.activeSession = sessionId;
return sessionId;
}
endSession(sessionId: string): CodegenSession | null {
const session = this.sessions.get(sessionId);
if (session) {
session.endTime = Date.now();
if (this.activeSession === sessionId) {
this.activeSession = null;
}
return session;
}
return null;
}
recordAction(toolName: string, parameters: Record<string, unknown>, result?: unknown): void {
if (!this.activeSession) {
return;
}
const session = this.sessions.get(this.activeSession);
if (!session) {
return;
}
const action: CodegenAction = {
toolName,
parameters,
timestamp: Date.now(),
result,
};
session.actions.push(action);
}
getSession(sessionId: string): CodegenSession | null {
return this.sessions.get(sessionId) || null;
}
getActiveSession(): CodegenSession | null {
return this.activeSession ? this.sessions.get(this.activeSession) : null;
}
clearSession(sessionId: string): boolean {
if (this.activeSession === sessionId) {
this.activeSession = null;
}
return this.sessions.delete(sessionId);
}
}
```
--------------------------------------------------------------------------------
/src/tools/api/base.ts:
--------------------------------------------------------------------------------
```typescript
import type { APIRequestContext } from "rebrowser-playwright";
import {
ToolHandler,
ToolContext,
ToolResponse,
createErrorResponse,
} from "../common/types.js";
/**
* Base class for all API-based tools
* Provides common functionality and error handling
*/
export abstract class ApiToolBase implements ToolHandler {
protected server: any;
constructor(server: any) {
this.server = server;
}
/**
* Main execution method that all tools must implement
*/
abstract execute(args: any, context: ToolContext): Promise<ToolResponse>;
/**
* Ensures an API context is available and returns it
* @param context The tool context containing apiContext
* @returns The apiContext or null if not available
*/
protected ensureApiContext(context: ToolContext): APIRequestContext | null {
if (!context.apiContext) {
return null;
}
return context.apiContext;
}
/**
* Validates that an API context is available and returns an error response if not
* @param context The tool context
* @returns Either null if apiContext is available, or an error response
*/
protected validateApiContextAvailable(
context: ToolContext
): ToolResponse | null {
if (!this.ensureApiContext(context)) {
return createErrorResponse("API context not initialized");
}
return null;
}
/**
* Safely executes an API operation with proper error handling
* @param context The tool context
* @param operation The async operation to perform
* @returns The tool response
*/
protected async safeExecute(
context: ToolContext,
operation: (apiContext: APIRequestContext) => Promise<ToolResponse>
): Promise<ToolResponse> {
const apiError = this.validateApiContextAvailable(context);
if (apiError) return apiError;
try {
return await operation(context.apiContext!);
} catch (error) {
return createErrorResponse(
`API operation failed: ${(error as Error).message}`
);
}
}
}
```
--------------------------------------------------------------------------------
/docs/docs/playwright-api/Examples.md:
--------------------------------------------------------------------------------
```markdown
---
sidebar_position: 2
---
# ⚙️Examples of API automation
Lets see how we can use the power of Playwright MCP Server to automate API of our application
### Scenario
```json
// Basic POST request
Perform POST operation for the URL https://api.restful-api.dev/objects with body
{
"name": "Apple MacBook Pro 16",
"data": {
"year": 2024,
"price": 2499,
"CPU model": "M4",
"Hard disk size": "5 TB"
}
}
And verify if the response has createdAt and id property and store the ID in a variable for future reference say variable productID
// POST request with Bearer token authorization
Perform POST operation for the URL https://api.restful-api.dev/objects with Bearer token "your-token-here" set in the headers
{
'Content-Type': 'application/json',
'Authorization': 'Bearer your-token-here'
},
and body
{
"name": "Secure MacBook Pro",
"data": {
"year": 2024,
"price": 2999,
"CPU model": "M4 Pro",
"Hard disk size": "8 TB",
"security": "enhanced"
}
}
Perform GET operation for the created ProductID using URL https://api.restful-api.dev/objects/productID and verify the response has properties like Id, name, data
Perform PUT operation for the created ProductID using URL https://api.restful-api.dev/objects/productID with body {
"name": "Apple MacBook Pro 16",
"data": {
"year": 2025,
"price": 4099,
"CPU model": "M5",
"Hard disk size": "10 TB",
"color": "Titanium"
}
}
And verify if the response has createdAt and id property
Perform PATCH operation for the created ProductID using URL https://api.restful-api.dev/objects/productID with body
{
"name": "Apple MacBook Pro 19 (Limited Edition)"
}
And verify if the response has updatedAt property with value Apple MacBook Pro 19 (Limited Edition)
```
And once the entire test operation completes, we will be presented with the entire details of how the automation did happened.

:::tip
You can also see the `Request/Response/StatusCode` from the execution of Playwright MCP Server

:::
```
--------------------------------------------------------------------------------
/docs/docs/intro.mdx:
--------------------------------------------------------------------------------
```markdown
---
sidebar_position: 1
---
import YouTubeVideoEmbed from '@site/src/components/HomepageFeatures/YouTubeVideoEmbed';
# Playwright MCP Server
The **Playwright Model Context Protocol (MCP) server** is a powerful solution for automating Browser and API testing using Playwright.
With the Playwright MCP server, you can:
- Enable LLMs to interact with web pages in a real browser environment.
- Perform tasks such as executing JavaScript, taking screenshots, and navigating web elements.
- Seamlessly handle API testing to validate endpoints and ensure reliability.
- Test across multiple browser engines including Chromium, Firefox, and WebKit.

## Installation
You can install Playwright MCP Server package using either **npm**, **mcp-get**, or **Smithery**:
:::info Playwright MCP Tips
To get started more quickly on Playwright MCP Server, watch the videos mentioned in the footer of this page under `Docs`
:::
### Installing via NPM
To install Playwright MCP for Claude Desktop automatically via Smithery:
```bash
npm install -g @executeautomation/playwright-mcp-server
```
### Installing via Smithery
To install Playwright MCP for Claude Desktop automatically via Smithery:
```bash
npx @smithery/cli install @executeautomation/playwright-mcp-server --client claude
```
You can type this command into Command Prompt, Powershell, Terminal, or any other integrated terminal of your code editor.
### Installing via MCP-GET
To install Playwright MCP for Claude Desktop automatically via Smithery:
```bash
npx @michaellatman/mcp-get@latest install @executeautomation/playwright-mcp-server
```
### Configuring Playwright MCP in Claude Desktop
Here's the Claude Desktop configuration to use the Playwright MCP server.
Modify your `claude-desktop-config.json` file as shown below
```json
{
"mcpServers": {
"playwright": {
"command": "npx",
"args": ["-y", "@executeautomation/playwright-mcp-server"]
}
}
}
```
### What is Model Context Protocol
This video should give you an high level overview of what Claude's MCP is and how helpful it will soon become for AI agents
<YouTubeVideoEmbed videoId="hGJQMbpsTi4" />
```
--------------------------------------------------------------------------------
/docs/static/img/EA-Icon.svg:
--------------------------------------------------------------------------------
```
<svg xmlns="http://www.w3.org/2000/svg" width="118.679" height="119.443" viewBox="0 0 118.679 119.443">
<g id="Group_21" data-name="Group 21" transform="translate(-2049 1709)">
<path id="Path_1" data-name="Path 1" d="M258.906,95.838a31.043,31.043,0,0,0-11.854-21.8,34.678,34.678,0,0,0-19.13-7.845c-.139-.013-.252-.041-.287-.194H170.976c-1.281.451-2.652.428-3.958.769q-18.86,4.92-25.913,23.083c-.066.172-.138.347-.194.532a4.7,4.7,0,0,1-.071.717c4.836-5,11.1-5.8,17.494-5.878,11.008-.136,22.019-.074,33.03-.078,6.762,0,13.525.028,20.287.045,2.863.007,3.154.52,1.738,3.03a69.825,69.825,0,0,0-3.844,7.47c-1.448,3.494-2.887,4.064-6.742,3.978-.554-.013-1.107-.069-1.661-.069q-27.542-.029-55.084-.051c-1.176,0-2.353.011-3.527-.03a1.949,1.949,0,0,0-1.943.988v.588a9.16,9.16,0,0,1,0,4.117v4.117c.411.924.119,1.9.184,2.842.047.67.007,1.346.006,2.02.092.544.52.594.949.662a21.963,21.963,0,0,0,3.515.015q22-.034,44-.047c3.136,0,6.271.042,9.406.029.694,0,.864.1.5.781-2.025,3.794-4.037,7.593-5.906,11.467a3.694,3.694,0,0,1-3.71,2.04q-15.14-.04-30.281-.035a42.16,42.16,0,0,0-5.279-.008c-1.385.176-1.989.766-1.983,2.175.009,2.284.1,4.568.114,6.853.014,2.086-.118,4.169.033,6.26.326,4.565,2.613,6.87,7.2,7.1,1.434.07,2.87.119,4.3.124,8.787.026,17.575.028,26.361.07a1.268,1.268,0,0,0,1.321-.825q12.12-23.877,24.271-47.737,4.274-8.4,8.541-16.812c.16-.314.347-.614.614-1.083l34.269,67.258.207-.02V112.067C259.252,106.654,259.4,101.223,258.906,95.838Z" transform="translate(1908.413 -1774.999)" fill="#185281" fill-rule="evenodd"/>
<path id="Path_2" data-name="Path 2" d="M140.588,131.5c.247-.034.326.178.441.319,3.4,4.159,7.939,6.212,13.1,7.153a45.47,45.47,0,0,0,8.094.56c11.663.024,23.326,0,34.989.051a3.679,3.679,0,0,0,3.634-2.256q7.783-15.344,15.622-30.658c2.58-5.048,5.188-10.082,7.785-15.121.132-.257.283-.5.5-.9,1.108,2.142,2.17,4.164,3.207,6.2Q238.084,116.7,248.2,136.56a5.163,5.163,0,0,0,6.209,3.022c.409-.078.82-.15,1.354-.246a38.629,38.629,0,0,1-4.232,6.453c-4.994,6.258-11.265,10.5-19.209,12.072a50.314,50.314,0,0,1-8.572.7c-8.947.223-17.9.252-26.843.23-5.584-.012-11.171,0-16.754-.136-3.424-.084-6.857.091-10.273-.261a32.066,32.066,0,0,1-17.578-7.646,34.849,34.849,0,0,1-11.518-18.225,2.886,2.886,0,0,0-.194-.433c0-.2,0-.4,0-.59Z" transform="translate(1908.412 -1748.354)" fill="#26303c" fill-rule="evenodd"/>
</g>
</svg>
```
--------------------------------------------------------------------------------
/docs/docs/ai-courses/GenAICourse.mdx:
--------------------------------------------------------------------------------
```markdown
---
sidebar_position: 1
---
import YouTubeVideoEmbed from '@site/src/components/HomepageFeatures/YouTubeVideoEmbed';
# 🤖 Using Generative AI in Software Automation Testing
<div align="center">
<YouTubeVideoEmbed videoId="Y7xkCWvUKEk" />
</div>
---
:::info 💡 **Note**
All the courses are available on Udemy, and they almost always have a `coupon code` available.
For discounts, please feel free to reach out at **[[email protected]](mailto:[email protected])**.
🎯 **Course Link:**
[Generative AI in Software Automation Testing](https://www.udemy.com/course/generative-ai-in-software-automation-testing/)
:::
---
## 📚 **Course Description**
This course is crafted for everyone, whether you're new to Software Testing or an experienced professional. Unlock the full potential of **Generative AI** and transform your testing process into something **faster**, **smarter**, and **more efficient**.
### 🚀 **What You’ll Master**
- **🧠 Introduction to Generative AI:**
Understand the foundations of Gen AI and its role in Software Testing.
- **💻 Running Large Language Models (LLMs) Locally:**
Learn how to run models on your machine without paying for external services.
- **📝 Manual Testing with Gen AI:**
Generate manual test cases, test data, and test requirements using grounded Models with the power of AI and RAG.
- **🤖 Automated UI Testing:**
Leverage AI to write, refactor, and optimize automated tests for UI applications.
- **🎭 Playwright UI Testing:**
Use Playwright and AI-driven tools to create smart test scripts and handle complex workflows.
- **🚫 No-code Automation with TestRigor:**
Create powerful automation suites in plain English, even automating SMS, phone calls, and intricate tables.
- **🔗 API Testing:**
Harness PostBots and Gen AI to streamline API testing.
- **🧬 Using Gen AI APIs:**
Add intelligence to your Test Automation code using OpenAI APIs.
- **📍 Model Context Protocol (MCP):**
Run Playwright tests for UI and APIs by leveraging the power of MCP.
---
By the end of this course, you'll have a deep understanding of how **Generative AI** can supercharge your testing process. With hands-on experience, you'll be able to use **AI-enhanced tools** and **LLMs** to simplify complex testing tasks, making your work smoother and more efficient.
```
--------------------------------------------------------------------------------
/src/tools/browser/screenshot.ts:
--------------------------------------------------------------------------------
```typescript
import fs from "node:fs";
import * as path from "node:path";
import * as os from "node:os";
import type { Page } from "rebrowser-playwright";
import { BrowserToolBase } from "./base.js";
import {
ToolContext,
ToolResponse,
createSuccessResponse,
} from "../common/types.js";
const defaultDownloadsPath = path.join(os.homedir(), "Downloads");
/**
* Tool for taking screenshots of pages or elements
*/
export class ScreenshotTool extends BrowserToolBase {
private screenshots = new Map<string, string>();
/**
* Execute the screenshot tool
*/
async execute(args: any, context: ToolContext): Promise<ToolResponse> {
return this.safeExecute(context, async (page) => {
const screenshotOptions: any = {
type: args.type || "png",
fullPage: !!args.fullPage,
};
if (args.selector) {
const element = await page.$(args.selector);
if (!element) {
return {
content: [
{
type: "text",
text: `Element not found: ${args.selector}`,
},
],
isError: true,
};
}
screenshotOptions.element = element;
}
// Generate output path
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
const filename = `${args.name || "screenshot"}-${timestamp}.png`;
const downloadsDir = args.downloadsDir || defaultDownloadsPath;
if (!fs.existsSync(downloadsDir)) {
fs.mkdirSync(downloadsDir, { recursive: true });
}
const outputPath = path.join(downloadsDir, filename);
screenshotOptions.path = outputPath;
const screenshot = await page.screenshot(screenshotOptions);
const base64Screenshot = screenshot.toString("base64");
const messages = [
`Screenshot saved to: ${path.relative(process.cwd(), outputPath)}`,
];
// Handle base64 storage
if (args.storeBase64 !== false) {
this.screenshots.set(args.name || "screenshot", base64Screenshot);
this.server.notification({
method: "notifications/resources/list_changed",
});
messages.push(
`Screenshot also stored in memory with name: '${
args.name || "screenshot"
}'`
);
}
return createSuccessResponse(messages);
});
}
/**
* Get all stored screenshots
*/
getScreenshots(): Map<string, string> {
return this.screenshots;
}
}
```
--------------------------------------------------------------------------------
/docs/docs/ai-courses/MachineLearning.mdx:
--------------------------------------------------------------------------------
```markdown
---
sidebar_position: 2
---
import YouTubeVideoEmbed from '@site/src/components/HomepageFeatures/YouTubeVideoEmbed';
# 🧠 Understand, Test, and Fine-tune AI Models with Hugging Face
<div align="center">
<YouTubeVideoEmbed videoId="T6-sL0TfAsM" />
</div>
---
:::info 💡 **Note**
All the courses are available on **Udemy**, and they almost always have a **`coupon code`** available.
For discounts, please feel free to reach out at **[[email protected]](mailto:[email protected])**.
🎯 **Course Link:**
[Understand, Test, and Fine-tune AI Models with Hugging Face](https://www.udemy.com/course/ai-with-huggingface/)
:::
---
## 📚 **Course Description**
This course provides a complete journey into **Understanding, Testing, and Fine-tuning AI Models** using the **Hugging Face** library. Whether you are a beginner or an experienced engineer, this course equips you with **hands-on expertise** in every step of the **machine learning pipeline**, from **basic concepts** to **advanced model testing**, **fine-tuning**, and **deployment**.
---
### 🚀 **What You’ll Learn**
1. **📈 Introduction to Machine Learning:**
Lay a strong foundation by exploring key ML concepts and essential terminology.
2. **📊 Working with Natural Language Processing (NLP) Libraries:**
Learn how to process, analyze, and derive insights from textual data using popular NLP tools.
3. **💡 Deep Dive into the Transformers Library:**
Master Hugging Face’s Transformers, the industry standard for building state-of-the-art **NLP** and **LLM** solutions.
4. **🧠 Working with Large Language Models (LLMs):**
Explore multiple methods to interact with and utilize **LLMs** for diverse real-world applications.
5. **🧪 Functional Testing of AI Models:**
Ensure your models perform reliably across different scenarios using systematic testing strategies.
6. **⚖️ Bias and Fairness Testing:**
Implement techniques to detect and mitigate unintended bias, promoting ethical and fair **AI practices**.
7. **📏 Evaluating AI Models:**
Measure performance with robust metrics and refine your models for optimal results.
8. **🤖 Working with AI Agents:**
Build, configure, and integrate **intelligent agents** into your workflows.
9. **🔬 Fine-tuning and Training AI Models:**
Customize pre-trained models or create your own from scratch to meet specific project requirements.
---
By the end of this course, you’ll gain the **knowledge** and **practical experience** needed to confidently **develop**, **test**, and **optimize** your own **Transformer-based models** and **LLMs**, empowering you to thrive in the rapidly evolving world of **AI**.
```
--------------------------------------------------------------------------------
/docs/docs/playwright-api/Supported-Tools.mdx:
--------------------------------------------------------------------------------
```markdown
---
sidebar_position: 1
---
import YouTubeVideoEmbed from '@site/src/components/HomepageFeatures/YouTubeVideoEmbed';
# 🛠️ Supported Tools
Playwright MCP for API automation has following key features
- Support of GET Request
- Support of POST Request
- Support of PATCH Request
- Support of PUT Request
- Support of DELETE Request
<YouTubeVideoEmbed videoId="BYYyoRxCcFE" />
---
:::warning Note
Still the library is not matured enough to support Oauth, Multi-form, Binary input or complex API requests. Please feel free to fork the repo and add the feature with a PR, will can build the library together!
:::
### Playwright_get
Perform a GET operation on any given API request.
- **Inputs:**
- **`url`** *(string)*:
URL to perform the GET operation.
- **Response:**
- **`statusCode`** *(string)*:
Status code of the API.
---
### Playwright_post
Perform a POST operation on any given API request.
- **Inputs:**
- **`url`** *(string)*:
URL to perform the POST operation.
- **`value`** *(string)*:
Data to include in the body of the POST request.
- **`token`** *(string, optional)*:
Bearer token for authorization. When provided, it will be sent as `Authorization: Bearer <token>` header.
- **`headers`** *(object, optional)*:
Additional headers to include in the request. Note: Content-Type: application/json is set by default.
- **Response:**
- **`statusCode`** *(string)*:
Status code of the API.
- **`responseData`** *(string)*:
Response data in JSON format.
---
### Playwright_put
Perform a PUT operation on any given API request.
- **Inputs:**
- **`url`** *(string)*:
URL to perform the PUT operation.
- **`value`** *(string)*:
Data to include in the body of the PUT request.
- **Response:**
- **`statusCode`** *(string)*:
Status code of the API.
- **`responseData`** *(string)*:
Response data in JSON format.
---
### Playwright_patch
Perform a PATCH operation on any given API request.
- **Inputs:**
- **`url`** *(string)*:
URL to perform the PATCH operation.
- **`value`** *(string)*:
Data to include in the body of the PATCH request.
- **Response:**
- **`statusCode`** *(string)*:
Status code of the API.
- **`responseData`** *(string)*:
Response data in JSON format.
---
### Playwright_delete
Perform a DELETE operation on any given API request.
- **Inputs:**
- **`url`** *(string)*:
URL to perform the DELETE operation.
- **Response:**
- **`statusCode`** *(string)*:
Status code of the API.
### Upon running the test Claude Desktop will run MCP Server to use above tools

```
--------------------------------------------------------------------------------
/src/tools/browser/response.ts:
--------------------------------------------------------------------------------
```typescript
import type { Response } from "rebrowser-playwright";
import { BrowserToolBase } from "./base.js";
import type { ToolContext, ToolResponse } from "../common/types.js";
import { createSuccessResponse, createErrorResponse } from "../common/types.js";
const responsePromises = new Map<string, Promise<Response>>();
interface ExpectResponseArgs {
id: string;
url: string;
}
interface AssertResponseArgs {
id: string;
value?: string;
}
/**
* Tool for setting up response wait operations
*/
export class ExpectResponseTool extends BrowserToolBase {
/**
* Execute the expect response tool
*/
async execute(
args: ExpectResponseArgs,
context: ToolContext
): Promise<ToolResponse> {
return this.safeExecute(context, async (page) => {
if (!args.id || !args.url) {
return createErrorResponse(
"Missing required parameters: id and url must be provided"
);
}
const responsePromise = page.waitForResponse(args.url);
responsePromises.set(args.id, responsePromise);
return createSuccessResponse(
`Started waiting for response with ID ${args.id}`
);
});
}
}
/**
* Tool for asserting and validating responses
*/
export class AssertResponseTool extends BrowserToolBase {
/**
* Execute the assert response tool
*/
async execute(
args: AssertResponseArgs,
context: ToolContext
): Promise<ToolResponse> {
return this.safeExecute(context, async () => {
if (!args.id) {
return createErrorResponse(
"Missing required parameter: id must be provided"
);
}
const responsePromise = responsePromises.get(args.id);
if (!responsePromise) {
return createErrorResponse(
`No response wait operation found with ID: ${args.id}`
);
}
try {
const response = await responsePromise;
const body = await response.json();
if (args.value) {
const bodyStr = JSON.stringify(body);
if (!bodyStr.includes(args.value)) {
const messages = [
`Response body does not contain expected value: ${args.value}`,
`Actual body: ${bodyStr}`,
];
return createErrorResponse(messages.join("\n"));
}
}
const messages = [
`Response assertion for ID ${args.id} successful`,
`URL: ${response.url()}`,
`Status: ${response.status()}`,
`Body: ${JSON.stringify(body, null, 2)}`,
];
return createSuccessResponse(messages.join("\n"));
} catch (error) {
return createErrorResponse(
`Failed to assert response: ${(error as Error).message}`
);
} finally {
responsePromises.delete(args.id);
}
});
}
}
```
--------------------------------------------------------------------------------
/docs/docs/ai-courses/AIAgents.mdx:
--------------------------------------------------------------------------------
```markdown
---
sidebar_position: 3
---
import YouTubeVideoEmbed from '@site/src/components/HomepageFeatures/YouTubeVideoEmbed';
# 🧠🤖 Build & Test AI Agents, ChatBots, and RAG with Ollama & Local LLM
<div align="center">
<YouTubeVideoEmbed videoId="qw-X4WUHs5s" />
</div>
---
:::info 💡 **Note**
All the courses are available on **Udemy**, and they almost always have a **`coupon code`** available.
For discounts, please feel free to reach out at **[[email protected]](mailto:[email protected])**.
🎯 **Course Link:**
[Build & Test AI Agents, ChatBots, and RAG with Ollama & Local LLM](https://www.udemy.com/course/build-ai-agent-chatbot-rag-langchain-local-llm/)
:::
---
## 📚 **Course Description**
This course is designed for complete beginners—even if you have **zero knowledge of LangChain**, you’ll learn step-by-step how to build **LLM-based applications** using **local Large Language Models (LLMs)**.
We’ll go beyond development and dive into **evaluating and testing AI agents**, **RAG applications**, and **chatbots** using **RAGAs** to ensure they deliver **accurate** and **reliable results**, following key industry metrics for **AI performance**.
---
### 🚀 **What You’ll Learn**
- **🧠 Fundamentals of LangChain & LangSmith**
Get a solid foundation in building and testing **LLM-based applications**.
- **💬 Chat Message History in LangChain**
Learn how to store conversation data for **chatbots** and **AI agents**.
- **⚙️ Running Parallel & Multiple Chains**
Master advanced techniques like **RunnableParallels** to optimize your **LLM workflows**.
- **🤖 Building Chatbots with LangChain & Streamlit**
Create chatbots with **message history** and an interactive **UI**.
- **🛠️ Tools & Tool Chains in LLMs**
Understand the power of **Tooling**, **Custom Tools**, and how to build **Tool Chains** for **AI applications**.
- **🧑💻 Creating AI Agents with LangChain**
Implement **AI agents** that can interact dynamically with **RAG applications**.
- **📚 Implementing RAG with Vector Stores & Local Embeddings**
Develop robust **RAG solutions** with local **LLM embeddings**.
- **🔧 Using AI Agents & RAG with Tooling**
Learn how to integrate **Tooling** effectively while building **LLM Apps**.
- **🚦 Optimizing & Debugging AI Applications with LangSmith**
Enhance your **AI models** and **applications** with **LangSmith's debugging** and **optimization tools**.
- **🧪 Evaluating & Testing LLM Applications with RAGAs**
Apply **hands-on testing strategies** to validate **RAG** and **AI agent** performance.
- **📊 Real-world Projects & Assessments**
Gain practical experience with **RAGAs** and learn to assess the quality and reliability of **AI solutions**.
---
## 🎯 **Learning Experience**
This entire course is taught inside a **Jupyter Notebook** with **Visual Studio**, offering an **interactive**, **guided experience** where you can **run the code seamlessly** and **follow along effortlessly**.
By the end of this course, you’ll have the **confidence** to **build**, **test**, and **optimize AI-powered applications** with ease!
```
--------------------------------------------------------------------------------
/src/tools/browser/base.ts:
--------------------------------------------------------------------------------
```typescript
import type { Browser, Page } from "rebrowser-playwright";
import {
ToolHandler,
ToolContext,
ToolResponse,
createErrorResponse,
} from "../common/types.js";
/**
* Base class for all browser-based tools
* Provides common functionality and error handling
*/
export abstract class BrowserToolBase implements ToolHandler {
protected server: any;
constructor(server: any) {
this.server = server;
}
/**
* Main execution method that all tools must implement
*/
abstract execute(args: any, context: ToolContext): Promise<ToolResponse>;
/**
* Ensures a page is available and returns it
* @param context The tool context containing browser and page
* @returns The page or null if not available
*/
protected ensurePage(context: ToolContext): Page | null {
if (!context.page) {
return null;
}
return context.page;
}
/**
* Validates that a page is available and returns an error response if not
* @param context The tool context
* @returns Either null if page is available, or an error response
*/
protected validatePageAvailable(context: ToolContext): ToolResponse | null {
if (!this.ensurePage(context)) {
return createErrorResponse("Browser page not initialized");
}
return null;
}
/**
* Safely executes a browser operation with proper error handling
* @param context The tool context
* @param operation The async operation to perform
* @returns The tool response
*/
protected async safeExecute(
context: ToolContext,
operation: (page: Page) => Promise<ToolResponse>
): Promise<ToolResponse> {
const pageError = this.validatePageAvailable(context);
if (pageError) return pageError;
try {
// Verify browser is connected before proceeding
if (context.browser && !context.browser.isConnected()) {
// If browser exists but is disconnected, reset state
const { resetBrowserState } = await import("../../toolHandler.js");
resetBrowserState();
return createErrorResponse(
"Browser is disconnected. Please retry the operation."
);
}
// Check if page is closed
if (context.page.isClosed()) {
return createErrorResponse(
"Page is closed. Please retry the operation."
);
}
return await operation(context.page!);
} catch (error) {
const errorMessage = (error as Error).message;
// Check for common browser disconnection errors
if (
errorMessage.includes(
"Target page, context or browser has been closed"
) ||
errorMessage.includes("Target closed") ||
errorMessage.includes("Browser has been disconnected") ||
errorMessage.includes("Protocol error") ||
errorMessage.includes("Connection closed")
) {
// Reset browser state on connection issues
const { resetBrowserState } = await import("../../toolHandler.js");
resetBrowserState();
return createErrorResponse(
`Browser connection error: ${errorMessage}. Connection has been reset - please retry the operation.`
);
}
return createErrorResponse(`Operation failed: ${errorMessage}`);
}
}
}
```
--------------------------------------------------------------------------------
/docs/docs/playwright-web/Recording-Actions.mdx:
--------------------------------------------------------------------------------
```markdown
---
sidebar_position: 5
---
import YouTubeVideoEmbed from '@site/src/components/HomepageFeatures/YouTubeVideoEmbed';
# 🎥 Recording Actions
Playwright MCP allows you to record your browser interactions and automatically generate test specifications. This feature helps you create automated tests without writing code manually.
## Getting Started
To start recording your actions and generate test files, you'll need to:
1. Start a recording session
2. Perform your actions using MCP tools
3. End the session to generate the test file
### Example
Here's a complete example of recording actions and generating a test spec:
1. First, start a new recording session by specifying:
- Where to save the generated tests (`tests/generated` directory)
- What to name your tests (using 'LoginTest' as a prefix)
- Whether to include helpful comments in the generated code
2. Then perform the test actions:
- Navigate to the Sauce Demo website (https://www.saucedemo.com)
- Enter "standard_user" in the username field
- Enter "secret_sauce" in the password field
- Click the login button
3. Finally, end the recording session to generate your test file
The generated test file will look something like this:
```typescript
import { test, expect } from '@playwright/test';
test('LoginTest_2024-03-23', async ({ page }) => {
// Navigate to the login page
await page.goto('https://www.saucedemo.com');
// Fill in username
await page.fill('#user-name', 'standard_user');
// Fill in password
await page.fill('#password', 'secret_sauce');
// Click login button
await page.click('#login-button');
// Verify successful login
await expect(page).toHaveURL(/.*inventory.html/);
});
```
## Configuration Options
When starting a recording session, you can configure several options:
- **outputPath**: Directory where the generated test files will be saved
- **testNamePrefix**: Prefix for the generated test names
- **includeComments**: Whether to include descriptive comments in the generated tests
For example, you might configure your session to:
- Save tests in a 'tests/generated' folder
- Name tests with a 'MyTest' prefix
- Include helpful comments in the generated code
## Session Management
You can manage your recording sessions using these tools:
- **get_codegen_session**: Retrieve information about a recording session
- **clear_codegen_session**: Clean up a recording session without generating a test
These tools help you check the status of your recording or clean up if you want to start over without generating a test file.
## Best Practices
1. **Organize Tests**: Use meaningful test name prefixes to organize your generated tests
2. **Clean Up**: Always end or clear your sessions after recording
3. **Verify Actions**: Include verification steps in your recordings
4. **Maintain Context**: Keep related actions in the same recording session
5. **Documentation**: Add comments during recording for better test maintainability
## Running Generated Tests
To run your generated tests, use the Playwright test runner:
```bash
npx playwright test tests/generated/logintest_*.spec.ts
```
:::tip
You can modify the generated test files to add additional assertions, setup, or teardown code as needed.
:::
```
--------------------------------------------------------------------------------
/src/tools/browser/visiblePage.ts:
--------------------------------------------------------------------------------
```typescript
import { resetBrowserState } from "../../toolHandler.js";
import { ToolContext, ToolResponse, createErrorResponse, createSuccessResponse } from "../common/types.js";
import { BrowserToolBase } from "./base.js";
/**
* Tool for getting the visible text content of the current page
*/
export class VisibleTextTool extends BrowserToolBase {
/**
* Execute the visible text page tool
*/
async execute(args: any, context: ToolContext): Promise<ToolResponse> {
// Check if browser is available
if (!context.browser || !context.browser.isConnected()) {
// If browser is not connected, we need to reset the state to force recreation
resetBrowserState();
return createErrorResponse(
"Browser is not connected. The connection has been reset - please retry your navigation."
);
}
// Check if page is available and not closed
if (!context.page || context.page.isClosed()) {
return createErrorResponse(
"Page is not available or has been closed. Please retry your navigation."
);
}
return this.safeExecute(context, async (page) => {
try {
const visibleText = await page!.evaluate(() => {
const walker = document.createTreeWalker(
document.body,
NodeFilter.SHOW_TEXT,
{
acceptNode: (node) => {
const style = window.getComputedStyle(node.parentElement!);
return (style.display !== "none" && style.visibility !== "hidden")
? NodeFilter.FILTER_ACCEPT
: NodeFilter.FILTER_REJECT;
},
}
);
let text = "";
let node;
while ((node = walker.nextNode())) {
const trimmedText = node.textContent?.trim();
if (trimmedText) {
text += trimmedText + "\n";
}
}
return text.trim();
});
return createSuccessResponse(`Visible text content:\n${visibleText}`);
} catch (error) {
return createErrorResponse(`Failed to get visible text content: ${(error as Error).message}`);
}
});
}
}
/**
* Tool for getting the visible HTML content of the current page
*/
export class VisibleHtmlTool extends BrowserToolBase {
/**
* Execute the visible HTML page tool
*/
async execute(args: any, context: ToolContext): Promise<ToolResponse> {
// Check if browser is available
if (!context.browser || !context.browser.isConnected()) {
// If browser is not connected, we need to reset the state to force recreation
resetBrowserState();
return createErrorResponse(
"Browser is not connected. The connection has been reset - please retry your navigation."
);
}
// Check if page is available and not closed
if (!context.page || context.page.isClosed()) {
return createErrorResponse(
"Page is not available or has been closed. Please retry your navigation."
);
}
return this.safeExecute(context, async (page) => {
try {
const htmlContent = await page!.content();
return createSuccessResponse(`HTML content:\n${htmlContent}`);
} catch (error) {
return createErrorResponse(`Failed to get visible HTML content: ${(error as Error).message}`);
}
});
}
}
```
--------------------------------------------------------------------------------
/docs/docusaurus.config.ts:
--------------------------------------------------------------------------------
```typescript
import {themes as prismThemes} from 'prism-react-renderer';
import type {Config} from '@docusaurus/types';
import type * as Preset from '@docusaurus/preset-classic';
// This runs in Node.js - Don't use client-side code here (browser APIs, JSX...)
const config: Config = {
title: 'Playwright MCP Server',
tagline: 'Fastest way to test your APIs and UI in Playwright with AI 🤖',
favicon: 'img/favicon.ico',
// Set the production url of your site here
url: 'https://executeautomation.github.io/',
// Set the /<baseUrl>/ pathname under which your site is served
// For GitHub pages deployment, it is often '/<projectName>/'
baseUrl: '/mcp-playwright/',
// GitHub pages deployment config.
// If you aren't using GitHub pages, you don't need these.
organizationName: 'executeautomation', // Usually your GitHub org/user name.
projectName: 'mcp-playwright', // Usually your repo name.
onBrokenLinks: 'ignore',
onBrokenMarkdownLinks: 'warn',
// Even if you don't use internationalization, you can use this field to set
// useful metadata like html lang. For example, if your site is Chinese, you
// may want to replace "en" with "zh-Hans".
i18n: {
defaultLocale: 'en',
locales: ['en'],
},
trailingSlash: false,
deploymentBranch: 'gh-pages',
presets: [
[
'classic',
{
docs: {
sidebarPath: './sidebars.ts',
// Please change this to your repo.
// Remove this to remove the "edit this page" links.
editUrl:
'https://github.com/facebook/docusaurus/tree/main/packages/create-docusaurus/templates/shared/',
},
theme: {
customCss: './src/css/custom.css',
},
} satisfies Preset.Options,
],
],
themeConfig: {
// Replace with your project's social card
image: 'img/EA-Icon.svg',
navbar: {
title: 'Playwright MCP Server',
logo: {
alt: 'Playwright MCP Server',
src: 'img/EA-Icon.svg',
},
items: [
{
type: 'docSidebar',
sidebarId: 'tutorialSidebar',
position: 'left',
label: 'Tutorial',
},
{
href: 'https://github.com/executeautomation/mcp-playwright',
label: 'GitHub',
position: 'right',
},
],
},
footer: {
style: 'dark',
links: [
{
title: 'Docs',
items: [
{
label: 'Tutorial',
to: '/docs/intro',
},
{
label: 'Playwright MCP for UI',
href: 'https://youtu.be/8CcgFUE16HM',
},
{
label: 'Playwright MCP for API',
href: 'https://youtu.be/BYYyoRxCcFE',
},
],
},
{
title: 'Community',
items: [
{
label: 'Youtube',
href: 'https://youtube.com/executeautomation',
},
{
label: 'Udemy',
href: 'https://www.udemy.com/user/karthik-kk',
},
{
label: 'X',
href: 'http://x.com/ExecuteAuto',
},
],
}
],
copyright: `Copyright © ${new Date().getFullYear()} ExecuteAutomation Pvt Ltd.`,
},
prism: {
theme: prismThemes.github,
darkTheme: prismThemes.dracula,
},
} satisfies Preset.ThemeConfig,
};
export default config;
```
--------------------------------------------------------------------------------
/src/__tests__/tools/browser/output.test.ts:
--------------------------------------------------------------------------------
```typescript
import { SaveAsPdfTool } from '../../../tools/browser/output.js';
import { ToolContext } from '../../../tools/common/types.js';
import { Page, Browser } from 'playwright';
import { jest } from '@jest/globals';
import * as path from 'path';
// Mock path.resolve to test path handling
jest.mock('path', () => ({
resolve: jest.fn().mockImplementation((dir, file) => `${dir}/${file}`)
}));
// Mock page functions
const mockPdf = jest.fn().mockImplementation(() => Promise.resolve());
const mockIsClosed = jest.fn().mockReturnValue(false);
// Mock the Page object with proper typing
const mockPage = {
pdf: mockPdf,
isClosed: mockIsClosed
} as unknown as Page;
// Mock the browser
const mockIsConnected = jest.fn().mockReturnValue(true);
const mockBrowser = {
isConnected: mockIsConnected
} as unknown as Browser;
// Mock the server
const mockServer = {
sendMessage: jest.fn()
};
// Mock context
const mockContext = {
page: mockPage,
browser: mockBrowser,
server: mockServer
} as ToolContext;
describe('Browser Output Tools', () => {
let saveAsPdfTool: SaveAsPdfTool;
beforeEach(() => {
jest.clearAllMocks();
saveAsPdfTool = new SaveAsPdfTool(mockServer);
// Reset browser and page mocks
mockIsConnected.mockReturnValue(true);
mockIsClosed.mockReturnValue(false);
});
describe('SaveAsPdfTool', () => {
test('should save page as PDF with default options', async () => {
const args = {
outputPath: '/downloads'
};
const result = await saveAsPdfTool.execute(args, mockContext);
expect(mockPdf).toHaveBeenCalledWith({
path: '/downloads/page.pdf',
format: 'A4',
printBackground: true,
margin: {
top: '1cm',
right: '1cm',
bottom: '1cm',
left: '1cm'
}
});
expect(result.isError).toBe(false);
expect(result.content[0].text).toContain('Saved page as PDF');
});
test('should save page as PDF with custom options', async () => {
const args = {
outputPath: '/downloads',
filename: 'custom.pdf',
format: 'Letter',
printBackground: false,
margin: {
top: '2cm',
right: '2cm',
bottom: '2cm',
left: '2cm'
}
};
const result = await saveAsPdfTool.execute(args, mockContext);
expect(mockPdf).toHaveBeenCalledWith({
path: '/downloads/custom.pdf',
format: 'Letter',
printBackground: false,
margin: {
top: '2cm',
right: '2cm',
bottom: '2cm',
left: '2cm'
}
});
expect(result.isError).toBe(false);
expect(result.content[0].text).toContain('Saved page as PDF');
});
test('should handle PDF generation errors', async () => {
const args = {
outputPath: '/downloads'
};
// Mock PDF generation error
mockPdf.mockImplementationOnce(() => Promise.reject(new Error('PDF generation failed')));
const result = await saveAsPdfTool.execute(args, mockContext);
expect(mockPdf).toHaveBeenCalled();
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('Operation failed');
});
test('should handle missing page', async () => {
const args = {
outputPath: '/downloads'
};
const result = await saveAsPdfTool.execute(args, { server: mockServer } as ToolContext);
expect(mockPdf).not.toHaveBeenCalled();
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('Browser page not initialized');
});
});
});
```
--------------------------------------------------------------------------------
/src/__tests__/tools/browser/goNavigation.test.ts:
--------------------------------------------------------------------------------
```typescript
import { GoBackTool, GoForwardTool } from '../../../tools/browser/navigation.js';
import { ToolContext } from '../../../tools/common/types.js';
import { Page, Browser } from 'playwright';
import { jest } from '@jest/globals';
// Mock page functions
const mockGoBack = jest.fn().mockImplementation(() => Promise.resolve());
const mockGoForward = jest.fn().mockImplementation(() => Promise.resolve());
const mockIsClosed = jest.fn().mockReturnValue(false);
// Mock the Page object with proper typing
const mockPage = {
goBack: mockGoBack,
goForward: mockGoForward,
isClosed: mockIsClosed
} as unknown as Page;
// Mock the browser
const mockIsConnected = jest.fn().mockReturnValue(true);
const mockBrowser = {
isConnected: mockIsConnected
} as unknown as Browser;
// Mock the server
const mockServer = {
sendMessage: jest.fn()
};
// Mock context
const mockContext = {
page: mockPage,
browser: mockBrowser,
server: mockServer
} as ToolContext;
describe('Browser Navigation History Tools', () => {
let goBackTool: GoBackTool;
let goForwardTool: GoForwardTool;
beforeEach(() => {
jest.clearAllMocks();
goBackTool = new GoBackTool(mockServer);
goForwardTool = new GoForwardTool(mockServer);
// Reset browser and page mocks
mockIsConnected.mockReturnValue(true);
mockIsClosed.mockReturnValue(false);
});
describe('GoBackTool', () => {
test('should navigate back in browser history', async () => {
const args = {};
const result = await goBackTool.execute(args, mockContext);
expect(mockGoBack).toHaveBeenCalled();
expect(result.isError).toBe(false);
expect(result.content[0].text).toContain('Navigated back');
});
test('should handle navigation back errors', async () => {
const args = {};
// Mock a navigation error
mockGoBack.mockImplementationOnce(() => Promise.reject(new Error('Navigation back failed')));
const result = await goBackTool.execute(args, mockContext);
expect(mockGoBack).toHaveBeenCalled();
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('Operation failed');
});
test('should handle missing page', async () => {
const args = {};
const result = await goBackTool.execute(args, { server: mockServer } as ToolContext);
expect(mockGoBack).not.toHaveBeenCalled();
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('Browser page not initialized');
});
});
describe('GoForwardTool', () => {
test('should navigate forward in browser history', async () => {
const args = {};
const result = await goForwardTool.execute(args, mockContext);
expect(mockGoForward).toHaveBeenCalled();
expect(result.isError).toBe(false);
expect(result.content[0].text).toContain('Navigated forward');
});
test('should handle navigation forward errors', async () => {
const args = {};
// Mock a navigation error
mockGoForward.mockImplementationOnce(() => Promise.reject(new Error('Navigation forward failed')));
const result = await goForwardTool.execute(args, mockContext);
expect(mockGoForward).toHaveBeenCalled();
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('Operation failed');
});
test('should handle missing page', async () => {
const args = {};
const result = await goForwardTool.execute(args, { server: mockServer } as ToolContext);
expect(mockGoForward).not.toHaveBeenCalled();
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('Browser page not initialized');
});
});
});
```
--------------------------------------------------------------------------------
/src/tools/browser/navigation.ts:
--------------------------------------------------------------------------------
```typescript
import { BrowserToolBase } from './base.js';
import { ToolContext, ToolResponse, createSuccessResponse, createErrorResponse } from '../common/types.js';
import { resetBrowserState } from '../../toolHandler.js';
/**
* Tool for navigating to URLs
*/
export class NavigationTool extends BrowserToolBase {
/**
* Execute the navigation tool
*/
async execute(args: any, context: ToolContext): Promise<ToolResponse> {
// Check if browser is available
if (!context.browser || !context.browser.isConnected()) {
// If browser is not connected, we need to reset the state to force recreation
resetBrowserState();
return createErrorResponse(
"Browser is not connected. The connection has been reset - please retry your navigation."
);
}
// Check if page is available and not closed
if (!context.page || context.page.isClosed()) {
return createErrorResponse(
"Page is not available or has been closed. Please retry your navigation."
);
}
return this.safeExecute(context, async (page) => {
try {
await page.goto(args.url, {
timeout: args.timeout || 30000,
waitUntil: args.waitUntil || "load"
});
return createSuccessResponse(`Navigated to ${args.url}`);
} catch (error) {
const errorMessage = (error as Error).message;
// Check for common disconnection errors
if (
errorMessage.includes("Target page, context or browser has been closed") ||
errorMessage.includes("Target closed") ||
errorMessage.includes("Browser has been disconnected")
) {
// Reset browser state to force recreation on next attempt
resetBrowserState();
return createErrorResponse(
`Browser connection issue: ${errorMessage}. Connection has been reset - please retry your navigation.`
);
}
// For other errors, return the standard error
throw error;
}
});
}
}
/**
* Tool for closing the browser
*/
export class CloseBrowserTool extends BrowserToolBase {
/**
* Execute the close browser tool
*/
async execute(args: any, context: ToolContext): Promise<ToolResponse> {
if (context.browser) {
try {
// Check if browser is still connected
if (context.browser.isConnected()) {
await context.browser.close().catch(error => {
console.error("Error while closing browser:", error);
});
} else {
console.error("Browser already disconnected, cleaning up state");
}
} catch (error) {
console.error("Error during browser close operation:", error);
// Continue with resetting state even if close fails
} finally {
// Always reset the global browser and page references
resetBrowserState();
}
return createSuccessResponse("Browser closed successfully");
}
return createSuccessResponse("No browser instance to close");
}
}
/**
* Tool for navigating back in browser history
*/
export class GoBackTool extends BrowserToolBase {
/**
* Execute the go back tool
*/
async execute(args: any, context: ToolContext): Promise<ToolResponse> {
return this.safeExecute(context, async (page) => {
await page.goBack();
return createSuccessResponse("Navigated back in browser history");
});
}
}
/**
* Tool for navigating forward in browser history
*/
export class GoForwardTool extends BrowserToolBase {
/**
* Execute the go forward tool
*/
async execute(args: any, context: ToolContext): Promise<ToolResponse> {
return this.safeExecute(context, async (page) => {
await page.goForward();
return createSuccessResponse("Navigated forward in browser history");
});
}
}
```
--------------------------------------------------------------------------------
/src/__tests__/tools/browser/console.test.ts:
--------------------------------------------------------------------------------
```typescript
import { ConsoleLogsTool } from '../../../tools/browser/console.js';
import { ToolContext } from '../../../tools/common/types.js';
import { jest } from '@jest/globals';
// Mock the server
const mockServer = {
sendMessage: jest.fn()
};
// Mock context
const mockContext = {
server: mockServer
} as ToolContext;
describe('ConsoleLogsTool', () => {
let consoleLogsTool: ConsoleLogsTool;
beforeEach(() => {
jest.clearAllMocks();
consoleLogsTool = new ConsoleLogsTool(mockServer);
});
test('should register console messages', () => {
consoleLogsTool.registerConsoleMessage('log', 'Test log message');
consoleLogsTool.registerConsoleMessage('error', 'Test error message');
consoleLogsTool.registerConsoleMessage('warning', 'Test warning message');
const logs = consoleLogsTool.getConsoleLogs();
expect(logs.length).toBe(3);
expect(logs[0]).toContain('Test log message');
expect(logs[1]).toContain('Test error message');
expect(logs[2]).toContain('Test warning message');
});
test('should retrieve console logs with type filter', async () => {
consoleLogsTool.registerConsoleMessage('log', 'Test log message');
consoleLogsTool.registerConsoleMessage('error', 'Test error message');
consoleLogsTool.registerConsoleMessage('warning', 'Test warning message');
const args = {
type: 'error'
};
const result = await consoleLogsTool.execute(args, mockContext);
expect(result.isError).toBe(false);
expect(result.content[0].text).toContain('Retrieved 1 console log(s)');
expect(result.content[1].text).toContain('Test error message');
expect(result.content[1].text).not.toContain('Test log message');
expect(result.content[1].text).not.toContain('Test warning message');
});
test('should retrieve console logs with search filter', async () => {
consoleLogsTool.registerConsoleMessage('log', 'Test log message');
consoleLogsTool.registerConsoleMessage('error', 'Test error with [special] characters');
consoleLogsTool.registerConsoleMessage('warning', 'Another warning message');
const args = {
search: 'special'
};
const result = await consoleLogsTool.execute(args, mockContext);
expect(result.isError).toBe(false);
expect(result.content[0].text).toContain('Retrieved 1 console log(s)');
expect(result.content[1].text).toContain('Test error with [special] characters');
expect(result.content[1].text).not.toContain('Test log message');
expect(result.content[1].text).not.toContain('Another warning message');
});
test('should retrieve console logs with limit', async () => {
for (let i = 0; i < 10; i++) {
consoleLogsTool.registerConsoleMessage('log', `Test log message ${i}`);
}
const args = {
limit: 5
};
const result = await consoleLogsTool.execute(args, mockContext);
expect(result.isError).toBe(false);
expect(result.content[0].text).toContain('Retrieved 5 console log(s)');
// The actual implementation might only show the first log in the content
// Just verify that at least one log message is present
const logText = result.content[1].text as string;
expect(logText).toContain('Test log message');
});
test('should clear console logs when requested', async () => {
consoleLogsTool.registerConsoleMessage('log', 'Test log message');
consoleLogsTool.registerConsoleMessage('error', 'Test error message');
const args = {
clear: true
};
const result = await consoleLogsTool.execute(args, mockContext);
expect(result.isError).toBe(false);
expect(result.content[0].text).toContain('Retrieved 2 console log(s)');
// Logs should be cleared after retrieval
const logs = consoleLogsTool.getConsoleLogs();
expect(logs.length).toBe(0);
});
test('should handle no logs', async () => {
const args = {};
const result = await consoleLogsTool.execute(args, mockContext);
expect(result.isError).toBe(false);
expect(result.content[0].text).toContain('No console logs matching the criteria');
});
});
```
--------------------------------------------------------------------------------
/docs/static/img/node.svg:
--------------------------------------------------------------------------------
```
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="1920" zoomAndPan="magnify" viewBox="0 0 1440 809.999993" height="1080" preserveAspectRatio="xMidYMid meet" version="1.0"><defs><clipPath id="80e02b1db9"><path d="M 378 2 L 979 2 L 979 678.609375 L 378 678.609375 Z M 378 2 " clip-rule="nonzero"/></clipPath><clipPath id="db02e54ba1"><path d="M 788.117188 315.632812 L 1083.363281 315.632812 L 1083.363281 810 L 788.117188 810 Z M 788.117188 315.632812 " clip-rule="nonzero"/></clipPath></defs><g clip-path="url(#80e02b1db9)"><path fill="#539e43" d="M 678.527344 678.515625 C 669.195312 678.515625 660.5 676.03125 652.433594 671.691406 L 569.808594 622.589844 C 557.382812 615.765625 563.597656 613.28125 567.324219 612.039062 C 584.097656 606.433594 587.203125 605.191406 604.597656 595.25 C 606.460938 594.007812 608.945312 594.640625 610.8125 595.882812 L 674.164062 633.777344 C 676.648438 635.019531 679.769531 635.019531 681.644531 633.777344 L 929.496094 490.246094 C 931.980469 489.003906 933.222656 486.515625 933.222656 483.398438 L 933.222656 196.921875 C 933.222656 193.820312 931.980469 191.332031 929.496094 190.089844 L 681.644531 47.164062 C 679.160156 45.917969 676.039062 45.917969 674.164062 47.164062 L 426.3125 190.089844 C 423.816406 191.332031 422.574219 194.441406 422.574219 196.925781 L 422.574219 483.402344 C 422.574219 485.886719 423.816406 489.003906 426.300781 490.246094 L 494.015625 529.382812 C 530.671875 548.027344 553.65625 526.289062 553.65625 504.527344 L 553.65625 221.78125 C 553.65625 218.054688 556.761719 214.328125 561.109375 214.328125 L 592.796875 214.328125 C 596.523438 214.328125 600.25 217.433594 600.25 221.785156 L 600.25 504.53125 C 600.25 553.632812 573.535156 582.214844 526.941406 582.214844 C 512.652344 582.214844 501.472656 582.214844 469.789062 566.667969 L 404.558594 529.386719 C 388.40625 520.078125 378.464844 502.679688 378.464844 484.035156 L 378.464844 197.5625 C 378.464844 178.90625 388.40625 161.507812 404.558594 152.1875 L 652.433594 8.640625 C 667.953125 -0.0585938 689.097656 -0.0585938 704.617188 8.640625 L 952.492188 152.1875 C 968.644531 161.507812 978.585938 178.90625 978.585938 197.550781 L 978.585938 484.027344 C 978.585938 502.667969 968.644531 520.066406 952.492188 529.375 L 704.617188 672.929688 C 696.550781 676.65625 687.222656 678.511719 678.527344 678.511719 Z M 754.945312 481.546875 C 646.222656 481.546875 623.859375 431.8125 623.859375 389.554688 C 623.859375 385.828125 626.953125 382.097656 631.3125 382.097656 L 663.617188 382.097656 C 667.34375 382.097656 670.4375 384.585938 670.4375 388.3125 C 675.40625 421.257812 689.707031 437.414062 755.554688 437.414062 C 807.738281 437.414062 830.101562 425.597656 830.101562 397.644531 C 830.101562 381.488281 823.890625 369.671875 742.523438 361.605469 C 674.796875 354.757812 632.554688 339.84375 632.554688 285.792969 C 632.554688 235.453125 674.796875 205.625 745.617188 205.625 C 825.132812 205.625 864.28125 232.964844 869.25 292.617188 C 869.25 294.496094 868.617188 296.347656 867.375 298.222656 C 866.132812 299.464844 864.28125 300.707031 862.40625 300.707031 L 830.101562 300.707031 C 827.007812 300.707031 823.890625 298.222656 823.28125 295.105469 C 815.824219 260.9375 796.558594 249.753906 745.617188 249.753906 C 688.464844 249.753906 681.644531 269.636719 681.644531 284.550781 C 681.644531 302.5625 689.707031 308.164062 766.738281 318.105469 C 843.160156 328.050781 879.191406 342.328125 879.191406 395.769531 C 878.558594 450.453125 833.828125 481.546875 754.945312 481.546875 Z M 754.945312 481.546875 " fill-opacity="1" fill-rule="nonzero"/></g><g clip-path="url(#db02e54ba1)"><path fill="#ffe13e" d="M 788.140625 810 L 889.296875 545.195312 L 796.605469 545.195312 L 889.296875 315.632812 L 1044.570312 315.632812 L 948.335938 482.785156 L 1083.335938 481.011719 Z M 788.140625 810 " fill-opacity="1" fill-rule="nonzero"/><path fill="#ffd13e" d="M 788.140625 810 L 980.804688 516.058594 L 891.066406 516.058594 L 1044.570312 315.632812 L 948.335938 482.785156 L 1083.335938 481.011719 Z M 788.140625 810 " fill-opacity="1" fill-rule="nonzero"/></g></svg>
```
--------------------------------------------------------------------------------
/src/__tests__/tools.test.ts:
--------------------------------------------------------------------------------
```typescript
import { createToolDefinitions, BROWSER_TOOLS, API_TOOLS } from '../tools';
describe('Tool Definitions', () => {
const toolDefinitions = createToolDefinitions();
test('should return an array of tool definitions', () => {
expect(Array.isArray(toolDefinitions)).toBe(true);
expect(toolDefinitions.length).toBeGreaterThan(0);
});
test('each tool definition should have required properties', () => {
toolDefinitions.forEach(tool => {
expect(tool).toHaveProperty('name');
expect(tool).toHaveProperty('description');
expect(tool).toHaveProperty('inputSchema');
expect(tool.inputSchema).toHaveProperty('type');
expect(tool.inputSchema).toHaveProperty('properties');
});
});
test('BROWSER_TOOLS should contain browser-related tool names', () => {
expect(Array.isArray(BROWSER_TOOLS)).toBe(true);
expect(BROWSER_TOOLS.length).toBeGreaterThan(0);
BROWSER_TOOLS.forEach(toolName => {
expect(toolDefinitions.some(tool => tool.name === toolName)).toBe(true);
});
});
test('API_TOOLS should contain API-related tool names', () => {
expect(Array.isArray(API_TOOLS)).toBe(true);
expect(API_TOOLS.length).toBeGreaterThan(0);
API_TOOLS.forEach(toolName => {
expect(toolDefinitions.some(tool => tool.name === toolName)).toBe(true);
});
});
test('should validate navigate tool schema', () => {
const navigateTool = toolDefinitions.find(tool => tool.name === 'playwright_navigate');
expect(navigateTool).toBeDefined();
expect(navigateTool!.inputSchema.properties).toHaveProperty('url');
expect(navigateTool!.inputSchema.properties).toHaveProperty('waitUntil');
expect(navigateTool!.inputSchema.properties).toHaveProperty('timeout');
expect(navigateTool!.inputSchema.properties).toHaveProperty('width');
expect(navigateTool!.inputSchema.properties).toHaveProperty('height');
expect(navigateTool!.inputSchema.properties).toHaveProperty('headless');
expect(navigateTool!.inputSchema.required).toEqual(['url']);
});
test('should validate go_back tool schema', () => {
const goBackTool = toolDefinitions.find(tool => tool.name === 'playwright_go_back');
expect(goBackTool).toBeDefined();
expect(goBackTool!.inputSchema.properties).toEqual({});
expect(goBackTool!.inputSchema.required).toEqual([]);
});
test('should validate go_forward tool schema', () => {
const goForwardTool = toolDefinitions.find(tool => tool.name === 'playwright_go_forward');
expect(goForwardTool).toBeDefined();
expect(goForwardTool!.inputSchema.properties).toEqual({});
expect(goForwardTool!.inputSchema.required).toEqual([]);
});
test('should validate drag tool schema', () => {
const dragTool = toolDefinitions.find(tool => tool.name === 'playwright_drag');
expect(dragTool).toBeDefined();
expect(dragTool!.inputSchema.properties).toHaveProperty('sourceSelector');
expect(dragTool!.inputSchema.properties).toHaveProperty('targetSelector');
expect(dragTool!.inputSchema.required).toEqual(['sourceSelector', 'targetSelector']);
});
test('should validate press_key tool schema', () => {
const pressKeyTool = toolDefinitions.find(tool => tool.name === 'playwright_press_key');
expect(pressKeyTool).toBeDefined();
expect(pressKeyTool!.inputSchema.properties).toHaveProperty('key');
expect(pressKeyTool!.inputSchema.properties).toHaveProperty('selector');
expect(pressKeyTool!.inputSchema.required).toEqual(['key']);
});
test('should validate save_as_pdf tool schema', () => {
const saveAsPdfTool = toolDefinitions.find(tool => tool.name === 'playwright_save_as_pdf');
expect(saveAsPdfTool).toBeDefined();
expect(saveAsPdfTool!.inputSchema.properties).toHaveProperty('outputPath');
expect(saveAsPdfTool!.inputSchema.properties).toHaveProperty('filename');
expect(saveAsPdfTool!.inputSchema.properties).toHaveProperty('format');
expect(saveAsPdfTool!.inputSchema.properties).toHaveProperty('printBackground');
expect(saveAsPdfTool!.inputSchema.properties).toHaveProperty('margin');
expect(saveAsPdfTool!.inputSchema.required).toEqual(['outputPath']);
});
});
```
--------------------------------------------------------------------------------
/docs/docs/playwright-web/Examples.md:
--------------------------------------------------------------------------------
```markdown
---
sidebar_position: 4
---
# 🌐 Examples of browser automation
Lets see how we can use the power of Playwright MCP Server to automate our browser and do webscrapping
### Using Different Browser Types
Playwright MCP now supports multiple browser engines. You can choose between Chromium (default), Firefox, and WebKit:
```bdd
Given I navigate to website "https://example.com" using the "firefox" browser
And I take a screenshot named "firefox-example"
Then I navigate to website "https://example.com" using the "webkit" browser
And I take a screenshot named "webkit-example"
```
When you send these commands to Claude, it will open the website in Firefox first, take a screenshot, then switch to WebKit and take another screenshot, allowing you to compare how different browsers render the same website.
### Scenario in BDD Format
```bdd
Given I navigate to website http://eaapp.somee.com and click login link
And I enter username and password as "admin" and "password" respectively and perform login
Then click the Employee List page
And click "Create New" button and enter realistic employee details to create for Name, Salary, DurationWorked,
Select dropdown for Grade as CLevel and Email.
```
Once I enter the above text in ***Claude Desktop Client*** I should see Claude desktop giving me prompt to perform operation
by opening real browser like this

And once the entire test operation completes, we will be presented with the entire details of how the automation did happened.

### Using Browser History Navigation
You can navigate through the browser's history using the new navigation controls:
```bdd
Given I navigate to website "https://example.com"
When I navigate to website "https://example.com/about"
And I navigate back in browser history
Then the current page should be "https://example.com"
When I navigate forward in browser history
Then the current page should be "https://example.com/about"
```
### Using Drag and Drop Functionality
You can drag and drop elements using the new drag tool:
```bdd
Given I navigate to website "https://example.com/drag-drop-demo"
When I drag element with id "draggable" to element with id "droppable"
Then I should see confirmation message "Dropped!"
```
### Using Keyboard Interactions
You can simulate keyboard presses with the new keyboard tool:
```bdd
Given I navigate to website "https://example.com/form"
When I focus on the input field with id "search-box"
And I press the "Enter" key
Then the search results should appear
```
### Saving Page as PDF
You can save the current page as a PDF file:
```bdd
Given I navigate to website "https://example.com/report"
When I save the current page as a PDF in "/downloads" folder with name "report.pdf"
Then I should see confirmation that the PDF was saved
```
Advanced example with custom options:
```bdd
Given I navigate to website "https://example.com/invoice"
When I save the page as PDF with the following settings:
| Setting | Value |
| ----------------- | --------- |
| Output Path | /downloads |
| Filename | invoice.pdf |
| Format | Letter |
| Print Background | true |
| Top Margin | 2cm |
| Right Margin | 1cm |
| Bottom Margin | 2cm |
| Left Margin | 1cm |
Then I should see confirmation that the PDF was saved
```
### Extracting Page Content
You can extract visible text content from the page:
```bdd
Given I navigate to website "https://example.com/article"
When I extract all visible text from the page
Then I should see the article content in plain text without hidden elements
```
You can also get the complete HTML of the page:
```bdd
Given I navigate to website "https://example.com/products"
When I extract the HTML content of the page
Then I should receive the complete HTML structure of the page
```
Example use case for content analysis:
```bdd
Given I navigate to website "https://example.com/pricing"
When I extract all visible text from the page
Then I should be able to analyze the text to find pricing information
And I can determine if the "Enterprise" plan mentions "custom pricing"
```
```
--------------------------------------------------------------------------------
/src/__tests__/tools/browser/navigation.test.ts:
--------------------------------------------------------------------------------
```typescript
import { NavigationTool } from '../../../tools/browser/navigation.js';
import { ToolContext } from '../../../tools/common/types.js';
import { Page, Browser } from 'playwright';
import { jest } from '@jest/globals';
// Mock the Page object
const mockGoto = jest.fn();
mockGoto.mockImplementation(() => Promise.resolve());
const mockIsClosed = jest.fn().mockReturnValue(false);
const mockPage = {
goto: mockGoto,
isClosed: mockIsClosed
} as unknown as Page;
// Mock the browser
const mockIsConnected = jest.fn().mockReturnValue(true);
const mockBrowser = {
isConnected: mockIsConnected
} as unknown as Browser;
// Mock the server
const mockServer = {
sendMessage: jest.fn()
};
// Mock context
const mockContext = {
page: mockPage,
browser: mockBrowser,
server: mockServer
} as ToolContext;
describe('NavigationTool', () => {
let navigationTool: NavigationTool;
beforeEach(() => {
jest.clearAllMocks();
navigationTool = new NavigationTool(mockServer);
// Reset mocks
mockIsConnected.mockReturnValue(true);
mockIsClosed.mockReturnValue(false);
});
test('should navigate to a URL', async () => {
const args = {
url: 'https://example.com',
waitUntil: 'networkidle'
};
const result = await navigationTool.execute(args, mockContext);
expect(mockGoto).toHaveBeenCalledWith('https://example.com', { waitUntil: 'networkidle', timeout: 30000 });
expect(result.isError).toBe(false);
expect(result.content[0].text).toContain('Navigated to');
});
test('should handle navigation with specific browser type', async () => {
const args = {
url: 'https://example.com',
waitUntil: 'networkidle',
browserType: 'firefox'
};
const result = await navigationTool.execute(args, mockContext);
expect(mockGoto).toHaveBeenCalledWith('https://example.com', { waitUntil: 'networkidle', timeout: 30000 });
expect(result.isError).toBe(false);
expect(result.content[0].text).toContain('Navigated to');
});
test('should handle navigation with webkit browser type', async () => {
const args = {
url: 'https://example.com',
browserType: 'webkit'
};
const result = await navigationTool.execute(args, mockContext);
expect(mockGoto).toHaveBeenCalledWith('https://example.com', { waitUntil: 'load', timeout: 30000 });
expect(result.isError).toBe(false);
expect(result.content[0].text).toContain('Navigated to');
});
test('should handle navigation errors', async () => {
const args = {
url: 'https://example.com'
};
// Mock a navigation error
mockGoto.mockImplementationOnce(() => Promise.reject(new Error('Navigation failed')));
const result = await navigationTool.execute(args, mockContext);
expect(mockGoto).toHaveBeenCalledWith('https://example.com', { waitUntil: 'load', timeout: 30000 });
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('Operation failed');
});
test('should handle missing page', async () => {
const args = {
url: 'https://example.com'
};
// Context with browser but without page
const contextWithoutPage = {
browser: mockBrowser,
server: mockServer
} as unknown as ToolContext;
const result = await navigationTool.execute(args, contextWithoutPage);
expect(mockGoto).not.toHaveBeenCalled();
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('Page is not available');
});
test('should handle disconnected browser', async () => {
const args = {
url: 'https://example.com'
};
// Mock disconnected browser
mockIsConnected.mockReturnValueOnce(false);
const result = await navigationTool.execute(args, mockContext);
expect(mockGoto).not.toHaveBeenCalled();
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('Browser is not connected');
});
test('should handle closed page', async () => {
const args = {
url: 'https://example.com'
};
// Mock closed page
mockIsClosed.mockReturnValueOnce(true);
const result = await navigationTool.execute(args, mockContext);
expect(mockGoto).not.toHaveBeenCalled();
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('Page is not available or has been closed');
});
});
```
--------------------------------------------------------------------------------
/src/tools/browser/interaction.ts:
--------------------------------------------------------------------------------
```typescript
import { BrowserToolBase } from './base.js';
import { ToolContext, ToolResponse, createSuccessResponse, createErrorResponse } from '../common/types.js';
/**
* Tool for clicking elements on the page
*/
export class ClickTool extends BrowserToolBase {
/**
* Execute the click tool
*/
async execute(args: any, context: ToolContext): Promise<ToolResponse> {
return this.safeExecute(context, async (page) => {
await page.click(args.selector);
return createSuccessResponse(`Clicked element: ${args.selector}`);
});
}
}
/**
* Tool for clicking elements inside iframes
*/
export class IframeClickTool extends BrowserToolBase {
/**
* Execute the iframe click tool
*/
async execute(args: any, context: ToolContext): Promise<ToolResponse> {
return this.safeExecute(context, async (page) => {
const frame = page.frameLocator(args.iframeSelector);
if (!frame) {
return createErrorResponse(`Iframe not found: ${args.iframeSelector}`);
}
await frame.locator(args.selector).click();
return createSuccessResponse(`Clicked element ${args.selector} inside iframe ${args.iframeSelector}`);
});
}
}
/**
* Tool for filling form fields
*/
export class FillTool extends BrowserToolBase {
/**
* Execute the fill tool
*/
async execute(args: any, context: ToolContext): Promise<ToolResponse> {
return this.safeExecute(context, async (page) => {
await page.waitForSelector(args.selector);
await page.fill(args.selector, args.value);
return createSuccessResponse(`Filled ${args.selector} with: ${args.value}`);
});
}
}
/**
* Tool for selecting options from dropdown menus
*/
export class SelectTool extends BrowserToolBase {
/**
* Execute the select tool
*/
async execute(args: any, context: ToolContext): Promise<ToolResponse> {
return this.safeExecute(context, async (page) => {
await page.waitForSelector(args.selector);
await page.selectOption(args.selector, args.value);
return createSuccessResponse(`Selected ${args.selector} with: ${args.value}`);
});
}
}
/**
* Tool for hovering over elements
*/
export class HoverTool extends BrowserToolBase {
/**
* Execute the hover tool
*/
async execute(args: any, context: ToolContext): Promise<ToolResponse> {
return this.safeExecute(context, async (page) => {
await page.waitForSelector(args.selector);
await page.hover(args.selector);
return createSuccessResponse(`Hovered ${args.selector}`);
});
}
}
/**
* Tool for executing JavaScript in the browser
*/
export class EvaluateTool extends BrowserToolBase {
/**
* Execute the evaluate tool
*/
async execute(args: any, context: ToolContext): Promise<ToolResponse> {
return this.safeExecute(context, async (page) => {
const result = await page.evaluate(args.script);
// Convert result to string for display
let resultStr: string;
try {
resultStr = JSON.stringify(result, null, 2);
} catch (error) {
resultStr = String(result);
}
return createSuccessResponse([
`Executed JavaScript:`,
`${args.script}`,
`Result:`,
`${resultStr}`
]);
});
}
}
/**
* Tool for dragging elements on the page
*/
export class DragTool extends BrowserToolBase {
/**
* Execute the drag tool
*/
async execute(args: any, context: ToolContext): Promise<ToolResponse> {
return this.safeExecute(context, async (page) => {
const sourceElement = await page.waitForSelector(args.sourceSelector);
const targetElement = await page.waitForSelector(args.targetSelector);
const sourceBound = await sourceElement.boundingBox();
const targetBound = await targetElement.boundingBox();
if (!sourceBound || !targetBound) {
return createErrorResponse("Could not get element positions for drag operation");
}
await page.mouse.move(
sourceBound.x + sourceBound.width / 2,
sourceBound.y + sourceBound.height / 2
);
await page.mouse.down();
await page.mouse.move(
targetBound.x + targetBound.width / 2,
targetBound.y + targetBound.height / 2
);
await page.mouse.up();
return createSuccessResponse(`Dragged element from ${args.sourceSelector} to ${args.targetSelector}`);
});
}
}
/**
* Tool for pressing keyboard keys
*/
export class PressKeyTool extends BrowserToolBase {
/**
* Execute the key press tool
*/
async execute(args: any, context: ToolContext): Promise<ToolResponse> {
return this.safeExecute(context, async (page) => {
if (args.selector) {
await page.waitForSelector(args.selector);
await page.focus(args.selector);
}
await page.keyboard.press(args.key);
return createSuccessResponse(`Pressed key: ${args.key}`);
});
}
}
```
--------------------------------------------------------------------------------
/src/__tests__/tools/browser/screenshot.test.ts:
--------------------------------------------------------------------------------
```typescript
import { ScreenshotTool } from '../../../tools/browser/screenshot.js';
import { ToolContext } from '../../../tools/common/types.js';
import { Page, Browser } from 'playwright';
import { jest } from '@jest/globals';
import fs from 'node:fs';
import path from 'node:path';
// Mock fs module
jest.mock('node:fs', () => ({
existsSync: jest.fn().mockReturnValue(true),
mkdirSync: jest.fn(),
writeFileSync: jest.fn()
}));
// Mock the Page object
const mockScreenshot = jest.fn().mockImplementation(() =>
Promise.resolve(Buffer.from('mock-screenshot')));
const mockLocatorScreenshot = jest.fn().mockImplementation(() =>
Promise.resolve(Buffer.from('mock-element-screenshot')));
const mockElementHandle = {
screenshot: mockLocatorScreenshot
};
const mockElement = jest.fn().mockImplementation(() => Promise.resolve(mockElementHandle));
const mockLocator = jest.fn().mockReturnValue({
screenshot: mockLocatorScreenshot
});
const mockIsClosed = jest.fn().mockReturnValue(false);
const mockPage = {
screenshot: mockScreenshot,
locator: mockLocator,
$: mockElement,
isClosed: mockIsClosed
} as unknown as Page;
// Mock browser
const mockIsConnected = jest.fn().mockReturnValue(true);
const mockBrowser = {
isConnected: mockIsConnected
} as unknown as Browser;
// Mock the server
const mockServer = {
sendMessage: jest.fn(),
notification: jest.fn()
};
// Mock context
const mockContext = {
page: mockPage,
browser: mockBrowser,
server: mockServer
} as ToolContext;
describe('ScreenshotTool', () => {
let screenshotTool: ScreenshotTool;
beforeEach(() => {
jest.clearAllMocks();
screenshotTool = new ScreenshotTool(mockServer);
// Mock Date to return a consistent value for testing
jest.spyOn(global.Date.prototype, 'toISOString').mockReturnValue('2023-01-01T12:00:00.000Z');
(fs.existsSync as jest.Mock).mockReturnValue(true);
});
afterEach(() => {
jest.restoreAllMocks();
});
test('should take a full page screenshot', async () => {
const args = {
name: 'test-screenshot',
fullPage: true
};
// Return a buffer for the screenshot
const screenshotBuffer = Buffer.from('mock-screenshot');
mockScreenshot.mockImplementationOnce(() => Promise.resolve(screenshotBuffer));
const result = await screenshotTool.execute(args, mockContext);
// Check if screenshot was called with correct options
expect(mockScreenshot).toHaveBeenCalledWith(expect.objectContaining({
fullPage: true,
type: 'png'
}));
// Check that the result contains success message
expect(result.isError).toBe(false);
expect(result.content[0].text).toContain('Screenshot saved to');
});
test('should handle element screenshot', async () => {
const args = {
name: 'test-element-screenshot',
selector: '#test-element'
};
// Return a buffer for the screenshot
const screenshotBuffer = Buffer.from('mock-element-screenshot');
mockLocatorScreenshot.mockImplementationOnce(() => Promise.resolve(screenshotBuffer));
const result = await screenshotTool.execute(args, mockContext);
// Check that the result contains success message
expect(result.isError).toBe(false);
expect(result.content[0].text).toContain('Screenshot saved to');
});
test('should handle screenshot errors', async () => {
const args = {
name: 'test-screenshot'
};
// Mock a screenshot error
mockScreenshot.mockImplementationOnce(() => Promise.reject(new Error('Screenshot failed')));
const result = await screenshotTool.execute(args, mockContext);
expect(mockScreenshot).toHaveBeenCalled();
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('Operation failed');
});
test('should handle missing page', async () => {
const args = {
name: 'test-screenshot'
};
// Context without page but with browser
const contextWithoutPage = {
browser: mockBrowser,
server: mockServer
} as unknown as ToolContext;
const result = await screenshotTool.execute(args, contextWithoutPage);
expect(mockScreenshot).not.toHaveBeenCalled();
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('Browser page not initialized');
});
test('should store screenshots in a map', async () => {
const args = {
name: 'test-screenshot',
storeBase64: true
};
// Return a buffer for the screenshot
const screenshotBuffer = Buffer.from('mock-screenshot');
mockScreenshot.mockImplementationOnce(() => Promise.resolve(screenshotBuffer));
await screenshotTool.execute(args, mockContext);
// Check that the screenshot was stored in the map
const screenshots = screenshotTool.getScreenshots();
expect(screenshots.has('test-screenshot')).toBe(true);
});
test('should take a screenshot with specific browser type', async () => {
const args = {
name: 'browser-type-test',
browserType: 'firefox'
};
// Execute with browser type
const result = await screenshotTool.execute(args, mockContext);
expect(mockScreenshot).toHaveBeenCalled();
expect(result.isError).toBe(false);
expect(result.content[0].text).toContain('Screenshot saved to');
});
});
```
--------------------------------------------------------------------------------
/src/tools/api/requests.ts:
--------------------------------------------------------------------------------
```typescript
import { ApiToolBase } from './base.js';
import { ToolContext, ToolResponse, createSuccessResponse, createErrorResponse } from '../common/types.js';
/**
* Tool for making GET requests
*/
export class GetRequestTool extends ApiToolBase {
/**
* Execute the GET request tool
*/
async execute(args: any, context: ToolContext): Promise<ToolResponse> {
return this.safeExecute(context, async (apiContext) => {
const response = await apiContext.get(args.url);
let responseText;
try {
responseText = await response.text();
} catch (error) {
responseText = "Unable to get response text";
}
return createSuccessResponse([
`GET request to ${args.url}`,
`Status: ${response.status()} ${response.statusText()}`,
`Response: ${responseText.substring(0, 1000)}${responseText.length > 1000 ? '...' : ''}`
]);
});
}
}
/**
* Tool for making POST requests
*/
export class PostRequestTool extends ApiToolBase {
/**
* Execute the POST request tool
*/
async execute(args: any, context: ToolContext): Promise<ToolResponse> {
return this.safeExecute(context, async (apiContext) => {
// Check if the value is valid JSON if it starts with { or [
if (args.value && typeof args.value === 'string' &&
(args.value.startsWith('{') || args.value.startsWith('['))) {
try {
JSON.parse(args.value);
} catch (error) {
return createErrorResponse(`Failed to parse request body: ${(error as Error).message}`);
}
}
const response = await apiContext.post(args.url, {
data: typeof args.value === 'string' ? JSON.parse(args.value) : args.value,
headers: {
'Content-Type': 'application/json',
...(args.token ? { 'Authorization': `Bearer ${args.token}` } : {}),
...(args.headers || {})
}
});
let responseText;
try {
responseText = await response.text();
} catch (error) {
responseText = "Unable to get response text";
}
return createSuccessResponse([
`POST request to ${args.url}`,
`Status: ${response.status()} ${response.statusText()}`,
`Response: ${responseText.substring(0, 1000)}${responseText.length > 1000 ? '...' : ''}`
]);
});
}
}
/**
* Tool for making PUT requests
*/
export class PutRequestTool extends ApiToolBase {
/**
* Execute the PUT request tool
*/
async execute(args: any, context: ToolContext): Promise<ToolResponse> {
return this.safeExecute(context, async (apiContext) => {
// Check if the value is valid JSON if it starts with { or [
if (args.value && typeof args.value === 'string' &&
(args.value.startsWith('{') || args.value.startsWith('['))) {
try {
JSON.parse(args.value);
} catch (error) {
return createErrorResponse(`Failed to parse request body: ${(error as Error).message}`);
}
}
const response = await apiContext.put(args.url, {
data: args.value
});
let responseText;
try {
responseText = await response.text();
} catch (error) {
responseText = "Unable to get response text";
}
return createSuccessResponse([
`PUT request to ${args.url}`,
`Status: ${response.status()} ${response.statusText()}`,
`Response: ${responseText.substring(0, 1000)}${responseText.length > 1000 ? '...' : ''}`
]);
});
}
}
/**
* Tool for making PATCH requests
*/
export class PatchRequestTool extends ApiToolBase {
/**
* Execute the PATCH request tool
*/
async execute(args: any, context: ToolContext): Promise<ToolResponse> {
return this.safeExecute(context, async (apiContext) => {
// Check if the value is valid JSON if it starts with { or [
if (args.value && typeof args.value === 'string' &&
(args.value.startsWith('{') || args.value.startsWith('['))) {
try {
JSON.parse(args.value);
} catch (error) {
return createErrorResponse(`Failed to parse request body: ${(error as Error).message}`);
}
}
const response = await apiContext.patch(args.url, {
data: args.value
});
let responseText;
try {
responseText = await response.text();
} catch (error) {
responseText = "Unable to get response text";
}
return createSuccessResponse([
`PATCH request to ${args.url}`,
`Status: ${response.status()} ${response.statusText()}`,
`Response: ${responseText.substring(0, 1000)}${responseText.length > 1000 ? '...' : ''}`
]);
});
}
}
/**
* Tool for making DELETE requests
*/
export class DeleteRequestTool extends ApiToolBase {
/**
* Execute the DELETE request tool
*/
async execute(args: any, context: ToolContext): Promise<ToolResponse> {
return this.safeExecute(context, async (apiContext) => {
const response = await apiContext.delete(args.url);
let responseText;
try {
responseText = await response.text();
} catch (error) {
responseText = "Unable to get response text";
}
return createSuccessResponse([
`DELETE request to ${args.url}`,
`Status: ${response.status()} ${response.statusText()}`,
`Response: ${responseText.substring(0, 1000)}${responseText.length > 1000 ? '...' : ''}`
]);
});
}
}
```
--------------------------------------------------------------------------------
/docs/docs/release.mdx:
--------------------------------------------------------------------------------
```markdown
---
sidebar_position: 2
---
import YouTubeVideoEmbed from '@site/src/components/HomepageFeatures/YouTubeVideoEmbed';
# Release Notes
## Version 1.0.3
- **Code Generation Capabilities**: Added new code generation capability 🎭
- `start_codegen_session`: Start a new session to record Playwright actions
- `end_codegen_session`: End a session and generate test file
- `get_codegen_session`: Retrieve information about a session
- `clear_codegen_session`: Clear a session without generating a test
- Ability to record real browser interactions and convert them to reusable Playwright tests
- Support for customizing test output path, test names, and including descriptive comments
- **Enhanced Browser Navigation**: Added new navigation control tools 🧭
- `playwright_go_back`: Navigate back in browser history
- `playwright_go_forward`: Navigate forward in browser history
- **Advanced Interaction**: Added new interaction tools for more complex scenarios 🔄
- `playwright_drag`: Drag elements from one location to another
- `playwright_press_key`: Press keyboard keys with optional element focus
- **Output Capabilities**: Added content export functionality 📄
- `playwright_save_as_pdf`: Save the current page as a PDF file with customizable options
- **Content Extraction**: Added tools for retrieving page content 📝
- `playwright_get_visible_text`: Extract all visible text content from the current page
- `playwright_get_visible_html`: Get the complete HTML content of the current page
- Comprehensive test coverage for all new tools
- Updated documentation with examples and usage detail
## Version 1.0.2
- **Multi-Browser Support**: Added support for Firefox and WebKit browsers in addition to Chromium 🌐
- New `browserType` parameter for `playwright_navigate` tool allows specifying browser engine
- Supported browser types: "chromium" (default), "firefox", and "webkit"
- Seamless browser engine switching during automation sessions
- Enhanced test coverage for different browser engines
- Updated documentation with browser-specific examples
## Version 1.0.0
- First major release of Playwright MCP Server with the tool structure changes 🚀
- Fixed issue with headless mode in Playwright #62
- Fixed issue Navigation failed: page.goto: Target page, context or browser has been closed #63
- Completed RFC: Refactor handleToolCall for better maintainability #46
- New feature: Optional Bearer Authorization to API POST (Thanks to ***@CopilotMe***)
- Fixed issue Exit process on host close (Thanks to ***@kiracih***)
- New Feature: Three new tools (Thanks to ***@VinceOPS***)
- `playwright_except_response`
- `playwright_assert_response`
Here is the scenario for the above two tools
```BDD
Scenario: Logging in requires captcha verification
Given I expect the browser to receive an HTTP response from "**/security/captcha-precheck"
When I enter "[email protected]" in the input and I submit
Then The browser should have received the HTTP response
And Its body should contain a property "captchaFamily"
```
- A new tool `playwright_custom_user_agent` to define a custom user agent.
## Version 0.3.1
- Fixed BROWSER_TOOLS as Playwright_console_logs is not required (Thanks to https://github.com/kfern)
- Added Tests for all the Playwright MCP Server tools (Thanks to https://github.com/kfern)
- Updated documentation with AI Courses
- Gen AI Course [Details here](/docs/ai-courses/AIAgents)
- AI Agents Course [Details here](/docs/ai-courses/AIAgents)
- Machine Learning Course [Details here](/docs/ai-courses/MachineLearning)
## Version 0.3.0
- Added support for `Playwright_console_logs` to get the console logs from the browser. Following logs types
are supported.[More Detail available here](/docs/playwright-web/Console-Logging)
- `log`
- `info`
- `warn`
- `error`
- `debug`
- `all`
:::tip Usage Example
To invoke `Playwright_console_logs` via MCP Playwright, use the following prompt:
```plaintext
Get the console log from the browser whenever you perform any action.
:::
- Added support for `Playwright_close` to close the browser and release all resources.
:::tip Usage Example
To invoke `Playwright_close` via MCP Playwright, use the following prompt:
```plaintext
Close the browser once the operation is completed.
:::
## Version 0.2.9
- Fixed Screenshot issue with Cline, Cursor and Windows 11 (Reported by @MackDing, @mengjian-github)
## Version 0.2.8
- Support of iFrame while running Playwright test via MCP (Supports Cline as well). Thanks to @VinceOPS
- Fixed issue while saving PNG file. Thanks to @BayLee4
- Fixed issue with full page screenshot arguments to be passed to tool, thanks for the report @unipro-LeighMason
- Updated to latest version of Playwright and MCP Server library
## Version 0.2.7
- Fixed the issue with Playwright MCP server not working Cline, VSCode reported in #26, #16
- Fixed issue #28 and now chrome version is updated
- Updated to latest version of Playwright and MCP Server library
## Version 0.2.6
- New Documentation site powered by docusaurus hosted in GH-Pages https://executeautomation.github.io/mcp-playwright/
---
## Version 0.2.5
#### API Test Support
- Playwright MCP Server now supports API Testing for
- `GET` request
- `POST` request
- `PUT` request
- `PATCH` request
- `DELETE` request
<YouTubeVideoEmbed videoId="BYYyoRxCcFE" />
---
## Version 0.2.4
- Added support for smithery
- Added Support to save Playwright screenshot in local directory, thanks to `@s4l4x`
---
## Version 0.2.3
- Added quality of life improvement
---
```
--------------------------------------------------------------------------------
/src/tools/codegen/generator.ts:
--------------------------------------------------------------------------------
```typescript
import * as path from 'path';
import { CodegenAction, CodegenOptions, CodegenResult, CodegenSession, PlaywrightTestCase } from './types.js';
export class PlaywrightGenerator {
private static readonly DEFAULT_OPTIONS: Required<CodegenOptions> = {
outputPath: 'tests',
testNamePrefix: 'MCP',
includeComments: true,
};
private options: Required<CodegenOptions>;
constructor(options: CodegenOptions = {}) {
this.validateOptions(options);
this.options = { ...PlaywrightGenerator.DEFAULT_OPTIONS, ...options };
}
private validateOptions(options: CodegenOptions): void {
if (options.outputPath && typeof options.outputPath !== 'string') {
throw new Error('outputPath must be a string');
}
if (options.testNamePrefix && typeof options.testNamePrefix !== 'string') {
throw new Error('testNamePrefix must be a string');
}
if (options.includeComments !== undefined && typeof options.includeComments !== 'boolean') {
throw new Error('includeComments must be a boolean');
}
}
async generateTest(session: CodegenSession): Promise<CodegenResult> {
if (!session || !Array.isArray(session.actions)) {
throw new Error('Invalid session data');
}
const testCase = this.createTestCase(session);
const testCode = this.generateTestCode(testCase);
const filePath = this.getOutputFilePath(session);
return {
testCode,
filePath,
sessionId: session.id,
};
}
private createTestCase(session: CodegenSession): PlaywrightTestCase {
const testCase: PlaywrightTestCase = {
name: `${this.options.testNamePrefix}_${new Date(session.startTime).toISOString().split('T')[0]}`,
steps: [],
imports: new Set(['test', 'expect']),
};
for (const action of session.actions) {
const step = this.convertActionToStep(action);
if (step) {
testCase.steps.push(step);
}
}
return testCase;
}
private convertActionToStep(action: CodegenAction): string | null {
const { toolName, parameters } = action;
switch (toolName) {
case 'playwright_navigate':
return this.generateNavigateStep(parameters);
case 'playwright_fill':
return this.generateFillStep(parameters);
case 'playwright_click':
return this.generateClickStep(parameters);
case 'playwright_screenshot':
return this.generateScreenshotStep(parameters);
case 'playwright_expect_response':
return this.generateExpectResponseStep(parameters);
case 'playwright_assert_response':
return this.generateAssertResponseStep(parameters);
case 'playwright_hover':
return this.generateHoverStep(parameters);
case 'playwright_select':
return this.generateSelectStep(parameters);
case 'playwright_custom_user_agent':
return this.generateCustomUserAgentStep(parameters);
default:
console.warn(`Unsupported tool: ${toolName}`);
return null;
}
}
private generateNavigateStep(parameters: Record<string, unknown>): string {
const { url, waitUntil } = parameters;
const options = waitUntil ? `, { waitUntil: '${waitUntil}' }` : '';
return `
// Navigate to URL
await page.goto('${url}'${options});`;
}
private generateFillStep(parameters: Record<string, unknown>): string {
const { selector, value } = parameters;
return `
// Fill input field
await page.fill('${selector}', '${value}');`;
}
private generateClickStep(parameters: Record<string, unknown>): string {
const { selector } = parameters;
return `
// Click element
await page.click('${selector}');`;
}
private generateScreenshotStep(parameters: Record<string, unknown>): string {
const { name, fullPage = false, path } = parameters;
const options = [];
if (fullPage) options.push('fullPage: true');
if (path) options.push(`path: '${path}'`);
const optionsStr = options.length > 0 ? `, { ${options.join(', ')} }` : '';
return `
// Take screenshot
await page.screenshot({ path: '${name}.png'${optionsStr} });`;
}
private generateExpectResponseStep(parameters: Record<string, unknown>): string {
const { url, id } = parameters;
return `
// Wait for response
const ${id}Response = page.waitForResponse('${url}');`;
}
private generateAssertResponseStep(parameters: Record<string, unknown>): string {
const { id, value } = parameters;
const assertion = value
? `\n const responseText = await ${id}Response.text();\n expect(responseText).toContain('${value}');`
: `\n expect(${id}Response.ok()).toBeTruthy();`;
return `
// Assert response${assertion}`;
}
private generateHoverStep(parameters: Record<string, unknown>): string {
const { selector } = parameters;
return `
// Hover over element
await page.hover('${selector}');`;
}
private generateSelectStep(parameters: Record<string, unknown>): string {
const { selector, value } = parameters;
return `
// Select option
await page.selectOption('${selector}', '${value}');`;
}
private generateCustomUserAgentStep(parameters: Record<string, unknown>): string {
const { userAgent } = parameters;
return `
// Set custom user agent
await context.setUserAgent('${userAgent}');`;
}
private generateTestCode(testCase: PlaywrightTestCase): string {
const imports = Array.from(testCase.imports)
.map(imp => `import { ${imp} } from '@playwright/test';`)
.join('\n');
return `
${imports}
test('${testCase.name}', async ({ page, context }) => {
${testCase.steps.join('\n')}
});`;
}
private getOutputFilePath(session: CodegenSession): string {
if (!session.id) {
throw new Error('Session ID is required');
}
const sanitizedPrefix = this.options.testNamePrefix.toLowerCase().replace(/[^a-z0-9_]/g, '_');
const fileName = `${sanitizedPrefix}_${session.id}.spec.ts`;
return path.resolve(this.options.outputPath, fileName);
}
}
```
--------------------------------------------------------------------------------
/src/__tests__/tools/browser/visiblePage.test.ts:
--------------------------------------------------------------------------------
```typescript
import { VisibleTextTool, VisibleHtmlTool } from '../../../tools/browser/visiblePage.js';
import { ToolContext } from '../../../tools/common/types.js';
import { Page, Browser } from 'playwright';
import { jest } from '@jest/globals';
// Mock the Page object
const mockEvaluate = jest.fn();
const mockContent = jest.fn();
const mockIsClosed = jest.fn().mockReturnValue(false);
const mockPage = {
evaluate: mockEvaluate,
content: mockContent,
isClosed: mockIsClosed
} as unknown as Page;
// Mock the browser
const mockIsConnected = jest.fn().mockReturnValue(true);
const mockBrowser = {
isConnected: mockIsConnected
} as unknown as Browser;
// Mock the server
const mockServer = {
sendMessage: jest.fn()
};
// Mock context
const mockContext = {
page: mockPage,
browser: mockBrowser,
server: mockServer
} as ToolContext;
describe('VisibleTextTool', () => {
let visibleTextTool: VisibleTextTool;
beforeEach(() => {
jest.clearAllMocks();
visibleTextTool = new VisibleTextTool(mockServer);
// Reset mocks
mockIsConnected.mockReturnValue(true);
mockIsClosed.mockReturnValue(false);
mockEvaluate.mockImplementation(() => Promise.resolve('Sample visible text content'));
});
test('should retrieve visible text content', async () => {
const args = {};
const result = await visibleTextTool.execute(args, mockContext);
expect(mockEvaluate).toHaveBeenCalled();
expect(result.isError).toBe(false);
expect(result.content[0].text).toContain('Visible text content');
expect(result.content[0].text).toContain('Sample visible text content');
});
test('should handle missing page', async () => {
const args = {};
// Context with browser but without page
const contextWithoutPage = {
browser: mockBrowser,
server: mockServer
} as unknown as ToolContext;
const result = await visibleTextTool.execute(args, contextWithoutPage);
expect(mockEvaluate).not.toHaveBeenCalled();
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('Page is not available');
});
test('should handle disconnected browser', async () => {
const args = {};
// Mock disconnected browser
mockIsConnected.mockReturnValueOnce(false);
const result = await visibleTextTool.execute(args, mockContext);
expect(mockEvaluate).not.toHaveBeenCalled();
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('Browser is not connected');
});
test('should handle closed page', async () => {
const args = {};
// Mock closed page
mockIsClosed.mockReturnValueOnce(true);
const result = await visibleTextTool.execute(args, mockContext);
expect(mockEvaluate).not.toHaveBeenCalled();
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('Page is not available or has been closed');
});
test('should handle evaluation errors', async () => {
const args = {};
// Mock evaluation error
mockEvaluate.mockImplementationOnce(() => Promise.reject(new Error('Evaluation failed')));
const result = await visibleTextTool.execute(args, mockContext);
expect(mockEvaluate).toHaveBeenCalled();
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('Failed to get visible text content');
expect(result.content[0].text).toContain('Evaluation failed');
});
});
describe('VisibleHtmlTool', () => {
let visibleHtmlTool: VisibleHtmlTool;
beforeEach(() => {
jest.clearAllMocks();
visibleHtmlTool = new VisibleHtmlTool(mockServer);
// Reset mocks
mockIsConnected.mockReturnValue(true);
mockIsClosed.mockReturnValue(false);
mockContent.mockImplementation(() => Promise.resolve('<html><body>Sample HTML content</body></html>'));
});
test('should retrieve HTML content', async () => {
const args = {};
const result = await visibleHtmlTool.execute(args, mockContext);
expect(mockContent).toHaveBeenCalled();
expect(result.isError).toBe(false);
expect(result.content[0].text).toContain('HTML content');
expect(result.content[0].text).toContain('<html><body>Sample HTML content</body></html>');
});
test('should handle missing page', async () => {
const args = {};
// Context with browser but without page
const contextWithoutPage = {
browser: mockBrowser,
server: mockServer
} as unknown as ToolContext;
const result = await visibleHtmlTool.execute(args, contextWithoutPage);
expect(mockContent).not.toHaveBeenCalled();
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('Page is not available');
});
test('should handle disconnected browser', async () => {
const args = {};
// Mock disconnected browser
mockIsConnected.mockReturnValueOnce(false);
const result = await visibleHtmlTool.execute(args, mockContext);
expect(mockContent).not.toHaveBeenCalled();
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('Browser is not connected');
});
test('should handle closed page', async () => {
const args = {};
// Mock closed page
mockIsClosed.mockReturnValueOnce(true);
const result = await visibleHtmlTool.execute(args, mockContext);
expect(mockContent).not.toHaveBeenCalled();
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('Page is not available or has been closed');
});
test('should handle content retrieval errors', async () => {
const args = {};
// Mock content error
mockContent.mockImplementationOnce(() => Promise.reject(new Error('Content retrieval failed')));
const result = await visibleHtmlTool.execute(args, mockContext);
expect(mockContent).toHaveBeenCalled();
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('Failed to get visible HTML content');
expect(result.content[0].text).toContain('Content retrieval failed');
});
});
```
--------------------------------------------------------------------------------
/src/tools/codegen/index.ts:
--------------------------------------------------------------------------------
```typescript
import { Tool } from "../../types.js";
import { ActionRecorder } from "./recorder.js";
import { PlaywrightGenerator } from "./generator.js";
import { CodegenOptions } from "./types.js";
import * as fs from "fs/promises";
import * as path from "path";
import type { Browser, Page } from "rebrowser-playwright";
declare global {
var browser: Browser | undefined;
var page: Page | undefined;
}
// Helper function to get workspace root path
const getWorkspaceRoot = () => {
return process.cwd();
};
const DEFAULT_OPTIONS: Required<CodegenOptions> = {
outputPath: path.join(getWorkspaceRoot(), "e2e"),
testNamePrefix: "Test",
includeComments: true,
};
export const startCodegenSession: Tool = {
name: "start_codegen_session",
description: "Start a new code generation session to record MCP tool actions",
parameters: {
type: "object",
properties: {
options: {
type: "object",
description: "Code generation options",
properties: {
outputPath: { type: "string" },
testNamePrefix: { type: "string" },
includeComments: { type: "boolean" },
},
},
},
},
handler: async ({ options = {} }: { options?: CodegenOptions }) => {
try {
// Merge provided options with defaults
const mergedOptions = { ...DEFAULT_OPTIONS, ...options };
// Ensure output path is absolute and normalized
const workspaceRoot = getWorkspaceRoot();
const outputPath = path.isAbsolute(mergedOptions.outputPath)
? mergedOptions.outputPath
: path.join(workspaceRoot, mergedOptions.outputPath);
mergedOptions.outputPath = outputPath;
// Ensure output directory exists
try {
await fs.mkdir(outputPath, { recursive: true });
} catch (mkdirError: any) {
throw new Error(
`Failed to create output directory: ${mkdirError.message}`
);
}
const sessionId = ActionRecorder.getInstance().startSession();
// Store options with the session
const recorder = ActionRecorder.getInstance();
const session = recorder.getSession(sessionId);
if (session) {
session.options = mergedOptions;
}
return {
sessionId,
options: mergedOptions,
message: `Started codegen session. Tests will be generated in: ${outputPath}`,
};
} catch (error: any) {
throw new Error(`Failed to start codegen session: ${error.message}`);
}
},
};
export const endCodegenSession: Tool = {
name: "end_codegen_session",
description:
"End the current code generation session and generate Playwright test",
parameters: {
type: "object",
properties: {
sessionId: {
type: "string",
description: "ID of the session to end",
},
},
required: ["sessionId"],
},
handler: async ({ sessionId }: { sessionId: string }) => {
try {
const recorder = ActionRecorder.getInstance();
const session = recorder.endSession(sessionId);
if (!session) {
throw new Error(`Session ${sessionId} not found`);
}
if (!session.options) {
throw new Error(`Session ${sessionId} has no options configured`);
}
const generator = new PlaywrightGenerator(session.options);
const result = await generator.generateTest(session);
// Double check output directory exists
const outputDir = path.dirname(result.filePath);
await fs.mkdir(outputDir, { recursive: true });
// Write test file
try {
await fs.writeFile(result.filePath, result.testCode, "utf-8");
} catch (writeError: any) {
throw new Error(`Failed to write test file: ${writeError.message}`);
}
// Close Playwright browser and cleanup
try {
if (global.browser?.isConnected()) {
await global.browser.close();
}
} catch (browserError: any) {
console.warn("Failed to close browser:", browserError.message);
} finally {
global.browser = undefined;
global.page = undefined;
}
const absolutePath = path.resolve(result.filePath);
return {
filePath: absolutePath,
outputDirectory: outputDir,
testCode: result.testCode,
message: `Generated test file at: ${absolutePath}\nOutput directory: ${outputDir}`,
};
} catch (error: any) {
// Ensure browser cleanup even on error
try {
if (global.browser?.isConnected()) {
await global.browser.close();
}
} catch {
// Ignore cleanup errors
} finally {
global.browser = undefined;
global.page = undefined;
}
throw new Error(`Failed to end codegen session: ${error.message}`);
}
},
};
export const getCodegenSession: Tool = {
name: "get_codegen_session",
description: "Get information about a code generation session",
parameters: {
type: "object",
properties: {
sessionId: {
type: "string",
description: "ID of the session to retrieve",
},
},
required: ["sessionId"],
},
handler: async ({ sessionId }: { sessionId: string }) => {
const session = ActionRecorder.getInstance().getSession(sessionId);
if (!session) {
throw new Error(`Session ${sessionId} not found`);
}
return session;
},
};
export const clearCodegenSession: Tool = {
name: "clear_codegen_session",
description: "Clear a code generation session",
parameters: {
type: "object",
properties: {
sessionId: {
type: "string",
description: "ID of the session to clear",
},
},
required: ["sessionId"],
},
handler: async ({ sessionId }: { sessionId: string }) => {
const success = ActionRecorder.getInstance().clearSession(sessionId);
if (!success) {
throw new Error(`Session ${sessionId} not found`);
}
return { success };
},
};
export const codegenTools = [
startCodegenSession,
endCodegenSession,
getCodegenSession,
clearCodegenSession,
];
```
--------------------------------------------------------------------------------
/docs/static/img/logo.svg:
--------------------------------------------------------------------------------
```
<svg width="200" height="200" viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd"><path fill="#FFF" d="M99 52h84v34H99z"/><path d="M23 163c-7.398 0-13.843-4.027-17.303-10A19.886 19.886 0 0 0 3 163c0 11.046 8.954 20 20 20h20v-20H23z" fill="#3ECC5F"/><path d="M112.98 57.376L183 53V43c0-11.046-8.954-20-20-20H73l-2.5-4.33c-1.112-1.925-3.889-1.925-5 0L63 23l-2.5-4.33c-1.111-1.925-3.889-1.925-5 0L53 23l-2.5-4.33c-1.111-1.925-3.889-1.925-5 0L43 23c-.022 0-.042.003-.065.003l-4.142-4.141c-1.57-1.571-4.252-.853-4.828 1.294l-1.369 5.104-5.192-1.392c-2.148-.575-4.111 1.389-3.535 3.536l1.39 5.193-5.102 1.367c-2.148.576-2.867 3.259-1.296 4.83l4.142 4.142c0 .021-.003.042-.003.064l-4.33 2.5c-1.925 1.111-1.925 3.889 0 5L23 53l-4.33 2.5c-1.925 1.111-1.925 3.889 0 5L23 63l-4.33 2.5c-1.925 1.111-1.925 3.889 0 5L23 73l-4.33 2.5c-1.925 1.111-1.925 3.889 0 5L23 83l-4.33 2.5c-1.925 1.111-1.925 3.889 0 5L23 93l-4.33 2.5c-1.925 1.111-1.925 3.889 0 5L23 103l-4.33 2.5c-1.925 1.111-1.925 3.889 0 5L23 113l-4.33 2.5c-1.925 1.111-1.925 3.889 0 5L23 123l-4.33 2.5c-1.925 1.111-1.925 3.889 0 5L23 133l-4.33 2.5c-1.925 1.111-1.925 3.889 0 5L23 143l-4.33 2.5c-1.925 1.111-1.925 3.889 0 5L23 153l-4.33 2.5c-1.925 1.111-1.925 3.889 0 5L23 163c0 11.046 8.954 20 20 20h120c11.046 0 20-8.954 20-20V83l-70.02-4.376A10.645 10.645 0 0 1 103 68c0-5.621 4.37-10.273 9.98-10.624" fill="#3ECC5F"/><path fill="#3ECC5F" d="M143 183h30v-40h-30z"/><path d="M193 158c-.219 0-.428.037-.639.064-.038-.15-.074-.301-.116-.451A5 5 0 0 0 190.32 148a4.96 4.96 0 0 0-3.016 1.036 26.531 26.531 0 0 0-.335-.336 4.955 4.955 0 0 0 1.011-2.987 5 5 0 0 0-9.599-1.959c-.148-.042-.297-.077-.445-.115.027-.211.064-.42.064-.639a5 5 0 0 0-5-5 5 5 0 0 0-5 5c0 .219.037.428.064.639-.148.038-.297.073-.445.115a4.998 4.998 0 0 0-9.599 1.959c0 1.125.384 2.151 1.011 2.987-3.717 3.632-6.031 8.693-6.031 14.3 0 11.046 8.954 20 20 20 9.339 0 17.16-6.41 19.361-15.064.211.027.42.064.639.064a5 5 0 0 0 5-5 5 5 0 0 0-5-5" fill="#44D860"/><path fill="#3ECC5F" d="M153 123h30v-20h-30z"/><path d="M193 115.5a2.5 2.5 0 1 0 0-5c-.109 0-.214.019-.319.032-.02-.075-.037-.15-.058-.225a2.501 2.501 0 0 0-.963-4.807c-.569 0-1.088.197-1.508.518a6.653 6.653 0 0 0-.168-.168c.314-.417.506-.931.506-1.494a2.5 2.5 0 0 0-4.8-.979A9.987 9.987 0 0 0 183 103c-5.522 0-10 4.478-10 10s4.478 10 10 10c.934 0 1.833-.138 2.69-.377a2.5 2.5 0 0 0 4.8-.979c0-.563-.192-1.077-.506-1.494.057-.055.113-.111.168-.168.42.321.939.518 1.508.518a2.5 2.5 0 0 0 .963-4.807c.021-.074.038-.15.058-.225.105.013.21.032.319.032" fill="#44D860"/><path d="M63 55.5a2.5 2.5 0 0 1-2.5-2.5c0-4.136-3.364-7.5-7.5-7.5s-7.5 3.364-7.5 7.5a2.5 2.5 0 1 1-5 0c0-6.893 5.607-12.5 12.5-12.5S65.5 46.107 65.5 53a2.5 2.5 0 0 1-2.5 2.5" fill="#000"/><path d="M103 183h60c11.046 0 20-8.954 20-20V93h-60c-11.046 0-20 8.954-20 20v70z" fill="#FFFF50"/><path d="M168.02 124h-50.04a1 1 0 1 1 0-2h50.04a1 1 0 1 1 0 2m0 20h-50.04a1 1 0 1 1 0-2h50.04a1 1 0 1 1 0 2m0 20h-50.04a1 1 0 1 1 0-2h50.04a1 1 0 1 1 0 2m0-49.814h-50.04a1 1 0 1 1 0-2h50.04a1 1 0 1 1 0 2m0 19.814h-50.04a1 1 0 1 1 0-2h50.04a1 1 0 1 1 0 2m0 20h-50.04a1 1 0 1 1 0-2h50.04a1 1 0 1 1 0 2M183 61.611c-.012 0-.022-.006-.034-.005-3.09.105-4.552 3.196-5.842 5.923-1.346 2.85-2.387 4.703-4.093 4.647-1.889-.068-2.969-2.202-4.113-4.46-1.314-2.594-2.814-5.536-5.963-5.426-3.046.104-4.513 2.794-5.807 5.167-1.377 2.528-2.314 4.065-4.121 3.994-1.927-.07-2.951-1.805-4.136-3.813-1.321-2.236-2.848-4.75-5.936-4.664-2.994.103-4.465 2.385-5.763 4.4-1.373 2.13-2.335 3.428-4.165 3.351-1.973-.07-2.992-1.51-4.171-3.177-1.324-1.873-2.816-3.993-5.895-3.89-2.928.1-4.399 1.97-5.696 3.618-1.232 1.564-2.194 2.802-4.229 2.724a1 1 0 0 0-.072 2c3.017.101 4.545-1.8 5.872-3.487 1.177-1.496 2.193-2.787 4.193-2.855 1.926-.082 2.829 1.115 4.195 3.045 1.297 1.834 2.769 3.914 5.731 4.021 3.103.104 4.596-2.215 5.918-4.267 1.182-1.834 2.202-3.417 4.15-3.484 1.793-.067 2.769 1.35 4.145 3.681 1.297 2.197 2.766 4.686 5.787 4.796 3.125.108 4.634-2.62 5.949-5.035 1.139-2.088 2.214-4.06 4.119-4.126 1.793-.042 2.728 1.595 4.111 4.33 1.292 2.553 2.757 5.445 5.825 5.556l.169.003c3.064 0 4.518-3.075 5.805-5.794 1.139-2.41 2.217-4.68 4.067-4.773v-2z" fill="#000"/><path fill="#3ECC5F" d="M83 183h40v-40H83z"/><path d="M143 158c-.219 0-.428.037-.639.064-.038-.15-.074-.301-.116-.451A5 5 0 0 0 140.32 148a4.96 4.96 0 0 0-3.016 1.036 26.531 26.531 0 0 0-.335-.336 4.955 4.955 0 0 0 1.011-2.987 5 5 0 0 0-9.599-1.959c-.148-.042-.297-.077-.445-.115.027-.211.064-.42.064-.639a5 5 0 0 0-5-5 5 5 0 0 0-5 5c0 .219.037.428.064.639-.148.038-.297.073-.445.115a4.998 4.998 0 0 0-9.599 1.959c0 1.125.384 2.151 1.011 2.987-3.717 3.632-6.031 8.693-6.031 14.3 0 11.046 8.954 20 20 20 9.339 0 17.16-6.41 19.361-15.064.211.027.42.064.639.064a5 5 0 0 0 5-5 5 5 0 0 0-5-5" fill="#44D860"/><path fill="#3ECC5F" d="M83 123h40v-20H83z"/><path d="M133 115.5a2.5 2.5 0 1 0 0-5c-.109 0-.214.019-.319.032-.02-.075-.037-.15-.058-.225a2.501 2.501 0 0 0-.963-4.807c-.569 0-1.088.197-1.508.518a6.653 6.653 0 0 0-.168-.168c.314-.417.506-.931.506-1.494a2.5 2.5 0 0 0-4.8-.979A9.987 9.987 0 0 0 123 103c-5.522 0-10 4.478-10 10s4.478 10 10 10c.934 0 1.833-.138 2.69-.377a2.5 2.5 0 0 0 4.8-.979c0-.563-.192-1.077-.506-1.494.057-.055.113-.111.168-.168.42.321.939.518 1.508.518a2.5 2.5 0 0 0 .963-4.807c.021-.074.038-.15.058-.225.105.013.21.032.319.032" fill="#44D860"/><path d="M143 41.75c-.16 0-.33-.02-.49-.05a2.52 2.52 0 0 1-.47-.14c-.15-.06-.29-.14-.431-.23-.13-.09-.259-.2-.38-.31-.109-.12-.219-.24-.309-.38s-.17-.28-.231-.43a2.619 2.619 0 0 1-.189-.96c0-.16.02-.33.05-.49.03-.16.08-.31.139-.47.061-.15.141-.29.231-.43.09-.13.2-.26.309-.38.121-.11.25-.22.38-.31.141-.09.281-.17.431-.23.149-.06.31-.11.47-.14.32-.07.65-.07.98 0 .159.03.32.08.47.14.149.06.29.14.43.23.13.09.259.2.38.31.11.12.22.25.31.38.09.14.17.28.23.43.06.16.11.31.14.47.029.16.05.33.05.49 0 .66-.271 1.31-.73 1.77-.121.11-.25.22-.38.31-.14.09-.281.17-.43.23a2.565 2.565 0 0 1-.96.19m20-1.25c-.66 0-1.3-.27-1.771-.73a3.802 3.802 0 0 1-.309-.38c-.09-.14-.17-.28-.231-.43a2.619 2.619 0 0 1-.189-.96c0-.66.27-1.3.729-1.77.121-.11.25-.22.38-.31.141-.09.281-.17.431-.23.149-.06.31-.11.47-.14.32-.07.66-.07.98 0 .159.03.32.08.47.14.149.06.29.14.43.23.13.09.259.2.38.31.459.47.73 1.11.73 1.77 0 .16-.021.33-.05.49-.03.16-.08.32-.14.47-.07.15-.14.29-.23.43-.09.13-.2.26-.31.38-.121.11-.25.22-.38.31-.14.09-.281.17-.43.23a2.565 2.565 0 0 1-.96.19" fill="#000"/></g></svg>
```
--------------------------------------------------------------------------------
/src/__tests__/tools/browser/advancedInteraction.test.ts:
--------------------------------------------------------------------------------
```typescript
import { DragTool, PressKeyTool } from '../../../tools/browser/interaction.js';
import { ToolContext } from '../../../tools/common/types.js';
import { Page, Browser, ElementHandle } from 'playwright';
import { jest } from '@jest/globals';
// Mock page functions
const mockWaitForSelector = jest.fn();
const mockMouseMove = jest.fn().mockImplementation(() => Promise.resolve());
const mockMouseDown = jest.fn().mockImplementation(() => Promise.resolve());
const mockMouseUp = jest.fn().mockImplementation(() => Promise.resolve());
const mockKeyboardPress = jest.fn().mockImplementation(() => Promise.resolve());
const mockFocus = jest.fn().mockImplementation(() => Promise.resolve());
const mockIsClosed = jest.fn().mockReturnValue(false);
// Mock element handle
const mockBoundingBox = jest.fn().mockReturnValue({ x: 10, y: 10, width: 100, height: 50 });
const mockElementHandle = {
boundingBox: mockBoundingBox
} as unknown as ElementHandle;
// Wait for selector returns element handle
mockWaitForSelector.mockImplementation(() => Promise.resolve(mockElementHandle));
// Mock mouse
const mockMouse = {
move: mockMouseMove,
down: mockMouseDown,
up: mockMouseUp
};
// Mock keyboard
const mockKeyboard = {
press: mockKeyboardPress
};
// Mock the Page object with proper typing
const mockPage = {
waitForSelector: mockWaitForSelector,
mouse: mockMouse,
keyboard: mockKeyboard,
focus: mockFocus,
isClosed: mockIsClosed
} as unknown as Page;
// Mock the browser
const mockIsConnected = jest.fn().mockReturnValue(true);
const mockBrowser = {
isConnected: mockIsConnected
} as unknown as Browser;
// Mock the server
const mockServer = {
sendMessage: jest.fn()
};
// Mock context
const mockContext = {
page: mockPage,
browser: mockBrowser,
server: mockServer
} as ToolContext;
describe('Advanced Browser Interaction Tools', () => {
let dragTool: DragTool;
let pressKeyTool: PressKeyTool;
beforeEach(() => {
jest.clearAllMocks();
dragTool = new DragTool(mockServer);
pressKeyTool = new PressKeyTool(mockServer);
// Reset browser and page mocks
mockIsConnected.mockReturnValue(true);
mockIsClosed.mockReturnValue(false);
});
describe('DragTool', () => {
test('should drag an element to a target location', async () => {
const args = {
sourceSelector: '#source-element',
targetSelector: '#target-element'
};
const result = await dragTool.execute(args, mockContext);
expect(mockWaitForSelector).toHaveBeenCalledWith('#source-element');
expect(mockWaitForSelector).toHaveBeenCalledWith('#target-element');
expect(mockBoundingBox).toHaveBeenCalledTimes(2);
expect(mockMouseMove).toHaveBeenCalledWith(60, 35); // Source center (10+100/2, 10+50/2)
expect(mockMouseDown).toHaveBeenCalled();
expect(mockMouseMove).toHaveBeenCalledWith(60, 35); // Target center (same mock values)
expect(mockMouseUp).toHaveBeenCalled();
expect(result.isError).toBe(false);
expect(result.content[0].text).toContain('Dragged element from');
});
test('should handle errors when element positions cannot be determined', async () => {
const args = {
sourceSelector: '#source-element',
targetSelector: '#target-element'
};
// Mock failure to get bounding box
mockBoundingBox.mockReturnValueOnce(null);
const result = await dragTool.execute(args, mockContext);
expect(mockWaitForSelector).toHaveBeenCalledWith('#source-element');
expect(mockBoundingBox).toHaveBeenCalled();
expect(mockMouseMove).not.toHaveBeenCalled();
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('Could not get element positions');
});
test('should handle drag errors', async () => {
const args = {
sourceSelector: '#source-element',
targetSelector: '#target-element'
};
// Mock a mouse operation error
mockMouseDown.mockImplementationOnce(() => Promise.reject(new Error('Mouse operation failed')));
const result = await dragTool.execute(args, mockContext);
expect(mockWaitForSelector).toHaveBeenCalledWith('#source-element');
expect(mockWaitForSelector).toHaveBeenCalledWith('#target-element');
expect(mockBoundingBox).toHaveBeenCalled();
expect(mockMouseMove).toHaveBeenCalled();
expect(mockMouseDown).toHaveBeenCalled();
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('Operation failed');
});
test('should handle missing page', async () => {
const args = {
sourceSelector: '#source-element',
targetSelector: '#target-element'
};
const result = await dragTool.execute(args, { server: mockServer } as ToolContext);
expect(mockWaitForSelector).not.toHaveBeenCalled();
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('Browser page not initialized');
});
});
describe('PressKeyTool', () => {
test('should press a keyboard key', async () => {
const args = {
key: 'Enter'
};
const result = await pressKeyTool.execute(args, mockContext);
expect(mockKeyboardPress).toHaveBeenCalledWith('Enter');
expect(result.isError).toBe(false);
expect(result.content[0].text).toContain('Pressed key: Enter');
});
test('should focus an element before pressing a key if selector provided', async () => {
const args = {
key: 'Enter',
selector: '#input-field'
};
const result = await pressKeyTool.execute(args, mockContext);
expect(mockWaitForSelector).toHaveBeenCalledWith('#input-field');
expect(mockFocus).toHaveBeenCalledWith('#input-field');
expect(mockKeyboardPress).toHaveBeenCalledWith('Enter');
expect(result.isError).toBe(false);
expect(result.content[0].text).toContain('Pressed key: Enter');
});
test('should handle key press errors', async () => {
const args = {
key: 'Enter'
};
// Mock a keyboard operation error
mockKeyboardPress.mockImplementationOnce(() => Promise.reject(new Error('Keyboard operation failed')));
const result = await pressKeyTool.execute(args, mockContext);
expect(mockKeyboardPress).toHaveBeenCalledWith('Enter');
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('Operation failed');
});
test('should handle missing page', async () => {
const args = {
key: 'Enter'
};
const result = await pressKeyTool.execute(args, { server: mockServer } as ToolContext);
expect(mockKeyboardPress).not.toHaveBeenCalled();
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('Browser page not initialized');
});
});
});
```
--------------------------------------------------------------------------------
/src/__tests__/tools/api/requests.test.ts:
--------------------------------------------------------------------------------
```typescript
import { GetRequestTool, PostRequestTool, PutRequestTool, PatchRequestTool, DeleteRequestTool } from '../../../tools/api/requests.js';
import { ToolContext } from '../../../tools/common/types.js';
import { APIRequestContext } from 'playwright';
import { jest } from '@jest/globals';
// Mock response
const mockStatus200 = jest.fn().mockReturnValue(200);
const mockStatus201 = jest.fn().mockReturnValue(201);
const mockStatus204 = jest.fn().mockReturnValue(204);
const mockText = jest.fn().mockImplementation(() => Promise.resolve('{"success": true}'));
const mockStatusText = jest.fn().mockReturnValue('OK');
const mockResponse = {
status: mockStatus200,
statusText: mockStatusText,
text: mockText
};
// Mock API context
const mockGet = jest.fn().mockImplementation(() => Promise.resolve(mockResponse));
const mockPost = jest.fn().mockImplementation(() => Promise.resolve({...mockResponse, status: mockStatus201}));
const mockPut = jest.fn().mockImplementation(() => Promise.resolve(mockResponse));
const mockPatch = jest.fn().mockImplementation(() => Promise.resolve(mockResponse));
const mockDelete = jest.fn().mockImplementation(() => Promise.resolve({...mockResponse, status: mockStatus204}));
const mockDispose = jest.fn().mockImplementation(() => Promise.resolve());
const mockApiContext = {
get: mockGet,
post: mockPost,
put: mockPut,
patch: mockPatch,
delete: mockDelete,
dispose: mockDispose
} as unknown as APIRequestContext;
// Mock server
const mockServer = {
sendMessage: jest.fn()
};
// Mock context
const mockContext = {
apiContext: mockApiContext,
server: mockServer
} as ToolContext;
describe('API Request Tools', () => {
let getRequestTool: GetRequestTool;
let postRequestTool: PostRequestTool;
let putRequestTool: PutRequestTool;
let patchRequestTool: PatchRequestTool;
let deleteRequestTool: DeleteRequestTool;
beforeEach(() => {
jest.clearAllMocks();
getRequestTool = new GetRequestTool(mockServer);
postRequestTool = new PostRequestTool(mockServer);
putRequestTool = new PutRequestTool(mockServer);
patchRequestTool = new PatchRequestTool(mockServer);
deleteRequestTool = new DeleteRequestTool(mockServer);
});
describe('GetRequestTool', () => {
test('should make a GET request', async () => {
const args = {
url: 'https://api.example.com'
};
const result = await getRequestTool.execute(args, mockContext);
expect(mockGet).toHaveBeenCalledWith('https://api.example.com');
expect(result.isError).toBe(false);
expect(result.content[0].text).toContain('GET request to');
});
test('should handle GET request errors', async () => {
const args = {
url: 'https://api.example.com'
};
// Mock a request error
mockGet.mockImplementationOnce(() => Promise.reject(new Error('Request failed')));
const result = await getRequestTool.execute(args, mockContext);
expect(mockGet).toHaveBeenCalledWith('https://api.example.com');
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('API operation failed');
});
test('should handle missing API context', async () => {
const args = {
url: 'https://api.example.com'
};
const result = await getRequestTool.execute(args, { server: mockServer } as ToolContext);
expect(mockGet).not.toHaveBeenCalled();
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('API context not initialized');
});
});
describe('PostRequestTool', () => {
test('should make a POST request without token', async () => {
const args = {
url: 'https://api.example.com',
value: '{"data": "test"}'
};
const result = await postRequestTool.execute(args, mockContext);
expect(mockPost).toHaveBeenCalledWith('https://api.example.com', {
data: { data: "test" },
headers: {
'Content-Type': 'application/json'
}
});
expect(result.isError).toBe(false);
expect(result.content[0].text).toContain('POST request to');
});
test('should make a POST request with Bearer token', async () => {
const args = {
url: 'https://api.example.com',
value: '{"data": "test"}',
token: 'test-token'
};
const result = await postRequestTool.execute(args, mockContext);
expect(mockPost).toHaveBeenCalledWith('https://api.example.com', {
data: { data: "test" },
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer test-token'
}
});
expect(result.isError).toBe(false);
expect(result.content[0].text).toContain('POST request to');
});
test('should make a POST request with Bearer token and custom headers', async () => {
const args = {
url: 'https://api.example.com',
value: '{"data": "test"}',
token: 'test-token',
headers: {
'X-Custom-Header': 'custom-value'
}
};
const result = await postRequestTool.execute(args, mockContext);
expect(mockPost).toHaveBeenCalledWith('https://api.example.com', {
data: { data: "test" },
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer test-token',
'X-Custom-Header': 'custom-value'
}
});
expect(result.isError).toBe(false);
expect(result.content[0].text).toContain('POST request to');
});
});
describe('PutRequestTool', () => {
test('should make a PUT request', async () => {
const args = {
url: 'https://api.example.com',
value: '{"data": "test"}'
};
const result = await putRequestTool.execute(args, mockContext);
expect(mockPut).toHaveBeenCalledWith('https://api.example.com', { data: args.value });
expect(result.isError).toBe(false);
expect(result.content[0].text).toContain('PUT request to');
});
});
describe('PatchRequestTool', () => {
test('should make a PATCH request', async () => {
const args = {
url: 'https://api.example.com',
value: '{"data": "test"}'
};
const result = await patchRequestTool.execute(args, mockContext);
expect(mockPatch).toHaveBeenCalledWith('https://api.example.com', { data: args.value });
expect(result.isError).toBe(false);
expect(result.content[0].text).toContain('PATCH request to');
});
});
describe('DeleteRequestTool', () => {
test('should make a DELETE request', async () => {
const args = {
url: 'https://api.example.com/1'
};
const result = await deleteRequestTool.execute(args, mockContext);
expect(mockDelete).toHaveBeenCalledWith('https://api.example.com/1');
expect(result.isError).toBe(false);
expect(result.content[0].text).toContain('DELETE request to');
});
});
});
```
--------------------------------------------------------------------------------
/src/__tests__/codegen.test.ts:
--------------------------------------------------------------------------------
```typescript
import { ActionRecorder } from '../tools/codegen/recorder';
import { PlaywrightGenerator } from '../tools/codegen/generator';
import { handleToolCall } from '../toolHandler';
import * as fs from 'fs/promises';
import * as path from 'path';
jest.mock('../toolHandler');
const mockedHandleToolCall = jest.mocked(handleToolCall);
// Test configuration
const TEST_CONFIG = {
OUTPUT_DIR: path.join(__dirname, '../../tests/generated'),
MOCK_SESSION_ID: 'test-session-123'
} as const;
// Response types
interface ToolResponseContent {
[key: string]: unknown;
type: 'text';
text: string;
}
interface ToolResponse {
[key: string]: unknown;
content: ToolResponseContent[];
isError: boolean;
_meta?: Record<string, unknown>;
}
function createMockResponse(data: unknown): ToolResponse {
return {
content: [{
type: 'text',
text: JSON.stringify(data)
}],
isError: false,
_meta: {}
};
}
function parseJsonResponse<T>(response: unknown): T {
if (!response || typeof response !== 'object' || !('content' in response)) {
throw new Error('Invalid response format');
}
const content = (response as { content: unknown[] }).content;
if (!Array.isArray(content) || content.length === 0) {
throw new Error('Invalid response content');
}
const textContent = content.find(c =>
typeof c === 'object' &&
c !== null &&
'type' in c &&
(c as { type: string }).type === 'text' &&
'text' in c &&
typeof (c as { text: unknown }).text === 'string'
) as { type: 'text'; text: string } | undefined;
if (!textContent?.text) {
throw new Error('No text content found in response');
}
return JSON.parse(textContent.text) as T;
}
describe('Code Generation', () => {
beforeAll(async () => {
// Ensure test output directory exists
await fs.mkdir(TEST_CONFIG.OUTPUT_DIR, { recursive: true });
});
afterEach(() => {
// Clear all mocks
jest.clearAllMocks();
});
afterAll(async () => {
// Clean up test files
try {
const files = await fs.readdir(TEST_CONFIG.OUTPUT_DIR);
await Promise.all(
files.map(file => fs.unlink(path.join(TEST_CONFIG.OUTPUT_DIR, file)))
);
} catch (error) {
console.error('Error cleaning up test files:', error);
}
});
describe('Action Recording', () => {
beforeEach(() => {
// Mock session info response
mockedHandleToolCall.mockImplementation(async (name, args, server) => {
if (name === 'get_codegen_session') {
return createMockResponse({
id: TEST_CONFIG.MOCK_SESSION_ID,
actions: []
});
}
return createMockResponse({ success: true });
});
});
it('should record navigation actions', async () => {
// Setup mock for session info
mockedHandleToolCall.mockImplementation(async (name, args, server) => {
if (name === 'get_codegen_session') {
return createMockResponse({
id: TEST_CONFIG.MOCK_SESSION_ID,
actions: [{
toolName: 'playwright_navigate',
params: { url: 'https://example.com' }
}]
});
}
return createMockResponse({ success: true });
});
await handleToolCall('playwright_navigate', {
url: 'https://example.com'
}, {});
const sessionInfo = parseJsonResponse<{ id: string; actions: any[] }>(
await handleToolCall('get_codegen_session', { sessionId: TEST_CONFIG.MOCK_SESSION_ID }, {})
);
expect(sessionInfo.actions).toHaveLength(1);
expect(sessionInfo.actions[0].toolName).toBe('playwright_navigate');
expect(sessionInfo.actions[0].params).toEqual({
url: 'https://example.com'
});
});
it('should record multiple actions in sequence', async () => {
// Setup mock for session info
mockedHandleToolCall.mockImplementation(async (name, args, server) => {
if (name === 'get_codegen_session') {
return createMockResponse({
id: TEST_CONFIG.MOCK_SESSION_ID,
actions: [
{
toolName: 'playwright_navigate',
params: { url: 'https://example.com' }
},
{
toolName: 'playwright_click',
params: { selector: '#submit-button' }
},
{
toolName: 'playwright_fill',
params: { selector: '#search-input', value: 'test query' }
}
]
});
}
return createMockResponse({ success: true });
});
await handleToolCall('playwright_navigate', {
url: 'https://example.com'
}, {});
await handleToolCall('playwright_click', {
selector: '#submit-button'
}, {});
await handleToolCall('playwright_fill', {
selector: '#search-input',
value: 'test query'
}, {});
const sessionInfo = parseJsonResponse<{ id: string; actions: any[] }>(
await handleToolCall('get_codegen_session', { sessionId: TEST_CONFIG.MOCK_SESSION_ID }, {})
);
expect(sessionInfo.actions).toHaveLength(3);
expect(sessionInfo.actions.map(a => a.toolName)).toEqual([
'playwright_navigate',
'playwright_click',
'playwright_fill'
]);
expect(sessionInfo.actions.map(a => a.params)).toEqual([
{ url: 'https://example.com' },
{ selector: '#submit-button' },
{ selector: '#search-input', value: 'test query' }
]);
});
});
describe('Test Generation', () => {
it('should generate valid Playwright test code', async () => {
// Setup mock for end session response
mockedHandleToolCall.mockImplementation(async (name, args, server) => {
if (name === 'end_codegen_session') {
return createMockResponse({
filePath: path.join(TEST_CONFIG.OUTPUT_DIR, 'test.spec.ts'),
testCode: `
import { test, expect } from '@playwright/test';
test('generated test', async ({ page }) => {
await page.goto('https://example.com');
await page.click('#submit-button');
await page.fill('#search-input', 'test query');
});
`
});
}
return createMockResponse({ success: true });
});
// Record actions
await handleToolCall('playwright_navigate', {
url: 'https://example.com'
}, {});
await handleToolCall('playwright_click', {
selector: '#submit-button'
}, {});
await handleToolCall('playwright_fill', {
selector: '#search-input',
value: 'test query'
}, {});
// Generate test
const endResult = await handleToolCall('end_codegen_session', {
sessionId: TEST_CONFIG.MOCK_SESSION_ID
}, {});
const { filePath, testCode } = parseJsonResponse<{ filePath: string; testCode: string }>(endResult);
// Verify test code content
expect(filePath).toBeDefined();
expect(testCode).toContain('import { test, expect } from \'@playwright/test\'');
expect(testCode).toContain('await page.goto(\'https://example.com\')');
expect(testCode).toContain('await page.click(\'#submit-button\')');
expect(testCode).toContain('await page.fill(\'#search-input\', \'test query\')');
// Verify mock was called correctly
expect(mockedHandleToolCall).toHaveBeenCalledWith(
'end_codegen_session',
{ sessionId: TEST_CONFIG.MOCK_SESSION_ID },
{}
);
});
});
});
```