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