#
tokens: 9541/50000 31/31 files
lines: off (toggle) GitHub
raw markdown copy
# 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:
--------------------------------------------------------------------------------

```
node_modules/
dist/
coverage/
*.md
*.yml
*.yaml
```

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

```
node_modules/
dist/
coverage/
*.js
*.d.ts
*test*
scripts/*
```

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

```
# Environment mode
NODE_ENV=development

# Browser settings
HEADLESS=true

# Set to 'true' when running in Docker
DOCKER_CONTAINER=false

```

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

```
# Production environment
NODE_ENV=production

# Browser settings
HEADLESS=true

# Set to 'true' when running in Docker
DOCKER_CONTAINER=false

```

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

```
{
  "semi": true,
  "trailingComma": "all",
  "singleQuote": true,
  "printWidth": 100,
  "tabWidth": 2,
  "useTabs": false,
  "endOfLine": "auto",
  "arrowParens": "avoid",
  "bracketSpacing": true
}
```

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

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

```

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

```
# Dependencies
node_modules/

# Build
dist/
build/

# Logs
logs/
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# Environment
.env
.env.local
.env.development
.env.*.local

# Testing
coverage/

# IDE
.idea/
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json

# OS
.DS_Store
Thumbs.db
desktop.ini

# Windows
*.lnk

# ESLint
.eslintcache

# Temp files
*.swp
*.swo
*~
```

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

```javascript
module.exports = {
  parser: '@typescript-eslint/parser',
  extends: [
    'plugin:@typescript-eslint/recommended',
    'plugin:import/errors',
    'plugin:import/warnings',
    'plugin:import/typescript',
    'plugin:prettier/recommended',
  ],
  parserOptions: {
    ecmaVersion: 2020,
    sourceType: 'module',
    project: './tsconfig.json',
    tsconfigRootDir: __dirname,
  },
  env: {
    node: true,
    jest: true,
  },
  rules: {
    '@typescript-eslint/explicit-function-return-type': 'off',
    '@typescript-eslint/explicit-module-boundary-types': 'off',
    '@typescript-eslint/no-explicit-any': process.env.NODE_ENV === 'production' ? 'error' : 'warn',
    '@typescript-eslint/no-unused-vars': process.env.NODE_ENV === 'production' ? 'error' : 'warn',
    '@typescript-eslint/no-non-null-assertion': 'warn',
    // Disable the problematic import rules
    'import/no-named-as-default': 'off',
    'import/no-named-as-default-member': 'off',
    'import/namespace': 'off',
    'import/default': 'off',
    'import/no-named-default': 'off',
    'import/no-duplicates': 'off',
    'import/no-unresolved': 'off',
  },
  settings: {
    'import/parsers': {
      '@typescript-eslint/parser': ['.ts'],
    },
    'import/resolver': {
      typescript: {
        alwaysTryTypes: true,
        project: ['./tsconfig.json'],
      },
      node: true,
    },
  },
};

```

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

```markdown
# Puppeteer-Extra MCP Server

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.

## Features

- Enhanced browser automation with Puppeteer-Extra
- Stealth mode to avoid bot detection
- Screenshot capabilities for pages and elements
- Console logging and JavaScript execution
- Full suite of interaction methods (click, fill, select, hover)

## Components

### Tools

- **puppeteer_navigate**
  - Navigate to any URL in the browser
  - Input: `url` (string)

- **puppeteer_screenshot**
  - Capture screenshots of the entire page or specific elements
  - Inputs:
    - `name` (string, required): Name for the screenshot
    - `selector` (string, optional): CSS selector for element to screenshot
    - `width` (number, optional, default: 800): Screenshot width
    - `height` (number, optional, default: 600): Screenshot height

- **puppeteer_click**
  - Click elements on the page
  - Input: `selector` (string): CSS selector for element to click

- **puppeteer_hover**
  - Hover elements on the page
  - Input: `selector` (string): CSS selector for element to hover

- **puppeteer_fill**
  - Fill out input fields
  - Inputs:
    - `selector` (string): CSS selector for input field
    - `value` (string): Value to fill

- **puppeteer_select**
  - Select an element with SELECT tag
  - Inputs:
    - `selector` (string): CSS selector for element to select
    - `value` (string): Value to select

- **puppeteer_evaluate**
  - Execute JavaScript in the browser console
  - Input: `script` (string): JavaScript code to execute

### Resources

The server provides access to two types of resources:

1. **Console Logs** (`console://logs`)
   - Browser console output in text format
   - Includes all console messages from the browser

2. **Screenshots** (`screenshot://<name>`)
   - PNG images of captured screenshots
   - Accessible via the screenshot name specified during capture

## Development

### Installation

```bash
# Clone the repository
git clone <repository-url>
cd puppeteer_extra

# Install dependencies
npm install

# Copy environment file
cp .env.example .env.development
```

### Running Locally

```bash
# Development mode (non-headless browser)
npm run dev

# Production mode (headless browser)
npm run prod
```

### Building

```bash
npm run build
```

## Docker

### Building the Docker Image

```bash
docker build -t mcp/puppeteer-extra .
```

### Running with Docker

```bash
docker run -i --rm --init -e DOCKER_CONTAINER=true mcp/puppeteer-extra
```

## Configuration for Claude Desktop

### Docker

```json
{
  "mcpServers": {
    "puppeteer": {
      "command": "docker",
      "args": ["run", "-i", "--rm", "--init", "-e", "DOCKER_CONTAINER=true", "mcp/puppeteer-extra"]
    }
  }
}
```

### NPX

```json
{
  "mcpServers": {
    "puppeteer": {
      "command": "npx",
      "args": ["-y", "MCP_puppeteer_extra"]
    }
  }
}
```

## License

This MCP server is licensed under the MIT License.

```

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

```typescript
export interface Screenshot {
  name: string;
  data: string;
  timestamp: Date;
}

```

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

```json
{
  "watch": [
    "src"
  ],
  "ext": ".ts,.js",
  "ignore": [],
  "exec": "ts-node -r tsconfig-paths/register ./src/index.ts"
}
```

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

```typescript
import './navigate';
import './screenshot';
import './click';
import './fill';
import './select';
import './hover';
import './evaluate';

```

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

```javascript
module.exports = {
  preset: 'ts-jest',
  testEnvironment: 'node',
  transform: {
    '^.+\\.ts$': 'ts-jest',
  },
  transformIgnorePatterns: ['<rootDir>/node_modules/'],
  moduleFileExtensions: ['ts', 'js', 'json', 'node'],
  moduleNameMapper: {
    '@/(.*)': '<rootDir>/src/$1',
  },
};

```

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

```json
{
  "editor.formatOnSave": true,
  "editor.codeActionsOnSave": {
    "source.fixAll.eslint": "explicit",
    "source.organizeImports": "explicit"
  },
  "eslint.validate": ["typescript"],
  "[typescript]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  },
  "typescript.preferences.importModuleSpecifier": "non-relative",
  "javascript.preferences.importModuleSpecifier": "non-relative"
}

```

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

```json
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "Node16",
    "lib": ["ES2020", "dom"],
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "moduleResolution": "node16",
    "resolveJsonModule": true,
    "baseUrl": "./src",
    "paths": {
      "@/*": ["*"],
      "@utils/*": ["utils/*"],
      "@class/*": ["class/*"],
      "@types/*": ["types/*"]
    }
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.spec.ts", "src/**/__tests__/**"]
}

```

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

```typescript
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';

// Singleton for managing server reference
class NotificationManager {
  private server: McpServer | null = null;

  setServer(server: McpServer): void {
    this.server = server;
  }

  /**
   * Send a resource list changed notification if connected
   */
  resourcesChanged(): void {
    if (!this.server) return;

    try {
      this.server.server.notification({
        method: 'notifications/resources/list_changed',
      });
    } catch (error) {
      console.error('Failed to send resource change notification:', error);
    }
  }
}

// Export singleton instance
export const notificationManager = new NotificationManager();

```

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

```typescript
import { mcpServer } from '@/index';
import z from 'zod';
import { getBrowser } from '@/utils/browserManager';

mcpServer.tool(
  'puppeteer_hover',
  'Hover an element on the page',
  {
    selector: z.string().describe('CSS selector for element to hover'),
  },
  async ({ selector }) => {
    const browser = getBrowser();

    try {
      await browser.hover(selector);

      return {
        content: [
          {
            type: 'text',
            text: `Hovered ${selector}`,
          },
        ],
      };
    } catch (error) {
      return {
        content: [
          {
            type: 'text',
            text: `Failed to hover ${selector}: ${(error as Error).message}`,
          },
        ],
        isError: true,
      };
    }
  },
);

```

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

```typescript
import { mcpServer } from '@/index';
import z from 'zod';
import { getBrowser } from '@/utils/browserManager';

mcpServer.tool(
  'puppeteer_click',
  'Click an element on the page',
  {
    selector: z.string().describe('CSS selector for element to click'),
  },
  async ({ selector }) => {
    const browser = getBrowser();

    try {
      await browser.click(selector);

      return {
        content: [
          {
            type: 'text',
            text: `Clicked: ${selector}`,
          },
        ],
      };
    } catch (error) {
      return {
        content: [
          {
            type: 'text',
            text: `Failed to click ${selector}: ${(error as Error).message}`,
          },
        ],
        isError: true,
      };
    }
  },
);

```

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

```typescript
import PuppeteerBrowser from '@/class/browser.class';

// Singleton instance of the browser
let browserInstance: PuppeteerBrowser;

/**
 * Gets the singleton instance of the browser
 */
export function getBrowser(): PuppeteerBrowser {
  if (!browserInstance) {
    browserInstance = new PuppeteerBrowser();
  }
  return browserInstance;
}

/**
 * Initialize the browser
 * @param headless Whether to run in headless mode
 */
export async function initBrowser(headless: boolean = true): Promise<void> {
  const browser = getBrowser();
  await browser.init(headless);
}

/**
 * Close the browser instance
 */
export async function closeBrowser(): Promise<void> {
  if (browserInstance) {
    await browserInstance.close();
    browserInstance = undefined as any;
  }
}

```

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

```typescript
import { mcpServer } from '@/index';
import z from 'zod';
import { getBrowser } from '@/utils/browserManager';

mcpServer.tool(
  'puppeteer_navigate',
  'Navigate to a URL',
  {
    url: z.string().describe('URL to navigate to'),
  },
  async ({ url }) => {
    const browser = getBrowser();
    const page = await browser.getPage();

    try {
      await browser.navigate(url);

      return {
        content: [
          {
            type: 'text',
            text: `Navigated to ${url}`,
          },
        ],
      };
    } catch (error) {
      return {
        content: [
          {
            type: 'text',
            text: `Failed to navigate to ${url}: ${(error as Error).message}`,
          },
        ],
        isError: true,
      };
    }
  },
);

```

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

```typescript
import { mcpServer } from '@/index';
import z from 'zod';
import { getBrowser } from '@/utils/browserManager';

mcpServer.tool(
  'puppeteer_fill',
  'Fill out an input field',
  {
    selector: z.string().describe('CSS selector for input field'),
    value: z.string().describe('Value to fill'),
  },
  async ({ selector, value }) => {
    const browser = getBrowser();

    try {
      await browser.fill(selector, value);

      return {
        content: [
          {
            type: 'text',
            text: `Filled ${selector} with: ${value}`,
          },
        ],
      };
    } catch (error) {
      return {
        content: [
          {
            type: 'text',
            text: `Failed to fill ${selector}: ${(error as Error).message}`,
          },
        ],
        isError: true,
      };
    }
  },
);

```

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

```typescript
import { mcpServer } from '@/index';
import z from 'zod';
import { getBrowser } from '@/utils/browserManager';

mcpServer.tool(
  'puppeteer_select',
  'Select an element on the page with Select tag',
  {
    selector: z.string().describe('CSS selector for element to select'),
    value: z.string().describe('Value to select'),
  },
  async ({ selector, value }) => {
    const browser = getBrowser();

    try {
      await browser.select(selector, value);

      return {
        content: [
          {
            type: 'text',
            text: `Selected ${selector} with: ${value}`,
          },
        ],
      };
    } catch (error) {
      return {
        content: [
          {
            type: 'text',
            text: `Failed to select ${selector}: ${(error as Error).message}`,
          },
        ],
        isError: true,
      };
    }
  },
);

```

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

```typescript
import { mcpServer } from '@/index';
import z from 'zod';
import { getBrowser } from '@/utils/browserManager';

mcpServer.tool(
  'puppeteer_evaluate',
  'Execute JavaScript in the browser console',
  {
    script: z.string().describe('JavaScript code to execute'),
  },
  async ({ script }) => {
    const browser = getBrowser();

    try {
      const { result, logs } = await browser.evaluate(script);

      return {
        content: [
          {
            type: 'text',
            text: `Execution result:\n${JSON.stringify(
              result,
              null,
              2,
            )}\n\nConsole output:\n${logs.join('\n')}`,
          },
        ],
      };
    } catch (error) {
      return {
        content: [
          {
            type: 'text',
            text: `Script execution failed: ${(error as Error).message}`,
          },
        ],
        isError: true,
      };
    }
  },
);

```

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

```javascript
const fs = require('fs');
const path = require('path');

const huskyDir = path.join(__dirname, '..', '.husky');
const preCommitFile = path.join(huskyDir, 'pre-commit');

// Create .husky directory if it doesn't exist
if (!fs.existsSync(huskyDir)) {
  fs.mkdirSync(huskyDir, { recursive: true });
}

// Create pre-commit hook with correct line endings for the platform
const preCommitContent = `#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

# Run lint-staged for code quality checks
npx lint-staged

# Run tests only for affected files
npx ts-node scripts/run-relevant-tests.ts
`;

// Write the file with platform-specific line endings
fs.writeFileSync(preCommitFile, preCommitContent.replace(/\n/g, require('os').EOL));

// Make the pre-commit hook executable (Unix only)
if (process.platform !== 'win32') {
  fs.chmodSync(preCommitFile, '755');
}

console.log('✅ Husky pre-commit hook has been configured successfully');

```

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

```dockerfile
FROM node:20-bookworm-slim

# Set environment variables
ENV DEBIAN_FRONTEND=noninteractive
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true
ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium
ENV NODE_ENV=production
ENV DOCKER_CONTAINER=true
ENV HEADLESS=true

# Install dependencies for Chromium and Puppeteer
RUN apt-get update && \
    apt-get install -y wget gnupg && \
    apt-get install -y fonts-ipafont-gothic fonts-wqy-zenhei fonts-thai-tlwg fonts-kacst fonts-freefont-ttf && \
    apt-get install -y libxss1 libgtk2.0-0 libnss3 libatk-bridge2.0-0 libdrm2 libxkbcommon0 libgbm1 libasound2 && \
    apt-get install -y chromium && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/*

# Create app directory
WORKDIR /app

# Copy package files and install dependencies
COPY package*.json ./
RUN npm ci

# Copy app source
COPY . .

# Build the TypeScript project
RUN npm run build

# Set executable permissions on the entry point
RUN chmod +x dist/index.js

# Set the entry point
ENTRYPOINT ["node", "dist/index.js"]

```

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

```typescript
import { Screenshot } from '@/types/screenshot';
import { notificationManager } from './notificationUtil';

/**
 * Class to manage screenshot storage
 */
class ScreenshotStore {
  private screenshots: Map<string, Screenshot> = new Map();

  /**
   * Save a screenshot to the store
   */
  save(name: string, data: string, notifyChange: boolean = true): void {
    this.screenshots.set(name, {
      name,
      data,
      timestamp: new Date(),
    });

    // When a screenshot is saved, notify that resources have changed
    if (notifyChange) {
      notificationManager.resourcesChanged();
    }
  }

  /**
   * Get a screenshot by name
   */
  get(name: string): Screenshot | undefined {
    return this.screenshots.get(name);
  }

  /**
   * Get all screenshot names
   */
  getAll(): Screenshot[] {
    return Array.from(this.screenshots.values());
  }

  /**
   * Check if a screenshot exists
   */
  exists(name: string): boolean {
    return this.screenshots.has(name);
  }

  /**
   * Get the count of screenshots
   */
  count(): number {
    return this.screenshots.size;
  }

  /**
   * Delete a screenshot
   */
  delete(name: string): boolean {
    return this.screenshots.delete(name);
  }

  /**
   * Clear all screenshots
   */
  clear(): void {
    this.screenshots.clear();
  }
}

// Export a singleton instance
export const screenshotStore = new ScreenshotStore();

```

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

```json
{
  "name": "MCP_puppeteer_extra",
  "version": "1.0.0",
  "author": "Gpaul | Faldin",
  "scripts": {
    "start": "cross-env NODE_ENV=production node -r tsconfig-paths/register dist/index.js",
    "dev": "cross-env NODE_ENV=development nodemon -r tsconfig-paths/register src/index.ts",
    "build": "tsc --project tsconfig.json && tscpaths -p tsconfig.json -s ./src -o ./dist",
    "lint": "eslint . --ext .ts",
    "lint:fix": "eslint . --ext .ts --fix",
    "format": "prettier --write \"src/**/*.ts\"",
    "format:check": "prettier --check \"src/**/*.ts\"",
    "prepare": "husky install && node ./scripts/setup-husky.js",
    "prod": "npm run build && npm run start",
    "test": "jest"
  },
  "dependencies": {
    "@modelcontextprotocol/sdk": "^1.0.1",
    "dotenv": "^16.0.3",
    "puppeteer": "^23.4.0",
    "puppeteer-extra": "^3.3.6",
    "puppeteer-extra-plugin-stealth": "^2.11.2",
    "zod": "^3.22.4"
  },
  "devDependencies": {
    "@types/jest": "^29.5.14",
    "@types/node": "^18.15.11",
    "@typescript-eslint/eslint-plugin": "^6.13.0",
    "@typescript-eslint/parser": "^6.13.0",
    "cross-env": "^7.0.3",
    "eslint": "^8.37.0",
    "eslint-config-prettier": "^8.8.0",
    "eslint-import-resolver-node": "^0.3.9",
    "eslint-import-resolver-typescript": "^3.6.1",
    "eslint-plugin-import": "^2.29.1",
    "eslint-plugin-prettier": "^4.2.1",
    "husky": "^8.0.3",
    "jest": "^29.7.0",
    "lint-staged": "^13.2.1",
    "nodemon": "^2.0.22",
    "prettier": "^2.8.7",
    "ts-jest": "^29.2.6",
    "ts-node": "^10.9.1",
    "tsconfig-paths": "^4.2.0",
    "tscpaths": "^0.0.9",
    "typescript": "~5.1.6"
  },
  "description": "An extension of Puppeteer with puppeteer-extra and stealth mode for web automation"
}
```

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

```typescript
import { mcpServer } from '@/index';
import z from 'zod';
import { getBrowser } from '@/utils/browserManager';
import { screenshotStore } from '@/utils/screenshotManager';

mcpServer.tool(
  'puppeteer_screenshot',
  'Take a screenshot of the current page or a specific element',
  {
    name: z.string().describe('Name for the screenshot'),
    selector: z.string().optional().describe('CSS selector for element to screenshot'),
    width: z.number().optional().describe('Width in pixels (default: 800)'),
    height: z.number().optional().describe('Height in pixels (default: 600)'),
  },
  async ({ name, selector, width = 800, height = 600 }) => {
    const browser = getBrowser();
    const page = await browser.getPage();

    try {
      // Set viewport dimensions
      await page.setViewport({ width, height });

      // Take the screenshot
      const screenshot = await browser.takeScreenshot(selector);

      if (!screenshot) {
        return {
          content: [
            {
              type: 'text',
              text: selector
                ? `Element not found or could not capture: ${selector}`
                : 'Failed to take screenshot',
            },
          ],
          isError: true,
        };
      }

      // Store the screenshot and trigger a resource list change notification
      screenshotStore.save(name, screenshot, true);

      return {
        content: [
          {
            type: 'text',
            text: `Screenshot '${name}' taken at ${width}x${height}`,
          },
          {
            type: 'image',
            data: screenshot,
            mimeType: 'image/png',
          },
        ],
      };
    } catch (error) {
      return {
        content: [
          {
            type: 'text',
            text: `Failed to take screenshot: ${(error as Error).message}`,
          },
        ],
        isError: true,
      };
    }
  },
);

```

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

```typescript
import { closeBrowser, getBrowser, initBrowser } from '@/utils/browserManager';
import { notificationManager } from '@/utils/notificationUtil';
import { screenshotStore } from '@/utils/screenshotManager';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
  ListResourcesRequestSchema,
  ReadResourceRequestSchema,
} from '@modelcontextprotocol/sdk/types.js';

// Configure environment variables
// const __filename = fileURLToPath(import.meta.url);
// const __dirname = path.dirname(__filename);
// const envFile = process.env.NODE_ENV === 'production' ? '.env.production' : '.env.development';

// config({
//   path: path.resolve(__dirname, '..', envFile),
// });

process.env.HEADLESS = 'false';

// Create MCP server instance
export const mcpServer = new McpServer(
  {
    name: 'puppeteer-extra-stealth',
    version: '1.0.0',
  },
  {
    capabilities: {
      resources: {},
      tools: {},
    },
  },
);

// Set the server in notification manager
notificationManager.setServer(mcpServer);

// Set up resource handlers
mcpServer.server.setRequestHandler(ListResourcesRequestSchema, async () => ({
  resources: [
    {
      uri: 'console://logs',
      mimeType: 'text/plain',
      name: 'Browser console logs',
    },
    ...Array.from(screenshotStore.getAll()).map(screenshot => ({
      uri: `screenshot://${screenshot.name}`,
      mimeType: 'image/png',
      name: `Screenshot: ${screenshot.name}`,
    })),
  ],
}));

// Set up resource reading for screenshots
mcpServer.server.setRequestHandler(ReadResourceRequestSchema, async request => {
  const uri = request.params.uri.toString();

  // Handle console logs
  if (uri === 'console://logs') {
    const browser = getBrowser();
    return {
      contents: [
        {
          uri,
          mimeType: 'text/plain',
          text: browser.getConsoleLogs().join('\n'),
        },
      ],
    };
  }

  // Handle screenshots
  if (uri.startsWith('screenshot://')) {
    const name = uri.replace('screenshot://', '');
    const screenshot = screenshotStore.get(name);

    if (screenshot) {
      return {
        contents: [
          {
            uri,
            mimeType: 'image/png',
            blob: screenshot.data,
          },
        ],
      };
    }
  }

  throw new Error(`Resource not found: ${uri}`);
});

// Import all tool endpoints
import './endpoints';

// Main function to start the server
async function main() {
  try {
    // Initialize browser with stealth mode
    const isHeadless = process.env.HEADLESS !== 'false';
    await initBrowser(isHeadless);

    // Connect to the transport layer
    const transport = new StdioServerTransport();
    await mcpServer.connect(transport);

    // Now that we're connected, we can send notifications
    notificationManager.resourcesChanged();

    if (process.env.NODE_ENV === 'development') {
      console.error('Puppeteer-Extra MCP Server started in development mode');
      console.error(`Headless mode: ${isHeadless ? 'enabled' : 'disabled'}`);
    }
  } catch (error) {
    console.error('Failed to start MCP server:', error);
    process.exit(1);
  }
}

// Start the server
main().catch(error => {
  console.error('Fatal error:', error);
  process.exit(1);
});

// Handle graceful shutdown
const shutdown = async () => {
  try {
    console.error('Shutting down Puppeteer-Extra MCP Server...');
    await closeBrowser();
    await mcpServer.close();
    process.exit(0);
  } catch (error) {
    console.error('Error during shutdown:', error);
    process.exit(1);
  }
};

// Listen for termination signals
process.on('SIGINT', shutdown);
process.on('SIGTERM', shutdown);
process.on('exit', () => {
  console.error('Puppeteer-Extra MCP Server exited');
});

// Handle errors on stdin which likely means the parent process closed
process.stdin.on('error', () => {
  console.error('stdin error - parent process likely closed');
  shutdown();
});

// Handle stdin close which means the parent process closed
process.stdin.on('close', () => {
  console.error('stdin closed - parent process likely closed');
  shutdown();
});

```

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

```typescript
import { execSync } from 'child_process';
import * as fs from 'fs';
import * as path from 'path';

// Get modified files in the current commit
const getModifiedFiles = (): string[] => {
  try {
    const gitOutput = execSync('git diff --cached --name-only --diff-filter=ACMR').toString();
    return gitOutput.split('\n').filter(Boolean);
  } catch (error) {
    console.error('Error getting modified files:', error);
    return [];
  }
};

// Check if any source files have been modified
const hasSourceChanges = (files: string[]): boolean => {
  return files.some(
    file =>
      file.startsWith('src/') &&
      file.endsWith('.ts') &&
      !file.endsWith('.d.ts') &&
      !file.endsWith('.test.ts') &&
      !file.endsWith('.spec.ts'),
  );
};

// Check if any test files have been modified
const hasTestChanges = (files: string[]): boolean => {
  return files.some(
    file => file.endsWith('.test.ts') || file.endsWith('.spec.ts') || file.includes('/__tests__/'),
  );
};

// Find corresponding test files for changed source files
const findAffectedTests = (files: string[]): string[] => {
  const affectedTests: string[] = [];

  for (const file of files) {
    // Normalize path separators to forward slashes for consistency
    const normalizedFile = file.replace(/\\/g, '/');

    if (
      normalizedFile.startsWith('src/') &&
      normalizedFile.endsWith('.ts') &&
      !normalizedFile.endsWith('.test.ts') &&
      !normalizedFile.endsWith('.spec.ts')
    ) {
      const baseName = path.basename(normalizedFile, '.ts');
      const dirName = path.dirname(normalizedFile);

      // Check for component.test.ts pattern
      const potentialTest1 = path.join(dirName, `${baseName}.test.ts`).replace(/\\/g, '/');
      if (fs.existsSync(potentialTest1)) {
        console.log(`Found test file: ${potentialTest1}`);
        affectedTests.push(potentialTest1);
      }

      // Check for component.spec.ts pattern
      const potentialTest2 = path.join(dirName, `${baseName}.spec.ts`).replace(/\\/g, '/');
      if (fs.existsSync(potentialTest2)) {
        console.log(`Found test file: ${potentialTest2}`);
        affectedTests.push(potentialTest2);
      }

      // Check for __tests__ directory
      const potentialTestDir = path.join(dirName, '__tests__').replace(/\\/g, '/');
      if (fs.existsSync(potentialTestDir)) {
        const testDirFiles = fs.readdirSync(potentialTestDir);
        const matchingTests = testDirFiles.filter(
          testFile =>
            testFile.includes(baseName) &&
            (testFile.endsWith('.test.ts') || testFile.endsWith('.spec.ts')),
        );
        matchingTests.forEach(test => {
          const testPath = path.join(potentialTestDir, test).replace(/\\/g, '/');
          console.log(`Found test file: ${testPath}`);
          affectedTests.push(testPath);
        });
      }
    }
  }

  return affectedTests;
};

// Main function to decide and run tests
const runRelevantTests = (): void => {
  const modifiedFiles = getModifiedFiles();

  if (modifiedFiles.length === 0) {
    console.log('No modified files detected, skipping tests');
    process.exit(0);
  }

  // If any test files have been changed, run those specific tests
  if (hasTestChanges(modifiedFiles)) {
    const testFiles = modifiedFiles.filter(
      file =>
        file.endsWith('.test.ts') || file.endsWith('.spec.ts') || file.includes('/__tests__/'),
    );

    console.log('Test files have been modified, running only those tests...');
    try {
      // Run only the modified test files
      execSync(`npx jest ${testFiles.join(' ')} --passWithNoTests`, { stdio: 'inherit' });
    } catch (error) {
      process.exit(1); // Exit with error code if tests fail
    }
    process.exit(0);
  }

  // If source files have been changed, find and run affected tests
  if (hasSourceChanges(modifiedFiles)) {
    console.log(
      'Modified source files:',
      modifiedFiles.filter(
        file =>
          file.startsWith('src/') &&
          file.endsWith('.ts') &&
          !file.endsWith('.test.ts') &&
          !file.endsWith('.spec.ts'),
      ),
    );

    const affectedTests = findAffectedTests(modifiedFiles);

    if (affectedTests.length > 0) {
      console.log(
        'Source files with corresponding tests have been modified, running affected tests...',
      );
      console.log('Running tests:', affectedTests);

      try {
        // Use relative paths for Jest instead of absolute paths
        const relativeTests = affectedTests.map(file => file.replace(/\\/g, '/'));
        console.log(`Running Jest command: npx jest ${relativeTests.join(' ')} --passWithNoTests`);
        execSync(`npx jest ${relativeTests.join(' ')} --passWithNoTests`, { stdio: 'inherit' });
      } catch (error) {
        console.error('Jest execution failed:', error);
        process.exit(1); // Exit with error code if tests fail
      }
    } else {
      console.log('No corresponding tests found for modified source files');
    }
    process.exit(0);
  }

  console.log('No relevant source or test files changed, skipping tests');
  process.exit(0);
};

// Run the script
runRelevantTests();

```

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

```typescript
import { Browser, LaunchOptions, Page } from 'puppeteer';
import puppeteer from 'puppeteer-extra';
import StealthPlugin from 'puppeteer-extra-plugin-stealth';

// Add stealth plugin to puppeteer-extra
puppeteer.use(StealthPlugin());

/**
 * PuppeteerBrowser class for managing browser instances and pages
 */
export default class PuppeteerBrowser {
  private browser: Browser | null = null;
  private page: Page | null = null;
  private consoleLogs: string[] = [];

  /**
   * Initializes a new browser session with stealth mode
   */
  async init(headless: boolean = true) {
    try {
      const launchOptions: LaunchOptions = {
        headless: headless ? true : false,
        args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage'],
      };

      // Handle Docker-specific needs
      if (process.env.DOCKER_CONTAINER === 'true') {
        launchOptions.args?.push('--single-process', '--no-zygote');
      }

      this.browser = await puppeteer.launch(launchOptions);
      const pages = await this.browser.pages();
      this.page = pages[0] || (await this.browser.newPage());

      // Set up console logging
      this.page.on('console', msg => {
        const logEntry = `[${msg.type()}] ${msg.text()}`;
        this.consoleLogs.push(logEntry);
      });

      return this.page;
    } catch (error) {
      console.error('Failed to initialize browser:', error);
      throw error;
    }
  }

  /**
   * Returns the current page or creates a new one if none exists
   */
  async getPage(): Promise<Page> {
    if (!this.browser) {
      await this.init();
    }

    if (!this.page) {
      this.page = await this.browser!.newPage();

      // Set up console logging for the new page
      this.page.on('console', msg => {
        const logEntry = `[${msg.type()}] ${msg.text()}`;
        this.consoleLogs.push(logEntry);
      });
    }

    return this.page;
  }

  /**
   * Takes a screenshot of the current page or a specific element
   */
  async takeScreenshot(
    selector?: string,
    options: { encoding: 'base64' } = { encoding: 'base64' },
  ): Promise<string | null> {
    try {
      if (!this.page) {
        throw new Error('No page available');
      }

      if (selector) {
        const element = await this.page.$(selector);
        if (!element) {
          throw new Error(`Element not found: ${selector}`);
        }
        const screenshot = await element.screenshot(options);
        return screenshot as string;
      } else {
        const screenshot = await this.page.screenshot({
          ...options,
          fullPage: false,
        });
        return screenshot as string;
      }
    } catch (error) {
      console.error('Failed to take screenshot:', error);
      return null;
    }
  }

  /**
   * Clicks an element on the page
   */
  async click(selector: string): Promise<void> {
    if (!this.page) {
      throw new Error('No page available');
    }

    await this.page.waitForSelector(selector, { visible: true });
    await this.page.click(selector);
  }

  /**
   * Fills a form field
   */
  async fill(selector: string, value: string): Promise<void> {
    if (!this.page) {
      throw new Error('No page available');
    }

    await this.page.waitForSelector(selector, { visible: true });
    await this.page.type(selector, value);
  }

  /**
   * Selects an option from a dropdown
   */
  async select(selector: string, value: string): Promise<void> {
    if (!this.page) {
      throw new Error('No page available');
    }

    await this.page.waitForSelector(selector, { visible: true });
    await this.page.select(selector, value);
  }

  /**
   * Hovers over an element
   */
  async hover(selector: string): Promise<void> {
    if (!this.page) {
      throw new Error('No page available');
    }

    await this.page.waitForSelector(selector, { visible: true });
    await this.page.hover(selector);
  }

  /**
   * Executes JavaScript in the browser
   */
  async evaluate(script: string): Promise<{ result: any; logs: string[] }> {
    if (!this.page) {
      throw new Error('No page available');
    }

    // Set up a helper to capture console logs during script execution
    await this.page.evaluate(() => {
      window.mcpHelper = {
        logs: [],
        originalConsole: { ...console },
      };

      ['log', 'info', 'warn', 'error'].forEach(method => {
        (console as any)[method] = (...args: any[]) => {
          window.mcpHelper.logs.push(`[${method}] ${args.join(' ')}`);
          (window.mcpHelper.originalConsole as any)[method](...args);
        };
      });
    });

    // Execute the script
    const result = await this.page.evaluate(new Function(script) as any);

    // Retrieve logs and restore console
    const logs = await this.page.evaluate(() => {
      const logs = window.mcpHelper.logs;
      Object.assign(console, window.mcpHelper.originalConsole);
      delete (window as any).mcpHelper;
      return logs;
    });

    return { result, logs };
  }

  /**
   * Navigates to a URL
   */
  async navigate(url: string): Promise<void> {
    if (!this.page) {
      throw new Error('No page available');
    }

    await this.page.goto(url, { waitUntil: 'networkidle2' });
  }

  /**
   * Get all console logs
   */
  getConsoleLogs(): string[] {
    return [...this.consoleLogs];
  }

  /**
   * Closes the browser
   */
  async close(): Promise<void> {
    if (this.browser) {
      await this.browser.close();
      this.browser = null;
      this.page = null;
    }
  }
}

// Extend the global Window interface for our console capturing
declare global {
  interface Window {
    mcpHelper: {
      logs: string[];
      originalConsole: Partial<typeof console>;
    };
  }
}

```