playwright-stealth-mcp-server 0.0.2 → 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 CHANGED
@@ -60,14 +60,38 @@ 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
- | 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 timeout for Playwright actions (click, fill, etc.) in milliseconds | `30000` |
70
- | `NAVIGATION_TIMEOUT` | Default timeout for page navigation (goto, reload, etc.) in milliseconds | `60000` |
84
+ | Variable | Description | Default |
85
+ | ------------------------- | -------------------------------------------------------------------------- | ----------------------------- |
86
+ | `STEALTH_MODE` | Enable stealth mode with anti-detection measures | `false` |
87
+ | `HEADLESS` | Run browser in headless mode | `true` |
88
+ | `TIMEOUT` | Default timeout for Playwright actions (click, fill, etc.) in milliseconds | `30000` |
89
+ | `NAVIGATION_TIMEOUT` | Default timeout for page navigation (goto, reload, etc.) in milliseconds | `60000` |
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 | - |
71
95
 
72
96
  ## Available Tools
73
97
 
@@ -90,11 +114,19 @@ return title;
90
114
 
91
115
  ### `browser_screenshot`
92
116
 
93
- Take a screenshot of the current page.
117
+ Take a screenshot of the current page. Screenshots are saved to filesystem storage and can be accessed later via MCP resources.
94
118
 
95
119
  **Parameters:**
96
120
 
97
121
  - `fullPage` (optional): Capture full scrollable page. Default: `false`
122
+ - `resultHandling` (optional): How to handle the result:
123
+ - `saveAndReturn` (default): Saves to storage AND returns inline base64 image
124
+ - `saveOnly`: Saves to storage and returns only the resource URI (more efficient for large screenshots)
125
+
126
+ **Returns:**
127
+
128
+ - With `saveAndReturn`: Inline base64 PNG image plus a `resource_link` with the file URI
129
+ - With `saveOnly`: Only a `resource_link` with the `file://` URI to the saved screenshot
98
130
 
99
131
  ### `browser_get_state`
100
132
 
@@ -104,6 +136,15 @@ Get the current browser state including URL, title, and configuration.
104
136
 
105
137
  Close the browser session. A new browser will be launched on the next `browser_execute` call.
106
138
 
139
+ ## MCP Resources
140
+
141
+ The server exposes saved screenshots as MCP resources. Clients can use:
142
+
143
+ - `resources/list`: List all saved screenshots with their URIs and metadata
144
+ - `resources/read`: Read a screenshot by its `file://` URI
145
+
146
+ This allows clients to access previously captured screenshots without needing to take new ones.
147
+
107
148
  ## Usage Examples
108
149
 
109
150
  ### Navigate and Extract Data
@@ -183,6 +224,24 @@ Stealth mode includes:
183
224
  - Plugin/mime type spoofing
184
225
  - Navigator property patching
185
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
+
186
245
  ## Development
187
246
 
188
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
- const current = process.env[name] || defaultValue;
44
- console.error(` - ${name}: ${description}`);
45
- console.error(` Current: ${current}`);
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: Create server using factory
57
- const { server, registerHandlers, cleanup } = createMCPServer();
58
- // Step 3: Register all handlers (tools)
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 4: Set up graceful shutdown
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 5: Start server with stdio transport
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
- logServerStart(`Playwright${stealthMode ? ' (Stealth)' : ''}`);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "playwright-stealth-mcp-server",
3
- "version": "0.0.2",
3
+ "version": "0.0.4",
4
4
  "description": "Local implementation of Playwright Stealth MCP server",
5
5
  "mcpName": "com.pulsemcp.servers/playwright-stealth",
6
6
  "main": "build/index.js",
package/shared/index.d.ts CHANGED
@@ -1,4 +1,6 @@
1
1
  export { createMCPServer, type IPlaywrightClient, type ClientFactory } from './server.js';
2
2
  export { createRegisterTools } from './tools.js';
3
+ export { registerResources } from './resources.js';
3
4
  export { logServerStart, logError, logWarning, logDebug } from './logging.js';
5
+ export * from './storage/index.js';
4
6
  //# sourceMappingURL=index.d.ts.map
package/shared/index.js CHANGED
@@ -1,3 +1,5 @@
1
1
  export { createMCPServer } from './server.js';
2
2
  export { createRegisterTools } from './tools.js';
3
+ export { registerResources } from './resources.js';
3
4
  export { logServerStart, logError, logWarning, logDebug } from './logging.js';
5
+ export * from './storage/index.js';
@@ -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
  */
@@ -0,0 +1,6 @@
1
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
2
+ /**
3
+ * Register screenshot resources handlers to an MCP server
4
+ */
5
+ export declare function registerResources(server: Server): void;
6
+ //# sourceMappingURL=resources.d.ts.map
@@ -0,0 +1,40 @@
1
+ import { ListResourcesRequestSchema, ReadResourceRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
2
+ import { ScreenshotStorageFactory } from './storage/index.js';
3
+ /**
4
+ * Register screenshot resources handlers to an MCP server
5
+ */
6
+ export function registerResources(server) {
7
+ // Register resource list handler
8
+ server.setRequestHandler(ListResourcesRequestSchema, async () => {
9
+ const storage = await ScreenshotStorageFactory.create();
10
+ const resources = await storage.list();
11
+ return {
12
+ resources: resources.map((resource) => ({
13
+ uri: resource.uri,
14
+ name: resource.name,
15
+ description: resource.description,
16
+ mimeType: resource.mimeType,
17
+ })),
18
+ };
19
+ });
20
+ // Register resource read handler
21
+ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
22
+ const { uri } = request.params;
23
+ const storage = await ScreenshotStorageFactory.create();
24
+ try {
25
+ const content = await storage.read(uri);
26
+ return {
27
+ contents: [
28
+ {
29
+ uri: content.uri,
30
+ mimeType: content.mimeType,
31
+ blob: content.blob,
32
+ },
33
+ ],
34
+ };
35
+ }
36
+ catch {
37
+ throw new Error(`Resource not found: ${uri}`);
38
+ }
39
+ });
40
+ }
@@ -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
@@ -1,5 +1,6 @@
1
1
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
2
2
  import { createRegisterTools } from './tools.js';
3
+ import { registerResources } from './resources.js';
3
4
  /**
4
5
  * Playwright client implementation with optional stealth mode
5
6
  */
@@ -16,6 +17,15 @@ export class PlaywrightClient {
16
17
  if (this.page) {
17
18
  return this.page;
18
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;
19
29
  if (this.config.stealthMode) {
20
30
  // Use playwright-extra with stealth plugin
21
31
  const { chromium } = await import('playwright-extra');
@@ -28,6 +38,7 @@ export class PlaywrightClient {
28
38
  '--disable-dev-shm-usage',
29
39
  '--no-sandbox',
30
40
  ],
41
+ proxy: proxyOptions,
31
42
  });
32
43
  }
33
44
  else {
@@ -35,6 +46,7 @@ export class PlaywrightClient {
35
46
  const { chromium } = await import('playwright');
36
47
  this.browser = await chromium.launch({
37
48
  headless: this.config.headless,
49
+ proxy: proxyOptions,
38
50
  });
39
51
  }
40
52
  this.context = await this.browser.newContext({
@@ -42,6 +54,9 @@ export class PlaywrightClient {
42
54
  userAgent: this.config.stealthMode
43
55
  ? 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
44
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,
45
60
  });
46
61
  this.page = await this.context.newPage();
47
62
  // Apply timeout configuration to Playwright
@@ -124,14 +139,15 @@ export class PlaywrightClient {
124
139
  return this.config;
125
140
  }
126
141
  }
127
- export function createMCPServer() {
142
+ export function createMCPServer(proxyConfig) {
128
143
  const stealthMode = process.env.STEALTH_MODE === 'true';
129
144
  const server = new Server({
130
145
  name: 'playwright-stealth-mcp-server',
131
- version: '0.0.1',
146
+ version: '0.0.4',
132
147
  }, {
133
148
  capabilities: {
134
149
  tools: {},
150
+ resources: {},
135
151
  },
136
152
  });
137
153
  // Track active client for cleanup
@@ -148,11 +164,14 @@ export function createMCPServer() {
148
164
  headless,
149
165
  timeout,
150
166
  navigationTimeout,
167
+ proxy: proxyConfig,
151
168
  });
152
169
  return activeClient;
153
170
  });
154
171
  const registerTools = createRegisterTools(factory);
155
172
  registerTools(server);
173
+ // Register resources handlers for screenshot storage
174
+ registerResources(server);
156
175
  };
157
176
  const cleanup = async () => {
158
177
  if (activeClient) {
@@ -0,0 +1,7 @@
1
+ import { ScreenshotStorage } from './types.js';
2
+ export declare class ScreenshotStorageFactory {
3
+ private static instance;
4
+ static create(): Promise<ScreenshotStorage>;
5
+ static reset(): void;
6
+ }
7
+ //# sourceMappingURL=factory.d.ts.map
@@ -0,0 +1,17 @@
1
+ import { FileSystemScreenshotStorage } from './filesystem.js';
2
+ export class ScreenshotStorageFactory {
3
+ static instance = null;
4
+ static async create() {
5
+ if (this.instance) {
6
+ return this.instance;
7
+ }
8
+ const rootDir = process.env.SCREENSHOT_STORAGE_PATH;
9
+ const fsStorage = new FileSystemScreenshotStorage(rootDir);
10
+ await fsStorage.init();
11
+ this.instance = fsStorage;
12
+ return this.instance;
13
+ }
14
+ static reset() {
15
+ this.instance = null;
16
+ }
17
+ }
@@ -0,0 +1,16 @@
1
+ import { ScreenshotStorage, ScreenshotResourceData, ScreenshotResourceContent, ScreenshotMetadata } from './types.js';
2
+ export declare class FileSystemScreenshotStorage implements ScreenshotStorage {
3
+ private rootDir;
4
+ private initialized;
5
+ constructor(rootDir?: string);
6
+ init(): Promise<void>;
7
+ list(): Promise<ScreenshotResourceData[]>;
8
+ read(uri: string): Promise<ScreenshotResourceContent>;
9
+ write(base64Data: string, metadata: Omit<ScreenshotMetadata, 'timestamp'>): Promise<string>;
10
+ exists(uri: string): Promise<boolean>;
11
+ delete(uri: string): Promise<void>;
12
+ private fileExists;
13
+ private uriToFilePath;
14
+ private generateFileName;
15
+ }
16
+ //# sourceMappingURL=filesystem.d.ts.map
@@ -0,0 +1,126 @@
1
+ import { promises as fs } from 'fs';
2
+ import path from 'path';
3
+ import os from 'os';
4
+ export class FileSystemScreenshotStorage {
5
+ rootDir;
6
+ initialized = false;
7
+ constructor(rootDir) {
8
+ this.rootDir = rootDir || path.join(os.tmpdir(), 'playwright-screenshots');
9
+ }
10
+ async init() {
11
+ if (this.initialized) {
12
+ return;
13
+ }
14
+ await fs.mkdir(this.rootDir, { recursive: true });
15
+ this.initialized = true;
16
+ }
17
+ async list() {
18
+ await this.init();
19
+ const resources = [];
20
+ try {
21
+ const files = await fs.readdir(this.rootDir);
22
+ for (const file of files) {
23
+ if (file.endsWith('.png')) {
24
+ const metadataFile = file.replace('.png', '.json');
25
+ const filePath = path.join(this.rootDir, file);
26
+ const metadataPath = path.join(this.rootDir, metadataFile);
27
+ try {
28
+ const metadataContent = await fs.readFile(metadataPath, 'utf-8');
29
+ const metadata = JSON.parse(metadataContent);
30
+ const uri = `file://${filePath}`;
31
+ resources.push({
32
+ uri,
33
+ name: file,
34
+ description: metadata.pageUrl
35
+ ? `Screenshot of ${metadata.pageUrl}`
36
+ : `Screenshot taken at ${metadata.timestamp}`,
37
+ mimeType: 'image/png',
38
+ metadata,
39
+ });
40
+ }
41
+ catch {
42
+ // Ignore files without metadata
43
+ }
44
+ }
45
+ }
46
+ }
47
+ catch {
48
+ // Directory might not exist yet
49
+ }
50
+ // Sort by timestamp descending (most recent first)
51
+ resources.sort((a, b) => {
52
+ const timeA = new Date(a.metadata.timestamp).getTime();
53
+ const timeB = new Date(b.metadata.timestamp).getTime();
54
+ return timeB - timeA;
55
+ });
56
+ return resources;
57
+ }
58
+ async read(uri) {
59
+ const filePath = this.uriToFilePath(uri);
60
+ if (!(await this.fileExists(filePath))) {
61
+ throw new Error(`Resource not found: ${uri}`);
62
+ }
63
+ const buffer = await fs.readFile(filePath);
64
+ const blob = buffer.toString('base64');
65
+ return {
66
+ uri,
67
+ mimeType: 'image/png',
68
+ blob,
69
+ };
70
+ }
71
+ async write(base64Data, metadata) {
72
+ await this.init();
73
+ const timestamp = new Date().toISOString();
74
+ const fileName = this.generateFileName(timestamp);
75
+ const filePath = path.join(this.rootDir, fileName);
76
+ const metadataPath = path.join(this.rootDir, fileName.replace('.png', '.json'));
77
+ const fullMetadata = {
78
+ ...metadata,
79
+ timestamp,
80
+ };
81
+ // Write the screenshot image
82
+ const buffer = Buffer.from(base64Data, 'base64');
83
+ await fs.writeFile(filePath, buffer);
84
+ // Write the metadata
85
+ await fs.writeFile(metadataPath, JSON.stringify(fullMetadata, null, 2), 'utf-8');
86
+ return `file://${filePath}`;
87
+ }
88
+ async exists(uri) {
89
+ const filePath = this.uriToFilePath(uri);
90
+ return this.fileExists(filePath);
91
+ }
92
+ async delete(uri) {
93
+ const filePath = this.uriToFilePath(uri);
94
+ if (!(await this.fileExists(filePath))) {
95
+ throw new Error(`Resource not found: ${uri}`);
96
+ }
97
+ await fs.unlink(filePath);
98
+ // Also delete metadata file if it exists
99
+ const metadataPath = filePath.replace('.png', '.json');
100
+ try {
101
+ await fs.unlink(metadataPath);
102
+ }
103
+ catch {
104
+ // Ignore if metadata file doesn't exist
105
+ }
106
+ }
107
+ async fileExists(filePath) {
108
+ try {
109
+ await fs.access(filePath);
110
+ return true;
111
+ }
112
+ catch {
113
+ return false;
114
+ }
115
+ }
116
+ uriToFilePath(uri) {
117
+ if (uri.startsWith('file://')) {
118
+ return uri.substring(7);
119
+ }
120
+ throw new Error(`Invalid file URI: ${uri}`);
121
+ }
122
+ generateFileName(timestamp) {
123
+ const timestampPart = timestamp.replace(/[^0-9T-]/g, '').replace('T', '_');
124
+ return `screenshot_${timestampPart}.png`;
125
+ }
126
+ }
@@ -0,0 +1,4 @@
1
+ export * from './types.js';
2
+ export * from './factory.js';
3
+ export * from './filesystem.js';
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,3 @@
1
+ export * from './types.js';
2
+ export * from './factory.js';
3
+ export * from './filesystem.js';
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Types for screenshot resource storage
3
+ */
4
+ export interface ScreenshotMetadata {
5
+ timestamp: string;
6
+ pageUrl?: string;
7
+ pageTitle?: string;
8
+ fullPage: boolean;
9
+ }
10
+ export interface ScreenshotResourceData {
11
+ uri: string;
12
+ name: string;
13
+ description?: string;
14
+ mimeType: string;
15
+ metadata: ScreenshotMetadata;
16
+ }
17
+ export interface ScreenshotResourceContent {
18
+ uri: string;
19
+ mimeType: string;
20
+ blob: string;
21
+ }
22
+ export interface ScreenshotStorage {
23
+ list(): Promise<ScreenshotResourceData[]>;
24
+ read(uri: string): Promise<ScreenshotResourceContent>;
25
+ write(base64Data: string, metadata: Omit<ScreenshotMetadata, 'timestamp'>): Promise<string>;
26
+ exists(uri: string): Promise<boolean>;
27
+ delete(uri: string): Promise<void>;
28
+ }
29
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Types for screenshot resource storage
3
+ */
4
+ export {};
package/shared/tools.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { ListToolsRequestSchema, CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js';
2
2
  import { z } from 'zod';
3
+ import { ScreenshotStorageFactory } from './storage/index.js';
3
4
  // =============================================================================
4
5
  // TOOL SCHEMAS
5
6
  // =============================================================================
@@ -9,6 +10,10 @@ const ExecuteSchema = z.object({
9
10
  });
10
11
  const ScreenshotSchema = z.object({
11
12
  fullPage: z.boolean().optional().describe('Capture the full scrollable page. Default: false'),
13
+ resultHandling: z
14
+ .enum(['saveAndReturn', 'saveOnly'])
15
+ .optional()
16
+ .describe("How to handle the screenshot result. 'saveAndReturn' (default) saves to storage and returns inline base64, 'saveOnly' saves to storage and returns only the resource URI"),
12
17
  });
13
18
  // =============================================================================
14
19
  // TOOL DESCRIPTIONS
@@ -56,15 +61,23 @@ await page.click('button[type="submit"]');
56
61
  **Note:** When STEALTH_MODE=true, the browser includes anti-detection measures to help bypass bot protection.`;
57
62
  const SCREENSHOT_DESCRIPTION = `Take a screenshot of the current page.
58
63
 
59
- Captures the visible viewport or full page as a PNG image encoded in base64.
64
+ Captures the visible viewport or full page as a PNG image. Screenshots are saved to filesystem storage and can be accessed later via MCP resources.
65
+
66
+ **Parameters:**
67
+ - \`fullPage\`: Whether to capture the full scrollable page (default: false)
68
+ - \`resultHandling\`: How to handle the result:
69
+ - \`saveAndReturn\` (default): Saves to storage AND returns inline base64 image
70
+ - \`saveOnly\`: Saves to storage and returns only the resource URI (more efficient for large screenshots)
60
71
 
61
72
  **Returns:**
62
- - Base64-encoded PNG image data
73
+ - With \`saveAndReturn\`: Inline base64 PNG image data plus a resource_link to the saved file
74
+ - With \`saveOnly\`: A resource_link with the \`file://\` URI to the saved screenshot
63
75
 
64
76
  **Use cases:**
65
77
  - Verify page state after navigation
66
78
  - Debug automation issues
67
- - Capture visual content for analysis`;
79
+ - Capture visual content for analysis
80
+ - Store screenshots for later reference via MCP resources`;
68
81
  const GET_STATE_DESCRIPTION = `Get the current browser state.
69
82
 
70
83
  Returns information about the current browser session including the URL, page title, and whether a browser is open.
@@ -161,14 +174,47 @@ export function createRegisterTools(clientFactory) {
161
174
  type: 'boolean',
162
175
  description: 'Capture the full scrollable page. Default: false',
163
176
  },
177
+ resultHandling: {
178
+ type: 'string',
179
+ enum: ['saveAndReturn', 'saveOnly'],
180
+ description: "How to handle the screenshot result. 'saveAndReturn' (default) saves to storage and returns inline base64, 'saveOnly' saves to storage and returns only the resource URI",
181
+ },
164
182
  },
165
183
  },
166
184
  handler: async (args) => {
167
185
  try {
168
186
  const validated = ScreenshotSchema.parse(args);
169
- const base64 = await getClient().screenshot({
187
+ const client = getClient();
188
+ const base64 = await client.screenshot({
170
189
  fullPage: validated.fullPage,
171
190
  });
191
+ // Get page metadata for the screenshot
192
+ const state = await client.getState();
193
+ // Save to storage
194
+ const storage = await ScreenshotStorageFactory.create();
195
+ const uri = await storage.write(base64, {
196
+ pageUrl: state.currentUrl,
197
+ pageTitle: state.title,
198
+ fullPage: validated.fullPage ?? false,
199
+ });
200
+ const resultHandling = validated.resultHandling ?? 'saveAndReturn';
201
+ // Generate a name from the URI for the resource link
202
+ const fileName = uri.split('/').pop() || 'screenshot.png';
203
+ if (resultHandling === 'saveOnly') {
204
+ // Return only the resource link
205
+ return {
206
+ content: [
207
+ {
208
+ type: 'resource_link',
209
+ uri,
210
+ name: fileName,
211
+ description: `Screenshot saved to ${uri}`,
212
+ mimeType: 'image/png',
213
+ },
214
+ ],
215
+ };
216
+ }
217
+ // Default: saveAndReturn - return both inline image and resource link
172
218
  return {
173
219
  content: [
174
220
  {
@@ -176,6 +222,13 @@ export function createRegisterTools(clientFactory) {
176
222
  data: base64,
177
223
  mimeType: 'image/png',
178
224
  },
225
+ {
226
+ type: 'resource_link',
227
+ uri,
228
+ name: fileName,
229
+ description: `Screenshot also saved to ${uri}`,
230
+ mimeType: 'image/png',
231
+ },
179
232
  ],
180
233
  };
181
234
  }
@@ -211,6 +264,7 @@ export function createRegisterTools(clientFactory) {
211
264
  ...state,
212
265
  stealthMode: config.stealthMode,
213
266
  headless: config.headless,
267
+ proxyEnabled: !!config.proxy,
214
268
  }, null, 2),
215
269
  },
216
270
  ],
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