# 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 |
```