playwright-stealth-mcp-server 0.0.3 → 0.0.4
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 +41 -0
- package/build/index.js +120 -10
- package/package.json +1 -1
- package/shared/logging.d.ts +4 -0
- package/shared/logging.js +6 -0
- package/shared/server.d.ts +2 -2
- package/shared/server.js +17 -2
- package/shared/tools.js +1 -0
- package/shared/types.d.ts +22 -0
package/README.md
CHANGED
|
@@ -60,6 +60,25 @@ Add to your Claude Desktop config file:
|
|
|
60
60
|
}
|
|
61
61
|
```
|
|
62
62
|
|
|
63
|
+
**With Proxy** (e.g., BrightData Residential Proxy):
|
|
64
|
+
|
|
65
|
+
```json
|
|
66
|
+
{
|
|
67
|
+
"mcpServers": {
|
|
68
|
+
"playwright-proxy": {
|
|
69
|
+
"command": "npx",
|
|
70
|
+
"args": ["-y", "playwright-stealth-mcp-server"],
|
|
71
|
+
"env": {
|
|
72
|
+
"STEALTH_MODE": "true",
|
|
73
|
+
"PROXY_URL": "http://brd.superproxy.io:22225",
|
|
74
|
+
"PROXY_USERNAME": "brd-customer-XXXXX-zone-residential",
|
|
75
|
+
"PROXY_PASSWORD": "your-password"
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
63
82
|
### Environment Variables
|
|
64
83
|
|
|
65
84
|
| Variable | Description | Default |
|
|
@@ -69,6 +88,10 @@ Add to your Claude Desktop config file:
|
|
|
69
88
|
| `TIMEOUT` | Default timeout for Playwright actions (click, fill, etc.) in milliseconds | `30000` |
|
|
70
89
|
| `NAVIGATION_TIMEOUT` | Default timeout for page navigation (goto, reload, etc.) in milliseconds | `60000` |
|
|
71
90
|
| `SCREENSHOT_STORAGE_PATH` | Directory for storing screenshots | `/tmp/playwright-screenshots` |
|
|
91
|
+
| `PROXY_URL` | Proxy server URL (e.g., `http://proxy.example.com:8080`) | - |
|
|
92
|
+
| `PROXY_USERNAME` | Proxy authentication username | - |
|
|
93
|
+
| `PROXY_PASSWORD` | Proxy authentication password | - |
|
|
94
|
+
| `PROXY_BYPASS` | Comma-separated list of hosts to bypass proxy | - |
|
|
72
95
|
|
|
73
96
|
## Available Tools
|
|
74
97
|
|
|
@@ -201,6 +224,24 @@ Stealth mode includes:
|
|
|
201
224
|
- Plugin/mime type spoofing
|
|
202
225
|
- Navigator property patching
|
|
203
226
|
|
|
227
|
+
## When to Use Proxy
|
|
228
|
+
|
|
229
|
+
Configure proxy settings when:
|
|
230
|
+
|
|
231
|
+
- Scraping sites that rate-limit by IP address
|
|
232
|
+
- Accessing geo-restricted content
|
|
233
|
+
- Avoiding IP-based blocks or bans
|
|
234
|
+
- Rotating IPs for large-scale data collection
|
|
235
|
+
|
|
236
|
+
The server supports HTTP/HTTPS proxies with optional authentication, making it compatible with:
|
|
237
|
+
|
|
238
|
+
- **BrightData** (Residential, Datacenter, ISP proxies)
|
|
239
|
+
- **Oxylabs**, **Smartproxy**, and other residential proxy providers
|
|
240
|
+
- Self-hosted proxy servers
|
|
241
|
+
- Corporate HTTP proxies
|
|
242
|
+
|
|
243
|
+
**Note:** When proxy is configured, the server performs a health check on startup to verify the proxy connection works. If the health check fails, the server will exit with an error.
|
|
244
|
+
|
|
204
245
|
## Development
|
|
205
246
|
|
|
206
247
|
```bash
|
package/build/index.js
CHANGED
|
@@ -1,7 +1,66 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
3
3
|
import { createMCPServer } from '../shared/index.js';
|
|
4
|
-
import { logServerStart, logError, logWarning } from '../shared/logging.js';
|
|
4
|
+
import { logServerStart, logError, logWarning, logInfo } from '../shared/logging.js';
|
|
5
|
+
// =============================================================================
|
|
6
|
+
// PROXY CONFIGURATION
|
|
7
|
+
// =============================================================================
|
|
8
|
+
/**
|
|
9
|
+
* Build proxy configuration from environment variables
|
|
10
|
+
* @returns ProxyConfig if proxy is configured, undefined otherwise
|
|
11
|
+
*/
|
|
12
|
+
function buildProxyConfig() {
|
|
13
|
+
const proxyUrl = process.env.PROXY_URL;
|
|
14
|
+
if (!proxyUrl) {
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
return {
|
|
18
|
+
server: proxyUrl,
|
|
19
|
+
username: process.env.PROXY_USERNAME,
|
|
20
|
+
password: process.env.PROXY_PASSWORD,
|
|
21
|
+
bypass: process.env.PROXY_BYPASS,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Health check the proxy connection by making a test request
|
|
26
|
+
* @param proxy The proxy configuration to test
|
|
27
|
+
*/
|
|
28
|
+
async function healthCheckProxy(proxy) {
|
|
29
|
+
logInfo('proxy', 'Performing proxy health check...');
|
|
30
|
+
// Use Node.js built-in fetch with proxy agent
|
|
31
|
+
// We'll use a simple approach: launch a quick browser with the proxy to test
|
|
32
|
+
const { chromium } = await import('playwright');
|
|
33
|
+
let browser;
|
|
34
|
+
try {
|
|
35
|
+
browser = await chromium.launch({
|
|
36
|
+
headless: true,
|
|
37
|
+
proxy: {
|
|
38
|
+
server: proxy.server,
|
|
39
|
+
username: proxy.username,
|
|
40
|
+
password: proxy.password,
|
|
41
|
+
bypass: proxy.bypass,
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
// Ignore HTTPS errors for residential proxies that perform HTTPS inspection
|
|
45
|
+
const context = await browser.newContext({ ignoreHTTPSErrors: true });
|
|
46
|
+
const page = await context.newPage();
|
|
47
|
+
// Try to fetch a reliable endpoint to verify proxy works
|
|
48
|
+
const response = await page.goto('https://httpbin.org/ip', {
|
|
49
|
+
timeout: 30000,
|
|
50
|
+
waitUntil: 'domcontentloaded',
|
|
51
|
+
});
|
|
52
|
+
if (!response || !response.ok()) {
|
|
53
|
+
throw new Error(`Proxy health check failed: HTTP ${response?.status() ?? 'unknown'}`);
|
|
54
|
+
}
|
|
55
|
+
const body = await page.textContent('body');
|
|
56
|
+
logInfo('proxy', `Proxy health check passed. Response: ${body?.trim()}`);
|
|
57
|
+
}
|
|
58
|
+
finally {
|
|
59
|
+
if (browser) {
|
|
60
|
+
await browser.close();
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
5
64
|
// =============================================================================
|
|
6
65
|
// ENVIRONMENT VALIDATION
|
|
7
66
|
// =============================================================================
|
|
@@ -22,11 +81,32 @@ function validateEnvironment() {
|
|
|
22
81
|
description: 'Default execution timeout in milliseconds',
|
|
23
82
|
defaultValue: '30000',
|
|
24
83
|
},
|
|
84
|
+
{
|
|
85
|
+
name: 'PROXY_URL',
|
|
86
|
+
description: 'Proxy server URL (e.g., http://proxy.example.com:8080)',
|
|
87
|
+
defaultValue: undefined,
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
name: 'PROXY_USERNAME',
|
|
91
|
+
description: 'Proxy authentication username',
|
|
92
|
+
defaultValue: undefined,
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
name: 'PROXY_PASSWORD',
|
|
96
|
+
description: 'Proxy authentication password',
|
|
97
|
+
defaultValue: undefined,
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
name: 'PROXY_BYPASS',
|
|
101
|
+
description: 'Comma-separated list of hosts to bypass proxy',
|
|
102
|
+
defaultValue: undefined,
|
|
103
|
+
},
|
|
25
104
|
];
|
|
26
105
|
// Log configuration
|
|
27
106
|
const stealthMode = process.env.STEALTH_MODE === 'true';
|
|
28
107
|
const headless = process.env.HEADLESS !== 'false';
|
|
29
108
|
const timeout = process.env.TIMEOUT || '30000';
|
|
109
|
+
const proxyUrl = process.env.PROXY_URL;
|
|
30
110
|
if (stealthMode) {
|
|
31
111
|
logWarning('config', 'Stealth mode enabled - using anti-detection measures');
|
|
32
112
|
}
|
|
@@ -36,13 +116,29 @@ function validateEnvironment() {
|
|
|
36
116
|
if (process.env.TIMEOUT) {
|
|
37
117
|
logWarning('config', `Custom timeout configured: ${timeout}ms`);
|
|
38
118
|
}
|
|
119
|
+
if (proxyUrl) {
|
|
120
|
+
// Sanitize proxy URL to prevent credential leaks (in case URL contains embedded credentials)
|
|
121
|
+
const sanitizedUrl = proxyUrl.replace(/\/\/[^@]+@/, '//*****@');
|
|
122
|
+
logInfo('config', `Proxy configured: ${sanitizedUrl}`);
|
|
123
|
+
if (process.env.PROXY_USERNAME) {
|
|
124
|
+
logInfo('config', 'Proxy authentication enabled');
|
|
125
|
+
}
|
|
126
|
+
}
|
|
39
127
|
// Show optional configuration if DEBUG is set
|
|
40
128
|
if (process.env.DEBUG) {
|
|
41
129
|
console.error('\nOptional environment variables:');
|
|
42
130
|
optional.forEach(({ name, description, defaultValue }) => {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
131
|
+
// Don't log proxy password
|
|
132
|
+
if (name === 'PROXY_PASSWORD') {
|
|
133
|
+
const hasPassword = !!process.env[name];
|
|
134
|
+
console.error(` - ${name}: ${description}`);
|
|
135
|
+
console.error(` Current: ${hasPassword ? '***' : '(not set)'}`);
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
const current = process.env[name] || defaultValue;
|
|
139
|
+
console.error(` - ${name}: ${description}`);
|
|
140
|
+
console.error(` Current: ${current || '(not set)'}`);
|
|
141
|
+
}
|
|
46
142
|
});
|
|
47
143
|
console.error('');
|
|
48
144
|
}
|
|
@@ -53,11 +149,24 @@ function validateEnvironment() {
|
|
|
53
149
|
async function main() {
|
|
54
150
|
// Step 1: Validate environment variables
|
|
55
151
|
validateEnvironment();
|
|
56
|
-
// Step 2:
|
|
57
|
-
const
|
|
58
|
-
// Step 3:
|
|
152
|
+
// Step 2: Build proxy configuration if provided
|
|
153
|
+
const proxyConfig = buildProxyConfig();
|
|
154
|
+
// Step 3: If proxy is configured, perform health check
|
|
155
|
+
if (proxyConfig) {
|
|
156
|
+
try {
|
|
157
|
+
await healthCheckProxy(proxyConfig);
|
|
158
|
+
}
|
|
159
|
+
catch (error) {
|
|
160
|
+
logError('proxy', `Proxy health check failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
161
|
+
logError('proxy', 'Please verify your proxy configuration and try again.');
|
|
162
|
+
process.exit(1);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
// Step 4: Create server using factory, passing proxy config
|
|
166
|
+
const { server, registerHandlers, cleanup } = createMCPServer(proxyConfig);
|
|
167
|
+
// Step 5: Register all handlers (tools)
|
|
59
168
|
await registerHandlers(server);
|
|
60
|
-
// Step
|
|
169
|
+
// Step 6: Set up graceful shutdown
|
|
61
170
|
const handleShutdown = async () => {
|
|
62
171
|
logWarning('shutdown', 'Received shutdown signal, closing browser...');
|
|
63
172
|
await cleanup();
|
|
@@ -65,11 +174,12 @@ async function main() {
|
|
|
65
174
|
};
|
|
66
175
|
process.on('SIGINT', handleShutdown);
|
|
67
176
|
process.on('SIGTERM', handleShutdown);
|
|
68
|
-
// Step
|
|
177
|
+
// Step 7: Start server with stdio transport
|
|
69
178
|
const transport = new StdioServerTransport();
|
|
70
179
|
await server.connect(transport);
|
|
71
180
|
const stealthMode = process.env.STEALTH_MODE === 'true';
|
|
72
|
-
|
|
181
|
+
const proxyEnabled = !!proxyConfig;
|
|
182
|
+
logServerStart(`Playwright${stealthMode ? ' (Stealth)' : ''}${proxyEnabled ? ' (Proxy)' : ''}`);
|
|
73
183
|
}
|
|
74
184
|
// Run the server
|
|
75
185
|
main().catch((error) => {
|
package/package.json
CHANGED
package/shared/logging.d.ts
CHANGED
|
@@ -13,6 +13,10 @@ export declare function logError(context: string, error: unknown): void;
|
|
|
13
13
|
* Log a warning
|
|
14
14
|
*/
|
|
15
15
|
export declare function logWarning(context: string, message: string): void;
|
|
16
|
+
/**
|
|
17
|
+
* Log an informational message
|
|
18
|
+
*/
|
|
19
|
+
export declare function logInfo(context: string, message: string): void;
|
|
16
20
|
/**
|
|
17
21
|
* Log debug information (only in development)
|
|
18
22
|
*/
|
package/shared/logging.js
CHANGED
|
@@ -24,6 +24,12 @@ export function logError(context, error) {
|
|
|
24
24
|
export function logWarning(context, message) {
|
|
25
25
|
console.error(`[WARN] ${context}: ${message}`);
|
|
26
26
|
}
|
|
27
|
+
/**
|
|
28
|
+
* Log an informational message
|
|
29
|
+
*/
|
|
30
|
+
export function logInfo(context, message) {
|
|
31
|
+
console.error(`[INFO] ${context}: ${message}`);
|
|
32
|
+
}
|
|
27
33
|
/**
|
|
28
34
|
* Log debug information (only in development)
|
|
29
35
|
*/
|
package/shared/server.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
2
|
-
import type { ExecuteResult, BrowserState, PlaywrightConfig } from './types.js';
|
|
2
|
+
import type { ExecuteResult, BrowserState, PlaywrightConfig, ProxyConfig } from './types.js';
|
|
3
3
|
/**
|
|
4
4
|
* Playwright client interface
|
|
5
5
|
* Defines all methods for browser automation
|
|
@@ -52,7 +52,7 @@ export declare class PlaywrightClient implements IPlaywrightClient {
|
|
|
52
52
|
getConfig(): PlaywrightConfig;
|
|
53
53
|
}
|
|
54
54
|
export type ClientFactory = () => IPlaywrightClient;
|
|
55
|
-
export declare function createMCPServer(): {
|
|
55
|
+
export declare function createMCPServer(proxyConfig?: ProxyConfig): {
|
|
56
56
|
server: Server<{
|
|
57
57
|
method: string;
|
|
58
58
|
params?: {
|
package/shared/server.js
CHANGED
|
@@ -17,6 +17,15 @@ export class PlaywrightClient {
|
|
|
17
17
|
if (this.page) {
|
|
18
18
|
return this.page;
|
|
19
19
|
}
|
|
20
|
+
// Build proxy options for Playwright if configured
|
|
21
|
+
const proxyOptions = this.config.proxy
|
|
22
|
+
? {
|
|
23
|
+
server: this.config.proxy.server,
|
|
24
|
+
username: this.config.proxy.username,
|
|
25
|
+
password: this.config.proxy.password,
|
|
26
|
+
bypass: this.config.proxy.bypass,
|
|
27
|
+
}
|
|
28
|
+
: undefined;
|
|
20
29
|
if (this.config.stealthMode) {
|
|
21
30
|
// Use playwright-extra with stealth plugin
|
|
22
31
|
const { chromium } = await import('playwright-extra');
|
|
@@ -29,6 +38,7 @@ export class PlaywrightClient {
|
|
|
29
38
|
'--disable-dev-shm-usage',
|
|
30
39
|
'--no-sandbox',
|
|
31
40
|
],
|
|
41
|
+
proxy: proxyOptions,
|
|
32
42
|
});
|
|
33
43
|
}
|
|
34
44
|
else {
|
|
@@ -36,6 +46,7 @@ export class PlaywrightClient {
|
|
|
36
46
|
const { chromium } = await import('playwright');
|
|
37
47
|
this.browser = await chromium.launch({
|
|
38
48
|
headless: this.config.headless,
|
|
49
|
+
proxy: proxyOptions,
|
|
39
50
|
});
|
|
40
51
|
}
|
|
41
52
|
this.context = await this.browser.newContext({
|
|
@@ -43,6 +54,9 @@ export class PlaywrightClient {
|
|
|
43
54
|
userAgent: this.config.stealthMode
|
|
44
55
|
? 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
|
|
45
56
|
: undefined,
|
|
57
|
+
// Ignore HTTPS errors when using proxy (required for residential proxies like BrightData
|
|
58
|
+
// which perform HTTPS inspection and may re-sign certificates)
|
|
59
|
+
ignoreHTTPSErrors: !!this.config.proxy,
|
|
46
60
|
});
|
|
47
61
|
this.page = await this.context.newPage();
|
|
48
62
|
// Apply timeout configuration to Playwright
|
|
@@ -125,11 +139,11 @@ export class PlaywrightClient {
|
|
|
125
139
|
return this.config;
|
|
126
140
|
}
|
|
127
141
|
}
|
|
128
|
-
export function createMCPServer() {
|
|
142
|
+
export function createMCPServer(proxyConfig) {
|
|
129
143
|
const stealthMode = process.env.STEALTH_MODE === 'true';
|
|
130
144
|
const server = new Server({
|
|
131
145
|
name: 'playwright-stealth-mcp-server',
|
|
132
|
-
version: '0.0.
|
|
146
|
+
version: '0.0.4',
|
|
133
147
|
}, {
|
|
134
148
|
capabilities: {
|
|
135
149
|
tools: {},
|
|
@@ -150,6 +164,7 @@ export function createMCPServer() {
|
|
|
150
164
|
headless,
|
|
151
165
|
timeout,
|
|
152
166
|
navigationTimeout,
|
|
167
|
+
proxy: proxyConfig,
|
|
153
168
|
});
|
|
154
169
|
return activeClient;
|
|
155
170
|
});
|
package/shared/tools.js
CHANGED
package/shared/types.d.ts
CHANGED
|
@@ -1,11 +1,27 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Types for Playwright Stealth MCP server
|
|
3
3
|
*/
|
|
4
|
+
/**
|
|
5
|
+
* Proxy configuration for browser connections
|
|
6
|
+
* Compatible with BrightData Residential Proxies and other HTTP/HTTPS proxies
|
|
7
|
+
*/
|
|
8
|
+
export interface ProxyConfig {
|
|
9
|
+
/** Proxy server URL (e.g., "http://proxy.example.com:8080") */
|
|
10
|
+
server: string;
|
|
11
|
+
/** Optional username for proxy authentication */
|
|
12
|
+
username?: string;
|
|
13
|
+
/** Optional password for proxy authentication */
|
|
14
|
+
password?: string;
|
|
15
|
+
/** Optional comma-separated list of hosts to bypass proxy */
|
|
16
|
+
bypass?: string;
|
|
17
|
+
}
|
|
4
18
|
export interface PlaywrightConfig {
|
|
5
19
|
stealthMode: boolean;
|
|
6
20
|
headless: boolean;
|
|
7
21
|
timeout: number;
|
|
8
22
|
navigationTimeout: number;
|
|
23
|
+
/** Optional proxy configuration */
|
|
24
|
+
proxy?: ProxyConfig;
|
|
9
25
|
}
|
|
10
26
|
export interface ExecuteResult {
|
|
11
27
|
success: boolean;
|
|
@@ -17,5 +33,11 @@ export interface BrowserState {
|
|
|
17
33
|
currentUrl?: string;
|
|
18
34
|
title?: string;
|
|
19
35
|
isOpen: boolean;
|
|
36
|
+
/** Whether stealth mode is enabled */
|
|
37
|
+
stealthMode?: boolean;
|
|
38
|
+
/** Whether browser is running in headless mode */
|
|
39
|
+
headless?: boolean;
|
|
40
|
+
/** Whether proxy is enabled for browser connections */
|
|
41
|
+
proxyEnabled?: boolean;
|
|
20
42
|
}
|
|
21
43
|
//# sourceMappingURL=types.d.ts.map
|