#
tokens: 13274/50000 31/31 files
lines: on (toggle) GitHub
raw markdown copy reset
# Directory Structure

```
├── .env.example
├── .env.production
├── .eslintignore
├── .eslintrc.js
├── .gitignore
├── .husky
│   └── pre-commit
├── .lintstagedrc.js
├── .prettierignore
├── .prettierrc
├── .vscode
│   └── settings.json
├── Dockerfile
├── jest.config.js
├── LICENSE
├── nodemon.json
├── package-lock.json
├── package.json
├── README.md
├── scripts
│   ├── run-relevant-tests.ts
│   └── setup-husky.js
├── src
│   ├── class
│   │   └── browser.class.ts
│   ├── endpoints
│   │   ├── click.ts
│   │   ├── evaluate.ts
│   │   ├── fill.ts
│   │   ├── hover.ts
│   │   ├── index.ts
│   │   ├── navigate.ts
│   │   ├── screenshot.ts
│   │   └── select.ts
│   ├── index.ts
│   ├── types
│   │   └── screenshot.ts
│   └── utils
│       ├── browserManager.ts
│       ├── notificationUtil.ts
│       └── screenshotManager.ts
└── tsconfig.json
```

# Files

--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------

```
1 | node_modules/
2 | dist/
3 | coverage/
4 | *.md
5 | *.yml
6 | *.yaml
```

--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------

```
1 | node_modules/
2 | dist/
3 | coverage/
4 | *.js
5 | *.d.ts
6 | *test*
7 | scripts/*
```

--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------

```
1 | # Environment mode
2 | NODE_ENV=development
3 | 
4 | # Browser settings
5 | HEADLESS=true
6 | 
7 | # Set to 'true' when running in Docker
8 | DOCKER_CONTAINER=false
9 | 
```

--------------------------------------------------------------------------------
/.env.production:
--------------------------------------------------------------------------------

```
1 | # Production environment
2 | NODE_ENV=production
3 | 
4 | # Browser settings
5 | HEADLESS=true
6 | 
7 | # Set to 'true' when running in Docker
8 | DOCKER_CONTAINER=false
9 | 
```

--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------

```
 1 | {
 2 |   "semi": true,
 3 |   "trailingComma": "all",
 4 |   "singleQuote": true,
 5 |   "printWidth": 100,
 6 |   "tabWidth": 2,
 7 |   "useTabs": false,
 8 |   "endOfLine": "auto",
 9 |   "arrowParens": "avoid",
10 |   "bracketSpacing": true
11 | }
```

--------------------------------------------------------------------------------
/.lintstagedrc.js:
--------------------------------------------------------------------------------

```javascript
1 | module.exports = {
2 |   '*.{js,ts}': files => [
3 |     `cross-env NODE_ENV=development eslint --cache --fix ${files.join(' ')}`,
4 |     `cross-env NODE_ENV=development prettier --write ${files.join(' ')}`,
5 |   ],
6 | };
7 | 
```

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
 1 | # Dependencies
 2 | node_modules/
 3 | 
 4 | # Build
 5 | dist/
 6 | build/
 7 | 
 8 | # Logs
 9 | logs/
10 | *.log
11 | npm-debug.log*
12 | yarn-debug.log*
13 | yarn-error.log*
14 | 
15 | # Environment
16 | .env
17 | .env.local
18 | .env.development
19 | .env.*.local
20 | 
21 | # Testing
22 | coverage/
23 | 
24 | # IDE
25 | .idea/
26 | .vscode/*
27 | !.vscode/settings.json
28 | !.vscode/tasks.json
29 | !.vscode/launch.json
30 | !.vscode/extensions.json
31 | 
32 | # OS
33 | .DS_Store
34 | Thumbs.db
35 | desktop.ini
36 | 
37 | # Windows
38 | *.lnk
39 | 
40 | # ESLint
41 | .eslintcache
42 | 
43 | # Temp files
44 | *.swp
45 | *.swo
46 | *~
```

--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------

```javascript
 1 | module.exports = {
 2 |   parser: '@typescript-eslint/parser',
 3 |   extends: [
 4 |     'plugin:@typescript-eslint/recommended',
 5 |     'plugin:import/errors',
 6 |     'plugin:import/warnings',
 7 |     'plugin:import/typescript',
 8 |     'plugin:prettier/recommended',
 9 |   ],
10 |   parserOptions: {
11 |     ecmaVersion: 2020,
12 |     sourceType: 'module',
13 |     project: './tsconfig.json',
14 |     tsconfigRootDir: __dirname,
15 |   },
16 |   env: {
17 |     node: true,
18 |     jest: true,
19 |   },
20 |   rules: {
21 |     '@typescript-eslint/explicit-function-return-type': 'off',
22 |     '@typescript-eslint/explicit-module-boundary-types': 'off',
23 |     '@typescript-eslint/no-explicit-any': process.env.NODE_ENV === 'production' ? 'error' : 'warn',
24 |     '@typescript-eslint/no-unused-vars': process.env.NODE_ENV === 'production' ? 'error' : 'warn',
25 |     '@typescript-eslint/no-non-null-assertion': 'warn',
26 |     // Disable the problematic import rules
27 |     'import/no-named-as-default': 'off',
28 |     'import/no-named-as-default-member': 'off',
29 |     'import/namespace': 'off',
30 |     'import/default': 'off',
31 |     'import/no-named-default': 'off',
32 |     'import/no-duplicates': 'off',
33 |     'import/no-unresolved': 'off',
34 |   },
35 |   settings: {
36 |     'import/parsers': {
37 |       '@typescript-eslint/parser': ['.ts'],
38 |     },
39 |     'import/resolver': {
40 |       typescript: {
41 |         alwaysTryTypes: true,
42 |         project: ['./tsconfig.json'],
43 |       },
44 |       node: true,
45 |     },
46 |   },
47 | };
48 | 
```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
  1 | # Puppeteer-Extra MCP Server
  2 | 
  3 | A Model Context Protocol server that provides enhanced browser automation capabilities using Puppeteer-Extra with Stealth Plugin. This server enables LLMs to interact with web pages in a way that better emulates human behavior and avoids detection as automation.
  4 | 
  5 | ## Features
  6 | 
  7 | - Enhanced browser automation with Puppeteer-Extra
  8 | - Stealth mode to avoid bot detection
  9 | - Screenshot capabilities for pages and elements
 10 | - Console logging and JavaScript execution
 11 | - Full suite of interaction methods (click, fill, select, hover)
 12 | 
 13 | ## Components
 14 | 
 15 | ### Tools
 16 | 
 17 | - **puppeteer_navigate**
 18 |   - Navigate to any URL in the browser
 19 |   - Input: `url` (string)
 20 | 
 21 | - **puppeteer_screenshot**
 22 |   - Capture screenshots of the entire page or specific elements
 23 |   - Inputs:
 24 |     - `name` (string, required): Name for the screenshot
 25 |     - `selector` (string, optional): CSS selector for element to screenshot
 26 |     - `width` (number, optional, default: 800): Screenshot width
 27 |     - `height` (number, optional, default: 600): Screenshot height
 28 | 
 29 | - **puppeteer_click**
 30 |   - Click elements on the page
 31 |   - Input: `selector` (string): CSS selector for element to click
 32 | 
 33 | - **puppeteer_hover**
 34 |   - Hover elements on the page
 35 |   - Input: `selector` (string): CSS selector for element to hover
 36 | 
 37 | - **puppeteer_fill**
 38 |   - Fill out input fields
 39 |   - Inputs:
 40 |     - `selector` (string): CSS selector for input field
 41 |     - `value` (string): Value to fill
 42 | 
 43 | - **puppeteer_select**
 44 |   - Select an element with SELECT tag
 45 |   - Inputs:
 46 |     - `selector` (string): CSS selector for element to select
 47 |     - `value` (string): Value to select
 48 | 
 49 | - **puppeteer_evaluate**
 50 |   - Execute JavaScript in the browser console
 51 |   - Input: `script` (string): JavaScript code to execute
 52 | 
 53 | ### Resources
 54 | 
 55 | The server provides access to two types of resources:
 56 | 
 57 | 1. **Console Logs** (`console://logs`)
 58 |    - Browser console output in text format
 59 |    - Includes all console messages from the browser
 60 | 
 61 | 2. **Screenshots** (`screenshot://<name>`)
 62 |    - PNG images of captured screenshots
 63 |    - Accessible via the screenshot name specified during capture
 64 | 
 65 | ## Development
 66 | 
 67 | ### Installation
 68 | 
 69 | ```bash
 70 | # Clone the repository
 71 | git clone <repository-url>
 72 | cd puppeteer_extra
 73 | 
 74 | # Install dependencies
 75 | npm install
 76 | 
 77 | # Copy environment file
 78 | cp .env.example .env.development
 79 | ```
 80 | 
 81 | ### Running Locally
 82 | 
 83 | ```bash
 84 | # Development mode (non-headless browser)
 85 | npm run dev
 86 | 
 87 | # Production mode (headless browser)
 88 | npm run prod
 89 | ```
 90 | 
 91 | ### Building
 92 | 
 93 | ```bash
 94 | npm run build
 95 | ```
 96 | 
 97 | ## Docker
 98 | 
 99 | ### Building the Docker Image
100 | 
101 | ```bash
102 | docker build -t mcp/puppeteer-extra .
103 | ```
104 | 
105 | ### Running with Docker
106 | 
107 | ```bash
108 | docker run -i --rm --init -e DOCKER_CONTAINER=true mcp/puppeteer-extra
109 | ```
110 | 
111 | ## Configuration for Claude Desktop
112 | 
113 | ### Docker
114 | 
115 | ```json
116 | {
117 |   "mcpServers": {
118 |     "puppeteer": {
119 |       "command": "docker",
120 |       "args": ["run", "-i", "--rm", "--init", "-e", "DOCKER_CONTAINER=true", "mcp/puppeteer-extra"]
121 |     }
122 |   }
123 | }
124 | ```
125 | 
126 | ### NPX
127 | 
128 | ```json
129 | {
130 |   "mcpServers": {
131 |     "puppeteer": {
132 |       "command": "npx",
133 |       "args": ["-y", "MCP_puppeteer_extra"]
134 |     }
135 |   }
136 | }
137 | ```
138 | 
139 | ## License
140 | 
141 | This MCP server is licensed under the MIT License.
142 | 
```

--------------------------------------------------------------------------------
/src/types/screenshot.ts:
--------------------------------------------------------------------------------

```typescript
1 | export interface Screenshot {
2 |   name: string;
3 |   data: string;
4 |   timestamp: Date;
5 | }
6 | 
```

--------------------------------------------------------------------------------
/nodemon.json:
--------------------------------------------------------------------------------

```json
1 | {
2 |   "watch": [
3 |     "src"
4 |   ],
5 |   "ext": ".ts,.js",
6 |   "ignore": [],
7 |   "exec": "ts-node -r tsconfig-paths/register ./src/index.ts"
8 | }
```

--------------------------------------------------------------------------------
/src/endpoints/index.ts:
--------------------------------------------------------------------------------

```typescript
1 | import './navigate';
2 | import './screenshot';
3 | import './click';
4 | import './fill';
5 | import './select';
6 | import './hover';
7 | import './evaluate';
8 | 
```

--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------

```javascript
 1 | module.exports = {
 2 |   preset: 'ts-jest',
 3 |   testEnvironment: 'node',
 4 |   transform: {
 5 |     '^.+\\.ts$': 'ts-jest',
 6 |   },
 7 |   transformIgnorePatterns: ['<rootDir>/node_modules/'],
 8 |   moduleFileExtensions: ['ts', 'js', 'json', 'node'],
 9 |   moduleNameMapper: {
10 |     '@/(.*)': '<rootDir>/src/$1',
11 |   },
12 | };
13 | 
```

--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "editor.formatOnSave": true,
 3 |   "editor.codeActionsOnSave": {
 4 |     "source.fixAll.eslint": "explicit",
 5 |     "source.organizeImports": "explicit"
 6 |   },
 7 |   "eslint.validate": ["typescript"],
 8 |   "[typescript]": {
 9 |     "editor.defaultFormatter": "esbenp.prettier-vscode"
10 |   },
11 |   "typescript.preferences.importModuleSpecifier": "non-relative",
12 |   "javascript.preferences.importModuleSpecifier": "non-relative"
13 | }
14 | 
```

--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "compilerOptions": {
 3 |     "target": "ES2020",
 4 |     "module": "Node16",
 5 |     "lib": ["ES2020", "dom"],
 6 |     "outDir": "./dist",
 7 |     "rootDir": "./src",
 8 |     "strict": true,
 9 |     "esModuleInterop": true,
10 |     "skipLibCheck": true,
11 |     "forceConsistentCasingInFileNames": true,
12 |     "moduleResolution": "node16",
13 |     "resolveJsonModule": true,
14 |     "baseUrl": "./src",
15 |     "paths": {
16 |       "@/*": ["*"],
17 |       "@utils/*": ["utils/*"],
18 |       "@class/*": ["class/*"],
19 |       "@types/*": ["types/*"]
20 |     }
21 |   },
22 |   "include": ["src/**/*"],
23 |   "exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.spec.ts", "src/**/__tests__/**"]
24 | }
25 | 
```

--------------------------------------------------------------------------------
/src/utils/notificationUtil.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
 2 | 
 3 | // Singleton for managing server reference
 4 | class NotificationManager {
 5 |   private server: McpServer | null = null;
 6 | 
 7 |   setServer(server: McpServer): void {
 8 |     this.server = server;
 9 |   }
10 | 
11 |   /**
12 |    * Send a resource list changed notification if connected
13 |    */
14 |   resourcesChanged(): void {
15 |     if (!this.server) return;
16 | 
17 |     try {
18 |       this.server.server.notification({
19 |         method: 'notifications/resources/list_changed',
20 |       });
21 |     } catch (error) {
22 |       console.error('Failed to send resource change notification:', error);
23 |     }
24 |   }
25 | }
26 | 
27 | // Export singleton instance
28 | export const notificationManager = new NotificationManager();
29 | 
```

--------------------------------------------------------------------------------
/src/endpoints/hover.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { mcpServer } from '@/index';
 2 | import z from 'zod';
 3 | import { getBrowser } from '@/utils/browserManager';
 4 | 
 5 | mcpServer.tool(
 6 |   'puppeteer_hover',
 7 |   'Hover an element on the page',
 8 |   {
 9 |     selector: z.string().describe('CSS selector for element to hover'),
10 |   },
11 |   async ({ selector }) => {
12 |     const browser = getBrowser();
13 | 
14 |     try {
15 |       await browser.hover(selector);
16 | 
17 |       return {
18 |         content: [
19 |           {
20 |             type: 'text',
21 |             text: `Hovered ${selector}`,
22 |           },
23 |         ],
24 |       };
25 |     } catch (error) {
26 |       return {
27 |         content: [
28 |           {
29 |             type: 'text',
30 |             text: `Failed to hover ${selector}: ${(error as Error).message}`,
31 |           },
32 |         ],
33 |         isError: true,
34 |       };
35 |     }
36 |   },
37 | );
38 | 
```

--------------------------------------------------------------------------------
/src/endpoints/click.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { mcpServer } from '@/index';
 2 | import z from 'zod';
 3 | import { getBrowser } from '@/utils/browserManager';
 4 | 
 5 | mcpServer.tool(
 6 |   'puppeteer_click',
 7 |   'Click an element on the page',
 8 |   {
 9 |     selector: z.string().describe('CSS selector for element to click'),
10 |   },
11 |   async ({ selector }) => {
12 |     const browser = getBrowser();
13 | 
14 |     try {
15 |       await browser.click(selector);
16 | 
17 |       return {
18 |         content: [
19 |           {
20 |             type: 'text',
21 |             text: `Clicked: ${selector}`,
22 |           },
23 |         ],
24 |       };
25 |     } catch (error) {
26 |       return {
27 |         content: [
28 |           {
29 |             type: 'text',
30 |             text: `Failed to click ${selector}: ${(error as Error).message}`,
31 |           },
32 |         ],
33 |         isError: true,
34 |       };
35 |     }
36 |   },
37 | );
38 | 
```

--------------------------------------------------------------------------------
/src/utils/browserManager.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import PuppeteerBrowser from '@/class/browser.class';
 2 | 
 3 | // Singleton instance of the browser
 4 | let browserInstance: PuppeteerBrowser;
 5 | 
 6 | /**
 7 |  * Gets the singleton instance of the browser
 8 |  */
 9 | export function getBrowser(): PuppeteerBrowser {
10 |   if (!browserInstance) {
11 |     browserInstance = new PuppeteerBrowser();
12 |   }
13 |   return browserInstance;
14 | }
15 | 
16 | /**
17 |  * Initialize the browser
18 |  * @param headless Whether to run in headless mode
19 |  */
20 | export async function initBrowser(headless: boolean = true): Promise<void> {
21 |   const browser = getBrowser();
22 |   await browser.init(headless);
23 | }
24 | 
25 | /**
26 |  * Close the browser instance
27 |  */
28 | export async function closeBrowser(): Promise<void> {
29 |   if (browserInstance) {
30 |     await browserInstance.close();
31 |     browserInstance = undefined as any;
32 |   }
33 | }
34 | 
```

--------------------------------------------------------------------------------
/src/endpoints/navigate.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { mcpServer } from '@/index';
 2 | import z from 'zod';
 3 | import { getBrowser } from '@/utils/browserManager';
 4 | 
 5 | mcpServer.tool(
 6 |   'puppeteer_navigate',
 7 |   'Navigate to a URL',
 8 |   {
 9 |     url: z.string().describe('URL to navigate to'),
10 |   },
11 |   async ({ url }) => {
12 |     const browser = getBrowser();
13 |     const page = await browser.getPage();
14 | 
15 |     try {
16 |       await browser.navigate(url);
17 | 
18 |       return {
19 |         content: [
20 |           {
21 |             type: 'text',
22 |             text: `Navigated to ${url}`,
23 |           },
24 |         ],
25 |       };
26 |     } catch (error) {
27 |       return {
28 |         content: [
29 |           {
30 |             type: 'text',
31 |             text: `Failed to navigate to ${url}: ${(error as Error).message}`,
32 |           },
33 |         ],
34 |         isError: true,
35 |       };
36 |     }
37 |   },
38 | );
39 | 
```

--------------------------------------------------------------------------------
/src/endpoints/fill.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { mcpServer } from '@/index';
 2 | import z from 'zod';
 3 | import { getBrowser } from '@/utils/browserManager';
 4 | 
 5 | mcpServer.tool(
 6 |   'puppeteer_fill',
 7 |   'Fill out an input field',
 8 |   {
 9 |     selector: z.string().describe('CSS selector for input field'),
10 |     value: z.string().describe('Value to fill'),
11 |   },
12 |   async ({ selector, value }) => {
13 |     const browser = getBrowser();
14 | 
15 |     try {
16 |       await browser.fill(selector, value);
17 | 
18 |       return {
19 |         content: [
20 |           {
21 |             type: 'text',
22 |             text: `Filled ${selector} with: ${value}`,
23 |           },
24 |         ],
25 |       };
26 |     } catch (error) {
27 |       return {
28 |         content: [
29 |           {
30 |             type: 'text',
31 |             text: `Failed to fill ${selector}: ${(error as Error).message}`,
32 |           },
33 |         ],
34 |         isError: true,
35 |       };
36 |     }
37 |   },
38 | );
39 | 
```

--------------------------------------------------------------------------------
/src/endpoints/select.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { mcpServer } from '@/index';
 2 | import z from 'zod';
 3 | import { getBrowser } from '@/utils/browserManager';
 4 | 
 5 | mcpServer.tool(
 6 |   'puppeteer_select',
 7 |   'Select an element on the page with Select tag',
 8 |   {
 9 |     selector: z.string().describe('CSS selector for element to select'),
10 |     value: z.string().describe('Value to select'),
11 |   },
12 |   async ({ selector, value }) => {
13 |     const browser = getBrowser();
14 | 
15 |     try {
16 |       await browser.select(selector, value);
17 | 
18 |       return {
19 |         content: [
20 |           {
21 |             type: 'text',
22 |             text: `Selected ${selector} with: ${value}`,
23 |           },
24 |         ],
25 |       };
26 |     } catch (error) {
27 |       return {
28 |         content: [
29 |           {
30 |             type: 'text',
31 |             text: `Failed to select ${selector}: ${(error as Error).message}`,
32 |           },
33 |         ],
34 |         isError: true,
35 |       };
36 |     }
37 |   },
38 | );
39 | 
```

--------------------------------------------------------------------------------
/src/endpoints/evaluate.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { mcpServer } from '@/index';
 2 | import z from 'zod';
 3 | import { getBrowser } from '@/utils/browserManager';
 4 | 
 5 | mcpServer.tool(
 6 |   'puppeteer_evaluate',
 7 |   'Execute JavaScript in the browser console',
 8 |   {
 9 |     script: z.string().describe('JavaScript code to execute'),
10 |   },
11 |   async ({ script }) => {
12 |     const browser = getBrowser();
13 | 
14 |     try {
15 |       const { result, logs } = await browser.evaluate(script);
16 | 
17 |       return {
18 |         content: [
19 |           {
20 |             type: 'text',
21 |             text: `Execution result:\n${JSON.stringify(
22 |               result,
23 |               null,
24 |               2,
25 |             )}\n\nConsole output:\n${logs.join('\n')}`,
26 |           },
27 |         ],
28 |       };
29 |     } catch (error) {
30 |       return {
31 |         content: [
32 |           {
33 |             type: 'text',
34 |             text: `Script execution failed: ${(error as Error).message}`,
35 |           },
36 |         ],
37 |         isError: true,
38 |       };
39 |     }
40 |   },
41 | );
42 | 
```

--------------------------------------------------------------------------------
/scripts/setup-husky.js:
--------------------------------------------------------------------------------

```javascript
 1 | const fs = require('fs');
 2 | const path = require('path');
 3 | 
 4 | const huskyDir = path.join(__dirname, '..', '.husky');
 5 | const preCommitFile = path.join(huskyDir, 'pre-commit');
 6 | 
 7 | // Create .husky directory if it doesn't exist
 8 | if (!fs.existsSync(huskyDir)) {
 9 |   fs.mkdirSync(huskyDir, { recursive: true });
10 | }
11 | 
12 | // Create pre-commit hook with correct line endings for the platform
13 | const preCommitContent = `#!/usr/bin/env sh
14 | . "$(dirname -- "$0")/_/husky.sh"
15 | 
16 | # Run lint-staged for code quality checks
17 | npx lint-staged
18 | 
19 | # Run tests only for affected files
20 | npx ts-node scripts/run-relevant-tests.ts
21 | `;
22 | 
23 | // Write the file with platform-specific line endings
24 | fs.writeFileSync(preCommitFile, preCommitContent.replace(/\n/g, require('os').EOL));
25 | 
26 | // Make the pre-commit hook executable (Unix only)
27 | if (process.platform !== 'win32') {
28 |   fs.chmodSync(preCommitFile, '755');
29 | }
30 | 
31 | console.log('✅ Husky pre-commit hook has been configured successfully');
32 | 
```

--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------

```dockerfile
 1 | FROM node:20-bookworm-slim
 2 | 
 3 | # Set environment variables
 4 | ENV DEBIAN_FRONTEND=noninteractive
 5 | ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true
 6 | ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium
 7 | ENV NODE_ENV=production
 8 | ENV DOCKER_CONTAINER=true
 9 | ENV HEADLESS=true
10 | 
11 | # Install dependencies for Chromium and Puppeteer
12 | RUN apt-get update && \
13 |     apt-get install -y wget gnupg && \
14 |     apt-get install -y fonts-ipafont-gothic fonts-wqy-zenhei fonts-thai-tlwg fonts-kacst fonts-freefont-ttf && \
15 |     apt-get install -y libxss1 libgtk2.0-0 libnss3 libatk-bridge2.0-0 libdrm2 libxkbcommon0 libgbm1 libasound2 && \
16 |     apt-get install -y chromium && \
17 |     apt-get clean && \
18 |     rm -rf /var/lib/apt/lists/*
19 | 
20 | # Create app directory
21 | WORKDIR /app
22 | 
23 | # Copy package files and install dependencies
24 | COPY package*.json ./
25 | RUN npm ci
26 | 
27 | # Copy app source
28 | COPY . .
29 | 
30 | # Build the TypeScript project
31 | RUN npm run build
32 | 
33 | # Set executable permissions on the entry point
34 | RUN chmod +x dist/index.js
35 | 
36 | # Set the entry point
37 | ENTRYPOINT ["node", "dist/index.js"]
38 | 
```

--------------------------------------------------------------------------------
/src/utils/screenshotManager.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { Screenshot } from '@/types/screenshot';
 2 | import { notificationManager } from './notificationUtil';
 3 | 
 4 | /**
 5 |  * Class to manage screenshot storage
 6 |  */
 7 | class ScreenshotStore {
 8 |   private screenshots: Map<string, Screenshot> = new Map();
 9 | 
10 |   /**
11 |    * Save a screenshot to the store
12 |    */
13 |   save(name: string, data: string, notifyChange: boolean = true): void {
14 |     this.screenshots.set(name, {
15 |       name,
16 |       data,
17 |       timestamp: new Date(),
18 |     });
19 | 
20 |     // When a screenshot is saved, notify that resources have changed
21 |     if (notifyChange) {
22 |       notificationManager.resourcesChanged();
23 |     }
24 |   }
25 | 
26 |   /**
27 |    * Get a screenshot by name
28 |    */
29 |   get(name: string): Screenshot | undefined {
30 |     return this.screenshots.get(name);
31 |   }
32 | 
33 |   /**
34 |    * Get all screenshot names
35 |    */
36 |   getAll(): Screenshot[] {
37 |     return Array.from(this.screenshots.values());
38 |   }
39 | 
40 |   /**
41 |    * Check if a screenshot exists
42 |    */
43 |   exists(name: string): boolean {
44 |     return this.screenshots.has(name);
45 |   }
46 | 
47 |   /**
48 |    * Get the count of screenshots
49 |    */
50 |   count(): number {
51 |     return this.screenshots.size;
52 |   }
53 | 
54 |   /**
55 |    * Delete a screenshot
56 |    */
57 |   delete(name: string): boolean {
58 |     return this.screenshots.delete(name);
59 |   }
60 | 
61 |   /**
62 |    * Clear all screenshots
63 |    */
64 |   clear(): void {
65 |     this.screenshots.clear();
66 |   }
67 | }
68 | 
69 | // Export a singleton instance
70 | export const screenshotStore = new ScreenshotStore();
71 | 
```

--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "name": "MCP_puppeteer_extra",
 3 |   "version": "1.0.0",
 4 |   "author": "Gpaul | Faldin",
 5 |   "scripts": {
 6 |     "start": "cross-env NODE_ENV=production node -r tsconfig-paths/register dist/index.js",
 7 |     "dev": "cross-env NODE_ENV=development nodemon -r tsconfig-paths/register src/index.ts",
 8 |     "build": "tsc --project tsconfig.json && tscpaths -p tsconfig.json -s ./src -o ./dist",
 9 |     "lint": "eslint . --ext .ts",
10 |     "lint:fix": "eslint . --ext .ts --fix",
11 |     "format": "prettier --write \"src/**/*.ts\"",
12 |     "format:check": "prettier --check \"src/**/*.ts\"",
13 |     "prepare": "husky install && node ./scripts/setup-husky.js",
14 |     "prod": "npm run build && npm run start",
15 |     "test": "jest"
16 |   },
17 |   "dependencies": {
18 |     "@modelcontextprotocol/sdk": "^1.0.1",
19 |     "dotenv": "^16.0.3",
20 |     "puppeteer": "^23.4.0",
21 |     "puppeteer-extra": "^3.3.6",
22 |     "puppeteer-extra-plugin-stealth": "^2.11.2",
23 |     "zod": "^3.22.4"
24 |   },
25 |   "devDependencies": {
26 |     "@types/jest": "^29.5.14",
27 |     "@types/node": "^18.15.11",
28 |     "@typescript-eslint/eslint-plugin": "^6.13.0",
29 |     "@typescript-eslint/parser": "^6.13.0",
30 |     "cross-env": "^7.0.3",
31 |     "eslint": "^8.37.0",
32 |     "eslint-config-prettier": "^8.8.0",
33 |     "eslint-import-resolver-node": "^0.3.9",
34 |     "eslint-import-resolver-typescript": "^3.6.1",
35 |     "eslint-plugin-import": "^2.29.1",
36 |     "eslint-plugin-prettier": "^4.2.1",
37 |     "husky": "^8.0.3",
38 |     "jest": "^29.7.0",
39 |     "lint-staged": "^13.2.1",
40 |     "nodemon": "^2.0.22",
41 |     "prettier": "^2.8.7",
42 |     "ts-jest": "^29.2.6",
43 |     "ts-node": "^10.9.1",
44 |     "tsconfig-paths": "^4.2.0",
45 |     "tscpaths": "^0.0.9",
46 |     "typescript": "~5.1.6"
47 |   },
48 |   "description": "An extension of Puppeteer with puppeteer-extra and stealth mode for web automation"
49 | }
```

--------------------------------------------------------------------------------
/src/endpoints/screenshot.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { mcpServer } from '@/index';
 2 | import z from 'zod';
 3 | import { getBrowser } from '@/utils/browserManager';
 4 | import { screenshotStore } from '@/utils/screenshotManager';
 5 | 
 6 | mcpServer.tool(
 7 |   'puppeteer_screenshot',
 8 |   'Take a screenshot of the current page or a specific element',
 9 |   {
10 |     name: z.string().describe('Name for the screenshot'),
11 |     selector: z.string().optional().describe('CSS selector for element to screenshot'),
12 |     width: z.number().optional().describe('Width in pixels (default: 800)'),
13 |     height: z.number().optional().describe('Height in pixels (default: 600)'),
14 |   },
15 |   async ({ name, selector, width = 800, height = 600 }) => {
16 |     const browser = getBrowser();
17 |     const page = await browser.getPage();
18 | 
19 |     try {
20 |       // Set viewport dimensions
21 |       await page.setViewport({ width, height });
22 | 
23 |       // Take the screenshot
24 |       const screenshot = await browser.takeScreenshot(selector);
25 | 
26 |       if (!screenshot) {
27 |         return {
28 |           content: [
29 |             {
30 |               type: 'text',
31 |               text: selector
32 |                 ? `Element not found or could not capture: ${selector}`
33 |                 : 'Failed to take screenshot',
34 |             },
35 |           ],
36 |           isError: true,
37 |         };
38 |       }
39 | 
40 |       // Store the screenshot and trigger a resource list change notification
41 |       screenshotStore.save(name, screenshot, true);
42 | 
43 |       return {
44 |         content: [
45 |           {
46 |             type: 'text',
47 |             text: `Screenshot '${name}' taken at ${width}x${height}`,
48 |           },
49 |           {
50 |             type: 'image',
51 |             data: screenshot,
52 |             mimeType: 'image/png',
53 |           },
54 |         ],
55 |       };
56 |     } catch (error) {
57 |       return {
58 |         content: [
59 |           {
60 |             type: 'text',
61 |             text: `Failed to take screenshot: ${(error as Error).message}`,
62 |           },
63 |         ],
64 |         isError: true,
65 |       };
66 |     }
67 |   },
68 | );
69 | 
```

--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { closeBrowser, getBrowser, initBrowser } from '@/utils/browserManager';
  2 | import { notificationManager } from '@/utils/notificationUtil';
  3 | import { screenshotStore } from '@/utils/screenshotManager';
  4 | import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
  5 | import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
  6 | import {
  7 |   ListResourcesRequestSchema,
  8 |   ReadResourceRequestSchema,
  9 | } from '@modelcontextprotocol/sdk/types.js';
 10 | 
 11 | // Configure environment variables
 12 | // const __filename = fileURLToPath(import.meta.url);
 13 | // const __dirname = path.dirname(__filename);
 14 | // const envFile = process.env.NODE_ENV === 'production' ? '.env.production' : '.env.development';
 15 | 
 16 | // config({
 17 | //   path: path.resolve(__dirname, '..', envFile),
 18 | // });
 19 | 
 20 | process.env.HEADLESS = 'false';
 21 | 
 22 | // Create MCP server instance
 23 | export const mcpServer = new McpServer(
 24 |   {
 25 |     name: 'puppeteer-extra-stealth',
 26 |     version: '1.0.0',
 27 |   },
 28 |   {
 29 |     capabilities: {
 30 |       resources: {},
 31 |       tools: {},
 32 |     },
 33 |   },
 34 | );
 35 | 
 36 | // Set the server in notification manager
 37 | notificationManager.setServer(mcpServer);
 38 | 
 39 | // Set up resource handlers
 40 | mcpServer.server.setRequestHandler(ListResourcesRequestSchema, async () => ({
 41 |   resources: [
 42 |     {
 43 |       uri: 'console://logs',
 44 |       mimeType: 'text/plain',
 45 |       name: 'Browser console logs',
 46 |     },
 47 |     ...Array.from(screenshotStore.getAll()).map(screenshot => ({
 48 |       uri: `screenshot://${screenshot.name}`,
 49 |       mimeType: 'image/png',
 50 |       name: `Screenshot: ${screenshot.name}`,
 51 |     })),
 52 |   ],
 53 | }));
 54 | 
 55 | // Set up resource reading for screenshots
 56 | mcpServer.server.setRequestHandler(ReadResourceRequestSchema, async request => {
 57 |   const uri = request.params.uri.toString();
 58 | 
 59 |   // Handle console logs
 60 |   if (uri === 'console://logs') {
 61 |     const browser = getBrowser();
 62 |     return {
 63 |       contents: [
 64 |         {
 65 |           uri,
 66 |           mimeType: 'text/plain',
 67 |           text: browser.getConsoleLogs().join('\n'),
 68 |         },
 69 |       ],
 70 |     };
 71 |   }
 72 | 
 73 |   // Handle screenshots
 74 |   if (uri.startsWith('screenshot://')) {
 75 |     const name = uri.replace('screenshot://', '');
 76 |     const screenshot = screenshotStore.get(name);
 77 | 
 78 |     if (screenshot) {
 79 |       return {
 80 |         contents: [
 81 |           {
 82 |             uri,
 83 |             mimeType: 'image/png',
 84 |             blob: screenshot.data,
 85 |           },
 86 |         ],
 87 |       };
 88 |     }
 89 |   }
 90 | 
 91 |   throw new Error(`Resource not found: ${uri}`);
 92 | });
 93 | 
 94 | // Import all tool endpoints
 95 | import './endpoints';
 96 | 
 97 | // Main function to start the server
 98 | async function main() {
 99 |   try {
100 |     // Initialize browser with stealth mode
101 |     const isHeadless = process.env.HEADLESS !== 'false';
102 |     await initBrowser(isHeadless);
103 | 
104 |     // Connect to the transport layer
105 |     const transport = new StdioServerTransport();
106 |     await mcpServer.connect(transport);
107 | 
108 |     // Now that we're connected, we can send notifications
109 |     notificationManager.resourcesChanged();
110 | 
111 |     if (process.env.NODE_ENV === 'development') {
112 |       console.error('Puppeteer-Extra MCP Server started in development mode');
113 |       console.error(`Headless mode: ${isHeadless ? 'enabled' : 'disabled'}`);
114 |     }
115 |   } catch (error) {
116 |     console.error('Failed to start MCP server:', error);
117 |     process.exit(1);
118 |   }
119 | }
120 | 
121 | // Start the server
122 | main().catch(error => {
123 |   console.error('Fatal error:', error);
124 |   process.exit(1);
125 | });
126 | 
127 | // Handle graceful shutdown
128 | const shutdown = async () => {
129 |   try {
130 |     console.error('Shutting down Puppeteer-Extra MCP Server...');
131 |     await closeBrowser();
132 |     await mcpServer.close();
133 |     process.exit(0);
134 |   } catch (error) {
135 |     console.error('Error during shutdown:', error);
136 |     process.exit(1);
137 |   }
138 | };
139 | 
140 | // Listen for termination signals
141 | process.on('SIGINT', shutdown);
142 | process.on('SIGTERM', shutdown);
143 | process.on('exit', () => {
144 |   console.error('Puppeteer-Extra MCP Server exited');
145 | });
146 | 
147 | // Handle errors on stdin which likely means the parent process closed
148 | process.stdin.on('error', () => {
149 |   console.error('stdin error - parent process likely closed');
150 |   shutdown();
151 | });
152 | 
153 | // Handle stdin close which means the parent process closed
154 | process.stdin.on('close', () => {
155 |   console.error('stdin closed - parent process likely closed');
156 |   shutdown();
157 | });
158 | 
```

--------------------------------------------------------------------------------
/scripts/run-relevant-tests.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { execSync } from 'child_process';
  2 | import * as fs from 'fs';
  3 | import * as path from 'path';
  4 | 
  5 | // Get modified files in the current commit
  6 | const getModifiedFiles = (): string[] => {
  7 |   try {
  8 |     const gitOutput = execSync('git diff --cached --name-only --diff-filter=ACMR').toString();
  9 |     return gitOutput.split('\n').filter(Boolean);
 10 |   } catch (error) {
 11 |     console.error('Error getting modified files:', error);
 12 |     return [];
 13 |   }
 14 | };
 15 | 
 16 | // Check if any source files have been modified
 17 | const hasSourceChanges = (files: string[]): boolean => {
 18 |   return files.some(
 19 |     file =>
 20 |       file.startsWith('src/') &&
 21 |       file.endsWith('.ts') &&
 22 |       !file.endsWith('.d.ts') &&
 23 |       !file.endsWith('.test.ts') &&
 24 |       !file.endsWith('.spec.ts'),
 25 |   );
 26 | };
 27 | 
 28 | // Check if any test files have been modified
 29 | const hasTestChanges = (files: string[]): boolean => {
 30 |   return files.some(
 31 |     file => file.endsWith('.test.ts') || file.endsWith('.spec.ts') || file.includes('/__tests__/'),
 32 |   );
 33 | };
 34 | 
 35 | // Find corresponding test files for changed source files
 36 | const findAffectedTests = (files: string[]): string[] => {
 37 |   const affectedTests: string[] = [];
 38 | 
 39 |   for (const file of files) {
 40 |     // Normalize path separators to forward slashes for consistency
 41 |     const normalizedFile = file.replace(/\\/g, '/');
 42 | 
 43 |     if (
 44 |       normalizedFile.startsWith('src/') &&
 45 |       normalizedFile.endsWith('.ts') &&
 46 |       !normalizedFile.endsWith('.test.ts') &&
 47 |       !normalizedFile.endsWith('.spec.ts')
 48 |     ) {
 49 |       const baseName = path.basename(normalizedFile, '.ts');
 50 |       const dirName = path.dirname(normalizedFile);
 51 | 
 52 |       // Check for component.test.ts pattern
 53 |       const potentialTest1 = path.join(dirName, `${baseName}.test.ts`).replace(/\\/g, '/');
 54 |       if (fs.existsSync(potentialTest1)) {
 55 |         console.log(`Found test file: ${potentialTest1}`);
 56 |         affectedTests.push(potentialTest1);
 57 |       }
 58 | 
 59 |       // Check for component.spec.ts pattern
 60 |       const potentialTest2 = path.join(dirName, `${baseName}.spec.ts`).replace(/\\/g, '/');
 61 |       if (fs.existsSync(potentialTest2)) {
 62 |         console.log(`Found test file: ${potentialTest2}`);
 63 |         affectedTests.push(potentialTest2);
 64 |       }
 65 | 
 66 |       // Check for __tests__ directory
 67 |       const potentialTestDir = path.join(dirName, '__tests__').replace(/\\/g, '/');
 68 |       if (fs.existsSync(potentialTestDir)) {
 69 |         const testDirFiles = fs.readdirSync(potentialTestDir);
 70 |         const matchingTests = testDirFiles.filter(
 71 |           testFile =>
 72 |             testFile.includes(baseName) &&
 73 |             (testFile.endsWith('.test.ts') || testFile.endsWith('.spec.ts')),
 74 |         );
 75 |         matchingTests.forEach(test => {
 76 |           const testPath = path.join(potentialTestDir, test).replace(/\\/g, '/');
 77 |           console.log(`Found test file: ${testPath}`);
 78 |           affectedTests.push(testPath);
 79 |         });
 80 |       }
 81 |     }
 82 |   }
 83 | 
 84 |   return affectedTests;
 85 | };
 86 | 
 87 | // Main function to decide and run tests
 88 | const runRelevantTests = (): void => {
 89 |   const modifiedFiles = getModifiedFiles();
 90 | 
 91 |   if (modifiedFiles.length === 0) {
 92 |     console.log('No modified files detected, skipping tests');
 93 |     process.exit(0);
 94 |   }
 95 | 
 96 |   // If any test files have been changed, run those specific tests
 97 |   if (hasTestChanges(modifiedFiles)) {
 98 |     const testFiles = modifiedFiles.filter(
 99 |       file =>
100 |         file.endsWith('.test.ts') || file.endsWith('.spec.ts') || file.includes('/__tests__/'),
101 |     );
102 | 
103 |     console.log('Test files have been modified, running only those tests...');
104 |     try {
105 |       // Run only the modified test files
106 |       execSync(`npx jest ${testFiles.join(' ')} --passWithNoTests`, { stdio: 'inherit' });
107 |     } catch (error) {
108 |       process.exit(1); // Exit with error code if tests fail
109 |     }
110 |     process.exit(0);
111 |   }
112 | 
113 |   // If source files have been changed, find and run affected tests
114 |   if (hasSourceChanges(modifiedFiles)) {
115 |     console.log(
116 |       'Modified source files:',
117 |       modifiedFiles.filter(
118 |         file =>
119 |           file.startsWith('src/') &&
120 |           file.endsWith('.ts') &&
121 |           !file.endsWith('.test.ts') &&
122 |           !file.endsWith('.spec.ts'),
123 |       ),
124 |     );
125 | 
126 |     const affectedTests = findAffectedTests(modifiedFiles);
127 | 
128 |     if (affectedTests.length > 0) {
129 |       console.log(
130 |         'Source files with corresponding tests have been modified, running affected tests...',
131 |       );
132 |       console.log('Running tests:', affectedTests);
133 | 
134 |       try {
135 |         // Use relative paths for Jest instead of absolute paths
136 |         const relativeTests = affectedTests.map(file => file.replace(/\\/g, '/'));
137 |         console.log(`Running Jest command: npx jest ${relativeTests.join(' ')} --passWithNoTests`);
138 |         execSync(`npx jest ${relativeTests.join(' ')} --passWithNoTests`, { stdio: 'inherit' });
139 |       } catch (error) {
140 |         console.error('Jest execution failed:', error);
141 |         process.exit(1); // Exit with error code if tests fail
142 |       }
143 |     } else {
144 |       console.log('No corresponding tests found for modified source files');
145 |     }
146 |     process.exit(0);
147 |   }
148 | 
149 |   console.log('No relevant source or test files changed, skipping tests');
150 |   process.exit(0);
151 | };
152 | 
153 | // Run the script
154 | runRelevantTests();
155 | 
```

--------------------------------------------------------------------------------
/src/class/browser.class.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { Browser, LaunchOptions, Page } from 'puppeteer';
  2 | import puppeteer from 'puppeteer-extra';
  3 | import StealthPlugin from 'puppeteer-extra-plugin-stealth';
  4 | 
  5 | // Add stealth plugin to puppeteer-extra
  6 | puppeteer.use(StealthPlugin());
  7 | 
  8 | /**
  9 |  * PuppeteerBrowser class for managing browser instances and pages
 10 |  */
 11 | export default class PuppeteerBrowser {
 12 |   private browser: Browser | null = null;
 13 |   private page: Page | null = null;
 14 |   private consoleLogs: string[] = [];
 15 | 
 16 |   /**
 17 |    * Initializes a new browser session with stealth mode
 18 |    */
 19 |   async init(headless: boolean = true) {
 20 |     try {
 21 |       const launchOptions: LaunchOptions = {
 22 |         headless: headless ? true : false,
 23 |         args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage'],
 24 |       };
 25 | 
 26 |       // Handle Docker-specific needs
 27 |       if (process.env.DOCKER_CONTAINER === 'true') {
 28 |         launchOptions.args?.push('--single-process', '--no-zygote');
 29 |       }
 30 | 
 31 |       this.browser = await puppeteer.launch(launchOptions);
 32 |       const pages = await this.browser.pages();
 33 |       this.page = pages[0] || (await this.browser.newPage());
 34 | 
 35 |       // Set up console logging
 36 |       this.page.on('console', msg => {
 37 |         const logEntry = `[${msg.type()}] ${msg.text()}`;
 38 |         this.consoleLogs.push(logEntry);
 39 |       });
 40 | 
 41 |       return this.page;
 42 |     } catch (error) {
 43 |       console.error('Failed to initialize browser:', error);
 44 |       throw error;
 45 |     }
 46 |   }
 47 | 
 48 |   /**
 49 |    * Returns the current page or creates a new one if none exists
 50 |    */
 51 |   async getPage(): Promise<Page> {
 52 |     if (!this.browser) {
 53 |       await this.init();
 54 |     }
 55 | 
 56 |     if (!this.page) {
 57 |       this.page = await this.browser!.newPage();
 58 | 
 59 |       // Set up console logging for the new page
 60 |       this.page.on('console', msg => {
 61 |         const logEntry = `[${msg.type()}] ${msg.text()}`;
 62 |         this.consoleLogs.push(logEntry);
 63 |       });
 64 |     }
 65 | 
 66 |     return this.page;
 67 |   }
 68 | 
 69 |   /**
 70 |    * Takes a screenshot of the current page or a specific element
 71 |    */
 72 |   async takeScreenshot(
 73 |     selector?: string,
 74 |     options: { encoding: 'base64' } = { encoding: 'base64' },
 75 |   ): Promise<string | null> {
 76 |     try {
 77 |       if (!this.page) {
 78 |         throw new Error('No page available');
 79 |       }
 80 | 
 81 |       if (selector) {
 82 |         const element = await this.page.$(selector);
 83 |         if (!element) {
 84 |           throw new Error(`Element not found: ${selector}`);
 85 |         }
 86 |         const screenshot = await element.screenshot(options);
 87 |         return screenshot as string;
 88 |       } else {
 89 |         const screenshot = await this.page.screenshot({
 90 |           ...options,
 91 |           fullPage: false,
 92 |         });
 93 |         return screenshot as string;
 94 |       }
 95 |     } catch (error) {
 96 |       console.error('Failed to take screenshot:', error);
 97 |       return null;
 98 |     }
 99 |   }
100 | 
101 |   /**
102 |    * Clicks an element on the page
103 |    */
104 |   async click(selector: string): Promise<void> {
105 |     if (!this.page) {
106 |       throw new Error('No page available');
107 |     }
108 | 
109 |     await this.page.waitForSelector(selector, { visible: true });
110 |     await this.page.click(selector);
111 |   }
112 | 
113 |   /**
114 |    * Fills a form field
115 |    */
116 |   async fill(selector: string, value: string): Promise<void> {
117 |     if (!this.page) {
118 |       throw new Error('No page available');
119 |     }
120 | 
121 |     await this.page.waitForSelector(selector, { visible: true });
122 |     await this.page.type(selector, value);
123 |   }
124 | 
125 |   /**
126 |    * Selects an option from a dropdown
127 |    */
128 |   async select(selector: string, value: string): Promise<void> {
129 |     if (!this.page) {
130 |       throw new Error('No page available');
131 |     }
132 | 
133 |     await this.page.waitForSelector(selector, { visible: true });
134 |     await this.page.select(selector, value);
135 |   }
136 | 
137 |   /**
138 |    * Hovers over an element
139 |    */
140 |   async hover(selector: string): Promise<void> {
141 |     if (!this.page) {
142 |       throw new Error('No page available');
143 |     }
144 | 
145 |     await this.page.waitForSelector(selector, { visible: true });
146 |     await this.page.hover(selector);
147 |   }
148 | 
149 |   /**
150 |    * Executes JavaScript in the browser
151 |    */
152 |   async evaluate(script: string): Promise<{ result: any; logs: string[] }> {
153 |     if (!this.page) {
154 |       throw new Error('No page available');
155 |     }
156 | 
157 |     // Set up a helper to capture console logs during script execution
158 |     await this.page.evaluate(() => {
159 |       window.mcpHelper = {
160 |         logs: [],
161 |         originalConsole: { ...console },
162 |       };
163 | 
164 |       ['log', 'info', 'warn', 'error'].forEach(method => {
165 |         (console as any)[method] = (...args: any[]) => {
166 |           window.mcpHelper.logs.push(`[${method}] ${args.join(' ')}`);
167 |           (window.mcpHelper.originalConsole as any)[method](...args);
168 |         };
169 |       });
170 |     });
171 | 
172 |     // Execute the script
173 |     const result = await this.page.evaluate(new Function(script) as any);
174 | 
175 |     // Retrieve logs and restore console
176 |     const logs = await this.page.evaluate(() => {
177 |       const logs = window.mcpHelper.logs;
178 |       Object.assign(console, window.mcpHelper.originalConsole);
179 |       delete (window as any).mcpHelper;
180 |       return logs;
181 |     });
182 | 
183 |     return { result, logs };
184 |   }
185 | 
186 |   /**
187 |    * Navigates to a URL
188 |    */
189 |   async navigate(url: string): Promise<void> {
190 |     if (!this.page) {
191 |       throw new Error('No page available');
192 |     }
193 | 
194 |     await this.page.goto(url, { waitUntil: 'networkidle2' });
195 |   }
196 | 
197 |   /**
198 |    * Get all console logs
199 |    */
200 |   getConsoleLogs(): string[] {
201 |     return [...this.consoleLogs];
202 |   }
203 | 
204 |   /**
205 |    * Closes the browser
206 |    */
207 |   async close(): Promise<void> {
208 |     if (this.browser) {
209 |       await this.browser.close();
210 |       this.browser = null;
211 |       this.page = null;
212 |     }
213 |   }
214 | }
215 | 
216 | // Extend the global Window interface for our console capturing
217 | declare global {
218 |   interface Window {
219 |     mcpHelper: {
220 |       logs: string[];
221 |       originalConsole: Partial<typeof console>;
222 |     };
223 |   }
224 | }
225 | 
```