playwright-stealth-mcp-server 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,219 @@
1
+ # Playwright Stealth MCP Server
2
+
3
+ A Model Context Protocol (MCP) server for browser automation using Playwright with optional stealth mode to bypass anti-bot protection.
4
+
5
+ ## Features
6
+
7
+ - **Simplified API**: Single `browser_execute` tool that exposes the full Playwright API instead of many specialized tools
8
+ - **Stealth Mode**: Optional anti-detection measures using `playwright-extra` and `puppeteer-extra-plugin-stealth`
9
+ - **Persistent Sessions**: Browser session persists across tool calls for multi-step automation
10
+ - **Screenshot Support**: Capture page screenshots for visual verification
11
+ - **Code Execution**: Run arbitrary Playwright code with access to the `page` object
12
+
13
+ ## Installation
14
+
15
+ ```bash
16
+ npx playwright-stealth-mcp-server
17
+ ```
18
+
19
+ Or install globally:
20
+
21
+ ```bash
22
+ npm install -g playwright-stealth-mcp-server
23
+ ```
24
+
25
+ ## Configuration
26
+
27
+ ### Claude Desktop Configuration
28
+
29
+ Add to your Claude Desktop config file:
30
+
31
+ **Non-Stealth Mode** (Standard Playwright):
32
+
33
+ ```json
34
+ {
35
+ "mcpServers": {
36
+ "playwright": {
37
+ "command": "npx",
38
+ "args": ["-y", "playwright-stealth-mcp-server"],
39
+ "env": {
40
+ "STEALTH_MODE": "false"
41
+ }
42
+ }
43
+ }
44
+ }
45
+ ```
46
+
47
+ **Stealth Mode** (Anti-bot bypass):
48
+
49
+ ```json
50
+ {
51
+ "mcpServers": {
52
+ "playwright-stealth": {
53
+ "command": "npx",
54
+ "args": ["-y", "playwright-stealth-mcp-server"],
55
+ "env": {
56
+ "STEALTH_MODE": "true"
57
+ }
58
+ }
59
+ }
60
+ }
61
+ ```
62
+
63
+ ### Environment Variables
64
+
65
+ | Variable | Description | Default |
66
+ | -------------- | ------------------------------------------------ | ------- |
67
+ | `STEALTH_MODE` | Enable stealth mode with anti-detection measures | `false` |
68
+ | `HEADLESS` | Run browser in headless mode | `true` |
69
+ | `TIMEOUT` | Default execution timeout in milliseconds | `30000` |
70
+
71
+ ## Available Tools
72
+
73
+ ### `browser_execute`
74
+
75
+ Execute Playwright code with access to the `page` object.
76
+
77
+ **Parameters:**
78
+
79
+ - `code` (required): JavaScript code to execute. The `page` object is available in scope.
80
+ - `timeout` (optional): Execution timeout in milliseconds.
81
+
82
+ **Example:**
83
+
84
+ ```javascript
85
+ await page.goto('https://example.com');
86
+ const title = await page.title();
87
+ return title;
88
+ ```
89
+
90
+ ### `browser_screenshot`
91
+
92
+ Take a screenshot of the current page.
93
+
94
+ **Parameters:**
95
+
96
+ - `fullPage` (optional): Capture full scrollable page. Default: `false`
97
+
98
+ ### `browser_get_state`
99
+
100
+ Get the current browser state including URL, title, and configuration.
101
+
102
+ ### `browser_close`
103
+
104
+ Close the browser session. A new browser will be launched on the next `browser_execute` call.
105
+
106
+ ## Usage Examples
107
+
108
+ ### Navigate and Extract Data
109
+
110
+ ```javascript
111
+ // Navigate to a page
112
+ await page.goto('https://news.ycombinator.com');
113
+
114
+ // Extract headlines
115
+ const headlines = await page.$$eval('.titleline > a', (links) =>
116
+ links.slice(0, 5).map((a) => a.textContent)
117
+ );
118
+
119
+ return headlines;
120
+ ```
121
+
122
+ ### Fill and Submit a Form
123
+
124
+ ```javascript
125
+ await page.goto('https://example.com/login');
126
+
127
+ // Fill credentials
128
+ await page.fill('input[name="email"]', 'user@example.com');
129
+ await page.fill('input[name="password"]', 'password123');
130
+
131
+ // Submit form
132
+ await page.click('button[type="submit"]');
133
+
134
+ // Wait for navigation
135
+ await page.waitForNavigation();
136
+
137
+ return await page.title();
138
+ ```
139
+
140
+ ### Handle Dynamic Content
141
+
142
+ ```javascript
143
+ await page.goto('https://spa-example.com');
144
+
145
+ // Wait for element to appear
146
+ await page.waitForSelector('.loaded-content');
147
+
148
+ // Click to load more
149
+ await page.click('.load-more-button');
150
+
151
+ // Wait for new content
152
+ await page.waitForSelector('.new-items');
153
+
154
+ const items = await page.$$eval('.item', (els) => els.map((el) => el.textContent));
155
+ return items;
156
+ ```
157
+
158
+ ## Security Considerations
159
+
160
+ **Important:** The `browser_execute` tool executes arbitrary JavaScript code. This design provides full Playwright API access but has security implications:
161
+
162
+ - Only use this server with trusted input (e.g., from an LLM in a controlled environment)
163
+ - The code runs in a Node.js context with access to the `page` object
164
+ - Do not expose this server to untrusted users or public networks
165
+
166
+ This is intentional for maximum flexibility - it allows LLMs to leverage their existing Playwright knowledge. For production use, ensure proper access controls are in place.
167
+
168
+ ## When to Use Stealth Mode
169
+
170
+ Enable stealth mode (`STEALTH_MODE=true`) when:
171
+
172
+ - Accessing sites with Cloudflare protection
173
+ - Sites that block automation tools
174
+ - Pages that detect headless browsers
175
+ - OAuth flows that trigger CAPTCHA challenges
176
+
177
+ Stealth mode includes:
178
+
179
+ - WebDriver property masking
180
+ - Chrome automation flag removal
181
+ - User-Agent normalization
182
+ - Plugin/mime type spoofing
183
+ - Navigator property patching
184
+
185
+ ## Development
186
+
187
+ ```bash
188
+ # Install dependencies
189
+ npm run install-all
190
+
191
+ # Build the project
192
+ npm run build
193
+
194
+ # Run in development mode
195
+ npm run dev
196
+
197
+ # Run tests
198
+ npm test
199
+
200
+ # Run integration tests
201
+ npm run test:integration
202
+
203
+ # Run manual tests (requires Playwright browsers)
204
+ npm run test:manual:setup
205
+ npm run test:manual
206
+ ```
207
+
208
+ ## Architecture
209
+
210
+ This MCP server uses a simplified design inspired by [playwriter](https://github.com/remorses/playwriter):
211
+
212
+ - Single `browser_execute` tool instead of many specialized tools
213
+ - Reduces context window usage for LLMs
214
+ - Leverages existing Playwright knowledge in LLM training data
215
+ - Full API access without artificial constraints
216
+
217
+ ## License
218
+
219
+ MIT
@@ -0,0 +1,21 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Integration test entry point with mock client
4
+ * Used for testing the MCP server without launching a real browser
5
+ */
6
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
7
+ import { createMCPServer } from '../shared/index.js';
8
+ import { logServerStart, logError } from '../shared/logging.js';
9
+ import { createMockPlaywrightClient } from '../shared/playwright-client/playwright-client.integration-mock.js';
10
+ async function main() {
11
+ const { server, registerHandlers } = createMCPServer();
12
+ // Use mock client factory for integration tests
13
+ await registerHandlers(server, createMockPlaywrightClient);
14
+ const transport = new StdioServerTransport();
15
+ await server.connect(transport);
16
+ logServerStart('Playwright (Integration Mock)');
17
+ }
18
+ main().catch((error) => {
19
+ logError('main', error);
20
+ process.exit(1);
21
+ });
package/build/index.js ADDED
@@ -0,0 +1,78 @@
1
+ #!/usr/bin/env node
2
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
+ import { createMCPServer } from '../shared/index.js';
4
+ import { logServerStart, logError, logWarning } from '../shared/logging.js';
5
+ // =============================================================================
6
+ // ENVIRONMENT VALIDATION
7
+ // =============================================================================
8
+ function validateEnvironment() {
9
+ const optional = [
10
+ {
11
+ name: 'STEALTH_MODE',
12
+ description: 'Enable stealth mode to bypass anti-bot protection (true/false)',
13
+ defaultValue: 'false',
14
+ },
15
+ {
16
+ name: 'HEADLESS',
17
+ description: 'Run browser in headless mode (true/false)',
18
+ defaultValue: 'true',
19
+ },
20
+ {
21
+ name: 'TIMEOUT',
22
+ description: 'Default execution timeout in milliseconds',
23
+ defaultValue: '30000',
24
+ },
25
+ ];
26
+ // Log configuration
27
+ const stealthMode = process.env.STEALTH_MODE === 'true';
28
+ const headless = process.env.HEADLESS !== 'false';
29
+ const timeout = process.env.TIMEOUT || '30000';
30
+ if (stealthMode) {
31
+ logWarning('config', 'Stealth mode enabled - using anti-detection measures');
32
+ }
33
+ if (!headless) {
34
+ logWarning('config', 'Running in non-headless mode - browser window will be visible');
35
+ }
36
+ if (process.env.TIMEOUT) {
37
+ logWarning('config', `Custom timeout configured: ${timeout}ms`);
38
+ }
39
+ // Show optional configuration if DEBUG is set
40
+ if (process.env.DEBUG) {
41
+ console.error('\nOptional environment variables:');
42
+ optional.forEach(({ name, description, defaultValue }) => {
43
+ const current = process.env[name] || defaultValue;
44
+ console.error(` - ${name}: ${description}`);
45
+ console.error(` Current: ${current}`);
46
+ });
47
+ console.error('');
48
+ }
49
+ }
50
+ // =============================================================================
51
+ // MAIN ENTRY POINT
52
+ // =============================================================================
53
+ async function main() {
54
+ // Step 1: Validate environment variables
55
+ validateEnvironment();
56
+ // Step 2: Create server using factory
57
+ const { server, registerHandlers, cleanup } = createMCPServer();
58
+ // Step 3: Register all handlers (tools)
59
+ await registerHandlers(server);
60
+ // Step 4: Set up graceful shutdown
61
+ const handleShutdown = async () => {
62
+ logWarning('shutdown', 'Received shutdown signal, closing browser...');
63
+ await cleanup();
64
+ process.exit(0);
65
+ };
66
+ process.on('SIGINT', handleShutdown);
67
+ process.on('SIGTERM', handleShutdown);
68
+ // Step 5: Start server with stdio transport
69
+ const transport = new StdioServerTransport();
70
+ await server.connect(transport);
71
+ const stealthMode = process.env.STEALTH_MODE === 'true';
72
+ logServerStart(`Playwright${stealthMode ? ' (Stealth)' : ''}`);
73
+ }
74
+ // Run the server
75
+ main().catch((error) => {
76
+ logError('main', error);
77
+ process.exit(1);
78
+ });
package/package.json ADDED
@@ -0,0 +1,67 @@
1
+ {
2
+ "name": "playwright-stealth-mcp-server",
3
+ "version": "0.0.1",
4
+ "description": "Local implementation of Playwright Stealth MCP server",
5
+ "mcpName": "com.pulsemcp.servers/playwright-stealth",
6
+ "main": "build/index.js",
7
+ "type": "module",
8
+ "bin": {
9
+ "playwright-stealth-mcp-server": "./build/index.js"
10
+ },
11
+ "scripts": {
12
+ "build": "tsc && npm run build:integration",
13
+ "build:integration": "tsc -p tsconfig.integration.json",
14
+ "start": "node build/index.js",
15
+ "start:integration": "node build/index.integration-with-mock.js",
16
+ "dev": "tsx src/index.ts",
17
+ "dev:integration": "tsx src/index.integration-with-mock.ts",
18
+ "predev": "cd ../shared && npm run build && cd ../local && node setup-dev.js",
19
+ "prebuild": "cd ../shared && npm run build && cd ../local && node setup-dev.js",
20
+ "prepublishOnly": "node prepare-publish.js && node ../scripts/prepare-npm-readme.js",
21
+ "lint": "eslint . --ext .ts,.tsx",
22
+ "lint:fix": "eslint . --ext .ts,.tsx --fix",
23
+ "format": "prettier --write .",
24
+ "format:check": "prettier --check .",
25
+ "stage-publish": "npm version"
26
+ },
27
+ "dependencies": {
28
+ "@modelcontextprotocol/sdk": "^1.19.1",
29
+ "playwright": "^1.49.1",
30
+ "playwright-extra": "^4.3.6",
31
+ "puppeteer-extra-plugin-stealth": "^2.11.2",
32
+ "zod": "^3.24.1"
33
+ },
34
+ "devDependencies": {
35
+ "@types/node": "^22.15.31",
36
+ "tsx": "^4.19.4",
37
+ "typescript": "^5.7.3"
38
+ },
39
+ "keywords": [
40
+ "mcp",
41
+ "modelcontextprotocol",
42
+ "playwright",
43
+ "stealth",
44
+ "browser",
45
+ "automation"
46
+ ],
47
+ "author": "PulseMCP",
48
+ "license": "MIT",
49
+ "publishConfig": {
50
+ "access": "public"
51
+ },
52
+ "files": [
53
+ "build/**/*.js",
54
+ "shared/**/*.js",
55
+ "shared/**/*.d.ts",
56
+ "README.md"
57
+ ],
58
+ "repository": {
59
+ "type": "git",
60
+ "url": "https://github.com/pulsemcp/mcp-servers.git",
61
+ "directory": "experimental/playwright-stealth/local"
62
+ },
63
+ "bugs": {
64
+ "url": "https://github.com/pulsemcp/mcp-servers/issues"
65
+ },
66
+ "homepage": "https://github.com/pulsemcp/mcp-servers/tree/main/experimental/playwright-stealth"
67
+ }
@@ -0,0 +1,4 @@
1
+ export { createMCPServer, type IPlaywrightClient, type ClientFactory } from './server.js';
2
+ export { createRegisterTools } from './tools.js';
3
+ export { logServerStart, logError, logWarning, logDebug } from './logging.js';
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,3 @@
1
+ export { createMCPServer } from './server.js';
2
+ export { createRegisterTools } from './tools.js';
3
+ export { logServerStart, logError, logWarning, logDebug } from './logging.js';
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Logging utilities for consistent output across MCP servers
3
+ */
4
+ /**
5
+ * Log server startup message
6
+ */
7
+ export declare function logServerStart(serverName: string, transport?: string): void;
8
+ /**
9
+ * Log an error with context
10
+ */
11
+ export declare function logError(context: string, error: unknown): void;
12
+ /**
13
+ * Log a warning
14
+ */
15
+ export declare function logWarning(context: string, message: string): void;
16
+ /**
17
+ * Log debug information (only in development)
18
+ */
19
+ export declare function logDebug(context: string, message: string): void;
20
+ //# sourceMappingURL=logging.d.ts.map
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Logging utilities for consistent output across MCP servers
3
+ */
4
+ /**
5
+ * Log server startup message
6
+ */
7
+ export function logServerStart(serverName, transport = 'stdio') {
8
+ console.error(`MCP server ${serverName} running on ${transport}`);
9
+ }
10
+ /**
11
+ * Log an error with context
12
+ */
13
+ export function logError(context, error) {
14
+ const message = error instanceof Error ? error.message : String(error);
15
+ const stack = error instanceof Error ? error.stack : undefined;
16
+ console.error(`[ERROR] ${context}: ${message}`);
17
+ if (stack) {
18
+ console.error(stack);
19
+ }
20
+ }
21
+ /**
22
+ * Log a warning
23
+ */
24
+ export function logWarning(context, message) {
25
+ console.error(`[WARN] ${context}: ${message}`);
26
+ }
27
+ /**
28
+ * Log debug information (only in development)
29
+ */
30
+ export function logDebug(context, message) {
31
+ if (process.env.NODE_ENV === 'development' || process.env.DEBUG) {
32
+ console.error(`[DEBUG] ${context}: ${message}`);
33
+ }
34
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Mock Playwright client for integration tests
3
+ * Simulates browser behavior without launching a real browser
4
+ */
5
+ import type { IPlaywrightClient } from '../server.js';
6
+ import type { ExecuteResult, BrowserState, PlaywrightConfig } from '../types.js';
7
+ export declare class MockPlaywrightClient implements IPlaywrightClient {
8
+ private state;
9
+ private config;
10
+ constructor(config: PlaywrightConfig);
11
+ execute(code: string, options?: {
12
+ timeout?: number;
13
+ }): Promise<ExecuteResult>;
14
+ screenshot(): Promise<string>;
15
+ getState(): Promise<BrowserState>;
16
+ close(): Promise<void>;
17
+ getConfig(): PlaywrightConfig;
18
+ }
19
+ export declare function createMockPlaywrightClient(): IPlaywrightClient;
20
+ //# sourceMappingURL=playwright-client.integration-mock.d.ts.map
@@ -0,0 +1,71 @@
1
+ export class MockPlaywrightClient {
2
+ state = { isOpen: false };
3
+ config;
4
+ constructor(config) {
5
+ this.config = config;
6
+ }
7
+ async execute(code, options) {
8
+ // Simulate browser being opened
9
+ this.state.isOpen = true;
10
+ // Simple mock responses based on code content
11
+ if (code.includes('page.goto')) {
12
+ const urlMatch = code.match(/goto\(['"`]([^'"`]+)['"`]\)/);
13
+ if (urlMatch) {
14
+ this.state.currentUrl = urlMatch[1];
15
+ this.state.title = `Mock Page - ${urlMatch[1]}`;
16
+ }
17
+ return {
18
+ success: true,
19
+ result: undefined,
20
+ consoleOutput: [],
21
+ };
22
+ }
23
+ if (code.includes('page.title')) {
24
+ return {
25
+ success: true,
26
+ result: JSON.stringify(this.state.title || 'Mock Title'),
27
+ consoleOutput: [],
28
+ };
29
+ }
30
+ if (code.includes('error') || code.includes('throw')) {
31
+ return {
32
+ success: false,
33
+ error: 'Mock error for testing',
34
+ consoleOutput: ['[error] Mock error occurred'],
35
+ };
36
+ }
37
+ // Check for timeout
38
+ if (options?.timeout && options.timeout < 100) {
39
+ return {
40
+ success: false,
41
+ error: `Execution timed out after ${options.timeout}ms`,
42
+ consoleOutput: [],
43
+ };
44
+ }
45
+ return {
46
+ success: true,
47
+ result: JSON.stringify({ mock: true, code: code.substring(0, 50) }),
48
+ consoleOutput: ['[log] Mock execution completed'],
49
+ };
50
+ }
51
+ async screenshot() {
52
+ // Return a minimal valid PNG as base64 (1x1 transparent pixel)
53
+ return 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==';
54
+ }
55
+ async getState() {
56
+ return { ...this.state };
57
+ }
58
+ async close() {
59
+ this.state = { isOpen: false };
60
+ }
61
+ getConfig() {
62
+ return this.config;
63
+ }
64
+ }
65
+ export function createMockPlaywrightClient() {
66
+ return new MockPlaywrightClient({
67
+ stealthMode: false,
68
+ headless: true,
69
+ timeout: 30000,
70
+ });
71
+ }
@@ -0,0 +1,93 @@
1
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
2
+ import type { ExecuteResult, BrowserState, PlaywrightConfig } from './types.js';
3
+ /**
4
+ * Playwright client interface
5
+ * Defines all methods for browser automation
6
+ */
7
+ export interface IPlaywrightClient {
8
+ /**
9
+ * Execute Playwright code in the browser context
10
+ */
11
+ execute(code: string, options?: {
12
+ timeout?: number;
13
+ }): Promise<ExecuteResult>;
14
+ /**
15
+ * Take a screenshot of the current page
16
+ */
17
+ screenshot(options?: {
18
+ fullPage?: boolean;
19
+ }): Promise<string>;
20
+ /**
21
+ * Get the current browser state
22
+ */
23
+ getState(): Promise<BrowserState>;
24
+ /**
25
+ * Close the browser
26
+ */
27
+ close(): Promise<void>;
28
+ /**
29
+ * Get the configuration
30
+ */
31
+ getConfig(): PlaywrightConfig;
32
+ }
33
+ /**
34
+ * Playwright client implementation with optional stealth mode
35
+ */
36
+ export declare class PlaywrightClient implements IPlaywrightClient {
37
+ private browser;
38
+ private context;
39
+ private page;
40
+ private consoleMessages;
41
+ private config;
42
+ constructor(config: PlaywrightConfig);
43
+ private ensureBrowser;
44
+ execute(code: string, options?: {
45
+ timeout?: number;
46
+ }): Promise<ExecuteResult>;
47
+ screenshot(options?: {
48
+ fullPage?: boolean;
49
+ }): Promise<string>;
50
+ getState(): Promise<BrowserState>;
51
+ close(): Promise<void>;
52
+ getConfig(): PlaywrightConfig;
53
+ }
54
+ export type ClientFactory = () => IPlaywrightClient;
55
+ export declare function createMCPServer(): {
56
+ server: Server<{
57
+ method: string;
58
+ params?: {
59
+ [x: string]: unknown;
60
+ _meta?: {
61
+ [x: string]: unknown;
62
+ progressToken?: string | number | undefined;
63
+ "io.modelcontextprotocol/related-task"?: {
64
+ taskId: string;
65
+ } | undefined;
66
+ } | undefined;
67
+ } | undefined;
68
+ }, {
69
+ method: string;
70
+ params?: {
71
+ [x: string]: unknown;
72
+ _meta?: {
73
+ [x: string]: unknown;
74
+ progressToken?: string | number | undefined;
75
+ "io.modelcontextprotocol/related-task"?: {
76
+ taskId: string;
77
+ } | undefined;
78
+ } | undefined;
79
+ } | undefined;
80
+ }, {
81
+ [x: string]: unknown;
82
+ _meta?: {
83
+ [x: string]: unknown;
84
+ progressToken?: string | number | undefined;
85
+ "io.modelcontextprotocol/related-task"?: {
86
+ taskId: string;
87
+ } | undefined;
88
+ } | undefined;
89
+ }>;
90
+ registerHandlers: (server: Server, clientFactory?: ClientFactory) => Promise<void>;
91
+ cleanup: () => Promise<void>;
92
+ };
93
+ //# sourceMappingURL=server.d.ts.map
@@ -0,0 +1,159 @@
1
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
2
+ import { createRegisterTools } from './tools.js';
3
+ /**
4
+ * Playwright client implementation with optional stealth mode
5
+ */
6
+ export class PlaywrightClient {
7
+ browser = null;
8
+ context = null;
9
+ page = null;
10
+ consoleMessages = [];
11
+ config;
12
+ constructor(config) {
13
+ this.config = config;
14
+ }
15
+ async ensureBrowser() {
16
+ if (this.page) {
17
+ return this.page;
18
+ }
19
+ if (this.config.stealthMode) {
20
+ // Use playwright-extra with stealth plugin
21
+ const { chromium } = await import('playwright-extra');
22
+ const StealthPlugin = (await import('puppeteer-extra-plugin-stealth')).default;
23
+ chromium.use(StealthPlugin());
24
+ this.browser = await chromium.launch({
25
+ headless: this.config.headless,
26
+ args: [
27
+ '--disable-blink-features=AutomationControlled',
28
+ '--disable-dev-shm-usage',
29
+ '--no-sandbox',
30
+ ],
31
+ });
32
+ }
33
+ else {
34
+ // Use standard playwright
35
+ const { chromium } = await import('playwright');
36
+ this.browser = await chromium.launch({
37
+ headless: this.config.headless,
38
+ });
39
+ }
40
+ this.context = await this.browser.newContext({
41
+ viewport: { width: 1920, height: 1080 },
42
+ userAgent: this.config.stealthMode
43
+ ? 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
44
+ : undefined,
45
+ });
46
+ this.page = await this.context.newPage();
47
+ // Capture console messages
48
+ this.page.on('console', (msg) => {
49
+ this.consoleMessages.push(`[${msg.type()}] ${msg.text()}`);
50
+ // Keep only last 100 messages
51
+ if (this.consoleMessages.length > 100) {
52
+ this.consoleMessages.shift();
53
+ }
54
+ });
55
+ return this.page;
56
+ }
57
+ async execute(code, options) {
58
+ const timeout = options?.timeout ?? this.config.timeout;
59
+ try {
60
+ const page = await this.ensureBrowser();
61
+ // Clear console messages for this execution
62
+ const startIndex = this.consoleMessages.length;
63
+ // Create the execution context with page available
64
+ // eslint-disable-next-line @typescript-eslint/no-implied-eval
65
+ const AsyncFunction = Object.getPrototypeOf(async function () { }).constructor;
66
+ const fn = new AsyncFunction('page', code);
67
+ // Execute with timeout
68
+ const result = await Promise.race([
69
+ fn(page),
70
+ new Promise((_, reject) => setTimeout(() => reject(new Error(`Execution timed out after ${timeout}ms`)), timeout)),
71
+ ]);
72
+ // Get console output from this execution
73
+ const consoleOutput = this.consoleMessages.slice(startIndex);
74
+ return {
75
+ success: true,
76
+ result: result !== undefined ? JSON.stringify(result, null, 2) : undefined,
77
+ consoleOutput,
78
+ };
79
+ }
80
+ catch (error) {
81
+ return {
82
+ success: false,
83
+ error: error instanceof Error ? error.message : String(error),
84
+ consoleOutput: this.consoleMessages.slice(-10),
85
+ };
86
+ }
87
+ }
88
+ async screenshot(options) {
89
+ const page = await this.ensureBrowser();
90
+ const buffer = await page.screenshot({
91
+ fullPage: options?.fullPage ?? false,
92
+ type: 'png',
93
+ });
94
+ return buffer.toString('base64');
95
+ }
96
+ async getState() {
97
+ if (!this.page) {
98
+ return { isOpen: false };
99
+ }
100
+ try {
101
+ return {
102
+ currentUrl: this.page.url(),
103
+ title: await this.page.title(),
104
+ isOpen: true,
105
+ };
106
+ }
107
+ catch {
108
+ return { isOpen: false };
109
+ }
110
+ }
111
+ async close() {
112
+ if (this.browser) {
113
+ await this.browser.close();
114
+ this.browser = null;
115
+ this.context = null;
116
+ this.page = null;
117
+ this.consoleMessages = [];
118
+ }
119
+ }
120
+ getConfig() {
121
+ return this.config;
122
+ }
123
+ }
124
+ export function createMCPServer() {
125
+ const stealthMode = process.env.STEALTH_MODE === 'true';
126
+ const server = new Server({
127
+ name: 'playwright-stealth-mcp-server',
128
+ version: '0.0.1',
129
+ }, {
130
+ capabilities: {
131
+ tools: {},
132
+ },
133
+ });
134
+ // Track active client for cleanup
135
+ let activeClient = null;
136
+ const registerHandlers = async (server, clientFactory) => {
137
+ // Use provided factory or create default client
138
+ const factory = clientFactory ||
139
+ (() => {
140
+ const headless = process.env.HEADLESS !== 'false';
141
+ const timeout = parseInt(process.env.TIMEOUT || '30000', 10);
142
+ activeClient = new PlaywrightClient({
143
+ stealthMode,
144
+ headless,
145
+ timeout,
146
+ });
147
+ return activeClient;
148
+ });
149
+ const registerTools = createRegisterTools(factory);
150
+ registerTools(server);
151
+ };
152
+ const cleanup = async () => {
153
+ if (activeClient) {
154
+ await activeClient.close();
155
+ activeClient = null;
156
+ }
157
+ };
158
+ return { server, registerHandlers, cleanup };
159
+ }
@@ -0,0 +1,4 @@
1
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
2
+ import { ClientFactory } from './server.js';
3
+ export declare function createRegisterTools(clientFactory: ClientFactory): (server: Server) => void;
4
+ //# sourceMappingURL=tools.d.ts.map
@@ -0,0 +1,287 @@
1
+ import { ListToolsRequestSchema, CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js';
2
+ import { z } from 'zod';
3
+ // =============================================================================
4
+ // TOOL SCHEMAS
5
+ // =============================================================================
6
+ const ExecuteSchema = z.object({
7
+ code: z.string().describe('Playwright code to execute. The `page` object is available in scope.'),
8
+ timeout: z.number().optional().describe('Execution timeout in milliseconds. Default: 30000'),
9
+ });
10
+ const ScreenshotSchema = z.object({
11
+ fullPage: z.boolean().optional().describe('Capture the full scrollable page. Default: false'),
12
+ });
13
+ // =============================================================================
14
+ // TOOL DESCRIPTIONS
15
+ // =============================================================================
16
+ const EXECUTE_DESCRIPTION = `Execute Playwright code in the browser.
17
+
18
+ This tool runs JavaScript code with access to a Playwright \`page\` object. The browser session persists across calls, so you can navigate, interact with elements, and extract data across multiple tool invocations.
19
+
20
+ **Available in scope:**
21
+ - \`page\` - The Playwright Page object with full API access
22
+
23
+ **Example usage:**
24
+
25
+ Navigate to a page:
26
+ \`\`\`javascript
27
+ await page.goto('https://example.com');
28
+ return await page.title();
29
+ \`\`\`
30
+
31
+ Click an element and wait:
32
+ \`\`\`javascript
33
+ await page.click('button.submit');
34
+ await page.waitForSelector('.success-message');
35
+ \`\`\`
36
+
37
+ Extract text content:
38
+ \`\`\`javascript
39
+ const items = await page.$$eval('.item', els => els.map(el => el.textContent));
40
+ return items;
41
+ \`\`\`
42
+
43
+ Fill a form:
44
+ \`\`\`javascript
45
+ await page.fill('input[name="email"]', 'test@example.com');
46
+ await page.fill('input[name="password"]', 'secret');
47
+ await page.click('button[type="submit"]');
48
+ \`\`\`
49
+
50
+ **Returns:**
51
+ - \`success\`: boolean indicating if execution succeeded
52
+ - \`result\`: JSON stringified return value (if any)
53
+ - \`error\`: error message (if failed)
54
+ - \`consoleOutput\`: array of console messages from the page
55
+
56
+ **Note:** When STEALTH_MODE=true, the browser includes anti-detection measures to help bypass bot protection.`;
57
+ const SCREENSHOT_DESCRIPTION = `Take a screenshot of the current page.
58
+
59
+ Captures the visible viewport or full page as a PNG image encoded in base64.
60
+
61
+ **Returns:**
62
+ - Base64-encoded PNG image data
63
+
64
+ **Use cases:**
65
+ - Verify page state after navigation
66
+ - Debug automation issues
67
+ - Capture visual content for analysis`;
68
+ const GET_STATE_DESCRIPTION = `Get the current browser state.
69
+
70
+ Returns information about the current browser session including the URL, page title, and whether a browser is open.
71
+
72
+ **Returns:**
73
+ - \`currentUrl\`: Current page URL
74
+ - \`title\`: Current page title
75
+ - \`isOpen\`: Whether a browser session is active`;
76
+ const CLOSE_DESCRIPTION = `Close the browser session.
77
+
78
+ Shuts down the browser and clears all state. A new browser will be launched on the next execute call.`;
79
+ export function createRegisterTools(clientFactory) {
80
+ // Create a single client instance that persists across calls
81
+ let client = null;
82
+ const getClient = () => {
83
+ if (!client) {
84
+ client = clientFactory();
85
+ }
86
+ return client;
87
+ };
88
+ const tools = [
89
+ {
90
+ name: 'browser_execute',
91
+ description: EXECUTE_DESCRIPTION,
92
+ inputSchema: {
93
+ type: 'object',
94
+ properties: {
95
+ code: {
96
+ type: 'string',
97
+ description: 'Playwright code to execute. The `page` object is available in scope.',
98
+ },
99
+ timeout: {
100
+ type: 'number',
101
+ description: 'Execution timeout in milliseconds. Default: 30000',
102
+ },
103
+ },
104
+ required: ['code'],
105
+ },
106
+ handler: async (args) => {
107
+ try {
108
+ const validated = ExecuteSchema.parse(args);
109
+ const result = await getClient().execute(validated.code, {
110
+ timeout: validated.timeout,
111
+ });
112
+ if (result.success) {
113
+ const parts = [];
114
+ if (result.result) {
115
+ parts.push(`Result:\n${result.result}`);
116
+ }
117
+ if (result.consoleOutput && result.consoleOutput.length > 0) {
118
+ parts.push(`Console output:\n${result.consoleOutput.join('\n')}`);
119
+ }
120
+ return {
121
+ content: [
122
+ {
123
+ type: 'text',
124
+ text: parts.length > 0 ? parts.join('\n\n') : 'Execution completed successfully.',
125
+ },
126
+ ],
127
+ };
128
+ }
129
+ else {
130
+ return {
131
+ content: [
132
+ {
133
+ type: 'text',
134
+ text: `Error: ${result.error}`,
135
+ },
136
+ ],
137
+ isError: true,
138
+ };
139
+ }
140
+ }
141
+ catch (error) {
142
+ return {
143
+ content: [
144
+ {
145
+ type: 'text',
146
+ text: `Error: ${error instanceof Error ? error.message : String(error)}`,
147
+ },
148
+ ],
149
+ isError: true,
150
+ };
151
+ }
152
+ },
153
+ },
154
+ {
155
+ name: 'browser_screenshot',
156
+ description: SCREENSHOT_DESCRIPTION,
157
+ inputSchema: {
158
+ type: 'object',
159
+ properties: {
160
+ fullPage: {
161
+ type: 'boolean',
162
+ description: 'Capture the full scrollable page. Default: false',
163
+ },
164
+ },
165
+ },
166
+ handler: async (args) => {
167
+ try {
168
+ const validated = ScreenshotSchema.parse(args);
169
+ const base64 = await getClient().screenshot({
170
+ fullPage: validated.fullPage,
171
+ });
172
+ return {
173
+ content: [
174
+ {
175
+ type: 'image',
176
+ data: base64,
177
+ mimeType: 'image/png',
178
+ },
179
+ ],
180
+ };
181
+ }
182
+ catch (error) {
183
+ return {
184
+ content: [
185
+ {
186
+ type: 'text',
187
+ text: `Error taking screenshot: ${error instanceof Error ? error.message : String(error)}`,
188
+ },
189
+ ],
190
+ isError: true,
191
+ };
192
+ }
193
+ },
194
+ },
195
+ {
196
+ name: 'browser_get_state',
197
+ description: GET_STATE_DESCRIPTION,
198
+ inputSchema: {
199
+ type: 'object',
200
+ properties: {},
201
+ },
202
+ handler: async () => {
203
+ try {
204
+ const state = await getClient().getState();
205
+ const config = getClient().getConfig();
206
+ return {
207
+ content: [
208
+ {
209
+ type: 'text',
210
+ text: JSON.stringify({
211
+ ...state,
212
+ stealthMode: config.stealthMode,
213
+ headless: config.headless,
214
+ }, null, 2),
215
+ },
216
+ ],
217
+ };
218
+ }
219
+ catch (error) {
220
+ return {
221
+ content: [
222
+ {
223
+ type: 'text',
224
+ text: `Error getting state: ${error instanceof Error ? error.message : String(error)}`,
225
+ },
226
+ ],
227
+ isError: true,
228
+ };
229
+ }
230
+ },
231
+ },
232
+ {
233
+ name: 'browser_close',
234
+ description: CLOSE_DESCRIPTION,
235
+ inputSchema: {
236
+ type: 'object',
237
+ properties: {},
238
+ },
239
+ handler: async () => {
240
+ try {
241
+ await getClient().close();
242
+ client = null; // Clear the reference so a new browser is created on next call
243
+ return {
244
+ content: [
245
+ {
246
+ type: 'text',
247
+ text: 'Browser closed successfully.',
248
+ },
249
+ ],
250
+ };
251
+ }
252
+ catch (error) {
253
+ return {
254
+ content: [
255
+ {
256
+ type: 'text',
257
+ text: `Error closing browser: ${error instanceof Error ? error.message : String(error)}`,
258
+ },
259
+ ],
260
+ isError: true,
261
+ };
262
+ }
263
+ },
264
+ },
265
+ ];
266
+ return (server) => {
267
+ // List available tools
268
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
269
+ return {
270
+ tools: tools.map((tool) => ({
271
+ name: tool.name,
272
+ description: tool.description,
273
+ inputSchema: tool.inputSchema,
274
+ })),
275
+ };
276
+ });
277
+ // Handle tool calls
278
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
279
+ const { name, arguments: args } = request.params;
280
+ const tool = tools.find((t) => t.name === name);
281
+ if (!tool) {
282
+ throw new Error(`Unknown tool: ${name}`);
283
+ }
284
+ return await tool.handler(args);
285
+ });
286
+ };
287
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Types for Playwright Stealth MCP server
3
+ */
4
+ export interface PlaywrightConfig {
5
+ stealthMode: boolean;
6
+ headless: boolean;
7
+ timeout: number;
8
+ }
9
+ export interface ExecuteResult {
10
+ success: boolean;
11
+ result?: unknown;
12
+ error?: string;
13
+ consoleOutput?: string[];
14
+ }
15
+ export interface BrowserState {
16
+ currentUrl?: string;
17
+ title?: string;
18
+ isOpen: boolean;
19
+ }
20
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Types for Playwright Stealth MCP server
3
+ */
4
+ export {};