playwright-stealth-mcp-server 0.0.1 → 0.0.3

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
@@ -62,11 +62,13 @@ Add to your Claude Desktop config file:
62
62
 
63
63
  ### Environment Variables
64
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` |
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` |
71
+ | `SCREENSHOT_STORAGE_PATH` | Directory for storing screenshots | `/tmp/playwright-screenshots` |
70
72
 
71
73
  ## Available Tools
72
74
 
@@ -89,11 +91,19 @@ return title;
89
91
 
90
92
  ### `browser_screenshot`
91
93
 
92
- Take a screenshot of the current page.
94
+ Take a screenshot of the current page. Screenshots are saved to filesystem storage and can be accessed later via MCP resources.
93
95
 
94
96
  **Parameters:**
95
97
 
96
98
  - `fullPage` (optional): Capture full scrollable page. Default: `false`
99
+ - `resultHandling` (optional): How to handle the result:
100
+ - `saveAndReturn` (default): Saves to storage AND returns inline base64 image
101
+ - `saveOnly`: Saves to storage and returns only the resource URI (more efficient for large screenshots)
102
+
103
+ **Returns:**
104
+
105
+ - With `saveAndReturn`: Inline base64 PNG image plus a `resource_link` with the file URI
106
+ - With `saveOnly`: Only a `resource_link` with the `file://` URI to the saved screenshot
97
107
 
98
108
  ### `browser_get_state`
99
109
 
@@ -103,6 +113,15 @@ Get the current browser state including URL, title, and configuration.
103
113
 
104
114
  Close the browser session. A new browser will be launched on the next `browser_execute` call.
105
115
 
116
+ ## MCP Resources
117
+
118
+ The server exposes saved screenshots as MCP resources. Clients can use:
119
+
120
+ - `resources/list`: List all saved screenshots with their URIs and metadata
121
+ - `resources/read`: Read a screenshot by its `file://` URI
122
+
123
+ This allows clients to access previously captured screenshots without needing to take new ones.
124
+
106
125
  ## Usage Examples
107
126
 
108
127
  ### Navigate and Extract Data
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "playwright-stealth-mcp-server",
3
- "version": "0.0.1",
3
+ "version": "0.0.3",
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';
@@ -67,5 +67,6 @@ export function createMockPlaywrightClient() {
67
67
  stealthMode: false,
68
68
  headless: true,
69
69
  timeout: 30000,
70
+ navigationTimeout: 60000,
70
71
  });
71
72
  }
@@ -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
+ }
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
  */
@@ -44,6 +45,9 @@ export class PlaywrightClient {
44
45
  : undefined,
45
46
  });
46
47
  this.page = await this.context.newPage();
48
+ // Apply timeout configuration to Playwright
49
+ this.page.setDefaultTimeout(this.config.timeout);
50
+ this.page.setDefaultNavigationTimeout(this.config.navigationTimeout);
47
51
  // Capture console messages
48
52
  this.page.on('console', (msg) => {
49
53
  this.consoleMessages.push(`[${msg.type()}] ${msg.text()}`);
@@ -125,10 +129,11 @@ export function createMCPServer() {
125
129
  const stealthMode = process.env.STEALTH_MODE === 'true';
126
130
  const server = new Server({
127
131
  name: 'playwright-stealth-mcp-server',
128
- version: '0.0.1',
132
+ version: '0.0.3',
129
133
  }, {
130
134
  capabilities: {
131
135
  tools: {},
136
+ resources: {},
132
137
  },
133
138
  });
134
139
  // Track active client for cleanup
@@ -139,15 +144,19 @@ export function createMCPServer() {
139
144
  (() => {
140
145
  const headless = process.env.HEADLESS !== 'false';
141
146
  const timeout = parseInt(process.env.TIMEOUT || '30000', 10);
147
+ const navigationTimeout = parseInt(process.env.NAVIGATION_TIMEOUT || '60000', 10);
142
148
  activeClient = new PlaywrightClient({
143
149
  stealthMode,
144
150
  headless,
145
151
  timeout,
152
+ navigationTimeout,
146
153
  });
147
154
  return activeClient;
148
155
  });
149
156
  const registerTools = createRegisterTools(factory);
150
157
  registerTools(server);
158
+ // Register resources handlers for screenshot storage
159
+ registerResources(server);
151
160
  };
152
161
  const cleanup = async () => {
153
162
  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
  }
package/shared/types.d.ts CHANGED
@@ -5,6 +5,7 @@ export interface PlaywrightConfig {
5
5
  stealthMode: boolean;
6
6
  headless: boolean;
7
7
  timeout: number;
8
+ navigationTimeout: number;
8
9
  }
9
10
  export interface ExecuteResult {
10
11
  success: boolean;