This is page 1 of 5. Use http://codebase.md/pvinis/mcp-playwright-stealth?lines=true&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:
--------------------------------------------------------------------------------
```
1 |
```
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
```
1 | package-lock.json linguist-generated=true
2 |
```
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | lerna-debug.log*
8 | .pnpm-debug.log*
9 |
10 | # Diagnostic reports (https://nodejs.org/api/report.html)
11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
12 |
13 | # Runtime data
14 | pids
15 | *.pid
16 | *.seed
17 | *.pid.lock
18 |
19 | # Directory for instrumented libs generated by jscoverage/JSCover
20 | lib-cov
21 |
22 | # Coverage directory used by tools like istanbul
23 | coverage
24 | *.lcov
25 |
26 | # nyc test coverage
27 | .nyc_output
28 |
29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
30 | .grunt
31 |
32 | # Bower dependency directory (https://bower.io/)
33 | bower_components
34 |
35 | # node-waf configuration
36 | .lock-wscript
37 |
38 | # Compiled binary addons (https://nodejs.org/api/addons.html)
39 | build/Release
40 |
41 | # Dependency directories
42 | node_modules/
43 | jspm_packages/
44 |
45 | # Snowpack dependency directory (https://snowpack.dev/)
46 | web_modules/
47 |
48 | # TypeScript cache
49 | *.tsbuildinfo
50 |
51 | # Optional npm cache directory
52 | .npm
53 |
54 | # Optional eslint cache
55 | .eslintcache
56 |
57 | # Optional stylelint cache
58 | .stylelintcache
59 |
60 | # Microbundle cache
61 | .rpt2_cache/
62 | .rts2_cache_cjs/
63 | .rts2_cache_es/
64 | .rts2_cache_umd/
65 |
66 | # Optional REPL history
67 | .node_repl_history
68 |
69 | # Output of 'npm pack'
70 | *.tgz
71 |
72 | # Yarn Integrity file
73 | .yarn-integrity
74 |
75 | # dotenv environment variable files
76 | .env
77 | .env.development.local
78 | .env.test.local
79 | .env.production.local
80 | .env.local
81 |
82 | # parcel-bundler cache (https://parceljs.org/)
83 | .cache
84 | .parcel-cache
85 |
86 | # Next.js build output
87 | .next
88 | out
89 |
90 | # Nuxt.js build / generate output
91 | .nuxt
92 | dist
93 |
94 | # Gatsby files
95 | .cache/
96 | # Comment in the public line in if your project uses Gatsby and not Next.js
97 | # https://nextjs.org/blog/next-9-1#public-directory-support
98 | # public
99 |
100 | # vuepress build output
101 | .vuepress/dist
102 |
103 | # vuepress v2.x temp and cache directory
104 | .temp
105 | .cache
106 |
107 | # Docusaurus cache and generated files
108 | .docusaurus
109 |
110 | # Serverless directories
111 | .serverless/
112 |
113 | # FuseBox cache
114 | .fusebox/
115 |
116 | # DynamoDB Local files
117 | .dynamodb/
118 |
119 | # TernJS port file
120 | .tern-port
121 |
122 | # Stores VSCode versions used for testing VSCode extensions
123 | .vscode-test
124 |
125 | # yarn v2
126 | .yarn/cache
127 | .yarn/unplugged
128 | .yarn/build-state.yml
129 | .yarn/install-state.gz
130 | .pnp.*
131 |
132 |
133 | .DS_Store
134 | .DS_Store?
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
1 | # Playwright MCP Server 🎭
2 |
3 | [](https://smithery.ai/server/@executeautomation/playwright-mcp-server)
4 |
5 | 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.
6 |
7 | <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>
8 |
9 | ## Screenshot
10 | 
11 |
12 | ## [Documentation](https://executeautomation.github.io/mcp-playwright/) | [API reference](https://executeautomation.github.io/mcp-playwright/docs/playwright-web/Supported-Tools)
13 |
14 | ## Installation
15 |
16 | You can install the package using either npm, mcp-get, or Smithery:
17 |
18 | Using npm:
19 | ```bash
20 | npm install -g @executeautomation/playwright-mcp-server
21 | ```
22 |
23 | Using mcp-get:
24 | ```bash
25 | npx @michaellatman/mcp-get@latest install @executeautomation/playwright-mcp-server
26 | ```
27 | Using Smithery
28 |
29 | To install Playwright MCP for Claude Desktop automatically via [Smithery](https://smithery.ai/server/@executeautomation/playwright-mcp-server):
30 |
31 | ```bash
32 | npx -y @smithery/cli install @executeautomation/playwright-mcp-server --client claude
33 | ```
34 | #### Installation in VS Code
35 |
36 | Install the Playwright MCP server in VS Code using one of these buttons:
37 |
38 | <!--
39 | // Generate using?:
40 | const config = JSON.stringify({ name: 'playwright', command: 'npx', args: ["-y", "@executeautomation/playwright-mcp-server"] });
41 | const urlForWebsites = `vscode:mcp/install?${encodeURIComponent(config)}`;
42 | // Github markdown does not allow linking to `vscode:` directly, so you can use our redirect:
43 | const urlForGithub = `https://insiders.vscode.dev/redirect?url=${encodeURIComponent(urlForWebsites)}`;
44 | -->
45 |
46 | [<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)
47 |
48 | Alternatively, you can install the Playwright MCP server using the VS Code CLI:
49 |
50 | ```bash
51 | # For VS Code
52 | code --add-mcp '{"name":"playwright","command":"npx","args":["@executeautomation/playwright-mcp-server"]}'
53 | ```
54 |
55 | ```bash
56 | # For VS Code Insiders
57 | code-insiders --add-mcp '{"name":"playwright","command":"npx","args":["@executeautomation/playwright-mcp-server"]}'
58 | ```
59 |
60 | After installation, the ExecuteAutomation Playwright MCP server will be available for use with your GitHub Copilot agent in VS Code.
61 |
62 | ## Configuration to use Playwright Server
63 | Here's the Claude Desktop configuration to use the Playwright server:
64 |
65 | ```json
66 | {
67 | "mcpServers": {
68 | "playwright": {
69 | "command": "npx",
70 | "args": ["-y", "@executeautomation/playwright-mcp-server"]
71 | }
72 | }
73 | }
74 | ```
75 |
76 | ## Testing
77 |
78 | This project uses Jest for testing. The tests are located in the `src/__tests__` directory.
79 |
80 | ### Running Tests
81 |
82 | You can run the tests using one of the following commands:
83 |
84 | ```bash
85 | # Run tests using the custom script (with coverage)
86 | node run-tests.cjs
87 |
88 | # Run tests using npm scripts
89 | npm test # Run tests without coverage
90 | npm run test:coverage # Run tests with coverage
91 | npm run test:custom # Run tests with custom script (same as node run-tests.cjs)
92 | ```
93 |
94 | The test coverage report will be generated in the `coverage` directory.
95 |
96 | ## Star History
97 |
98 | [](https://star-history.com/#executeautomation/mcp-playwright&Date)
99 |
```
--------------------------------------------------------------------------------
/docs/src/pages/markdown-page.md:
--------------------------------------------------------------------------------
```markdown
1 | ---
2 | title: Markdown page example
3 | ---
4 |
5 | # Markdown page example
6 |
7 | You don't need React to write simple standalone pages.
8 |
```
--------------------------------------------------------------------------------
/src/tools/api/index.ts:
--------------------------------------------------------------------------------
```typescript
1 | export * from './base.js';
2 | export * from './requests.js';
3 |
4 | // TODO: Add exports for other API tools as they are implemented
```
--------------------------------------------------------------------------------
/docs/src/components/HomepageFeatures/styles.module.css:
--------------------------------------------------------------------------------
```css
1 | .features {
2 | display: flex;
3 | align-items: center;
4 | padding: 2rem 0;
5 | width: 100%;
6 | }
7 |
8 | .featureSvg {
9 | height: 200px;
10 | width: 200px;
11 | }
12 |
```
--------------------------------------------------------------------------------
/docs/docs/playwright-api/_category_.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "label": "Playwright API Features",
3 | "position": 5,
4 | "collapsed": false,
5 | "link": {
6 | "type": "generated-index",
7 | "description": "Supported features in Playwright API Testing."
8 | }
9 | }
10 |
```
--------------------------------------------------------------------------------
/docs/docs/playwright-web/_category_.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "label": "Playwright Web Features",
3 | "position": 4,
4 | "collapsed": false,
5 | "link": {
6 | "type": "generated-index",
7 | "description": "Supported features in Playwright Web browser automation."
8 | }
9 | }
10 |
```
--------------------------------------------------------------------------------
/docs/docs/local-setup/_category_.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "label": "Local Development",
3 | "position": 3,
4 | "collapsed": false,
5 | "link": {
6 | "type": "generated-index",
7 | "description": "Understand how to setup Playwright MCP Server to run in your local machine."
8 | }
9 | }
```
--------------------------------------------------------------------------------
/tsconfig.test.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "noImplicitAny": false,
5 | "strictNullChecks": false,
6 | "types": ["jest", "node"]
7 | },
8 | "include": ["src/**/*", "test/**/*", "src/__tests__/**/*"]
9 | }
```
--------------------------------------------------------------------------------
/docs/tsconfig.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | // This file is not used in compilation. It is here just for a nice editor experience.
3 | "extends": "@docusaurus/tsconfig",
4 | "compilerOptions": {
5 | "baseUrl": "."
6 | },
7 | "exclude": [".docusaurus", "build"]
8 | }
9 |
```
--------------------------------------------------------------------------------
/docs/docs/testing-videos/_category_.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "label": "MCP Testing Videos",
3 | "position": 7,
4 | "collapsed": false,
5 | "link": {
6 | "type": "generated-index",
7 | "description": "AI Testing Videos helps you learn using MCP for Testing and Development"
8 | }
9 | }
10 |
```
--------------------------------------------------------------------------------
/docs/docs/ai-courses/_category_.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "label": "AI Courses to Learn",
3 | "position": 6,
4 | "collapsed": false,
5 | "link": {
6 | "type": "generated-index",
7 | "description": "AI Courses which helps you learn more on Using it for Testing and Development"
8 | }
9 | }
10 |
```
--------------------------------------------------------------------------------
/test-import.js:
--------------------------------------------------------------------------------
```javascript
1 | // test-import.js
2 | import { setupRequestHandlers } from './dist/requestHandler.js';
3 | import { handleToolCall } from './dist/toolHandler.js';
4 |
5 | console.log('Imports successful!');
6 | console.log('setupRequestHandlers:', typeof setupRequestHandlers);
7 | console.log('handleToolCall:', typeof handleToolCall);
```
--------------------------------------------------------------------------------
/run-tests.js:
--------------------------------------------------------------------------------
```javascript
1 | const { execSync } = require('child_process');
2 |
3 | try {
4 | console.log('Running tests...');
5 | const output = execSync('npx jest --no-cache --coverage', { encoding: 'utf8' });
6 | console.log(output);
7 | } catch (error) {
8 | console.error('Error running tests:', error.message);
9 | console.log(error.stdout);
10 | }
```
--------------------------------------------------------------------------------
/src/tools/browser/index.ts:
--------------------------------------------------------------------------------
```typescript
1 | export * from './base.js';
2 | export * from './screenshot.js';
3 | export * from './navigation.js';
4 | export * from './console.js';
5 | export * from './interaction.js';
6 | export * from './response.js';
7 | export * from './useragent.js';
8 |
9 | // TODO: Add exports for other browser tools as they are implemented
10 | // export * from './interaction.js';
```
--------------------------------------------------------------------------------
/docs/src/pages/index.module.css:
--------------------------------------------------------------------------------
```css
1 | /**
2 | * CSS files with the .module.css suffix will be treated as CSS modules
3 | * and scoped locally.
4 | */
5 |
6 | .heroBanner {
7 | padding: 4rem 0;
8 | text-align: center;
9 | position: relative;
10 | overflow: hidden;
11 | }
12 |
13 | @media screen and (max-width: 996px) {
14 | .heroBanner {
15 | padding: 2rem;
16 | }
17 | }
18 |
19 | .buttons {
20 | display: flex;
21 | align-items: center;
22 | justify-content: center;
23 | }
24 |
```
--------------------------------------------------------------------------------
/docs/src/components/HomepageFeatures/YouTubeVideoEmbed.tsx:
--------------------------------------------------------------------------------
```typescript
1 | import React from 'react';
2 |
3 | export default function YouTubeVideoEmbed({ videoId }) {
4 | return (
5 | <iframe
6 | width="700"
7 | height="400"
8 | src={`https://www.youtube.com/embed/${videoId}`}
9 | title="ExecuteAutomation YouTube"
10 | allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
11 | allowFullScreen
12 | ></iframe>
13 | );
14 | }
```
--------------------------------------------------------------------------------
/smithery.yaml:
--------------------------------------------------------------------------------
```yaml
1 | # Smithery configuration file: https://smithery.ai/docs/config#smitheryyaml
2 |
3 | startCommand:
4 | type: stdio
5 | configSchema:
6 | # JSON Schema defining the configuration options for the MCP.
7 | type: object
8 | properties: {}
9 | commandFunction:
10 | # A function that produces the CLI command to start the MCP on stdio.
11 | |-
12 | (config) => ({command: 'node', args: ['dist/index.js'], env: {}})
```
--------------------------------------------------------------------------------
/src/types.ts:
--------------------------------------------------------------------------------
```typescript
1 | import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
2 |
3 | export interface Tool {
4 | name: string;
5 | description: string;
6 | parameters: {
7 | type: string;
8 | properties: Record<string, unknown>;
9 | required?: string[];
10 | };
11 | handler: (args: any) => Promise<any>;
12 | }
13 |
14 | export interface ToolCall {
15 | name: string;
16 | parameters: Record<string, unknown>;
17 | result?: CallToolResult;
18 | }
```
--------------------------------------------------------------------------------
/run-tests.cjs:
--------------------------------------------------------------------------------
```
1 | const { execSync } = require('child_process');
2 |
3 | try {
4 | console.log("Running tests with coverage...");
5 | execSync('npx jest --no-cache --coverage --testMatch="<rootDir>/src/__tests__/**/*.test.ts"', { stdio: 'inherit' });
6 | console.log("Tests completed successfully!");
7 | } catch (error) {
8 | console.error("Error running tests:", error.message);
9 | console.log(error.stdout?.toString() || "No output");
10 | process.exit(1);
11 | }
```
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "compilerOptions": {
3 | "target": "ES2020",
4 | "module": "ES2022",
5 | "moduleResolution": "bundler",
6 | "outDir": "./dist",
7 | "strict": true,
8 | "esModuleInterop": true,
9 | "skipLibCheck": true,
10 | "declaration": true,
11 | "noImplicitAny": false,
12 | "strictNullChecks": false,
13 | "allowJs": true,
14 | "resolveJsonModule": true
15 | },
16 | "include": ["src/**/*"],
17 | "exclude": ["node_modules", "dist", "src/__tests__"]
18 | }
```
--------------------------------------------------------------------------------
/src/tools/index.ts:
--------------------------------------------------------------------------------
```typescript
1 | export * from './common/types.js';
2 | export * from './browser/index.js';
3 | export * from './api/index.js';
4 |
5 | // Tool type constants
6 | export const BROWSER_TOOLS = [
7 | "playwright_navigate",
8 | "playwright_screenshot",
9 | "playwright_click",
10 | "playwright_iframe_click",
11 | "playwright_fill",
12 | "playwright_select",
13 | "playwright_hover",
14 | "playwright_evaluate",
15 | "playwright_console_logs",
16 | "playwright_close",
17 | "playwright_get_visible_text",
18 | "playwright_get_visible_html"
19 | ];
20 |
21 | export const API_TOOLS = [
22 | "playwright_get",
23 | "playwright_post",
24 | "playwright_put",
25 | "playwright_patch",
26 | "playwright_delete"
27 | ];
```
--------------------------------------------------------------------------------
/src/tools/codegen/types.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { ToolCall } from '../../types.js';
2 |
3 | export interface CodegenAction {
4 | toolName: string;
5 | parameters: Record<string, unknown>;
6 | timestamp: number;
7 | result?: unknown;
8 | }
9 |
10 | export interface CodegenSession {
11 | id: string;
12 | actions: CodegenAction[];
13 | startTime: number;
14 | endTime?: number;
15 | options?: CodegenOptions;
16 | }
17 |
18 | export interface PlaywrightTestCase {
19 | name: string;
20 | steps: string[];
21 | imports: Set<string>;
22 | }
23 |
24 | export interface CodegenOptions {
25 | outputPath?: string;
26 | testNamePrefix?: string;
27 | includeComments?: boolean;
28 | }
29 |
30 | export interface CodegenResult {
31 | testCode: string;
32 | filePath: string;
33 | sessionId: string;
34 | }
```
--------------------------------------------------------------------------------
/docs/docs/local-setup/Debugging.mdx:
--------------------------------------------------------------------------------
```markdown
1 | ---
2 | sidebar_position: 2
3 | ---
4 |
5 | import YouTubeVideoEmbed from '@site/src/components/HomepageFeatures/YouTubeVideoEmbed';
6 |
7 | ## Debugging MCP Server 🚀
8 |
9 | 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**.
10 |
11 |
12 | ## Step 1 : Install the MCP Inspector
13 | To get started, run the following command:
14 |
15 | ```bash
16 | npx @modelcontextprotocol/inspector node dist/index.js
17 |
18 | ```
19 |
20 | ## Step 2: Navigate to MCP Inspector
21 |
22 | ```bash
23 | http://localhost:5173 🚀
24 | ```
25 |
26 |
27 | ## Here is the video demonstration
28 | <YouTubeVideoEmbed videoId="98l_k0XYXKs" />
```
--------------------------------------------------------------------------------
/jest.config.cjs:
--------------------------------------------------------------------------------
```
1 | module.exports = {
2 | preset: 'ts-jest',
3 | testEnvironment: 'node',
4 | collectCoverage: true,
5 | coverageDirectory: 'coverage',
6 | coverageReporters: ['text', 'lcov'],
7 | collectCoverageFrom: [
8 | 'src/**/*.ts',
9 | '!src/index.ts', // exclude index.ts
10 | ],
11 | testMatch: [
12 | '<rootDir>/src/__tests__/**/*.test.ts'
13 | ],
14 | modulePathIgnorePatterns: [
15 | "<rootDir>/docs/",
16 | "<rootDir>/dist/"
17 | ],
18 | moduleNameMapper: {
19 | "^(.*)\\.js$": "$1"
20 | },
21 | transform: {
22 | '^.+\\.tsx?$': ['ts-jest', {
23 | useESM: true,
24 | tsconfig: 'tsconfig.test.json'
25 | }],
26 | },
27 | extensionsToTreatAsEsm: ['.ts'],
28 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
29 | };
30 |
```
--------------------------------------------------------------------------------
/docs/docs/playwright-web/Support-of-Cline-Cursor.mdx:
--------------------------------------------------------------------------------
```markdown
1 | ---
2 | sidebar_position: 3
3 | ---
4 |
5 | import YouTubeVideoEmbed from '@site/src/components/HomepageFeatures/YouTubeVideoEmbed';
6 |
7 | # 💻 Support of Cline and Cursor
8 |
9 | Playwright MCP Server now fully supports **Cline** and **Cursor**.
10 |
11 | Below, you will find video demonstrations showing how **Cline** and **Cursor** work with the Playwright MCP Server.
12 |
13 | ---
14 |
15 | ### 🎥 Support of **Cursor**
16 |
17 | Check out the demonstration of how **Cursor** integrates seamlessly with Playwright MCP Server:
18 |
19 | <YouTubeVideoEmbed videoId="Fp7D7RuzElo" />
20 |
21 | ---
22 |
23 | ### 🎥 Support of **Cline**
24 |
25 | Watch the demonstration of **Cline** in action with Playwright MCP Server:
26 |
27 | <YouTubeVideoEmbed videoId="bUD10uvW6lw" />
28 |
29 | ---
```
--------------------------------------------------------------------------------
/docs/docs/playwright-web/Console-Logging.mdx:
--------------------------------------------------------------------------------
```markdown
1 | ---
2 | sidebar_position: 2
3 | ---
4 |
5 | # 📁 Support of Console Logs
6 |
7 | Playwright MCP Server now supports Console logging of browsers with the power of Playwright.
8 | This feature is especially useful when you want to capture the logs of the browser console while performing any action during development and testing.
9 |
10 | Following logs types are supported
11 | - `log`
12 | - `info`
13 | - `warn`
14 | - `error`
15 | - `debug`
16 | - `all`
17 |
18 | :::tip Usage Example
19 | To invoke `Playwright_console_logs` via MCP Playwright, use the following prompt:
20 |
21 | ```plaintext
22 | Get the console log from the browser whenever you perform any action.
23 | :::
24 |
25 | ---
26 | :::info
27 | 
28 |
29 | Demo of how the console logs are captured in Playwright MCP Server
30 | :::
31 |
32 |
33 |
```
--------------------------------------------------------------------------------
/.github/workflows/node.js.yml:
--------------------------------------------------------------------------------
```yaml
1 | # 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
2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs
3 |
4 | name: Node.js CI
5 |
6 | on:
7 | push:
8 | branches: [ "main" ]
9 | pull_request:
10 | branches: [ "main" ]
11 |
12 | jobs:
13 | build:
14 |
15 | runs-on: ubuntu-latest
16 |
17 | strategy:
18 | matrix:
19 | node-version: [18.x, 20.x, 22.x]
20 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/
21 |
22 | steps:
23 | - uses: actions/checkout@v4
24 | - name: Use Node.js ${{ matrix.node-version }}
25 | uses: actions/setup-node@v4
26 | with:
27 | node-version: ${{ matrix.node-version }}
28 | cache: 'npm'
29 | - run: npm ci
30 | - run: npm run build --if-present
31 |
```
--------------------------------------------------------------------------------
/docs/sidebars.ts:
--------------------------------------------------------------------------------
```typescript
1 | import type {SidebarsConfig} from '@docusaurus/plugin-content-docs';
2 |
3 | // This runs in Node.js - Don't use client-side code here (browser APIs, JSX...)
4 |
5 | /**
6 | * Creating a sidebar enables you to:
7 | - create an ordered group of docs
8 | - render a sidebar for each doc of that group
9 | - provide next/previous navigation
10 |
11 | The sidebars can be generated from the filesystem, or explicitly defined here.
12 |
13 | Create as many sidebars as you want.
14 | */
15 | const sidebars: SidebarsConfig = {
16 | // By default, Docusaurus generates a sidebar from the docs folder structure
17 | tutorialSidebar: [{type: 'autogenerated', dirName: '.'}],
18 |
19 | // But you can create a sidebar manually
20 | /*
21 | tutorialSidebar: [
22 | 'intro',
23 | 'hello',
24 | {
25 | type: 'category',
26 | label: 'Tutorial',
27 | items: ['tutorial-basics/create-a-document'],
28 | },
29 | ],
30 | */
31 | };
32 |
33 | export default sidebars;
34 |
```
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
```typescript
1 | #!/usr/bin/env node
2 |
3 | import { Server } from "@modelcontextprotocol/sdk/server/index.js";
4 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5 | import { createToolDefinitions } from "./tools.js";
6 | import { setupRequestHandlers } from "./requestHandler.js";
7 |
8 | async function runServer() {
9 | const server = new Server(
10 | {
11 | name: "executeautomation/playwright-mcp-server",
12 | version: "1.0.3",
13 | },
14 | {
15 | capabilities: {
16 | resources: {},
17 | tools: {},
18 | },
19 | }
20 | );
21 |
22 | // Create tool definitions
23 | const TOOLS = createToolDefinitions();
24 |
25 | // Setup request handlers
26 | setupRequestHandlers(server, TOOLS);
27 |
28 | // Create transport and connect
29 | const transport = new StdioServerTransport();
30 | await server.connect(transport);
31 | }
32 |
33 | runServer().catch((error) => {
34 | console.error("Fatal error in main():", error);
35 | process.exit(1);
36 | });
```
--------------------------------------------------------------------------------
/src/tools/browser/output.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { BrowserToolBase } from './base.js';
2 | import { ToolContext, ToolResponse, createSuccessResponse, createErrorResponse } from '../common/types.js';
3 | import * as path from 'path';
4 |
5 | /**
6 | * Tool for saving page as PDF
7 | */
8 | export class SaveAsPdfTool extends BrowserToolBase {
9 | /**
10 | * Execute the save as PDF tool
11 | */
12 | async execute(args: any, context: ToolContext): Promise<ToolResponse> {
13 | return this.safeExecute(context, async (page) => {
14 | const filename = args.filename || 'page.pdf';
15 | const options = {
16 | path: path.resolve(args.outputPath || '.', filename),
17 | format: args.format || 'A4',
18 | printBackground: args.printBackground !== false,
19 | margin: args.margin || {
20 | top: '1cm',
21 | right: '1cm',
22 | bottom: '1cm',
23 | left: '1cm'
24 | }
25 | };
26 |
27 | await page.pdf(options);
28 | return createSuccessResponse(`Saved page as PDF: ${options.path}`);
29 | });
30 | }
31 | }
```
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
```yaml
1 | name: Test
2 |
3 | on:
4 | push:
5 | pull_request:
6 |
7 | jobs:
8 | build:
9 | runs-on: ubuntu-latest
10 |
11 | steps:
12 | - uses: actions/checkout@v3
13 |
14 | - name: Set up Node.js
15 | uses: actions/setup-node@v3
16 | with:
17 | node-version: 22 # Use current LTS version
18 | cache: 'npm'
19 |
20 | - name: Install dependencies
21 | run: npm install
22 |
23 | - name: Build project
24 | run: npm run build
25 |
26 | - name: Run tests with coverage (custom script)
27 | run: node run-tests.cjs
28 | continue-on-error: true
29 | id: custom-test
30 |
31 | - name: Run tests with coverage (npm script)
32 | if: steps.custom-test.outcome == 'failure'
33 | run: npm run test:coverage -- --testMatch="<rootDir>/src/__tests__/**/*.test.ts"
34 |
35 | - name: Upload coverage report
36 | uses: actions/upload-artifact@v4
37 | with:
38 | name: coverage-report
39 | path: coverage/
40 | if-no-files-found: warn
41 |
```
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
```dockerfile
1 | # Generated by https://smithery.ai. See: https://smithery.ai/docs/config#dockerfile
2 | # Use Node.js image for building the project
3 | FROM node:20-alpine AS builder
4 |
5 | # Set the working directory
6 | WORKDIR /app
7 |
8 | # Copy package.json and package-lock.json
9 | COPY package.json package-lock.json ./
10 |
11 | # Install dependencies without running scripts to prevent automatic build
12 | RUN npm install --ignore-scripts
13 |
14 | # Copy the entire source directory
15 | COPY src ./src
16 | COPY tsconfig.json ./
17 |
18 | # Build the project
19 | RUN npm run build
20 |
21 | # Use a minimal Node.js image for running the project
22 | FROM node:20-alpine AS release
23 |
24 | # Set the working directory
25 | WORKDIR /app
26 |
27 | # Copy the built files from the builder stage
28 | COPY --from=builder /app/dist ./dist
29 | COPY --from=builder /app/package.json ./package.json
30 | COPY --from=builder /app/package-lock.json ./package-lock.json
31 |
32 | # Install production dependencies
33 | RUN npm ci --ignore-scripts --omit=dev
34 |
35 | # Set the command to run the server
36 | ENTRYPOINT ["node", "dist/index.js"]
```
--------------------------------------------------------------------------------
/docs/src/css/custom.css:
--------------------------------------------------------------------------------
```css
1 | /**
2 | * Any CSS included here will be global. The classic template
3 | * bundles Infima by default. Infima is a CSS framework designed to
4 | * work well for content-centric websites.
5 | */
6 |
7 | /* You can override the default Infima variables here. */
8 | :root {
9 | --ifm-color-primary: #2e8555;
10 | --ifm-color-primary-dark: #29784c;
11 | --ifm-color-primary-darker: #277148;
12 | --ifm-color-primary-darkest: #205d3b;
13 | --ifm-color-primary-light: #33925d;
14 | --ifm-color-primary-lighter: #359962;
15 | --ifm-color-primary-lightest: #3cad6e;
16 | --ifm-code-font-size: 95%;
17 | --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.1);
18 | }
19 |
20 | /* For readability concerns, you should choose a lighter palette in dark mode. */
21 | [data-theme='dark'] {
22 | --ifm-color-primary: #25c2a0;
23 | --ifm-color-primary-dark: #21af90;
24 | --ifm-color-primary-darker: #1fa588;
25 | --ifm-color-primary-darkest: #1a8870;
26 | --ifm-color-primary-light: #29d5b0;
27 | --ifm-color-primary-lighter: #32d8b4;
28 | --ifm-color-primary-lightest: #4fddbf;
29 | --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.3);
30 | }
31 |
```
--------------------------------------------------------------------------------
/src/tools/common/types.ts:
--------------------------------------------------------------------------------
```typescript
1 | import type {
2 | CallToolResult,
3 | TextContent,
4 | ImageContent,
5 | } from "@modelcontextprotocol/sdk/types.js";
6 | import type { Page, Browser, APIRequestContext } from "rebrowser-playwright";
7 |
8 | // Context for tool execution
9 | export interface ToolContext {
10 | page?: Page;
11 | browser?: Browser;
12 | apiContext?: APIRequestContext;
13 | server?: any;
14 | }
15 |
16 | // Standard response format for all tools
17 | export interface ToolResponse extends CallToolResult {
18 | content: (TextContent | ImageContent)[];
19 | isError: boolean;
20 | }
21 |
22 | // Interface that all tool implementations must follow
23 | export interface ToolHandler {
24 | execute(args: any, context: ToolContext): Promise<ToolResponse>;
25 | }
26 |
27 | // Helper functions for creating responses
28 | export function createErrorResponse(message: string): ToolResponse {
29 | return {
30 | content: [
31 | {
32 | type: "text",
33 | text: message,
34 | },
35 | ],
36 | isError: true,
37 | };
38 | }
39 |
40 | export function createSuccessResponse(
41 | message: string | string[]
42 | ): ToolResponse {
43 | const messages = Array.isArray(message) ? message : [message];
44 | return {
45 | content: messages.map((msg) => ({
46 | type: "text",
47 | text: msg,
48 | })),
49 | isError: false,
50 | };
51 | }
52 |
```
--------------------------------------------------------------------------------
/docs/package.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "name": "playwright-mcp-server",
3 | "version": "0.0.0",
4 | "private": true,
5 | "scripts": {
6 | "docusaurus": "docusaurus",
7 | "start": "docusaurus start",
8 | "build": "docusaurus build",
9 | "swizzle": "docusaurus swizzle",
10 | "deploy": "docusaurus deploy",
11 | "clear": "docusaurus clear",
12 | "serve": "docusaurus serve",
13 | "write-translations": "docusaurus write-translations",
14 | "write-heading-ids": "docusaurus write-heading-ids",
15 | "typecheck": "tsc"
16 | },
17 | "dependencies": {
18 | "@docusaurus/core": "3.6.3",
19 | "@docusaurus/preset-classic": "3.6.3",
20 | "@mdx-js/react": "^3.0.0",
21 | "clsx": "^2.0.0",
22 | "gh-pages": "^6.2.0",
23 | "prism-react-renderer": "^2.3.0",
24 | "react": "^18.0.0",
25 | "react-dom": "^18.0.0"
26 | },
27 | "devDependencies": {
28 | "@docusaurus/module-type-aliases": "3.6.3",
29 | "@docusaurus/tsconfig": "3.6.3",
30 | "@docusaurus/types": "3.6.3",
31 | "typescript": "~5.6.2"
32 | },
33 | "browserslist": {
34 | "production": [
35 | ">0.5%",
36 | "not dead",
37 | "not op_mini all"
38 | ],
39 | "development": [
40 | "last 3 chrome version",
41 | "last 3 firefox version",
42 | "last 5 safari version"
43 | ]
44 | },
45 | "engines": {
46 | "node": ">=18.0"
47 | }
48 | }
49 |
```
--------------------------------------------------------------------------------
/docs/src/pages/index.tsx:
--------------------------------------------------------------------------------
```typescript
1 | import clsx from 'clsx';
2 | import Link from '@docusaurus/Link';
3 | import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
4 | import Layout from '@theme/Layout';
5 | import HomepageFeatures from '@site/src/components/HomepageFeatures';
6 | import Heading from '@theme/Heading';
7 |
8 | import styles from './index.module.css';
9 |
10 | function HomepageHeader() {
11 | const {siteConfig} = useDocusaurusContext();
12 | return (
13 | <header className={clsx('hero hero--primary', styles.heroBanner)}>
14 | <div className="container">
15 | <Heading as="h1" className="hero__title">
16 | {siteConfig.title}
17 | </Heading>
18 | <p className="hero__subtitle">{siteConfig.tagline}</p>
19 | <div className={styles.buttons}>
20 | <Link
21 | className="button button--secondary button--lg"
22 | to="/docs/intro">
23 | Playwright MCP Server Tutorial - 5min ⏱️
24 | </Link>
25 | </div>
26 | </div>
27 | </header>
28 | );
29 | }
30 |
31 | export default function Home(): JSX.Element {
32 | const {siteConfig} = useDocusaurusContext();
33 | return (
34 | <Layout
35 | title={`${siteConfig.title}`}
36 | description="Description will go into a meta tag in <head />">
37 | <HomepageHeader />
38 | <main>
39 | <HomepageFeatures />
40 | </main>
41 | </Layout>
42 | );
43 | }
44 |
```
--------------------------------------------------------------------------------
/docs/docs/local-setup/Installation.mdx:
--------------------------------------------------------------------------------
```markdown
1 | ---
2 | sidebar_position: 1
3 | ---
4 |
5 | ## Build and Run Playwright MCP Server locally
6 | To build/run Playwright MCP Server in your local machine follow the below steps
7 |
8 | ### Step 1 : Clone Repository
9 |
10 | ```bash
11 | git clone https://github.com/executeautomation/mcp-playwright.git
12 | ```
13 |
14 | ## Step 2: Install Dependencies
15 | ```bash
16 | npm install
17 | ```
18 |
19 | ## Step 3: Build Code
20 | ```bash
21 | npm run build
22 | npm link
23 | ```
24 |
25 | ## Step 4: Configuring Playwright MCP in Claude Desktop
26 |
27 | Modify your `claude-desktop-config.json` file as shown below to work with local playwright mcp server
28 |
29 | ```json
30 | {
31 | "mcpServers": {
32 | "playwright": {
33 | "command": "npx",
34 | "args": [
35 | "--directory",
36 | "/your-playwright-mcp-server-clone-directory",
37 | "run",
38 | "@executeautomation/playwright-mcp-server"
39 | ]
40 | }
41 | }
42 | }
43 | ```
44 |
45 | :::warning Important
46 | 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).
47 |
48 | ⚠️ If you skip this step, the configuration changes **may not take effect**.
49 | :::
50 |
51 | ## Reward
52 | If your setup is all correct, you should see Playwright MCP Server pointing your local machine source code
53 |
54 | 
55 |
```
--------------------------------------------------------------------------------
/src/tools/browser/useragent.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { BrowserToolBase } from './base.js';
2 | import type { ToolContext, ToolResponse } from '../common/types.js';
3 | import { createSuccessResponse, createErrorResponse } from '../common/types.js';
4 |
5 | interface CustomUserAgentArgs {
6 | userAgent: string;
7 | }
8 |
9 | /**
10 | * Tool for validating custom User Agent settings
11 | */
12 | export class CustomUserAgentTool extends BrowserToolBase {
13 | /**
14 | * Execute the custom user agent tool
15 | */
16 | async execute(args: CustomUserAgentArgs, context: ToolContext): Promise<ToolResponse> {
17 | return this.safeExecute(context, async (page) => {
18 | if (!args.userAgent) {
19 | return createErrorResponse("Missing required parameter: userAgent must be provided");
20 | }
21 |
22 | try {
23 | const currentUserAgent = await page.evaluate(() => navigator.userAgent);
24 |
25 | if (currentUserAgent !== args.userAgent) {
26 | const messages = [
27 | "Page was already initialized with a different User Agent.",
28 | `Requested: ${args.userAgent}`,
29 | `Current: ${currentUserAgent}`
30 | ];
31 | return createErrorResponse(messages.join('\n'));
32 | }
33 |
34 | return createSuccessResponse("User Agent validation successful");
35 | } catch (error) {
36 | return createErrorResponse(`Failed to validate User Agent: ${(error as Error).message}`);
37 | }
38 | });
39 | }
40 | }
```
--------------------------------------------------------------------------------
/.github/workflows/docusaurus-gh-pages.yml:
--------------------------------------------------------------------------------
```yaml
1 | name: Deploy Docusaurus documentation to GH Pages
2 |
3 | on:
4 | # Runs on pushes targeting the default branch
5 | push:
6 | branches: ["main"]
7 | pull_request:
8 | branches: [ "main" ]
9 |
10 | # Allows you to run this workflow manually from the Actions tab
11 | workflow_dispatch:
12 |
13 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
14 | permissions:
15 | contents: read
16 | pages: write
17 | id-token: write
18 |
19 | # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
20 | # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
21 | concurrency:
22 | group: "pages"
23 | cancel-in-progress: false
24 |
25 | jobs:
26 | # Build job
27 | build:
28 | runs-on: ubuntu-latest
29 | steps:
30 | - name: Checkout
31 | uses: actions/checkout@v4
32 | - name: Install dependencies
33 | working-directory: docs
34 | run: npm install
35 | - name: Build site
36 | working-directory: docs
37 | run: npm run build
38 | - name: Upload artifact
39 | uses: actions/upload-pages-artifact@v3
40 | with:
41 | path: docs/build
42 |
43 | # Deployment job
44 | deploy:
45 | environment:
46 | name: github-pages
47 | url: ${{ steps.deployment.outputs.page_url }}
48 | runs-on: ubuntu-latest
49 | needs: build
50 | steps:
51 | - name: Deploy to GitHub Pages
52 | id: deployment
53 | uses: actions/deploy-pages@v4
54 |
```
--------------------------------------------------------------------------------
/docs/docs/testing-videos/Bdd.mdx:
--------------------------------------------------------------------------------
```markdown
1 | ---
2 | sidebar_position: 2
3 | ---
4 |
5 | import YouTubeVideoEmbed from '@site/src/components/HomepageFeatures/YouTubeVideoEmbed';
6 |
7 | # 🎭 BDD Testing with Playwright MCP Server 🥒
8 |
9 | In this video, we explore **Behavior Driven Development (BDD) testing** using **Playwright MCP Server**, without writing a single line of code—just seamless automation!
10 |
11 | Watch how **Cursor + MCP Servers** transform testing into a fully automated, hands-free process! 🔥
12 |
13 | <div align="center">
14 | <YouTubeVideoEmbed videoId="5knmhGep2o4" />
15 | </div>
16 |
17 | ---
18 |
19 | :::info 💡 **Note**
20 | We will be using the following **MCP Server Tools**:
21 |
22 | 1. **Playwright MCP Server** (This is the tool referenced in the documentation)
23 | :::
24 |
25 | ---
26 |
27 | ## 📚 **BDD Scenario for Login**
28 |
29 | ```gherkin
30 | Feature: Login
31 |
32 | Scenario: To Perform login operation in EAApp website
33 | Given I navigate to "https://eaapp.somee.com/"
34 | When I click on "Login" button
35 | And I enter "admin" in "UserName" field
36 | And I enter "password" in "Password" field
37 | And I click on "Login" button
38 | Then I should see the Employee Details menu
39 | ```
40 |
41 | ---
42 |
43 | ## 📚 **BDD Scenario for Console Logs**
44 |
45 | ```gherkin
46 | Feature: Network Console
47 |
48 | Scenario: To Perform Network Console operation
49 | Given I navigate to amazon.com
50 | And I handle if there is any popup
51 | And I click the All menu from the Amazon website
52 | And I read the network console logs
53 | When I see any error in the network console logs
54 | Then I print them out
55 | And I close the browser
56 | ```
57 |
58 | ---
59 |
60 | This structured format enhances readability and ensures clarity for developers and testers. 🚀
61 |
```
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "name": "@pvinis/playwright-stealth-mcp-server",
3 | "version": "1.0.4",
4 | "description": "Model Context Protocol servers for Playwright (with stealth)",
5 | "license": "MIT",
6 | "author": "ExecuteAutomation, Ltd (https://executeautomation.com)",
7 | "homepage": "https://executeautomation.github.io/mcp-playwright/",
8 | "bugs": "https://github.com/executeautomation/mcp-playwright/issues",
9 | "types": "dist/index.d.ts",
10 | "type": "module",
11 | "bin": {
12 | "playwright-mcp-server": "dist/index.js"
13 | },
14 | "files": [
15 | "dist"
16 | ],
17 | "scripts": {
18 | "build": "tsc && shx chmod +x dist/*.js",
19 | "prepare": "npm run build",
20 | "watch": "tsc --watch",
21 | "test": "jest --testMatch=\"<rootDir>/src/__tests__/**/*.test.ts\"",
22 | "test:coverage": "jest --coverage --testMatch=\"<rootDir>/src/__tests__/**/*.test.ts\"",
23 | "test:custom": "node run-tests.cjs"
24 | },
25 | "repository": {
26 | "type": "git",
27 | "url": "https://github.com/executeautomation/mcp-playwright.git"
28 | },
29 | "dependencies": {
30 | "@modelcontextprotocol/sdk": "1.6.1",
31 | "@playwright/browser-chromium": "1.51.1",
32 | "@playwright/browser-firefox": "1.51.1",
33 | "@playwright/browser-webkit": "1.51.1",
34 | "@playwright/test": "^1.51.1",
35 | "rebrowser-playwright": "1.49.1",
36 | "uuid": "^9.0.1"
37 | },
38 | "keywords": [
39 | "playwright",
40 | "automation",
41 | "AI",
42 | "Claude MCP"
43 | ],
44 | "devDependencies": {
45 | "@types/jest": "^29.5.14",
46 | "@types/node": "^20.10.5",
47 | "jest": "^29.7.0",
48 | "jest-playwright-preset": "^4.0.0",
49 | "shx": "^0.3.4",
50 | "ts-jest": "^29.2.6",
51 | "typescript": "^5.8.2"
52 | }
53 | }
54 |
```
--------------------------------------------------------------------------------
/docs/src/components/HomepageFeatures/index.tsx:
--------------------------------------------------------------------------------
```typescript
1 | import clsx from 'clsx';
2 | import Heading from '@theme/Heading';
3 | import styles from './styles.module.css';
4 |
5 | type FeatureItem = {
6 | title: string;
7 | Svg: React.ComponentType<React.ComponentProps<'svg'>>;
8 | description: JSX.Element;
9 | };
10 |
11 | const FeatureList: FeatureItem[] = [
12 | {
13 | title: 'Easy to Use',
14 | Svg: require('@site/static/img/easy-to-use.svg').default,
15 | description: (
16 | <>
17 | Playwright MCP Server is easy to use, just change the Claude config file and you are done.
18 | </>
19 | ),
20 | },
21 | {
22 | title: 'Test UI and APIs',
23 | Svg: require('@site/static/img/playwright.svg').default,
24 | description: (
25 | <>
26 | Test both UI and API of your application with plain English text. No <code>code</code> required.
27 | </>
28 | ),
29 | },
30 | {
31 | title: 'Powered by NodeJS',
32 | Svg: require('@site/static/img/node.svg').default,
33 | description: (
34 | <>
35 | Playwright MCP Server is built on top of NodeJS, making it fast and efficient.
36 | </>
37 | ),
38 | },
39 | ];
40 |
41 | function Feature({title, Svg, description}: FeatureItem) {
42 | return (
43 | <div className={clsx('col col--4')}>
44 | <div className="text--center">
45 | <Svg className={styles.featureSvg} role="img" />
46 | </div>
47 | <div className="text--center padding-horiz--md">
48 | <Heading as="h3">{title}</Heading>
49 | <p>{description}</p>
50 | </div>
51 | </div>
52 | );
53 | }
54 |
55 | export default function HomepageFeatures(): JSX.Element {
56 | return (
57 | <section className={styles.features}>
58 | <div className="container">
59 | <div className="row">
60 | {FeatureList.map((props, idx) => (
61 | <Feature key={idx} {...props} />
62 | ))}
63 | </div>
64 | </div>
65 | </section>
66 | );
67 | }
68 |
```
--------------------------------------------------------------------------------
/docs/docs/testing-videos/AIAgents.mdx:
--------------------------------------------------------------------------------
```markdown
1 | ---
2 | sidebar_position: 1
3 | ---
4 |
5 | import YouTubeVideoEmbed from '@site/src/components/HomepageFeatures/YouTubeVideoEmbed';
6 |
7 | # 🎭 UI + Database Testing 💽
8 |
9 | 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.
10 | Watch how Cursor + MCP Servers transform testing into a fully automated, hands-free process! 🔥
11 |
12 | <div align="center">
13 | <YouTubeVideoEmbed videoId="FZpUw1p370o" />
14 | </div>
15 |
16 | ---
17 |
18 | :::info 💡 **Note**
19 | We will be using following MCP Server Tools
20 |
21 | 1. Playwright MCP server (Which is this tool that you are referring in the documentation)
22 | 2. [SQLite MCP Server](https://github.com/modelcontextprotocol/servers/tree/main/src/sqlite)
23 | :::
24 | ---
25 |
26 | ## ⌨ **Cursor Rules**
27 |
28 | We will be using Cursor Rules in this demonstration, Cursor Rules help define how AI agents interact with the
29 | code, automate tasks, and follow specific workflows. These rules guide how agents should generate, modify, or test code within the
30 | development environment.
31 |
32 | Here is the Cursor Rules we are using in this demonstration
33 |
34 | ```js title="api-ui-test.mdc"
35 | When I say /test, then perform following
36 |
37 | Navigate to http://localhost:8000/Product/List.
38 |
39 | Create product by clicking Create link .
40 |
41 | 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.
42 |
43 | Check the Database for the created record
44 |
45 | Use the Schema: http://localhost:8001/swagger/v1/swagger.json
46 |
47 | Here is the baseURL of API Base URL: http://localhost:8001/
48 |
49 | Also check the API which performs the GET operation if the values are correctly retreived for the created product from the above schema
50 |
51 |
```
--------------------------------------------------------------------------------
/src/requestHandler.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { Server } from "@modelcontextprotocol/sdk/server/index.js";
2 | import {
3 | ListResourcesRequestSchema,
4 | ReadResourceRequestSchema,
5 | ListToolsRequestSchema,
6 | CallToolRequestSchema,
7 | Tool
8 | } from "@modelcontextprotocol/sdk/types.js";
9 | import { handleToolCall, getConsoleLogs, getScreenshots } from "./toolHandler.js";
10 |
11 | export function setupRequestHandlers(server: Server, tools: Tool[]) {
12 | // List resources handler
13 | server.setRequestHandler(ListResourcesRequestSchema, async () => ({
14 | resources: [
15 | {
16 | uri: "console://logs",
17 | mimeType: "text/plain",
18 | name: "Browser console logs",
19 | },
20 | ...Array.from(getScreenshots().keys()).map(name => ({
21 | uri: `screenshot://${name}`,
22 | mimeType: "image/png",
23 | name: `Screenshot: ${name}`,
24 | })),
25 | ],
26 | }));
27 |
28 | // Read resource handler
29 | server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
30 | const uri = request.params.uri.toString();
31 |
32 | if (uri === "console://logs") {
33 | return {
34 | contents: [{
35 | uri,
36 | mimeType: "text/plain",
37 | text: getConsoleLogs().join("\n"),
38 | }],
39 | };
40 | }
41 |
42 | if (uri.startsWith("screenshot://")) {
43 | const name = uri.split("://")[1];
44 | const screenshot = getScreenshots().get(name);
45 | if (screenshot) {
46 | return {
47 | contents: [{
48 | uri,
49 | mimeType: "image/png",
50 | blob: screenshot,
51 | }],
52 | };
53 | }
54 | }
55 |
56 | throw new Error(`Resource not found: ${uri}`);
57 | });
58 |
59 | // List tools handler
60 | server.setRequestHandler(ListToolsRequestSchema, async () => ({
61 | tools: tools,
62 | }));
63 |
64 | // Call tool handler
65 | server.setRequestHandler(CallToolRequestSchema, async (request) =>
66 | handleToolCall(request.params.name, request.params.arguments ?? {}, server)
67 | );
68 | }
```
--------------------------------------------------------------------------------
/src/tools/browser/console.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { BrowserToolBase } from './base.js';
2 | import { ToolContext, ToolResponse, createSuccessResponse } from '../common/types.js';
3 |
4 | /**
5 | * Tool for retrieving and filtering console logs from the browser
6 | */
7 | export class ConsoleLogsTool extends BrowserToolBase {
8 | private consoleLogs: string[] = [];
9 |
10 | /**
11 | * Register a console message
12 | * @param type The type of console message
13 | * @param text The text content of the message
14 | */
15 | registerConsoleMessage(type: string, text: string): void {
16 | this.consoleLogs.push(`[${type}] ${text}`);
17 | }
18 |
19 | /**
20 | * Execute the console logs tool
21 | */
22 | async execute(args: any, context: ToolContext): Promise<ToolResponse> {
23 | // No need to use safeExecute here as we don't need to interact with the page
24 | // We're just filtering and returning logs that are already stored
25 |
26 | let logs = [...this.consoleLogs];
27 |
28 | // Filter by type if specified
29 | if (args.type && args.type !== 'all') {
30 | logs = logs.filter(log => log.startsWith(`[${args.type}]`));
31 | }
32 |
33 | // Filter by search text if specified
34 | if (args.search) {
35 | logs = logs.filter(log => log.includes(args.search));
36 | }
37 |
38 | // Limit the number of logs if specified
39 | if (args.limit && args.limit > 0) {
40 | logs = logs.slice(-args.limit);
41 | }
42 |
43 | // Clear logs if requested
44 | if (args.clear) {
45 | this.consoleLogs = [];
46 | }
47 |
48 | // Format the response
49 | if (logs.length === 0) {
50 | return createSuccessResponse("No console logs matching the criteria");
51 | } else {
52 | return createSuccessResponse([
53 | `Retrieved ${logs.length} console log(s):`,
54 | ...logs
55 | ]);
56 | }
57 | }
58 |
59 | /**
60 | * Get all console logs
61 | */
62 | getConsoleLogs(): string[] {
63 | return this.consoleLogs;
64 | }
65 |
66 | /**
67 | * Clear all console logs
68 | */
69 | clearConsoleLogs(): void {
70 | this.consoleLogs = [];
71 | }
72 | }
```
--------------------------------------------------------------------------------
/src/tools/codegen/recorder.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { v4 as uuidv4 } from 'uuid';
2 | import { CodegenAction, CodegenSession } from './types';
3 |
4 | export class ActionRecorder {
5 | private static instance: ActionRecorder;
6 | private sessions: Map<string, CodegenSession>;
7 | private activeSession: string | null;
8 |
9 | private constructor() {
10 | this.sessions = new Map();
11 | this.activeSession = null;
12 | }
13 |
14 | static getInstance(): ActionRecorder {
15 | if (!ActionRecorder.instance) {
16 | ActionRecorder.instance = new ActionRecorder();
17 | }
18 | return ActionRecorder.instance;
19 | }
20 |
21 | startSession(): string {
22 | const sessionId = uuidv4();
23 | this.sessions.set(sessionId, {
24 | id: sessionId,
25 | actions: [],
26 | startTime: Date.now(),
27 | });
28 | this.activeSession = sessionId;
29 | return sessionId;
30 | }
31 |
32 | endSession(sessionId: string): CodegenSession | null {
33 | const session = this.sessions.get(sessionId);
34 | if (session) {
35 | session.endTime = Date.now();
36 | if (this.activeSession === sessionId) {
37 | this.activeSession = null;
38 | }
39 | return session;
40 | }
41 | return null;
42 | }
43 |
44 | recordAction(toolName: string, parameters: Record<string, unknown>, result?: unknown): void {
45 | if (!this.activeSession) {
46 | return;
47 | }
48 |
49 | const session = this.sessions.get(this.activeSession);
50 | if (!session) {
51 | return;
52 | }
53 |
54 | const action: CodegenAction = {
55 | toolName,
56 | parameters,
57 | timestamp: Date.now(),
58 | result,
59 | };
60 |
61 | session.actions.push(action);
62 | }
63 |
64 | getSession(sessionId: string): CodegenSession | null {
65 | return this.sessions.get(sessionId) || null;
66 | }
67 |
68 | getActiveSession(): CodegenSession | null {
69 | return this.activeSession ? this.sessions.get(this.activeSession) : null;
70 | }
71 |
72 | clearSession(sessionId: string): boolean {
73 | if (this.activeSession === sessionId) {
74 | this.activeSession = null;
75 | }
76 | return this.sessions.delete(sessionId);
77 | }
78 | }
```
--------------------------------------------------------------------------------
/src/tools/api/base.ts:
--------------------------------------------------------------------------------
```typescript
1 | import type { APIRequestContext } from "rebrowser-playwright";
2 | import {
3 | ToolHandler,
4 | ToolContext,
5 | ToolResponse,
6 | createErrorResponse,
7 | } from "../common/types.js";
8 |
9 | /**
10 | * Base class for all API-based tools
11 | * Provides common functionality and error handling
12 | */
13 | export abstract class ApiToolBase implements ToolHandler {
14 | protected server: any;
15 |
16 | constructor(server: any) {
17 | this.server = server;
18 | }
19 |
20 | /**
21 | * Main execution method that all tools must implement
22 | */
23 | abstract execute(args: any, context: ToolContext): Promise<ToolResponse>;
24 |
25 | /**
26 | * Ensures an API context is available and returns it
27 | * @param context The tool context containing apiContext
28 | * @returns The apiContext or null if not available
29 | */
30 | protected ensureApiContext(context: ToolContext): APIRequestContext | null {
31 | if (!context.apiContext) {
32 | return null;
33 | }
34 | return context.apiContext;
35 | }
36 |
37 | /**
38 | * Validates that an API context is available and returns an error response if not
39 | * @param context The tool context
40 | * @returns Either null if apiContext is available, or an error response
41 | */
42 | protected validateApiContextAvailable(
43 | context: ToolContext
44 | ): ToolResponse | null {
45 | if (!this.ensureApiContext(context)) {
46 | return createErrorResponse("API context not initialized");
47 | }
48 | return null;
49 | }
50 |
51 | /**
52 | * Safely executes an API operation with proper error handling
53 | * @param context The tool context
54 | * @param operation The async operation to perform
55 | * @returns The tool response
56 | */
57 | protected async safeExecute(
58 | context: ToolContext,
59 | operation: (apiContext: APIRequestContext) => Promise<ToolResponse>
60 | ): Promise<ToolResponse> {
61 | const apiError = this.validateApiContextAvailable(context);
62 | if (apiError) return apiError;
63 |
64 | try {
65 | return await operation(context.apiContext!);
66 | } catch (error) {
67 | return createErrorResponse(
68 | `API operation failed: ${(error as Error).message}`
69 | );
70 | }
71 | }
72 | }
73 |
```
--------------------------------------------------------------------------------
/docs/docs/playwright-api/Examples.md:
--------------------------------------------------------------------------------
```markdown
1 | ---
2 | sidebar_position: 2
3 | ---
4 |
5 | # ⚙️Examples of API automation
6 |
7 | Lets see how we can use the power of Playwright MCP Server to automate API of our application
8 |
9 | ### Scenario
10 |
11 | ```json
12 | // Basic POST request
13 | Perform POST operation for the URL https://api.restful-api.dev/objects with body
14 | {
15 | "name": "Apple MacBook Pro 16",
16 | "data": {
17 | "year": 2024,
18 | "price": 2499,
19 | "CPU model": "M4",
20 | "Hard disk size": "5 TB"
21 | }
22 | }
23 | And verify if the response has createdAt and id property and store the ID in a variable for future reference say variable productID
24 |
25 | // POST request with Bearer token authorization
26 | Perform POST operation for the URL https://api.restful-api.dev/objects with Bearer token "your-token-here" set in the headers
27 | {
28 | 'Content-Type': 'application/json',
29 | 'Authorization': 'Bearer your-token-here'
30 | },
31 | and body
32 | {
33 | "name": "Secure MacBook Pro",
34 | "data": {
35 | "year": 2024,
36 | "price": 2999,
37 | "CPU model": "M4 Pro",
38 | "Hard disk size": "8 TB",
39 | "security": "enhanced"
40 | }
41 | }
42 |
43 | 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
44 |
45 | Perform PUT operation for the created ProductID using URL https://api.restful-api.dev/objects/productID with body {
46 | "name": "Apple MacBook Pro 16",
47 | "data": {
48 | "year": 2025,
49 | "price": 4099,
50 | "CPU model": "M5",
51 | "Hard disk size": "10 TB",
52 | "color": "Titanium"
53 | }
54 | }
55 |
56 | And verify if the response has createdAt and id property
57 |
58 | Perform PATCH operation for the created ProductID using URL https://api.restful-api.dev/objects/productID with body
59 | {
60 | "name": "Apple MacBook Pro 19 (Limited Edition)"
61 | }
62 | And verify if the response has updatedAt property with value Apple MacBook Pro 19 (Limited Edition)
63 |
64 | ```
65 |
66 | And once the entire test operation completes, we will be presented with the entire details of how the automation did happened.
67 |
68 | 
69 |
70 | :::tip
71 | You can also see the `Request/Response/StatusCode` from the execution of Playwright MCP Server
72 |
73 | 
74 | :::
75 |
```
--------------------------------------------------------------------------------
/docs/docs/intro.mdx:
--------------------------------------------------------------------------------
```markdown
1 | ---
2 | sidebar_position: 1
3 | ---
4 |
5 | import YouTubeVideoEmbed from '@site/src/components/HomepageFeatures/YouTubeVideoEmbed';
6 |
7 | # Playwright MCP Server
8 |
9 | The **Playwright Model Context Protocol (MCP) server** is a powerful solution for automating Browser and API testing using Playwright.
10 |
11 | With the Playwright MCP server, you can:
12 | - Enable LLMs to interact with web pages in a real browser environment.
13 | - Perform tasks such as executing JavaScript, taking screenshots, and navigating web elements.
14 | - Seamlessly handle API testing to validate endpoints and ensure reliability.
15 | - Test across multiple browser engines including Chromium, Firefox, and WebKit.
16 |
17 | 
18 |
19 |
20 | ## Installation
21 | You can install Playwright MCP Server package using either **npm**, **mcp-get**, or **Smithery**:
22 |
23 | :::info Playwright MCP Tips
24 |
25 | To get started more quickly on Playwright MCP Server, watch the videos mentioned in the footer of this page under `Docs`
26 |
27 | :::
28 |
29 |
30 | ### Installing via NPM
31 | To install Playwright MCP for Claude Desktop automatically via Smithery:
32 |
33 | ```bash
34 | npm install -g @executeautomation/playwright-mcp-server
35 | ```
36 |
37 | ### Installing via Smithery
38 | To install Playwright MCP for Claude Desktop automatically via Smithery:
39 |
40 | ```bash
41 | npx @smithery/cli install @executeautomation/playwright-mcp-server --client claude
42 | ```
43 |
44 | You can type this command into Command Prompt, Powershell, Terminal, or any other integrated terminal of your code editor.
45 |
46 | ### Installing via MCP-GET
47 | To install Playwright MCP for Claude Desktop automatically via Smithery:
48 |
49 | ```bash
50 | npx @michaellatman/mcp-get@latest install @executeautomation/playwright-mcp-server
51 | ```
52 |
53 | ### Configuring Playwright MCP in Claude Desktop
54 | Here's the Claude Desktop configuration to use the Playwright MCP server.
55 |
56 | Modify your `claude-desktop-config.json` file as shown below
57 |
58 | ```json
59 | {
60 | "mcpServers": {
61 | "playwright": {
62 | "command": "npx",
63 | "args": ["-y", "@executeautomation/playwright-mcp-server"]
64 | }
65 | }
66 | }
67 | ```
68 |
69 | ### What is Model Context Protocol
70 | 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
71 |
72 | <YouTubeVideoEmbed videoId="hGJQMbpsTi4" />
73 |
```
--------------------------------------------------------------------------------
/docs/static/img/EA-Icon.svg:
--------------------------------------------------------------------------------
```
1 | <svg xmlns="http://www.w3.org/2000/svg" width="118.679" height="119.443" viewBox="0 0 118.679 119.443">
2 | <g id="Group_21" data-name="Group 21" transform="translate(-2049 1709)">
3 | <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"/>
4 | <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"/>
5 | </g>
6 | </svg>
7 |
```
--------------------------------------------------------------------------------
/docs/docs/ai-courses/GenAICourse.mdx:
--------------------------------------------------------------------------------
```markdown
1 | ---
2 | sidebar_position: 1
3 | ---
4 |
5 | import YouTubeVideoEmbed from '@site/src/components/HomepageFeatures/YouTubeVideoEmbed';
6 |
7 | # 🤖 Using Generative AI in Software Automation Testing
8 |
9 | <div align="center">
10 | <YouTubeVideoEmbed videoId="Y7xkCWvUKEk" />
11 | </div>
12 | ---
13 |
14 | :::info 💡 **Note**
15 | All the courses are available on Udemy, and they almost always have a `coupon code` available.
16 | For discounts, please feel free to reach out at **[[email protected]](mailto:[email protected])**.
17 |
18 | 🎯 **Course Link:**
19 | [Generative AI in Software Automation Testing](https://www.udemy.com/course/generative-ai-in-software-automation-testing/)
20 | :::
21 |
22 | ---
23 |
24 | ## 📚 **Course Description**
25 |
26 | 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**.
27 |
28 | ### 🚀 **What You’ll Master**
29 |
30 | - **🧠 Introduction to Generative AI:**
31 | Understand the foundations of Gen AI and its role in Software Testing.
32 |
33 | - **💻 Running Large Language Models (LLMs) Locally:**
34 | Learn how to run models on your machine without paying for external services.
35 |
36 | - **📝 Manual Testing with Gen AI:**
37 | Generate manual test cases, test data, and test requirements using grounded Models with the power of AI and RAG.
38 |
39 | - **🤖 Automated UI Testing:**
40 | Leverage AI to write, refactor, and optimize automated tests for UI applications.
41 |
42 | - **🎭 Playwright UI Testing:**
43 | Use Playwright and AI-driven tools to create smart test scripts and handle complex workflows.
44 |
45 | - **🚫 No-code Automation with TestRigor:**
46 | Create powerful automation suites in plain English, even automating SMS, phone calls, and intricate tables.
47 |
48 | - **🔗 API Testing:**
49 | Harness PostBots and Gen AI to streamline API testing.
50 |
51 | - **🧬 Using Gen AI APIs:**
52 | Add intelligence to your Test Automation code using OpenAI APIs.
53 |
54 | - **📍 Model Context Protocol (MCP):**
55 | Run Playwright tests for UI and APIs by leveraging the power of MCP.
56 |
57 | ---
58 |
59 | 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.
60 |
61 |
62 |
```
--------------------------------------------------------------------------------
/src/tools/browser/screenshot.ts:
--------------------------------------------------------------------------------
```typescript
1 | import fs from "node:fs";
2 | import * as path from "node:path";
3 | import * as os from "node:os";
4 | import type { Page } from "rebrowser-playwright";
5 | import { BrowserToolBase } from "./base.js";
6 | import {
7 | ToolContext,
8 | ToolResponse,
9 | createSuccessResponse,
10 | } from "../common/types.js";
11 |
12 | const defaultDownloadsPath = path.join(os.homedir(), "Downloads");
13 |
14 | /**
15 | * Tool for taking screenshots of pages or elements
16 | */
17 | export class ScreenshotTool extends BrowserToolBase {
18 | private screenshots = new Map<string, string>();
19 |
20 | /**
21 | * Execute the screenshot tool
22 | */
23 | async execute(args: any, context: ToolContext): Promise<ToolResponse> {
24 | return this.safeExecute(context, async (page) => {
25 | const screenshotOptions: any = {
26 | type: args.type || "png",
27 | fullPage: !!args.fullPage,
28 | };
29 |
30 | if (args.selector) {
31 | const element = await page.$(args.selector);
32 | if (!element) {
33 | return {
34 | content: [
35 | {
36 | type: "text",
37 | text: `Element not found: ${args.selector}`,
38 | },
39 | ],
40 | isError: true,
41 | };
42 | }
43 | screenshotOptions.element = element;
44 | }
45 |
46 | // Generate output path
47 | const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
48 | const filename = `${args.name || "screenshot"}-${timestamp}.png`;
49 | const downloadsDir = args.downloadsDir || defaultDownloadsPath;
50 |
51 | if (!fs.existsSync(downloadsDir)) {
52 | fs.mkdirSync(downloadsDir, { recursive: true });
53 | }
54 |
55 | const outputPath = path.join(downloadsDir, filename);
56 | screenshotOptions.path = outputPath;
57 |
58 | const screenshot = await page.screenshot(screenshotOptions);
59 | const base64Screenshot = screenshot.toString("base64");
60 |
61 | const messages = [
62 | `Screenshot saved to: ${path.relative(process.cwd(), outputPath)}`,
63 | ];
64 |
65 | // Handle base64 storage
66 | if (args.storeBase64 !== false) {
67 | this.screenshots.set(args.name || "screenshot", base64Screenshot);
68 | this.server.notification({
69 | method: "notifications/resources/list_changed",
70 | });
71 |
72 | messages.push(
73 | `Screenshot also stored in memory with name: '${
74 | args.name || "screenshot"
75 | }'`
76 | );
77 | }
78 |
79 | return createSuccessResponse(messages);
80 | });
81 | }
82 |
83 | /**
84 | * Get all stored screenshots
85 | */
86 | getScreenshots(): Map<string, string> {
87 | return this.screenshots;
88 | }
89 | }
90 |
```
--------------------------------------------------------------------------------
/docs/docs/ai-courses/MachineLearning.mdx:
--------------------------------------------------------------------------------
```markdown
1 | ---
2 | sidebar_position: 2
3 | ---
4 |
5 | import YouTubeVideoEmbed from '@site/src/components/HomepageFeatures/YouTubeVideoEmbed';
6 |
7 | # 🧠 Understand, Test, and Fine-tune AI Models with Hugging Face
8 |
9 | <div align="center">
10 | <YouTubeVideoEmbed videoId="T6-sL0TfAsM" />
11 | </div>
12 |
13 | ---
14 |
15 | :::info 💡 **Note**
16 | All the courses are available on **Udemy**, and they almost always have a **`coupon code`** available.
17 | For discounts, please feel free to reach out at **[[email protected]](mailto:[email protected])**.
18 |
19 | 🎯 **Course Link:**
20 | [Understand, Test, and Fine-tune AI Models with Hugging Face](https://www.udemy.com/course/ai-with-huggingface/)
21 | :::
22 |
23 | ---
24 |
25 | ## 📚 **Course Description**
26 |
27 | 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**.
28 |
29 | ---
30 |
31 | ### 🚀 **What You’ll Learn**
32 |
33 | 1. **📈 Introduction to Machine Learning:**
34 | Lay a strong foundation by exploring key ML concepts and essential terminology.
35 |
36 | 2. **📊 Working with Natural Language Processing (NLP) Libraries:**
37 | Learn how to process, analyze, and derive insights from textual data using popular NLP tools.
38 |
39 | 3. **💡 Deep Dive into the Transformers Library:**
40 | Master Hugging Face’s Transformers, the industry standard for building state-of-the-art **NLP** and **LLM** solutions.
41 |
42 | 4. **🧠 Working with Large Language Models (LLMs):**
43 | Explore multiple methods to interact with and utilize **LLMs** for diverse real-world applications.
44 |
45 | 5. **🧪 Functional Testing of AI Models:**
46 | Ensure your models perform reliably across different scenarios using systematic testing strategies.
47 |
48 | 6. **⚖️ Bias and Fairness Testing:**
49 | Implement techniques to detect and mitigate unintended bias, promoting ethical and fair **AI practices**.
50 |
51 | 7. **📏 Evaluating AI Models:**
52 | Measure performance with robust metrics and refine your models for optimal results.
53 |
54 | 8. **🤖 Working with AI Agents:**
55 | Build, configure, and integrate **intelligent agents** into your workflows.
56 |
57 | 9. **🔬 Fine-tuning and Training AI Models:**
58 | Customize pre-trained models or create your own from scratch to meet specific project requirements.
59 |
60 | ---
61 |
62 | 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
1 | ---
2 | sidebar_position: 1
3 | ---
4 |
5 | import YouTubeVideoEmbed from '@site/src/components/HomepageFeatures/YouTubeVideoEmbed';
6 |
7 |
8 | # 🛠️ Supported Tools
9 |
10 | Playwright MCP for API automation has following key features
11 | - Support of GET Request
12 | - Support of POST Request
13 | - Support of PATCH Request
14 | - Support of PUT Request
15 | - Support of DELETE Request
16 |
17 |
18 | <YouTubeVideoEmbed videoId="BYYyoRxCcFE" />
19 |
20 | ---
21 |
22 | :::warning Note
23 | 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!
24 | :::
25 |
26 | ### Playwright_get
27 | Perform a GET operation on any given API request.
28 |
29 | - **Inputs:**
30 | - **`url`** *(string)*:
31 | URL to perform the GET operation.
32 |
33 | - **Response:**
34 | - **`statusCode`** *(string)*:
35 | Status code of the API.
36 |
37 | ---
38 |
39 | ### Playwright_post
40 | Perform a POST operation on any given API request.
41 |
42 | - **Inputs:**
43 | - **`url`** *(string)*:
44 | URL to perform the POST operation.
45 | - **`value`** *(string)*:
46 | Data to include in the body of the POST request.
47 | - **`token`** *(string, optional)*:
48 | Bearer token for authorization. When provided, it will be sent as `Authorization: Bearer <token>` header.
49 | - **`headers`** *(object, optional)*:
50 | Additional headers to include in the request. Note: Content-Type: application/json is set by default.
51 |
52 | - **Response:**
53 | - **`statusCode`** *(string)*:
54 | Status code of the API.
55 | - **`responseData`** *(string)*:
56 | Response data in JSON format.
57 |
58 | ---
59 |
60 | ### Playwright_put
61 | Perform a PUT operation on any given API request.
62 |
63 | - **Inputs:**
64 | - **`url`** *(string)*:
65 | URL to perform the PUT operation.
66 | - **`value`** *(string)*:
67 | Data to include in the body of the PUT request.
68 |
69 | - **Response:**
70 | - **`statusCode`** *(string)*:
71 | Status code of the API.
72 | - **`responseData`** *(string)*:
73 | Response data in JSON format.
74 |
75 | ---
76 |
77 | ### Playwright_patch
78 | Perform a PATCH operation on any given API request.
79 |
80 | - **Inputs:**
81 | - **`url`** *(string)*:
82 | URL to perform the PATCH operation.
83 | - **`value`** *(string)*:
84 | Data to include in the body of the PATCH request.
85 |
86 | - **Response:**
87 | - **`statusCode`** *(string)*:
88 | Status code of the API.
89 | - **`responseData`** *(string)*:
90 | Response data in JSON format.
91 |
92 | ---
93 |
94 | ### Playwright_delete
95 | Perform a DELETE operation on any given API request.
96 |
97 | - **Inputs:**
98 | - **`url`** *(string)*:
99 | URL to perform the DELETE operation.
100 |
101 | - **Response:**
102 | - **`statusCode`** *(string)*:
103 | Status code of the API.
104 |
105 | ### Upon running the test Claude Desktop will run MCP Server to use above tools
106 | 
```
--------------------------------------------------------------------------------
/src/tools/browser/response.ts:
--------------------------------------------------------------------------------
```typescript
1 | import type { Response } from "rebrowser-playwright";
2 | import { BrowserToolBase } from "./base.js";
3 | import type { ToolContext, ToolResponse } from "../common/types.js";
4 | import { createSuccessResponse, createErrorResponse } from "../common/types.js";
5 |
6 | const responsePromises = new Map<string, Promise<Response>>();
7 |
8 | interface ExpectResponseArgs {
9 | id: string;
10 | url: string;
11 | }
12 |
13 | interface AssertResponseArgs {
14 | id: string;
15 | value?: string;
16 | }
17 |
18 | /**
19 | * Tool for setting up response wait operations
20 | */
21 | export class ExpectResponseTool extends BrowserToolBase {
22 | /**
23 | * Execute the expect response tool
24 | */
25 | async execute(
26 | args: ExpectResponseArgs,
27 | context: ToolContext
28 | ): Promise<ToolResponse> {
29 | return this.safeExecute(context, async (page) => {
30 | if (!args.id || !args.url) {
31 | return createErrorResponse(
32 | "Missing required parameters: id and url must be provided"
33 | );
34 | }
35 |
36 | const responsePromise = page.waitForResponse(args.url);
37 | responsePromises.set(args.id, responsePromise);
38 |
39 | return createSuccessResponse(
40 | `Started waiting for response with ID ${args.id}`
41 | );
42 | });
43 | }
44 | }
45 |
46 | /**
47 | * Tool for asserting and validating responses
48 | */
49 | export class AssertResponseTool extends BrowserToolBase {
50 | /**
51 | * Execute the assert response tool
52 | */
53 | async execute(
54 | args: AssertResponseArgs,
55 | context: ToolContext
56 | ): Promise<ToolResponse> {
57 | return this.safeExecute(context, async () => {
58 | if (!args.id) {
59 | return createErrorResponse(
60 | "Missing required parameter: id must be provided"
61 | );
62 | }
63 |
64 | const responsePromise = responsePromises.get(args.id);
65 | if (!responsePromise) {
66 | return createErrorResponse(
67 | `No response wait operation found with ID: ${args.id}`
68 | );
69 | }
70 |
71 | try {
72 | const response = await responsePromise;
73 | const body = await response.json();
74 |
75 | if (args.value) {
76 | const bodyStr = JSON.stringify(body);
77 | if (!bodyStr.includes(args.value)) {
78 | const messages = [
79 | `Response body does not contain expected value: ${args.value}`,
80 | `Actual body: ${bodyStr}`,
81 | ];
82 | return createErrorResponse(messages.join("\n"));
83 | }
84 | }
85 |
86 | const messages = [
87 | `Response assertion for ID ${args.id} successful`,
88 | `URL: ${response.url()}`,
89 | `Status: ${response.status()}`,
90 | `Body: ${JSON.stringify(body, null, 2)}`,
91 | ];
92 | return createSuccessResponse(messages.join("\n"));
93 | } catch (error) {
94 | return createErrorResponse(
95 | `Failed to assert response: ${(error as Error).message}`
96 | );
97 | } finally {
98 | responsePromises.delete(args.id);
99 | }
100 | });
101 | }
102 | }
103 |
```
--------------------------------------------------------------------------------
/docs/docs/ai-courses/AIAgents.mdx:
--------------------------------------------------------------------------------
```markdown
1 | ---
2 | sidebar_position: 3
3 | ---
4 |
5 | import YouTubeVideoEmbed from '@site/src/components/HomepageFeatures/YouTubeVideoEmbed';
6 |
7 | # 🧠🤖 Build & Test AI Agents, ChatBots, and RAG with Ollama & Local LLM
8 |
9 | <div align="center">
10 | <YouTubeVideoEmbed videoId="qw-X4WUHs5s" />
11 | </div>
12 |
13 | ---
14 |
15 | :::info 💡 **Note**
16 | All the courses are available on **Udemy**, and they almost always have a **`coupon code`** available.
17 | For discounts, please feel free to reach out at **[[email protected]](mailto:[email protected])**.
18 |
19 | 🎯 **Course Link:**
20 | [Build & Test AI Agents, ChatBots, and RAG with Ollama & Local LLM](https://www.udemy.com/course/build-ai-agent-chatbot-rag-langchain-local-llm/)
21 | :::
22 |
23 | ---
24 |
25 | ## 📚 **Course Description**
26 |
27 | 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)**.
28 |
29 | 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**.
30 |
31 | ---
32 |
33 | ### 🚀 **What You’ll Learn**
34 |
35 | - **🧠 Fundamentals of LangChain & LangSmith**
36 | Get a solid foundation in building and testing **LLM-based applications**.
37 |
38 | - **💬 Chat Message History in LangChain**
39 | Learn how to store conversation data for **chatbots** and **AI agents**.
40 |
41 | - **⚙️ Running Parallel & Multiple Chains**
42 | Master advanced techniques like **RunnableParallels** to optimize your **LLM workflows**.
43 |
44 | - **🤖 Building Chatbots with LangChain & Streamlit**
45 | Create chatbots with **message history** and an interactive **UI**.
46 |
47 | - **🛠️ Tools & Tool Chains in LLMs**
48 | Understand the power of **Tooling**, **Custom Tools**, and how to build **Tool Chains** for **AI applications**.
49 |
50 | - **🧑💻 Creating AI Agents with LangChain**
51 | Implement **AI agents** that can interact dynamically with **RAG applications**.
52 |
53 | - **📚 Implementing RAG with Vector Stores & Local Embeddings**
54 | Develop robust **RAG solutions** with local **LLM embeddings**.
55 |
56 | - **🔧 Using AI Agents & RAG with Tooling**
57 | Learn how to integrate **Tooling** effectively while building **LLM Apps**.
58 |
59 | - **🚦 Optimizing & Debugging AI Applications with LangSmith**
60 | Enhance your **AI models** and **applications** with **LangSmith's debugging** and **optimization tools**.
61 |
62 | - **🧪 Evaluating & Testing LLM Applications with RAGAs**
63 | Apply **hands-on testing strategies** to validate **RAG** and **AI agent** performance.
64 |
65 | - **📊 Real-world Projects & Assessments**
66 | Gain practical experience with **RAGAs** and learn to assess the quality and reliability of **AI solutions**.
67 |
68 | ---
69 |
70 | ## 🎯 **Learning Experience**
71 |
72 | 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**.
73 |
74 | 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
1 | import type { Browser, Page } from "rebrowser-playwright";
2 | import {
3 | ToolHandler,
4 | ToolContext,
5 | ToolResponse,
6 | createErrorResponse,
7 | } from "../common/types.js";
8 |
9 | /**
10 | * Base class for all browser-based tools
11 | * Provides common functionality and error handling
12 | */
13 | export abstract class BrowserToolBase implements ToolHandler {
14 | protected server: any;
15 |
16 | constructor(server: any) {
17 | this.server = server;
18 | }
19 |
20 | /**
21 | * Main execution method that all tools must implement
22 | */
23 | abstract execute(args: any, context: ToolContext): Promise<ToolResponse>;
24 |
25 | /**
26 | * Ensures a page is available and returns it
27 | * @param context The tool context containing browser and page
28 | * @returns The page or null if not available
29 | */
30 | protected ensurePage(context: ToolContext): Page | null {
31 | if (!context.page) {
32 | return null;
33 | }
34 | return context.page;
35 | }
36 |
37 | /**
38 | * Validates that a page is available and returns an error response if not
39 | * @param context The tool context
40 | * @returns Either null if page is available, or an error response
41 | */
42 | protected validatePageAvailable(context: ToolContext): ToolResponse | null {
43 | if (!this.ensurePage(context)) {
44 | return createErrorResponse("Browser page not initialized");
45 | }
46 | return null;
47 | }
48 |
49 | /**
50 | * Safely executes a browser operation with proper error handling
51 | * @param context The tool context
52 | * @param operation The async operation to perform
53 | * @returns The tool response
54 | */
55 | protected async safeExecute(
56 | context: ToolContext,
57 | operation: (page: Page) => Promise<ToolResponse>
58 | ): Promise<ToolResponse> {
59 | const pageError = this.validatePageAvailable(context);
60 | if (pageError) return pageError;
61 |
62 | try {
63 | // Verify browser is connected before proceeding
64 | if (context.browser && !context.browser.isConnected()) {
65 | // If browser exists but is disconnected, reset state
66 | const { resetBrowserState } = await import("../../toolHandler.js");
67 | resetBrowserState();
68 | return createErrorResponse(
69 | "Browser is disconnected. Please retry the operation."
70 | );
71 | }
72 |
73 | // Check if page is closed
74 | if (context.page.isClosed()) {
75 | return createErrorResponse(
76 | "Page is closed. Please retry the operation."
77 | );
78 | }
79 |
80 | return await operation(context.page!);
81 | } catch (error) {
82 | const errorMessage = (error as Error).message;
83 |
84 | // Check for common browser disconnection errors
85 | if (
86 | errorMessage.includes(
87 | "Target page, context or browser has been closed"
88 | ) ||
89 | errorMessage.includes("Target closed") ||
90 | errorMessage.includes("Browser has been disconnected") ||
91 | errorMessage.includes("Protocol error") ||
92 | errorMessage.includes("Connection closed")
93 | ) {
94 | // Reset browser state on connection issues
95 | const { resetBrowserState } = await import("../../toolHandler.js");
96 | resetBrowserState();
97 | return createErrorResponse(
98 | `Browser connection error: ${errorMessage}. Connection has been reset - please retry the operation.`
99 | );
100 | }
101 |
102 | return createErrorResponse(`Operation failed: ${errorMessage}`);
103 | }
104 | }
105 | }
106 |
```
--------------------------------------------------------------------------------
/docs/docs/playwright-web/Recording-Actions.mdx:
--------------------------------------------------------------------------------
```markdown
1 | ---
2 | sidebar_position: 5
3 | ---
4 |
5 | import YouTubeVideoEmbed from '@site/src/components/HomepageFeatures/YouTubeVideoEmbed';
6 |
7 | # 🎥 Recording Actions
8 |
9 | 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.
10 |
11 | ## Getting Started
12 |
13 | To start recording your actions and generate test files, you'll need to:
14 |
15 | 1. Start a recording session
16 | 2. Perform your actions using MCP tools
17 | 3. End the session to generate the test file
18 |
19 | ### Example
20 |
21 | Here's a complete example of recording actions and generating a test spec:
22 |
23 | 1. First, start a new recording session by specifying:
24 | - Where to save the generated tests (`tests/generated` directory)
25 | - What to name your tests (using 'LoginTest' as a prefix)
26 | - Whether to include helpful comments in the generated code
27 |
28 | 2. Then perform the test actions:
29 | - Navigate to the Sauce Demo website (https://www.saucedemo.com)
30 | - Enter "standard_user" in the username field
31 | - Enter "secret_sauce" in the password field
32 | - Click the login button
33 |
34 | 3. Finally, end the recording session to generate your test file
35 |
36 | The generated test file will look something like this:
37 |
38 | ```typescript
39 | import { test, expect } from '@playwright/test';
40 |
41 | test('LoginTest_2024-03-23', async ({ page }) => {
42 | // Navigate to the login page
43 | await page.goto('https://www.saucedemo.com');
44 |
45 | // Fill in username
46 | await page.fill('#user-name', 'standard_user');
47 |
48 | // Fill in password
49 | await page.fill('#password', 'secret_sauce');
50 |
51 | // Click login button
52 | await page.click('#login-button');
53 |
54 | // Verify successful login
55 | await expect(page).toHaveURL(/.*inventory.html/);
56 | });
57 | ```
58 |
59 | ## Configuration Options
60 |
61 | When starting a recording session, you can configure several options:
62 |
63 | - **outputPath**: Directory where the generated test files will be saved
64 | - **testNamePrefix**: Prefix for the generated test names
65 | - **includeComments**: Whether to include descriptive comments in the generated tests
66 |
67 | For example, you might configure your session to:
68 | - Save tests in a 'tests/generated' folder
69 | - Name tests with a 'MyTest' prefix
70 | - Include helpful comments in the generated code
71 |
72 | ## Session Management
73 |
74 | You can manage your recording sessions using these tools:
75 |
76 | - **get_codegen_session**: Retrieve information about a recording session
77 | - **clear_codegen_session**: Clean up a recording session without generating a test
78 |
79 | These tools help you check the status of your recording or clean up if you want to start over without generating a test file.
80 |
81 | ## Best Practices
82 |
83 | 1. **Organize Tests**: Use meaningful test name prefixes to organize your generated tests
84 | 2. **Clean Up**: Always end or clear your sessions after recording
85 | 3. **Verify Actions**: Include verification steps in your recordings
86 | 4. **Maintain Context**: Keep related actions in the same recording session
87 | 5. **Documentation**: Add comments during recording for better test maintainability
88 |
89 | ## Running Generated Tests
90 |
91 | To run your generated tests, use the Playwright test runner:
92 |
93 | ```bash
94 | npx playwright test tests/generated/logintest_*.spec.ts
95 | ```
96 |
97 | :::tip
98 | You can modify the generated test files to add additional assertions, setup, or teardown code as needed.
99 | :::
```
--------------------------------------------------------------------------------
/src/tools/browser/visiblePage.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { resetBrowserState } from "../../toolHandler.js";
2 | import { ToolContext, ToolResponse, createErrorResponse, createSuccessResponse } from "../common/types.js";
3 | import { BrowserToolBase } from "./base.js";
4 |
5 | /**
6 | * Tool for getting the visible text content of the current page
7 | */
8 | export class VisibleTextTool extends BrowserToolBase {
9 | /**
10 | * Execute the visible text page tool
11 | */
12 | async execute(args: any, context: ToolContext): Promise<ToolResponse> {
13 | // Check if browser is available
14 | if (!context.browser || !context.browser.isConnected()) {
15 | // If browser is not connected, we need to reset the state to force recreation
16 | resetBrowserState();
17 | return createErrorResponse(
18 | "Browser is not connected. The connection has been reset - please retry your navigation."
19 | );
20 | }
21 |
22 | // Check if page is available and not closed
23 | if (!context.page || context.page.isClosed()) {
24 | return createErrorResponse(
25 | "Page is not available or has been closed. Please retry your navigation."
26 | );
27 | }
28 | return this.safeExecute(context, async (page) => {
29 | try {
30 | const visibleText = await page!.evaluate(() => {
31 | const walker = document.createTreeWalker(
32 | document.body,
33 | NodeFilter.SHOW_TEXT,
34 | {
35 | acceptNode: (node) => {
36 | const style = window.getComputedStyle(node.parentElement!);
37 | return (style.display !== "none" && style.visibility !== "hidden")
38 | ? NodeFilter.FILTER_ACCEPT
39 | : NodeFilter.FILTER_REJECT;
40 | },
41 | }
42 | );
43 | let text = "";
44 | let node;
45 | while ((node = walker.nextNode())) {
46 | const trimmedText = node.textContent?.trim();
47 | if (trimmedText) {
48 | text += trimmedText + "\n";
49 | }
50 | }
51 | return text.trim();
52 | });
53 | return createSuccessResponse(`Visible text content:\n${visibleText}`);
54 | } catch (error) {
55 | return createErrorResponse(`Failed to get visible text content: ${(error as Error).message}`);
56 | }
57 | });
58 | }
59 | }
60 |
61 | /**
62 | * Tool for getting the visible HTML content of the current page
63 | */
64 | export class VisibleHtmlTool extends BrowserToolBase {
65 | /**
66 | * Execute the visible HTML page tool
67 | */
68 | async execute(args: any, context: ToolContext): Promise<ToolResponse> {
69 | // Check if browser is available
70 | if (!context.browser || !context.browser.isConnected()) {
71 | // If browser is not connected, we need to reset the state to force recreation
72 | resetBrowserState();
73 | return createErrorResponse(
74 | "Browser is not connected. The connection has been reset - please retry your navigation."
75 | );
76 | }
77 |
78 | // Check if page is available and not closed
79 | if (!context.page || context.page.isClosed()) {
80 | return createErrorResponse(
81 | "Page is not available or has been closed. Please retry your navigation."
82 | );
83 | }
84 | return this.safeExecute(context, async (page) => {
85 | try {
86 | const htmlContent = await page!.content();
87 | return createSuccessResponse(`HTML content:\n${htmlContent}`);
88 | } catch (error) {
89 | return createErrorResponse(`Failed to get visible HTML content: ${(error as Error).message}`);
90 | }
91 | });
92 | }
93 | }
```
--------------------------------------------------------------------------------
/docs/docusaurus.config.ts:
--------------------------------------------------------------------------------
```typescript
1 | import {themes as prismThemes} from 'prism-react-renderer';
2 | import type {Config} from '@docusaurus/types';
3 | import type * as Preset from '@docusaurus/preset-classic';
4 |
5 | // This runs in Node.js - Don't use client-side code here (browser APIs, JSX...)
6 |
7 | const config: Config = {
8 | title: 'Playwright MCP Server',
9 | tagline: 'Fastest way to test your APIs and UI in Playwright with AI 🤖',
10 | favicon: 'img/favicon.ico',
11 |
12 | // Set the production url of your site here
13 | url: 'https://executeautomation.github.io/',
14 | // Set the /<baseUrl>/ pathname under which your site is served
15 | // For GitHub pages deployment, it is often '/<projectName>/'
16 | baseUrl: '/mcp-playwright/',
17 |
18 | // GitHub pages deployment config.
19 | // If you aren't using GitHub pages, you don't need these.
20 | organizationName: 'executeautomation', // Usually your GitHub org/user name.
21 | projectName: 'mcp-playwright', // Usually your repo name.
22 |
23 | onBrokenLinks: 'ignore',
24 | onBrokenMarkdownLinks: 'warn',
25 |
26 | // Even if you don't use internationalization, you can use this field to set
27 | // useful metadata like html lang. For example, if your site is Chinese, you
28 | // may want to replace "en" with "zh-Hans".
29 | i18n: {
30 | defaultLocale: 'en',
31 | locales: ['en'],
32 | },
33 | trailingSlash: false,
34 | deploymentBranch: 'gh-pages',
35 | presets: [
36 | [
37 | 'classic',
38 | {
39 | docs: {
40 | sidebarPath: './sidebars.ts',
41 | // Please change this to your repo.
42 | // Remove this to remove the "edit this page" links.
43 | editUrl:
44 | 'https://github.com/facebook/docusaurus/tree/main/packages/create-docusaurus/templates/shared/',
45 | },
46 | theme: {
47 | customCss: './src/css/custom.css',
48 | },
49 | } satisfies Preset.Options,
50 | ],
51 | ],
52 |
53 | themeConfig: {
54 | // Replace with your project's social card
55 | image: 'img/EA-Icon.svg',
56 | navbar: {
57 | title: 'Playwright MCP Server',
58 | logo: {
59 | alt: 'Playwright MCP Server',
60 | src: 'img/EA-Icon.svg',
61 | },
62 | items: [
63 | {
64 | type: 'docSidebar',
65 | sidebarId: 'tutorialSidebar',
66 | position: 'left',
67 | label: 'Tutorial',
68 | },
69 | {
70 | href: 'https://github.com/executeautomation/mcp-playwright',
71 | label: 'GitHub',
72 | position: 'right',
73 | },
74 | ],
75 | },
76 | footer: {
77 | style: 'dark',
78 | links: [
79 | {
80 | title: 'Docs',
81 | items: [
82 | {
83 | label: 'Tutorial',
84 | to: '/docs/intro',
85 | },
86 | {
87 | label: 'Playwright MCP for UI',
88 | href: 'https://youtu.be/8CcgFUE16HM',
89 | },
90 | {
91 | label: 'Playwright MCP for API',
92 | href: 'https://youtu.be/BYYyoRxCcFE',
93 | },
94 | ],
95 | },
96 | {
97 | title: 'Community',
98 | items: [
99 | {
100 | label: 'Youtube',
101 | href: 'https://youtube.com/executeautomation',
102 | },
103 | {
104 | label: 'Udemy',
105 | href: 'https://www.udemy.com/user/karthik-kk',
106 | },
107 | {
108 | label: 'X',
109 | href: 'http://x.com/ExecuteAuto',
110 | },
111 | ],
112 | }
113 | ],
114 | copyright: `Copyright © ${new Date().getFullYear()} ExecuteAutomation Pvt Ltd.`,
115 | },
116 | prism: {
117 | theme: prismThemes.github,
118 | darkTheme: prismThemes.dracula,
119 | },
120 | } satisfies Preset.ThemeConfig,
121 | };
122 |
123 | export default config;
124 |
```
--------------------------------------------------------------------------------
/src/__tests__/tools/browser/output.test.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { SaveAsPdfTool } from '../../../tools/browser/output.js';
2 | import { ToolContext } from '../../../tools/common/types.js';
3 | import { Page, Browser } from 'playwright';
4 | import { jest } from '@jest/globals';
5 | import * as path from 'path';
6 |
7 | // Mock path.resolve to test path handling
8 | jest.mock('path', () => ({
9 | resolve: jest.fn().mockImplementation((dir, file) => `${dir}/${file}`)
10 | }));
11 |
12 | // Mock page functions
13 | const mockPdf = jest.fn().mockImplementation(() => Promise.resolve());
14 | const mockIsClosed = jest.fn().mockReturnValue(false);
15 |
16 | // Mock the Page object with proper typing
17 | const mockPage = {
18 | pdf: mockPdf,
19 | isClosed: mockIsClosed
20 | } as unknown as Page;
21 |
22 | // Mock the browser
23 | const mockIsConnected = jest.fn().mockReturnValue(true);
24 | const mockBrowser = {
25 | isConnected: mockIsConnected
26 | } as unknown as Browser;
27 |
28 | // Mock the server
29 | const mockServer = {
30 | sendMessage: jest.fn()
31 | };
32 |
33 | // Mock context
34 | const mockContext = {
35 | page: mockPage,
36 | browser: mockBrowser,
37 | server: mockServer
38 | } as ToolContext;
39 |
40 | describe('Browser Output Tools', () => {
41 | let saveAsPdfTool: SaveAsPdfTool;
42 |
43 | beforeEach(() => {
44 | jest.clearAllMocks();
45 | saveAsPdfTool = new SaveAsPdfTool(mockServer);
46 | // Reset browser and page mocks
47 | mockIsConnected.mockReturnValue(true);
48 | mockIsClosed.mockReturnValue(false);
49 | });
50 |
51 | describe('SaveAsPdfTool', () => {
52 | test('should save page as PDF with default options', async () => {
53 | const args = {
54 | outputPath: '/downloads'
55 | };
56 |
57 | const result = await saveAsPdfTool.execute(args, mockContext);
58 |
59 | expect(mockPdf).toHaveBeenCalledWith({
60 | path: '/downloads/page.pdf',
61 | format: 'A4',
62 | printBackground: true,
63 | margin: {
64 | top: '1cm',
65 | right: '1cm',
66 | bottom: '1cm',
67 | left: '1cm'
68 | }
69 | });
70 | expect(result.isError).toBe(false);
71 | expect(result.content[0].text).toContain('Saved page as PDF');
72 | });
73 |
74 | test('should save page as PDF with custom options', async () => {
75 | const args = {
76 | outputPath: '/downloads',
77 | filename: 'custom.pdf',
78 | format: 'Letter',
79 | printBackground: false,
80 | margin: {
81 | top: '2cm',
82 | right: '2cm',
83 | bottom: '2cm',
84 | left: '2cm'
85 | }
86 | };
87 |
88 | const result = await saveAsPdfTool.execute(args, mockContext);
89 |
90 | expect(mockPdf).toHaveBeenCalledWith({
91 | path: '/downloads/custom.pdf',
92 | format: 'Letter',
93 | printBackground: false,
94 | margin: {
95 | top: '2cm',
96 | right: '2cm',
97 | bottom: '2cm',
98 | left: '2cm'
99 | }
100 | });
101 | expect(result.isError).toBe(false);
102 | expect(result.content[0].text).toContain('Saved page as PDF');
103 | });
104 |
105 | test('should handle PDF generation errors', async () => {
106 | const args = {
107 | outputPath: '/downloads'
108 | };
109 |
110 | // Mock PDF generation error
111 | mockPdf.mockImplementationOnce(() => Promise.reject(new Error('PDF generation failed')));
112 |
113 | const result = await saveAsPdfTool.execute(args, mockContext);
114 |
115 | expect(mockPdf).toHaveBeenCalled();
116 | expect(result.isError).toBe(true);
117 | expect(result.content[0].text).toContain('Operation failed');
118 | });
119 |
120 | test('should handle missing page', async () => {
121 | const args = {
122 | outputPath: '/downloads'
123 | };
124 |
125 | const result = await saveAsPdfTool.execute(args, { server: mockServer } as ToolContext);
126 |
127 | expect(mockPdf).not.toHaveBeenCalled();
128 | expect(result.isError).toBe(true);
129 | expect(result.content[0].text).toContain('Browser page not initialized');
130 | });
131 | });
132 | });
```
--------------------------------------------------------------------------------
/src/__tests__/tools/browser/goNavigation.test.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { GoBackTool, GoForwardTool } from '../../../tools/browser/navigation.js';
2 | import { ToolContext } from '../../../tools/common/types.js';
3 | import { Page, Browser } from 'playwright';
4 | import { jest } from '@jest/globals';
5 |
6 | // Mock page functions
7 | const mockGoBack = jest.fn().mockImplementation(() => Promise.resolve());
8 | const mockGoForward = jest.fn().mockImplementation(() => Promise.resolve());
9 | const mockIsClosed = jest.fn().mockReturnValue(false);
10 |
11 | // Mock the Page object with proper typing
12 | const mockPage = {
13 | goBack: mockGoBack,
14 | goForward: mockGoForward,
15 | isClosed: mockIsClosed
16 | } as unknown as Page;
17 |
18 | // Mock the browser
19 | const mockIsConnected = jest.fn().mockReturnValue(true);
20 | const mockBrowser = {
21 | isConnected: mockIsConnected
22 | } as unknown as Browser;
23 |
24 | // Mock the server
25 | const mockServer = {
26 | sendMessage: jest.fn()
27 | };
28 |
29 | // Mock context
30 | const mockContext = {
31 | page: mockPage,
32 | browser: mockBrowser,
33 | server: mockServer
34 | } as ToolContext;
35 |
36 | describe('Browser Navigation History Tools', () => {
37 | let goBackTool: GoBackTool;
38 | let goForwardTool: GoForwardTool;
39 |
40 | beforeEach(() => {
41 | jest.clearAllMocks();
42 | goBackTool = new GoBackTool(mockServer);
43 | goForwardTool = new GoForwardTool(mockServer);
44 | // Reset browser and page mocks
45 | mockIsConnected.mockReturnValue(true);
46 | mockIsClosed.mockReturnValue(false);
47 | });
48 |
49 | describe('GoBackTool', () => {
50 | test('should navigate back in browser history', async () => {
51 | const args = {};
52 |
53 | const result = await goBackTool.execute(args, mockContext);
54 |
55 | expect(mockGoBack).toHaveBeenCalled();
56 | expect(result.isError).toBe(false);
57 | expect(result.content[0].text).toContain('Navigated back');
58 | });
59 |
60 | test('should handle navigation back errors', async () => {
61 | const args = {};
62 |
63 | // Mock a navigation error
64 | mockGoBack.mockImplementationOnce(() => Promise.reject(new Error('Navigation back failed')));
65 |
66 | const result = await goBackTool.execute(args, mockContext);
67 |
68 | expect(mockGoBack).toHaveBeenCalled();
69 | expect(result.isError).toBe(true);
70 | expect(result.content[0].text).toContain('Operation failed');
71 | });
72 |
73 | test('should handle missing page', async () => {
74 | const args = {};
75 |
76 | const result = await goBackTool.execute(args, { server: mockServer } as ToolContext);
77 |
78 | expect(mockGoBack).not.toHaveBeenCalled();
79 | expect(result.isError).toBe(true);
80 | expect(result.content[0].text).toContain('Browser page not initialized');
81 | });
82 | });
83 |
84 | describe('GoForwardTool', () => {
85 | test('should navigate forward in browser history', async () => {
86 | const args = {};
87 |
88 | const result = await goForwardTool.execute(args, mockContext);
89 |
90 | expect(mockGoForward).toHaveBeenCalled();
91 | expect(result.isError).toBe(false);
92 | expect(result.content[0].text).toContain('Navigated forward');
93 | });
94 |
95 | test('should handle navigation forward errors', async () => {
96 | const args = {};
97 |
98 | // Mock a navigation error
99 | mockGoForward.mockImplementationOnce(() => Promise.reject(new Error('Navigation forward failed')));
100 |
101 | const result = await goForwardTool.execute(args, mockContext);
102 |
103 | expect(mockGoForward).toHaveBeenCalled();
104 | expect(result.isError).toBe(true);
105 | expect(result.content[0].text).toContain('Operation failed');
106 | });
107 |
108 | test('should handle missing page', async () => {
109 | const args = {};
110 |
111 | const result = await goForwardTool.execute(args, { server: mockServer } as ToolContext);
112 |
113 | expect(mockGoForward).not.toHaveBeenCalled();
114 | expect(result.isError).toBe(true);
115 | expect(result.content[0].text).toContain('Browser page not initialized');
116 | });
117 | });
118 | });
```
--------------------------------------------------------------------------------
/src/tools/browser/navigation.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { BrowserToolBase } from './base.js';
2 | import { ToolContext, ToolResponse, createSuccessResponse, createErrorResponse } from '../common/types.js';
3 | import { resetBrowserState } from '../../toolHandler.js';
4 |
5 | /**
6 | * Tool for navigating to URLs
7 | */
8 | export class NavigationTool extends BrowserToolBase {
9 | /**
10 | * Execute the navigation tool
11 | */
12 | async execute(args: any, context: ToolContext): Promise<ToolResponse> {
13 | // Check if browser is available
14 | if (!context.browser || !context.browser.isConnected()) {
15 | // If browser is not connected, we need to reset the state to force recreation
16 | resetBrowserState();
17 | return createErrorResponse(
18 | "Browser is not connected. The connection has been reset - please retry your navigation."
19 | );
20 | }
21 |
22 | // Check if page is available and not closed
23 | if (!context.page || context.page.isClosed()) {
24 | return createErrorResponse(
25 | "Page is not available or has been closed. Please retry your navigation."
26 | );
27 | }
28 |
29 | return this.safeExecute(context, async (page) => {
30 | try {
31 | await page.goto(args.url, {
32 | timeout: args.timeout || 30000,
33 | waitUntil: args.waitUntil || "load"
34 | });
35 |
36 | return createSuccessResponse(`Navigated to ${args.url}`);
37 | } catch (error) {
38 | const errorMessage = (error as Error).message;
39 |
40 | // Check for common disconnection errors
41 | if (
42 | errorMessage.includes("Target page, context or browser has been closed") ||
43 | errorMessage.includes("Target closed") ||
44 | errorMessage.includes("Browser has been disconnected")
45 | ) {
46 | // Reset browser state to force recreation on next attempt
47 | resetBrowserState();
48 | return createErrorResponse(
49 | `Browser connection issue: ${errorMessage}. Connection has been reset - please retry your navigation.`
50 | );
51 | }
52 |
53 | // For other errors, return the standard error
54 | throw error;
55 | }
56 | });
57 | }
58 | }
59 |
60 | /**
61 | * Tool for closing the browser
62 | */
63 | export class CloseBrowserTool extends BrowserToolBase {
64 | /**
65 | * Execute the close browser tool
66 | */
67 | async execute(args: any, context: ToolContext): Promise<ToolResponse> {
68 | if (context.browser) {
69 | try {
70 | // Check if browser is still connected
71 | if (context.browser.isConnected()) {
72 | await context.browser.close().catch(error => {
73 | console.error("Error while closing browser:", error);
74 | });
75 | } else {
76 | console.error("Browser already disconnected, cleaning up state");
77 | }
78 | } catch (error) {
79 | console.error("Error during browser close operation:", error);
80 | // Continue with resetting state even if close fails
81 | } finally {
82 | // Always reset the global browser and page references
83 | resetBrowserState();
84 | }
85 |
86 | return createSuccessResponse("Browser closed successfully");
87 | }
88 |
89 | return createSuccessResponse("No browser instance to close");
90 | }
91 | }
92 |
93 | /**
94 | * Tool for navigating back in browser history
95 | */
96 | export class GoBackTool extends BrowserToolBase {
97 | /**
98 | * Execute the go back tool
99 | */
100 | async execute(args: any, context: ToolContext): Promise<ToolResponse> {
101 | return this.safeExecute(context, async (page) => {
102 | await page.goBack();
103 | return createSuccessResponse("Navigated back in browser history");
104 | });
105 | }
106 | }
107 |
108 | /**
109 | * Tool for navigating forward in browser history
110 | */
111 | export class GoForwardTool extends BrowserToolBase {
112 | /**
113 | * Execute the go forward tool
114 | */
115 | async execute(args: any, context: ToolContext): Promise<ToolResponse> {
116 | return this.safeExecute(context, async (page) => {
117 | await page.goForward();
118 | return createSuccessResponse("Navigated forward in browser history");
119 | });
120 | }
121 | }
```
--------------------------------------------------------------------------------
/src/__tests__/tools/browser/console.test.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { ConsoleLogsTool } from '../../../tools/browser/console.js';
2 | import { ToolContext } from '../../../tools/common/types.js';
3 | import { jest } from '@jest/globals';
4 |
5 | // Mock the server
6 | const mockServer = {
7 | sendMessage: jest.fn()
8 | };
9 |
10 | // Mock context
11 | const mockContext = {
12 | server: mockServer
13 | } as ToolContext;
14 |
15 | describe('ConsoleLogsTool', () => {
16 | let consoleLogsTool: ConsoleLogsTool;
17 |
18 | beforeEach(() => {
19 | jest.clearAllMocks();
20 | consoleLogsTool = new ConsoleLogsTool(mockServer);
21 | });
22 |
23 | test('should register console messages', () => {
24 | consoleLogsTool.registerConsoleMessage('log', 'Test log message');
25 | consoleLogsTool.registerConsoleMessage('error', 'Test error message');
26 | consoleLogsTool.registerConsoleMessage('warning', 'Test warning message');
27 |
28 | const logs = consoleLogsTool.getConsoleLogs();
29 | expect(logs.length).toBe(3);
30 | expect(logs[0]).toContain('Test log message');
31 | expect(logs[1]).toContain('Test error message');
32 | expect(logs[2]).toContain('Test warning message');
33 | });
34 |
35 | test('should retrieve console logs with type filter', async () => {
36 | consoleLogsTool.registerConsoleMessage('log', 'Test log message');
37 | consoleLogsTool.registerConsoleMessage('error', 'Test error message');
38 | consoleLogsTool.registerConsoleMessage('warning', 'Test warning message');
39 |
40 | const args = {
41 | type: 'error'
42 | };
43 |
44 | const result = await consoleLogsTool.execute(args, mockContext);
45 |
46 | expect(result.isError).toBe(false);
47 | expect(result.content[0].text).toContain('Retrieved 1 console log(s)');
48 | expect(result.content[1].text).toContain('Test error message');
49 | expect(result.content[1].text).not.toContain('Test log message');
50 | expect(result.content[1].text).not.toContain('Test warning message');
51 | });
52 |
53 | test('should retrieve console logs with search filter', async () => {
54 | consoleLogsTool.registerConsoleMessage('log', 'Test log message');
55 | consoleLogsTool.registerConsoleMessage('error', 'Test error with [special] characters');
56 | consoleLogsTool.registerConsoleMessage('warning', 'Another warning message');
57 |
58 | const args = {
59 | search: 'special'
60 | };
61 |
62 | const result = await consoleLogsTool.execute(args, mockContext);
63 |
64 | expect(result.isError).toBe(false);
65 | expect(result.content[0].text).toContain('Retrieved 1 console log(s)');
66 | expect(result.content[1].text).toContain('Test error with [special] characters');
67 | expect(result.content[1].text).not.toContain('Test log message');
68 | expect(result.content[1].text).not.toContain('Another warning message');
69 | });
70 |
71 | test('should retrieve console logs with limit', async () => {
72 | for (let i = 0; i < 10; i++) {
73 | consoleLogsTool.registerConsoleMessage('log', `Test log message ${i}`);
74 | }
75 |
76 | const args = {
77 | limit: 5
78 | };
79 |
80 | const result = await consoleLogsTool.execute(args, mockContext);
81 |
82 | expect(result.isError).toBe(false);
83 | expect(result.content[0].text).toContain('Retrieved 5 console log(s)');
84 |
85 | // The actual implementation might only show the first log in the content
86 | // Just verify that at least one log message is present
87 | const logText = result.content[1].text as string;
88 | expect(logText).toContain('Test log message');
89 | });
90 |
91 | test('should clear console logs when requested', async () => {
92 | consoleLogsTool.registerConsoleMessage('log', 'Test log message');
93 | consoleLogsTool.registerConsoleMessage('error', 'Test error message');
94 |
95 | const args = {
96 | clear: true
97 | };
98 |
99 | const result = await consoleLogsTool.execute(args, mockContext);
100 |
101 | expect(result.isError).toBe(false);
102 | expect(result.content[0].text).toContain('Retrieved 2 console log(s)');
103 |
104 | // Logs should be cleared after retrieval
105 | const logs = consoleLogsTool.getConsoleLogs();
106 | expect(logs.length).toBe(0);
107 | });
108 |
109 | test('should handle no logs', async () => {
110 | const args = {};
111 |
112 | const result = await consoleLogsTool.execute(args, mockContext);
113 |
114 | expect(result.isError).toBe(false);
115 | expect(result.content[0].text).toContain('No console logs matching the criteria');
116 | });
117 | });
```
--------------------------------------------------------------------------------
/docs/static/img/node.svg:
--------------------------------------------------------------------------------
```
1 | <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
1 | import { createToolDefinitions, BROWSER_TOOLS, API_TOOLS } from '../tools';
2 |
3 | describe('Tool Definitions', () => {
4 | const toolDefinitions = createToolDefinitions();
5 |
6 | test('should return an array of tool definitions', () => {
7 | expect(Array.isArray(toolDefinitions)).toBe(true);
8 | expect(toolDefinitions.length).toBeGreaterThan(0);
9 | });
10 |
11 | test('each tool definition should have required properties', () => {
12 | toolDefinitions.forEach(tool => {
13 | expect(tool).toHaveProperty('name');
14 | expect(tool).toHaveProperty('description');
15 | expect(tool).toHaveProperty('inputSchema');
16 | expect(tool.inputSchema).toHaveProperty('type');
17 | expect(tool.inputSchema).toHaveProperty('properties');
18 | });
19 | });
20 |
21 | test('BROWSER_TOOLS should contain browser-related tool names', () => {
22 | expect(Array.isArray(BROWSER_TOOLS)).toBe(true);
23 | expect(BROWSER_TOOLS.length).toBeGreaterThan(0);
24 |
25 | BROWSER_TOOLS.forEach(toolName => {
26 | expect(toolDefinitions.some(tool => tool.name === toolName)).toBe(true);
27 | });
28 | });
29 |
30 | test('API_TOOLS should contain API-related tool names', () => {
31 | expect(Array.isArray(API_TOOLS)).toBe(true);
32 | expect(API_TOOLS.length).toBeGreaterThan(0);
33 |
34 | API_TOOLS.forEach(toolName => {
35 | expect(toolDefinitions.some(tool => tool.name === toolName)).toBe(true);
36 | });
37 | });
38 |
39 | test('should validate navigate tool schema', () => {
40 | const navigateTool = toolDefinitions.find(tool => tool.name === 'playwright_navigate');
41 | expect(navigateTool).toBeDefined();
42 | expect(navigateTool!.inputSchema.properties).toHaveProperty('url');
43 | expect(navigateTool!.inputSchema.properties).toHaveProperty('waitUntil');
44 | expect(navigateTool!.inputSchema.properties).toHaveProperty('timeout');
45 | expect(navigateTool!.inputSchema.properties).toHaveProperty('width');
46 | expect(navigateTool!.inputSchema.properties).toHaveProperty('height');
47 | expect(navigateTool!.inputSchema.properties).toHaveProperty('headless');
48 | expect(navigateTool!.inputSchema.required).toEqual(['url']);
49 | });
50 |
51 | test('should validate go_back tool schema', () => {
52 | const goBackTool = toolDefinitions.find(tool => tool.name === 'playwright_go_back');
53 | expect(goBackTool).toBeDefined();
54 | expect(goBackTool!.inputSchema.properties).toEqual({});
55 | expect(goBackTool!.inputSchema.required).toEqual([]);
56 | });
57 |
58 | test('should validate go_forward tool schema', () => {
59 | const goForwardTool = toolDefinitions.find(tool => tool.name === 'playwright_go_forward');
60 | expect(goForwardTool).toBeDefined();
61 | expect(goForwardTool!.inputSchema.properties).toEqual({});
62 | expect(goForwardTool!.inputSchema.required).toEqual([]);
63 | });
64 |
65 | test('should validate drag tool schema', () => {
66 | const dragTool = toolDefinitions.find(tool => tool.name === 'playwright_drag');
67 | expect(dragTool).toBeDefined();
68 | expect(dragTool!.inputSchema.properties).toHaveProperty('sourceSelector');
69 | expect(dragTool!.inputSchema.properties).toHaveProperty('targetSelector');
70 | expect(dragTool!.inputSchema.required).toEqual(['sourceSelector', 'targetSelector']);
71 | });
72 |
73 | test('should validate press_key tool schema', () => {
74 | const pressKeyTool = toolDefinitions.find(tool => tool.name === 'playwright_press_key');
75 | expect(pressKeyTool).toBeDefined();
76 | expect(pressKeyTool!.inputSchema.properties).toHaveProperty('key');
77 | expect(pressKeyTool!.inputSchema.properties).toHaveProperty('selector');
78 | expect(pressKeyTool!.inputSchema.required).toEqual(['key']);
79 | });
80 |
81 | test('should validate save_as_pdf tool schema', () => {
82 | const saveAsPdfTool = toolDefinitions.find(tool => tool.name === 'playwright_save_as_pdf');
83 | expect(saveAsPdfTool).toBeDefined();
84 | expect(saveAsPdfTool!.inputSchema.properties).toHaveProperty('outputPath');
85 | expect(saveAsPdfTool!.inputSchema.properties).toHaveProperty('filename');
86 | expect(saveAsPdfTool!.inputSchema.properties).toHaveProperty('format');
87 | expect(saveAsPdfTool!.inputSchema.properties).toHaveProperty('printBackground');
88 | expect(saveAsPdfTool!.inputSchema.properties).toHaveProperty('margin');
89 | expect(saveAsPdfTool!.inputSchema.required).toEqual(['outputPath']);
90 | });
91 | });
```
--------------------------------------------------------------------------------
/docs/docs/playwright-web/Examples.md:
--------------------------------------------------------------------------------
```markdown
1 | ---
2 | sidebar_position: 4
3 | ---
4 |
5 | # 🌐 Examples of browser automation
6 | Lets see how we can use the power of Playwright MCP Server to automate our browser and do webscrapping
7 |
8 | ### Using Different Browser Types
9 |
10 | Playwright MCP now supports multiple browser engines. You can choose between Chromium (default), Firefox, and WebKit:
11 |
12 | ```bdd
13 | Given I navigate to website "https://example.com" using the "firefox" browser
14 | And I take a screenshot named "firefox-example"
15 | Then I navigate to website "https://example.com" using the "webkit" browser
16 | And I take a screenshot named "webkit-example"
17 | ```
18 |
19 | 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.
20 |
21 | ### Scenario in BDD Format
22 | ```bdd
23 | Given I navigate to website http://eaapp.somee.com and click login link
24 | And I enter username and password as "admin" and "password" respectively and perform login
25 | Then click the Employee List page
26 | And click "Create New" button and enter realistic employee details to create for Name, Salary, DurationWorked,
27 | Select dropdown for Grade as CLevel and Email.
28 | ```
29 |
30 | Once I enter the above text in ***Claude Desktop Client*** I should see Claude desktop giving me prompt to perform operation
31 | by opening real browser like this
32 |
33 | 
34 |
35 | And once the entire test operation completes, we will be presented with the entire details of how the automation did happened.
36 |
37 | 
38 |
39 | ### Using Browser History Navigation
40 |
41 | You can navigate through the browser's history using the new navigation controls:
42 |
43 | ```bdd
44 | Given I navigate to website "https://example.com"
45 | When I navigate to website "https://example.com/about"
46 | And I navigate back in browser history
47 | Then the current page should be "https://example.com"
48 | When I navigate forward in browser history
49 | Then the current page should be "https://example.com/about"
50 | ```
51 |
52 | ### Using Drag and Drop Functionality
53 |
54 | You can drag and drop elements using the new drag tool:
55 |
56 | ```bdd
57 | Given I navigate to website "https://example.com/drag-drop-demo"
58 | When I drag element with id "draggable" to element with id "droppable"
59 | Then I should see confirmation message "Dropped!"
60 | ```
61 |
62 | ### Using Keyboard Interactions
63 |
64 | You can simulate keyboard presses with the new keyboard tool:
65 |
66 | ```bdd
67 | Given I navigate to website "https://example.com/form"
68 | When I focus on the input field with id "search-box"
69 | And I press the "Enter" key
70 | Then the search results should appear
71 | ```
72 |
73 | ### Saving Page as PDF
74 |
75 | You can save the current page as a PDF file:
76 |
77 | ```bdd
78 | Given I navigate to website "https://example.com/report"
79 | When I save the current page as a PDF in "/downloads" folder with name "report.pdf"
80 | Then I should see confirmation that the PDF was saved
81 | ```
82 |
83 | Advanced example with custom options:
84 |
85 | ```bdd
86 | Given I navigate to website "https://example.com/invoice"
87 | When I save the page as PDF with the following settings:
88 | | Setting | Value |
89 | | ----------------- | --------- |
90 | | Output Path | /downloads |
91 | | Filename | invoice.pdf |
92 | | Format | Letter |
93 | | Print Background | true |
94 | | Top Margin | 2cm |
95 | | Right Margin | 1cm |
96 | | Bottom Margin | 2cm |
97 | | Left Margin | 1cm |
98 | Then I should see confirmation that the PDF was saved
99 | ```
100 |
101 | ### Extracting Page Content
102 |
103 | You can extract visible text content from the page:
104 |
105 | ```bdd
106 | Given I navigate to website "https://example.com/article"
107 | When I extract all visible text from the page
108 | Then I should see the article content in plain text without hidden elements
109 | ```
110 |
111 | You can also get the complete HTML of the page:
112 |
113 | ```bdd
114 | Given I navigate to website "https://example.com/products"
115 | When I extract the HTML content of the page
116 | Then I should receive the complete HTML structure of the page
117 | ```
118 |
119 | Example use case for content analysis:
120 |
121 | ```bdd
122 | Given I navigate to website "https://example.com/pricing"
123 | When I extract all visible text from the page
124 | Then I should be able to analyze the text to find pricing information
125 | And I can determine if the "Enterprise" plan mentions "custom pricing"
126 | ```
```
--------------------------------------------------------------------------------
/src/__tests__/tools/browser/navigation.test.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { NavigationTool } from '../../../tools/browser/navigation.js';
2 | import { ToolContext } from '../../../tools/common/types.js';
3 | import { Page, Browser } from 'playwright';
4 | import { jest } from '@jest/globals';
5 |
6 | // Mock the Page object
7 | const mockGoto = jest.fn();
8 | mockGoto.mockImplementation(() => Promise.resolve());
9 | const mockIsClosed = jest.fn().mockReturnValue(false);
10 |
11 | const mockPage = {
12 | goto: mockGoto,
13 | isClosed: mockIsClosed
14 | } as unknown as Page;
15 |
16 | // Mock the browser
17 | const mockIsConnected = jest.fn().mockReturnValue(true);
18 | const mockBrowser = {
19 | isConnected: mockIsConnected
20 | } as unknown as Browser;
21 |
22 | // Mock the server
23 | const mockServer = {
24 | sendMessage: jest.fn()
25 | };
26 |
27 | // Mock context
28 | const mockContext = {
29 | page: mockPage,
30 | browser: mockBrowser,
31 | server: mockServer
32 | } as ToolContext;
33 |
34 | describe('NavigationTool', () => {
35 | let navigationTool: NavigationTool;
36 |
37 | beforeEach(() => {
38 | jest.clearAllMocks();
39 | navigationTool = new NavigationTool(mockServer);
40 | // Reset mocks
41 | mockIsConnected.mockReturnValue(true);
42 | mockIsClosed.mockReturnValue(false);
43 | });
44 |
45 | test('should navigate to a URL', async () => {
46 | const args = {
47 | url: 'https://example.com',
48 | waitUntil: 'networkidle'
49 | };
50 |
51 | const result = await navigationTool.execute(args, mockContext);
52 |
53 | expect(mockGoto).toHaveBeenCalledWith('https://example.com', { waitUntil: 'networkidle', timeout: 30000 });
54 | expect(result.isError).toBe(false);
55 | expect(result.content[0].text).toContain('Navigated to');
56 | });
57 |
58 | test('should handle navigation with specific browser type', async () => {
59 | const args = {
60 | url: 'https://example.com',
61 | waitUntil: 'networkidle',
62 | browserType: 'firefox'
63 | };
64 |
65 | const result = await navigationTool.execute(args, mockContext);
66 |
67 | expect(mockGoto).toHaveBeenCalledWith('https://example.com', { waitUntil: 'networkidle', timeout: 30000 });
68 | expect(result.isError).toBe(false);
69 | expect(result.content[0].text).toContain('Navigated to');
70 | });
71 |
72 | test('should handle navigation with webkit browser type', async () => {
73 | const args = {
74 | url: 'https://example.com',
75 | browserType: 'webkit'
76 | };
77 |
78 | const result = await navigationTool.execute(args, mockContext);
79 |
80 | expect(mockGoto).toHaveBeenCalledWith('https://example.com', { waitUntil: 'load', timeout: 30000 });
81 | expect(result.isError).toBe(false);
82 | expect(result.content[0].text).toContain('Navigated to');
83 | });
84 |
85 | test('should handle navigation errors', async () => {
86 | const args = {
87 | url: 'https://example.com'
88 | };
89 |
90 | // Mock a navigation error
91 | mockGoto.mockImplementationOnce(() => Promise.reject(new Error('Navigation failed')));
92 |
93 | const result = await navigationTool.execute(args, mockContext);
94 |
95 | expect(mockGoto).toHaveBeenCalledWith('https://example.com', { waitUntil: 'load', timeout: 30000 });
96 | expect(result.isError).toBe(true);
97 | expect(result.content[0].text).toContain('Operation failed');
98 | });
99 |
100 | test('should handle missing page', async () => {
101 | const args = {
102 | url: 'https://example.com'
103 | };
104 |
105 | // Context with browser but without page
106 | const contextWithoutPage = {
107 | browser: mockBrowser,
108 | server: mockServer
109 | } as unknown as ToolContext;
110 |
111 | const result = await navigationTool.execute(args, contextWithoutPage);
112 |
113 | expect(mockGoto).not.toHaveBeenCalled();
114 | expect(result.isError).toBe(true);
115 | expect(result.content[0].text).toContain('Page is not available');
116 | });
117 |
118 | test('should handle disconnected browser', async () => {
119 | const args = {
120 | url: 'https://example.com'
121 | };
122 |
123 | // Mock disconnected browser
124 | mockIsConnected.mockReturnValueOnce(false);
125 |
126 | const result = await navigationTool.execute(args, mockContext);
127 |
128 | expect(mockGoto).not.toHaveBeenCalled();
129 | expect(result.isError).toBe(true);
130 | expect(result.content[0].text).toContain('Browser is not connected');
131 | });
132 |
133 | test('should handle closed page', async () => {
134 | const args = {
135 | url: 'https://example.com'
136 | };
137 |
138 | // Mock closed page
139 | mockIsClosed.mockReturnValueOnce(true);
140 |
141 | const result = await navigationTool.execute(args, mockContext);
142 |
143 | expect(mockGoto).not.toHaveBeenCalled();
144 | expect(result.isError).toBe(true);
145 | expect(result.content[0].text).toContain('Page is not available or has been closed');
146 | });
147 | });
```
--------------------------------------------------------------------------------
/src/tools/browser/interaction.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { BrowserToolBase } from './base.js';
2 | import { ToolContext, ToolResponse, createSuccessResponse, createErrorResponse } from '../common/types.js';
3 |
4 | /**
5 | * Tool for clicking elements on the page
6 | */
7 | export class ClickTool extends BrowserToolBase {
8 | /**
9 | * Execute the click tool
10 | */
11 | async execute(args: any, context: ToolContext): Promise<ToolResponse> {
12 | return this.safeExecute(context, async (page) => {
13 | await page.click(args.selector);
14 | return createSuccessResponse(`Clicked element: ${args.selector}`);
15 | });
16 | }
17 | }
18 |
19 | /**
20 | * Tool for clicking elements inside iframes
21 | */
22 | export class IframeClickTool extends BrowserToolBase {
23 | /**
24 | * Execute the iframe click tool
25 | */
26 | async execute(args: any, context: ToolContext): Promise<ToolResponse> {
27 | return this.safeExecute(context, async (page) => {
28 | const frame = page.frameLocator(args.iframeSelector);
29 | if (!frame) {
30 | return createErrorResponse(`Iframe not found: ${args.iframeSelector}`);
31 | }
32 |
33 | await frame.locator(args.selector).click();
34 | return createSuccessResponse(`Clicked element ${args.selector} inside iframe ${args.iframeSelector}`);
35 | });
36 | }
37 | }
38 |
39 | /**
40 | * Tool for filling form fields
41 | */
42 | export class FillTool extends BrowserToolBase {
43 | /**
44 | * Execute the fill tool
45 | */
46 | async execute(args: any, context: ToolContext): Promise<ToolResponse> {
47 | return this.safeExecute(context, async (page) => {
48 | await page.waitForSelector(args.selector);
49 | await page.fill(args.selector, args.value);
50 | return createSuccessResponse(`Filled ${args.selector} with: ${args.value}`);
51 | });
52 | }
53 | }
54 |
55 | /**
56 | * Tool for selecting options from dropdown menus
57 | */
58 | export class SelectTool extends BrowserToolBase {
59 | /**
60 | * Execute the select tool
61 | */
62 | async execute(args: any, context: ToolContext): Promise<ToolResponse> {
63 | return this.safeExecute(context, async (page) => {
64 | await page.waitForSelector(args.selector);
65 | await page.selectOption(args.selector, args.value);
66 | return createSuccessResponse(`Selected ${args.selector} with: ${args.value}`);
67 | });
68 | }
69 | }
70 |
71 | /**
72 | * Tool for hovering over elements
73 | */
74 | export class HoverTool extends BrowserToolBase {
75 | /**
76 | * Execute the hover tool
77 | */
78 | async execute(args: any, context: ToolContext): Promise<ToolResponse> {
79 | return this.safeExecute(context, async (page) => {
80 | await page.waitForSelector(args.selector);
81 | await page.hover(args.selector);
82 | return createSuccessResponse(`Hovered ${args.selector}`);
83 | });
84 | }
85 | }
86 |
87 | /**
88 | * Tool for executing JavaScript in the browser
89 | */
90 | export class EvaluateTool extends BrowserToolBase {
91 | /**
92 | * Execute the evaluate tool
93 | */
94 | async execute(args: any, context: ToolContext): Promise<ToolResponse> {
95 | return this.safeExecute(context, async (page) => {
96 | const result = await page.evaluate(args.script);
97 |
98 | // Convert result to string for display
99 | let resultStr: string;
100 | try {
101 | resultStr = JSON.stringify(result, null, 2);
102 | } catch (error) {
103 | resultStr = String(result);
104 | }
105 |
106 | return createSuccessResponse([
107 | `Executed JavaScript:`,
108 | `${args.script}`,
109 | `Result:`,
110 | `${resultStr}`
111 | ]);
112 | });
113 | }
114 | }
115 |
116 | /**
117 | * Tool for dragging elements on the page
118 | */
119 | export class DragTool extends BrowserToolBase {
120 | /**
121 | * Execute the drag tool
122 | */
123 | async execute(args: any, context: ToolContext): Promise<ToolResponse> {
124 | return this.safeExecute(context, async (page) => {
125 | const sourceElement = await page.waitForSelector(args.sourceSelector);
126 | const targetElement = await page.waitForSelector(args.targetSelector);
127 |
128 | const sourceBound = await sourceElement.boundingBox();
129 | const targetBound = await targetElement.boundingBox();
130 |
131 | if (!sourceBound || !targetBound) {
132 | return createErrorResponse("Could not get element positions for drag operation");
133 | }
134 |
135 | await page.mouse.move(
136 | sourceBound.x + sourceBound.width / 2,
137 | sourceBound.y + sourceBound.height / 2
138 | );
139 | await page.mouse.down();
140 | await page.mouse.move(
141 | targetBound.x + targetBound.width / 2,
142 | targetBound.y + targetBound.height / 2
143 | );
144 | await page.mouse.up();
145 |
146 | return createSuccessResponse(`Dragged element from ${args.sourceSelector} to ${args.targetSelector}`);
147 | });
148 | }
149 | }
150 |
151 | /**
152 | * Tool for pressing keyboard keys
153 | */
154 | export class PressKeyTool extends BrowserToolBase {
155 | /**
156 | * Execute the key press tool
157 | */
158 | async execute(args: any, context: ToolContext): Promise<ToolResponse> {
159 | return this.safeExecute(context, async (page) => {
160 | if (args.selector) {
161 | await page.waitForSelector(args.selector);
162 | await page.focus(args.selector);
163 | }
164 |
165 | await page.keyboard.press(args.key);
166 | return createSuccessResponse(`Pressed key: ${args.key}`);
167 | });
168 | }
169 | }
```
--------------------------------------------------------------------------------
/src/__tests__/tools/browser/screenshot.test.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { ScreenshotTool } from '../../../tools/browser/screenshot.js';
2 | import { ToolContext } from '../../../tools/common/types.js';
3 | import { Page, Browser } from 'playwright';
4 | import { jest } from '@jest/globals';
5 | import fs from 'node:fs';
6 | import path from 'node:path';
7 |
8 | // Mock fs module
9 | jest.mock('node:fs', () => ({
10 | existsSync: jest.fn().mockReturnValue(true),
11 | mkdirSync: jest.fn(),
12 | writeFileSync: jest.fn()
13 | }));
14 |
15 | // Mock the Page object
16 | const mockScreenshot = jest.fn().mockImplementation(() =>
17 | Promise.resolve(Buffer.from('mock-screenshot')));
18 |
19 | const mockLocatorScreenshot = jest.fn().mockImplementation(() =>
20 | Promise.resolve(Buffer.from('mock-element-screenshot')));
21 |
22 | const mockElementHandle = {
23 | screenshot: mockLocatorScreenshot
24 | };
25 |
26 | const mockElement = jest.fn().mockImplementation(() => Promise.resolve(mockElementHandle));
27 |
28 | const mockLocator = jest.fn().mockReturnValue({
29 | screenshot: mockLocatorScreenshot
30 | });
31 |
32 | const mockIsClosed = jest.fn().mockReturnValue(false);
33 | const mockPage = {
34 | screenshot: mockScreenshot,
35 | locator: mockLocator,
36 | $: mockElement,
37 | isClosed: mockIsClosed
38 | } as unknown as Page;
39 |
40 | // Mock browser
41 | const mockIsConnected = jest.fn().mockReturnValue(true);
42 | const mockBrowser = {
43 | isConnected: mockIsConnected
44 | } as unknown as Browser;
45 |
46 | // Mock the server
47 | const mockServer = {
48 | sendMessage: jest.fn(),
49 | notification: jest.fn()
50 | };
51 |
52 | // Mock context
53 | const mockContext = {
54 | page: mockPage,
55 | browser: mockBrowser,
56 | server: mockServer
57 | } as ToolContext;
58 |
59 | describe('ScreenshotTool', () => {
60 | let screenshotTool: ScreenshotTool;
61 |
62 | beforeEach(() => {
63 | jest.clearAllMocks();
64 | screenshotTool = new ScreenshotTool(mockServer);
65 |
66 | // Mock Date to return a consistent value for testing
67 | jest.spyOn(global.Date.prototype, 'toISOString').mockReturnValue('2023-01-01T12:00:00.000Z');
68 | (fs.existsSync as jest.Mock).mockReturnValue(true);
69 | });
70 |
71 | afterEach(() => {
72 | jest.restoreAllMocks();
73 | });
74 |
75 | test('should take a full page screenshot', async () => {
76 | const args = {
77 | name: 'test-screenshot',
78 | fullPage: true
79 | };
80 |
81 | // Return a buffer for the screenshot
82 | const screenshotBuffer = Buffer.from('mock-screenshot');
83 | mockScreenshot.mockImplementationOnce(() => Promise.resolve(screenshotBuffer));
84 |
85 | const result = await screenshotTool.execute(args, mockContext);
86 |
87 | // Check if screenshot was called with correct options
88 | expect(mockScreenshot).toHaveBeenCalledWith(expect.objectContaining({
89 | fullPage: true,
90 | type: 'png'
91 | }));
92 |
93 | // Check that the result contains success message
94 | expect(result.isError).toBe(false);
95 | expect(result.content[0].text).toContain('Screenshot saved to');
96 | });
97 |
98 | test('should handle element screenshot', async () => {
99 | const args = {
100 | name: 'test-element-screenshot',
101 | selector: '#test-element'
102 | };
103 |
104 | // Return a buffer for the screenshot
105 | const screenshotBuffer = Buffer.from('mock-element-screenshot');
106 | mockLocatorScreenshot.mockImplementationOnce(() => Promise.resolve(screenshotBuffer));
107 |
108 | const result = await screenshotTool.execute(args, mockContext);
109 |
110 | // Check that the result contains success message
111 | expect(result.isError).toBe(false);
112 | expect(result.content[0].text).toContain('Screenshot saved to');
113 | });
114 |
115 | test('should handle screenshot errors', async () => {
116 | const args = {
117 | name: 'test-screenshot'
118 | };
119 |
120 | // Mock a screenshot error
121 | mockScreenshot.mockImplementationOnce(() => Promise.reject(new Error('Screenshot failed')));
122 |
123 | const result = await screenshotTool.execute(args, mockContext);
124 |
125 | expect(mockScreenshot).toHaveBeenCalled();
126 | expect(result.isError).toBe(true);
127 | expect(result.content[0].text).toContain('Operation failed');
128 | });
129 |
130 | test('should handle missing page', async () => {
131 | const args = {
132 | name: 'test-screenshot'
133 | };
134 |
135 | // Context without page but with browser
136 | const contextWithoutPage = {
137 | browser: mockBrowser,
138 | server: mockServer
139 | } as unknown as ToolContext;
140 |
141 | const result = await screenshotTool.execute(args, contextWithoutPage);
142 |
143 | expect(mockScreenshot).not.toHaveBeenCalled();
144 | expect(result.isError).toBe(true);
145 | expect(result.content[0].text).toContain('Browser page not initialized');
146 | });
147 |
148 | test('should store screenshots in a map', async () => {
149 | const args = {
150 | name: 'test-screenshot',
151 | storeBase64: true
152 | };
153 |
154 | // Return a buffer for the screenshot
155 | const screenshotBuffer = Buffer.from('mock-screenshot');
156 | mockScreenshot.mockImplementationOnce(() => Promise.resolve(screenshotBuffer));
157 |
158 | await screenshotTool.execute(args, mockContext);
159 |
160 | // Check that the screenshot was stored in the map
161 | const screenshots = screenshotTool.getScreenshots();
162 | expect(screenshots.has('test-screenshot')).toBe(true);
163 | });
164 |
165 | test('should take a screenshot with specific browser type', async () => {
166 | const args = {
167 | name: 'browser-type-test',
168 | browserType: 'firefox'
169 | };
170 |
171 | // Execute with browser type
172 | const result = await screenshotTool.execute(args, mockContext);
173 |
174 | expect(mockScreenshot).toHaveBeenCalled();
175 | expect(result.isError).toBe(false);
176 | expect(result.content[0].text).toContain('Screenshot saved to');
177 | });
178 | });
```
--------------------------------------------------------------------------------
/src/tools/api/requests.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { ApiToolBase } from './base.js';
2 | import { ToolContext, ToolResponse, createSuccessResponse, createErrorResponse } from '../common/types.js';
3 |
4 | /**
5 | * Tool for making GET requests
6 | */
7 | export class GetRequestTool extends ApiToolBase {
8 | /**
9 | * Execute the GET request tool
10 | */
11 | async execute(args: any, context: ToolContext): Promise<ToolResponse> {
12 | return this.safeExecute(context, async (apiContext) => {
13 | const response = await apiContext.get(args.url);
14 |
15 | let responseText;
16 | try {
17 | responseText = await response.text();
18 | } catch (error) {
19 | responseText = "Unable to get response text";
20 | }
21 |
22 | return createSuccessResponse([
23 | `GET request to ${args.url}`,
24 | `Status: ${response.status()} ${response.statusText()}`,
25 | `Response: ${responseText.substring(0, 1000)}${responseText.length > 1000 ? '...' : ''}`
26 | ]);
27 | });
28 | }
29 | }
30 |
31 | /**
32 | * Tool for making POST requests
33 | */
34 | export class PostRequestTool extends ApiToolBase {
35 | /**
36 | * Execute the POST request tool
37 | */
38 | async execute(args: any, context: ToolContext): Promise<ToolResponse> {
39 | return this.safeExecute(context, async (apiContext) => {
40 | // Check if the value is valid JSON if it starts with { or [
41 | if (args.value && typeof args.value === 'string' &&
42 | (args.value.startsWith('{') || args.value.startsWith('['))) {
43 | try {
44 | JSON.parse(args.value);
45 | } catch (error) {
46 | return createErrorResponse(`Failed to parse request body: ${(error as Error).message}`);
47 | }
48 | }
49 |
50 | const response = await apiContext.post(args.url, {
51 | data: typeof args.value === 'string' ? JSON.parse(args.value) : args.value,
52 | headers: {
53 | 'Content-Type': 'application/json',
54 | ...(args.token ? { 'Authorization': `Bearer ${args.token}` } : {}),
55 | ...(args.headers || {})
56 | }
57 | });
58 |
59 | let responseText;
60 | try {
61 | responseText = await response.text();
62 | } catch (error) {
63 | responseText = "Unable to get response text";
64 | }
65 |
66 | return createSuccessResponse([
67 | `POST request to ${args.url}`,
68 | `Status: ${response.status()} ${response.statusText()}`,
69 | `Response: ${responseText.substring(0, 1000)}${responseText.length > 1000 ? '...' : ''}`
70 | ]);
71 | });
72 | }
73 | }
74 |
75 | /**
76 | * Tool for making PUT requests
77 | */
78 | export class PutRequestTool extends ApiToolBase {
79 | /**
80 | * Execute the PUT request tool
81 | */
82 | async execute(args: any, context: ToolContext): Promise<ToolResponse> {
83 | return this.safeExecute(context, async (apiContext) => {
84 | // Check if the value is valid JSON if it starts with { or [
85 | if (args.value && typeof args.value === 'string' &&
86 | (args.value.startsWith('{') || args.value.startsWith('['))) {
87 | try {
88 | JSON.parse(args.value);
89 | } catch (error) {
90 | return createErrorResponse(`Failed to parse request body: ${(error as Error).message}`);
91 | }
92 | }
93 |
94 | const response = await apiContext.put(args.url, {
95 | data: args.value
96 | });
97 |
98 | let responseText;
99 | try {
100 | responseText = await response.text();
101 | } catch (error) {
102 | responseText = "Unable to get response text";
103 | }
104 |
105 | return createSuccessResponse([
106 | `PUT request to ${args.url}`,
107 | `Status: ${response.status()} ${response.statusText()}`,
108 | `Response: ${responseText.substring(0, 1000)}${responseText.length > 1000 ? '...' : ''}`
109 | ]);
110 | });
111 | }
112 | }
113 |
114 | /**
115 | * Tool for making PATCH requests
116 | */
117 | export class PatchRequestTool extends ApiToolBase {
118 | /**
119 | * Execute the PATCH request tool
120 | */
121 | async execute(args: any, context: ToolContext): Promise<ToolResponse> {
122 | return this.safeExecute(context, async (apiContext) => {
123 | // Check if the value is valid JSON if it starts with { or [
124 | if (args.value && typeof args.value === 'string' &&
125 | (args.value.startsWith('{') || args.value.startsWith('['))) {
126 | try {
127 | JSON.parse(args.value);
128 | } catch (error) {
129 | return createErrorResponse(`Failed to parse request body: ${(error as Error).message}`);
130 | }
131 | }
132 |
133 | const response = await apiContext.patch(args.url, {
134 | data: args.value
135 | });
136 |
137 | let responseText;
138 | try {
139 | responseText = await response.text();
140 | } catch (error) {
141 | responseText = "Unable to get response text";
142 | }
143 |
144 | return createSuccessResponse([
145 | `PATCH request to ${args.url}`,
146 | `Status: ${response.status()} ${response.statusText()}`,
147 | `Response: ${responseText.substring(0, 1000)}${responseText.length > 1000 ? '...' : ''}`
148 | ]);
149 | });
150 | }
151 | }
152 |
153 | /**
154 | * Tool for making DELETE requests
155 | */
156 | export class DeleteRequestTool extends ApiToolBase {
157 | /**
158 | * Execute the DELETE request tool
159 | */
160 | async execute(args: any, context: ToolContext): Promise<ToolResponse> {
161 | return this.safeExecute(context, async (apiContext) => {
162 | const response = await apiContext.delete(args.url);
163 |
164 | let responseText;
165 | try {
166 | responseText = await response.text();
167 | } catch (error) {
168 | responseText = "Unable to get response text";
169 | }
170 |
171 | return createSuccessResponse([
172 | `DELETE request to ${args.url}`,
173 | `Status: ${response.status()} ${response.statusText()}`,
174 | `Response: ${responseText.substring(0, 1000)}${responseText.length > 1000 ? '...' : ''}`
175 | ]);
176 | });
177 | }
178 | }
```
--------------------------------------------------------------------------------
/docs/docs/release.mdx:
--------------------------------------------------------------------------------
```markdown
1 | ---
2 | sidebar_position: 2
3 | ---
4 | import YouTubeVideoEmbed from '@site/src/components/HomepageFeatures/YouTubeVideoEmbed';
5 |
6 | # Release Notes
7 |
8 | ## Version 1.0.3
9 | - **Code Generation Capabilities**: Added new code generation capability 🎭
10 | - `start_codegen_session`: Start a new session to record Playwright actions
11 | - `end_codegen_session`: End a session and generate test file
12 | - `get_codegen_session`: Retrieve information about a session
13 | - `clear_codegen_session`: Clear a session without generating a test
14 | - Ability to record real browser interactions and convert them to reusable Playwright tests
15 | - Support for customizing test output path, test names, and including descriptive comments
16 | - **Enhanced Browser Navigation**: Added new navigation control tools 🧭
17 | - `playwright_go_back`: Navigate back in browser history
18 | - `playwright_go_forward`: Navigate forward in browser history
19 | - **Advanced Interaction**: Added new interaction tools for more complex scenarios 🔄
20 | - `playwright_drag`: Drag elements from one location to another
21 | - `playwright_press_key`: Press keyboard keys with optional element focus
22 | - **Output Capabilities**: Added content export functionality 📄
23 | - `playwright_save_as_pdf`: Save the current page as a PDF file with customizable options
24 | - **Content Extraction**: Added tools for retrieving page content 📝
25 | - `playwright_get_visible_text`: Extract all visible text content from the current page
26 | - `playwright_get_visible_html`: Get the complete HTML content of the current page
27 | - Comprehensive test coverage for all new tools
28 | - Updated documentation with examples and usage detail
29 |
30 | ## Version 1.0.2
31 | - **Multi-Browser Support**: Added support for Firefox and WebKit browsers in addition to Chromium 🌐
32 | - New `browserType` parameter for `playwright_navigate` tool allows specifying browser engine
33 | - Supported browser types: "chromium" (default), "firefox", and "webkit"
34 | - Seamless browser engine switching during automation sessions
35 | - Enhanced test coverage for different browser engines
36 | - Updated documentation with browser-specific examples
37 |
38 | ## Version 1.0.0
39 | - First major release of Playwright MCP Server with the tool structure changes 🚀
40 | - Fixed issue with headless mode in Playwright #62
41 | - Fixed issue Navigation failed: page.goto: Target page, context or browser has been closed #63
42 | - Completed RFC: Refactor handleToolCall for better maintainability #46
43 | - New feature: Optional Bearer Authorization to API POST (Thanks to ***@CopilotMe***)
44 | - Fixed issue Exit process on host close (Thanks to ***@kiracih***)
45 | - New Feature: Three new tools (Thanks to ***@VinceOPS***)
46 | - `playwright_except_response`
47 | - `playwright_assert_response`
48 |
49 | Here is the scenario for the above two tools
50 | ```BDD
51 | Scenario: Logging in requires captcha verification
52 | Given I expect the browser to receive an HTTP response from "**/security/captcha-precheck"
53 | When I enter "[email protected]" in the input and I submit
54 | Then The browser should have received the HTTP response
55 | And Its body should contain a property "captchaFamily"
56 | ```
57 | - A new tool `playwright_custom_user_agent` to define a custom user agent.
58 |
59 |
60 | ## Version 0.3.1
61 | - Fixed BROWSER_TOOLS as Playwright_console_logs is not required (Thanks to https://github.com/kfern)
62 | - Added Tests for all the Playwright MCP Server tools (Thanks to https://github.com/kfern)
63 | - Updated documentation with AI Courses
64 | - Gen AI Course [Details here](/docs/ai-courses/AIAgents)
65 | - AI Agents Course [Details here](/docs/ai-courses/AIAgents)
66 | - Machine Learning Course [Details here](/docs/ai-courses/MachineLearning)
67 |
68 | ## Version 0.3.0
69 | - Added support for `Playwright_console_logs` to get the console logs from the browser. Following logs types
70 | are supported.[More Detail available here](/docs/playwright-web/Console-Logging)
71 | - `log`
72 | - `info`
73 | - `warn`
74 | - `error`
75 | - `debug`
76 | - `all`
77 |
78 |
79 | :::tip Usage Example
80 | To invoke `Playwright_console_logs` via MCP Playwright, use the following prompt:
81 |
82 | ```plaintext
83 | Get the console log from the browser whenever you perform any action.
84 | :::
85 | - Added support for `Playwright_close` to close the browser and release all resources.
86 |
87 | :::tip Usage Example
88 | To invoke `Playwright_close` via MCP Playwright, use the following prompt:
89 |
90 | ```plaintext
91 | Close the browser once the operation is completed.
92 | :::
93 |
94 | ## Version 0.2.9
95 | - Fixed Screenshot issue with Cline, Cursor and Windows 11 (Reported by @MackDing, @mengjian-github)
96 |
97 | ## Version 0.2.8
98 | - Support of iFrame while running Playwright test via MCP (Supports Cline as well). Thanks to @VinceOPS
99 | - Fixed issue while saving PNG file. Thanks to @BayLee4
100 | - Fixed issue with full page screenshot arguments to be passed to tool, thanks for the report @unipro-LeighMason
101 | - Updated to latest version of Playwright and MCP Server library
102 |
103 |
104 | ## Version 0.2.7
105 | - Fixed the issue with Playwright MCP server not working Cline, VSCode reported in #26, #16
106 | - Fixed issue #28 and now chrome version is updated
107 | - Updated to latest version of Playwright and MCP Server library
108 |
109 | ## Version 0.2.6
110 | - New Documentation site powered by docusaurus hosted in GH-Pages https://executeautomation.github.io/mcp-playwright/
111 |
112 | ---
113 |
114 | ## Version 0.2.5
115 |
116 | #### API Test Support
117 | - Playwright MCP Server now supports API Testing for
118 | - `GET` request
119 | - `POST` request
120 | - `PUT` request
121 | - `PATCH` request
122 | - `DELETE` request
123 |
124 | <YouTubeVideoEmbed videoId="BYYyoRxCcFE" />
125 |
126 | ---
127 |
128 | ## Version 0.2.4
129 | - Added support for smithery
130 | - Added Support to save Playwright screenshot in local directory, thanks to `@s4l4x`
131 |
132 | ---
133 |
134 | ## Version 0.2.3
135 | - Added quality of life improvement
136 |
137 | ---
138 |
```
--------------------------------------------------------------------------------
/src/tools/codegen/generator.ts:
--------------------------------------------------------------------------------
```typescript
1 | import * as path from 'path';
2 | import { CodegenAction, CodegenOptions, CodegenResult, CodegenSession, PlaywrightTestCase } from './types.js';
3 |
4 | export class PlaywrightGenerator {
5 | private static readonly DEFAULT_OPTIONS: Required<CodegenOptions> = {
6 | outputPath: 'tests',
7 | testNamePrefix: 'MCP',
8 | includeComments: true,
9 | };
10 |
11 | private options: Required<CodegenOptions>;
12 |
13 | constructor(options: CodegenOptions = {}) {
14 | this.validateOptions(options);
15 | this.options = { ...PlaywrightGenerator.DEFAULT_OPTIONS, ...options };
16 | }
17 |
18 | private validateOptions(options: CodegenOptions): void {
19 | if (options.outputPath && typeof options.outputPath !== 'string') {
20 | throw new Error('outputPath must be a string');
21 | }
22 | if (options.testNamePrefix && typeof options.testNamePrefix !== 'string') {
23 | throw new Error('testNamePrefix must be a string');
24 | }
25 | if (options.includeComments !== undefined && typeof options.includeComments !== 'boolean') {
26 | throw new Error('includeComments must be a boolean');
27 | }
28 | }
29 |
30 | async generateTest(session: CodegenSession): Promise<CodegenResult> {
31 | if (!session || !Array.isArray(session.actions)) {
32 | throw new Error('Invalid session data');
33 | }
34 |
35 | const testCase = this.createTestCase(session);
36 | const testCode = this.generateTestCode(testCase);
37 | const filePath = this.getOutputFilePath(session);
38 |
39 | return {
40 | testCode,
41 | filePath,
42 | sessionId: session.id,
43 | };
44 | }
45 |
46 | private createTestCase(session: CodegenSession): PlaywrightTestCase {
47 | const testCase: PlaywrightTestCase = {
48 | name: `${this.options.testNamePrefix}_${new Date(session.startTime).toISOString().split('T')[0]}`,
49 | steps: [],
50 | imports: new Set(['test', 'expect']),
51 | };
52 |
53 | for (const action of session.actions) {
54 | const step = this.convertActionToStep(action);
55 | if (step) {
56 | testCase.steps.push(step);
57 | }
58 | }
59 |
60 | return testCase;
61 | }
62 |
63 | private convertActionToStep(action: CodegenAction): string | null {
64 | const { toolName, parameters } = action;
65 |
66 | switch (toolName) {
67 | case 'playwright_navigate':
68 | return this.generateNavigateStep(parameters);
69 | case 'playwright_fill':
70 | return this.generateFillStep(parameters);
71 | case 'playwright_click':
72 | return this.generateClickStep(parameters);
73 | case 'playwright_screenshot':
74 | return this.generateScreenshotStep(parameters);
75 | case 'playwright_expect_response':
76 | return this.generateExpectResponseStep(parameters);
77 | case 'playwright_assert_response':
78 | return this.generateAssertResponseStep(parameters);
79 | case 'playwright_hover':
80 | return this.generateHoverStep(parameters);
81 | case 'playwright_select':
82 | return this.generateSelectStep(parameters);
83 | case 'playwright_custom_user_agent':
84 | return this.generateCustomUserAgentStep(parameters);
85 | default:
86 | console.warn(`Unsupported tool: ${toolName}`);
87 | return null;
88 | }
89 | }
90 |
91 | private generateNavigateStep(parameters: Record<string, unknown>): string {
92 | const { url, waitUntil } = parameters;
93 | const options = waitUntil ? `, { waitUntil: '${waitUntil}' }` : '';
94 | return `
95 | // Navigate to URL
96 | await page.goto('${url}'${options});`;
97 | }
98 |
99 | private generateFillStep(parameters: Record<string, unknown>): string {
100 | const { selector, value } = parameters;
101 | return `
102 | // Fill input field
103 | await page.fill('${selector}', '${value}');`;
104 | }
105 |
106 | private generateClickStep(parameters: Record<string, unknown>): string {
107 | const { selector } = parameters;
108 | return `
109 | // Click element
110 | await page.click('${selector}');`;
111 | }
112 |
113 | private generateScreenshotStep(parameters: Record<string, unknown>): string {
114 | const { name, fullPage = false, path } = parameters;
115 | const options = [];
116 | if (fullPage) options.push('fullPage: true');
117 | if (path) options.push(`path: '${path}'`);
118 |
119 | const optionsStr = options.length > 0 ? `, { ${options.join(', ')} }` : '';
120 | return `
121 | // Take screenshot
122 | await page.screenshot({ path: '${name}.png'${optionsStr} });`;
123 | }
124 |
125 | private generateExpectResponseStep(parameters: Record<string, unknown>): string {
126 | const { url, id } = parameters;
127 | return `
128 | // Wait for response
129 | const ${id}Response = page.waitForResponse('${url}');`;
130 | }
131 |
132 | private generateAssertResponseStep(parameters: Record<string, unknown>): string {
133 | const { id, value } = parameters;
134 | const assertion = value
135 | ? `\n const responseText = await ${id}Response.text();\n expect(responseText).toContain('${value}');`
136 | : `\n expect(${id}Response.ok()).toBeTruthy();`;
137 | return `
138 | // Assert response${assertion}`;
139 | }
140 |
141 | private generateHoverStep(parameters: Record<string, unknown>): string {
142 | const { selector } = parameters;
143 | return `
144 | // Hover over element
145 | await page.hover('${selector}');`;
146 | }
147 |
148 | private generateSelectStep(parameters: Record<string, unknown>): string {
149 | const { selector, value } = parameters;
150 | return `
151 | // Select option
152 | await page.selectOption('${selector}', '${value}');`;
153 | }
154 |
155 | private generateCustomUserAgentStep(parameters: Record<string, unknown>): string {
156 | const { userAgent } = parameters;
157 | return `
158 | // Set custom user agent
159 | await context.setUserAgent('${userAgent}');`;
160 | }
161 |
162 | private generateTestCode(testCase: PlaywrightTestCase): string {
163 | const imports = Array.from(testCase.imports)
164 | .map(imp => `import { ${imp} } from '@playwright/test';`)
165 | .join('\n');
166 |
167 | return `
168 | ${imports}
169 |
170 | test('${testCase.name}', async ({ page, context }) => {
171 | ${testCase.steps.join('\n')}
172 | });`;
173 | }
174 |
175 | private getOutputFilePath(session: CodegenSession): string {
176 | if (!session.id) {
177 | throw new Error('Session ID is required');
178 | }
179 |
180 | const sanitizedPrefix = this.options.testNamePrefix.toLowerCase().replace(/[^a-z0-9_]/g, '_');
181 | const fileName = `${sanitizedPrefix}_${session.id}.spec.ts`;
182 | return path.resolve(this.options.outputPath, fileName);
183 | }
184 | }
```