playwright-stealth-mcp-server 0.1.0 → 0.2.0
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 +7 -3
- package/package.json +1 -1
- package/shared/playwright-client/playwright-client.integration-mock.d.ts +10 -1
- package/shared/playwright-client/playwright-client.integration-mock.js +1 -1
- package/shared/server.d.ts +29 -3
- package/shared/server.js +115 -32
- package/shared/storage/types.d.ts +7 -0
- package/shared/tools.js +52 -18
package/README.md
CHANGED
|
@@ -122,15 +122,19 @@ return title;
|
|
|
122
122
|
|
|
123
123
|
### `browser_screenshot`
|
|
124
124
|
|
|
125
|
-
Take a screenshot of the current page. Screenshots are saved to filesystem storage and can be accessed later via MCP resources.
|
|
125
|
+
Take a screenshot of the current page, a specific element, or a page region. Screenshots are saved to filesystem storage and can be accessed later via MCP resources.
|
|
126
126
|
|
|
127
127
|
**Parameters:**
|
|
128
128
|
|
|
129
129
|
- `fullPage` (optional): Capture full scrollable page. Default: `false`
|
|
130
|
+
- `selector` (optional): CSS selector of a specific element to screenshot (e.g., `#main-content`, `.hero-banner`, `table.results`)
|
|
131
|
+
- `clip` (optional): Region of the page to screenshot as `{x, y, width, height}` in pixels
|
|
130
132
|
- `resultHandling` (optional): How to handle the result:
|
|
131
133
|
- `saveAndReturn` (default): Saves to storage AND returns inline base64 image
|
|
132
134
|
- `saveOnly`: Saves to storage and returns only the resource URI (more efficient for large screenshots)
|
|
133
135
|
|
|
136
|
+
**Note:** `fullPage`, `selector`, and `clip` are mutually exclusive. Only one can be specified per call.
|
|
137
|
+
|
|
134
138
|
**Returns:**
|
|
135
139
|
|
|
136
140
|
- With `saveAndReturn`: Inline base64 PNG image plus a `resource_link` with the file URI
|
|
@@ -148,7 +152,7 @@ Close the browser session. A new browser will be launched on the next `browser_e
|
|
|
148
152
|
|
|
149
153
|
Start recording the browser session as a WebM video.
|
|
150
154
|
|
|
151
|
-
This tool recycles the browser context with video recording enabled.
|
|
155
|
+
This tool recycles the browser context with video recording enabled. Session state (cookies, localStorage, sessionStorage) is automatically preserved across the context recycling.
|
|
152
156
|
|
|
153
157
|
If called while already recording, the current recording is automatically stopped and saved before starting a new one.
|
|
154
158
|
|
|
@@ -156,7 +160,7 @@ If called while already recording, the current recording is automatically stoppe
|
|
|
156
160
|
|
|
157
161
|
Stop recording and save the video.
|
|
158
162
|
|
|
159
|
-
Returns a `resource_link` with the `file://` URI to the saved video (WebM format).
|
|
163
|
+
Returns a `resource_link` with the `file://` URI to the saved video (WebM format). Session state is preserved — the browser navigates back to the previous URL automatically.
|
|
160
164
|
|
|
161
165
|
Returns an error if no recording is active.
|
|
162
166
|
|
package/package.json
CHANGED
|
@@ -12,7 +12,16 @@ export declare class MockPlaywrightClient implements IPlaywrightClient {
|
|
|
12
12
|
execute(code: string, options?: {
|
|
13
13
|
timeout?: number;
|
|
14
14
|
}): Promise<ExecuteResult>;
|
|
15
|
-
screenshot(
|
|
15
|
+
screenshot(_options?: {
|
|
16
|
+
fullPage?: boolean;
|
|
17
|
+
selector?: string;
|
|
18
|
+
clip?: {
|
|
19
|
+
x: number;
|
|
20
|
+
y: number;
|
|
21
|
+
width: number;
|
|
22
|
+
height: number;
|
|
23
|
+
};
|
|
24
|
+
}): Promise<ScreenshotResult>;
|
|
16
25
|
getState(): Promise<BrowserState>;
|
|
17
26
|
close(): Promise<void>;
|
|
18
27
|
getConfig(): PlaywrightConfig;
|
|
@@ -49,7 +49,7 @@ export class MockPlaywrightClient {
|
|
|
49
49
|
consoleOutput: ['[log] Mock execution completed'],
|
|
50
50
|
};
|
|
51
51
|
}
|
|
52
|
-
async screenshot() {
|
|
52
|
+
async screenshot(_options) {
|
|
53
53
|
// Return a minimal valid PNG as base64 (1x1 transparent pixel)
|
|
54
54
|
return {
|
|
55
55
|
data: 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==',
|
package/shared/server.d.ts
CHANGED
|
@@ -39,12 +39,19 @@ export interface IPlaywrightClient {
|
|
|
39
39
|
timeout?: number;
|
|
40
40
|
}): Promise<ExecuteResult>;
|
|
41
41
|
/**
|
|
42
|
-
* Take a screenshot of the current page.
|
|
42
|
+
* Take a screenshot of the current page, a specific element, or a page region.
|
|
43
43
|
* Screenshots are automatically limited to MAX_SCREENSHOT_DIMENSION pixels.
|
|
44
44
|
* If fullPage is requested but would exceed the limit, the screenshot is clipped.
|
|
45
45
|
*/
|
|
46
46
|
screenshot(options?: {
|
|
47
47
|
fullPage?: boolean;
|
|
48
|
+
selector?: string;
|
|
49
|
+
clip?: {
|
|
50
|
+
x: number;
|
|
51
|
+
y: number;
|
|
52
|
+
width: number;
|
|
53
|
+
height: number;
|
|
54
|
+
};
|
|
48
55
|
}): Promise<ScreenshotResult>;
|
|
49
56
|
/**
|
|
50
57
|
* Get the current browser state
|
|
@@ -66,7 +73,7 @@ export interface IPlaywrightClient {
|
|
|
66
73
|
* Start video recording by recycling the browser context.
|
|
67
74
|
* Closes the current context and creates a new one with recordVideo enabled.
|
|
68
75
|
* Navigates back to the previous URL to maintain continuity.
|
|
69
|
-
*
|
|
76
|
+
* Session state (cookies, localStorage, sessionStorage) is preserved on a best-effort basis.
|
|
70
77
|
*/
|
|
71
78
|
startRecording(videoDir: string): Promise<{
|
|
72
79
|
previousUrl?: string;
|
|
@@ -75,7 +82,7 @@ export interface IPlaywrightClient {
|
|
|
75
82
|
* Stop video recording by recycling the browser context.
|
|
76
83
|
* Gets the video path before closing the recording context, then creates
|
|
77
84
|
* a new context without recording and navigates back to the previous URL.
|
|
78
|
-
*
|
|
85
|
+
* Session state (cookies, localStorage, sessionStorage) is preserved on a best-effort basis.
|
|
79
86
|
*/
|
|
80
87
|
stopRecording(): Promise<StopRecordingResult>;
|
|
81
88
|
}
|
|
@@ -98,11 +105,30 @@ export declare class PlaywrightClient implements IPlaywrightClient {
|
|
|
98
105
|
}): Promise<ExecuteResult>;
|
|
99
106
|
screenshot(options?: {
|
|
100
107
|
fullPage?: boolean;
|
|
108
|
+
selector?: string;
|
|
109
|
+
clip?: {
|
|
110
|
+
x: number;
|
|
111
|
+
y: number;
|
|
112
|
+
width: number;
|
|
113
|
+
height: number;
|
|
114
|
+
};
|
|
101
115
|
}): Promise<ScreenshotResult>;
|
|
102
116
|
getState(): Promise<BrowserState>;
|
|
103
117
|
close(): Promise<void>;
|
|
104
118
|
getConfig(): PlaywrightConfig;
|
|
105
119
|
isRecording(): boolean;
|
|
120
|
+
/**
|
|
121
|
+
* Capture session state (cookies, localStorage, sessionStorage) from the current context.
|
|
122
|
+
* Uses Playwright's context.storageState() for cookies + localStorage,
|
|
123
|
+
* and page.evaluate() for sessionStorage (not covered by storageState).
|
|
124
|
+
*/
|
|
125
|
+
private captureSessionState;
|
|
126
|
+
/**
|
|
127
|
+
* Restore sessionStorage after navigation.
|
|
128
|
+
* Cookies and localStorage are handled by passing storageState to createContext().
|
|
129
|
+
*/
|
|
130
|
+
private restoreSessionStorage;
|
|
131
|
+
private setupPageHandlers;
|
|
106
132
|
startRecording(videoDir: string): Promise<{
|
|
107
133
|
previousUrl?: string;
|
|
108
134
|
}>;
|
package/shared/server.js
CHANGED
|
@@ -28,17 +28,7 @@ export class PlaywrightClient {
|
|
|
28
28
|
await this.launchBrowserIfNeeded();
|
|
29
29
|
await this.createContext(options);
|
|
30
30
|
this.page = await this.context.newPage();
|
|
31
|
-
|
|
32
|
-
this.page.setDefaultTimeout(this.config.timeout);
|
|
33
|
-
this.page.setDefaultNavigationTimeout(this.config.navigationTimeout);
|
|
34
|
-
// Capture console messages
|
|
35
|
-
this.page.on('console', (msg) => {
|
|
36
|
-
this.consoleMessages.push(`[${msg.type()}] ${msg.text()}`);
|
|
37
|
-
// Keep only last 100 messages
|
|
38
|
-
if (this.consoleMessages.length > 100) {
|
|
39
|
-
this.consoleMessages.shift();
|
|
40
|
-
}
|
|
41
|
-
});
|
|
31
|
+
this.setupPageHandlers(this.page);
|
|
42
32
|
return this.page;
|
|
43
33
|
}
|
|
44
34
|
async launchBrowserIfNeeded() {
|
|
@@ -103,6 +93,8 @@ export class PlaywrightClient {
|
|
|
103
93
|
ignoreHTTPSErrors: this.config.ignoreHttpsErrors ?? true,
|
|
104
94
|
// Video recording options (only set when recording)
|
|
105
95
|
...(options?.recordVideo ? { recordVideo: options.recordVideo } : {}),
|
|
96
|
+
// Restore session state (cookies + localStorage) if provided
|
|
97
|
+
...(options?.storageState ? { storageState: options.storageState } : {}),
|
|
106
98
|
});
|
|
107
99
|
// Grant browser permissions (defaults to all permissions if not specified)
|
|
108
100
|
const permissionsToGrant = this.config.permissions ?? [...ALL_BROWSER_PERMISSIONS];
|
|
@@ -152,6 +144,22 @@ export class PlaywrightClient {
|
|
|
152
144
|
async screenshot(options) {
|
|
153
145
|
const page = await this.ensureBrowser();
|
|
154
146
|
const fullPage = options?.fullPage ?? false;
|
|
147
|
+
const selector = options?.selector;
|
|
148
|
+
const clip = options?.clip;
|
|
149
|
+
// Element screenshot mode
|
|
150
|
+
if (selector) {
|
|
151
|
+
const locator = page.locator(selector);
|
|
152
|
+
const buffer = await locator.screenshot({ type: 'png' });
|
|
153
|
+
return { data: buffer.toString('base64'), wasClipped: false };
|
|
154
|
+
}
|
|
155
|
+
// Clip region screenshot mode
|
|
156
|
+
if (clip) {
|
|
157
|
+
if (clip.width <= 0 || clip.height <= 0) {
|
|
158
|
+
throw new Error('Clip width and height must be positive numbers');
|
|
159
|
+
}
|
|
160
|
+
const buffer = await page.screenshot({ type: 'png', clip });
|
|
161
|
+
return { data: buffer.toString('base64'), wasClipped: false };
|
|
162
|
+
}
|
|
155
163
|
// Check page dimensions before taking a full-page screenshot
|
|
156
164
|
if (fullPage) {
|
|
157
165
|
// Get page dimensions from the browser context
|
|
@@ -225,8 +233,86 @@ export class PlaywrightClient {
|
|
|
225
233
|
isRecording() {
|
|
226
234
|
return this._recording;
|
|
227
235
|
}
|
|
236
|
+
/**
|
|
237
|
+
* Capture session state (cookies, localStorage, sessionStorage) from the current context.
|
|
238
|
+
* Uses Playwright's context.storageState() for cookies + localStorage,
|
|
239
|
+
* and page.evaluate() for sessionStorage (not covered by storageState).
|
|
240
|
+
*/
|
|
241
|
+
async captureSessionState() {
|
|
242
|
+
if (!this.context || !this.page)
|
|
243
|
+
return null;
|
|
244
|
+
try {
|
|
245
|
+
const storageState = await this.context.storageState();
|
|
246
|
+
// Capture sessionStorage via page.evaluate (only for current origin)
|
|
247
|
+
let sessionStorage;
|
|
248
|
+
let currentOrigin;
|
|
249
|
+
try {
|
|
250
|
+
const currentUrl = this.page.url();
|
|
251
|
+
if (currentUrl && currentUrl !== 'about:blank') {
|
|
252
|
+
currentOrigin = new URL(currentUrl).origin;
|
|
253
|
+
const entries = await this.page.evaluate(() => {
|
|
254
|
+
const items = [];
|
|
255
|
+
for (let i = 0; i < window.sessionStorage.length; i++) {
|
|
256
|
+
const key = window.sessionStorage.key(i);
|
|
257
|
+
if (key !== null) {
|
|
258
|
+
items.push({ name: key, value: window.sessionStorage.getItem(key) || '' });
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
return items;
|
|
262
|
+
});
|
|
263
|
+
if (entries.length > 0) {
|
|
264
|
+
sessionStorage = entries;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
catch {
|
|
269
|
+
// sessionStorage capture is best-effort
|
|
270
|
+
}
|
|
271
|
+
return { storageState, sessionStorage, currentOrigin };
|
|
272
|
+
}
|
|
273
|
+
catch {
|
|
274
|
+
logWarning('session', 'Failed to capture session state before context recycling');
|
|
275
|
+
return null;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Restore sessionStorage after navigation.
|
|
280
|
+
* Cookies and localStorage are handled by passing storageState to createContext().
|
|
281
|
+
*/
|
|
282
|
+
async restoreSessionStorage(state) {
|
|
283
|
+
if (!this.page || !state.sessionStorage || !state.currentOrigin)
|
|
284
|
+
return;
|
|
285
|
+
try {
|
|
286
|
+
const currentUrl = this.page.url();
|
|
287
|
+
if (currentUrl && currentUrl !== 'about:blank') {
|
|
288
|
+
const pageOrigin = new URL(currentUrl).origin;
|
|
289
|
+
if (pageOrigin === state.currentOrigin) {
|
|
290
|
+
await this.page.evaluate((items) => {
|
|
291
|
+
for (const item of items) {
|
|
292
|
+
window.sessionStorage.setItem(item.name, item.value);
|
|
293
|
+
}
|
|
294
|
+
}, state.sessionStorage);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
catch {
|
|
299
|
+
logWarning('session', 'Failed to restore sessionStorage after context recycling');
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
setupPageHandlers(page) {
|
|
303
|
+
page.setDefaultTimeout(this.config.timeout);
|
|
304
|
+
page.setDefaultNavigationTimeout(this.config.navigationTimeout);
|
|
305
|
+
page.on('console', (msg) => {
|
|
306
|
+
this.consoleMessages.push(`[${msg.type()}] ${msg.text()}`);
|
|
307
|
+
if (this.consoleMessages.length > 100) {
|
|
308
|
+
this.consoleMessages.shift();
|
|
309
|
+
}
|
|
310
|
+
});
|
|
311
|
+
}
|
|
228
312
|
async startRecording(videoDir) {
|
|
229
313
|
await this.launchBrowserIfNeeded();
|
|
314
|
+
// Capture session state BEFORE recycling the context
|
|
315
|
+
const savedState = await this.captureSessionState();
|
|
230
316
|
// Capture the current URL before recycling the context
|
|
231
317
|
let previousUrl;
|
|
232
318
|
if (this.page) {
|
|
@@ -247,24 +333,21 @@ export class PlaywrightClient {
|
|
|
247
333
|
await this.context.close();
|
|
248
334
|
this.context = null;
|
|
249
335
|
}
|
|
250
|
-
// Create a new context with video recording enabled
|
|
336
|
+
// Create a new context with video recording enabled, restoring session state
|
|
251
337
|
await this.createContext({
|
|
252
338
|
recordVideo: { dir: videoDir, size: { width: 1920, height: 1080 } },
|
|
339
|
+
storageState: savedState?.storageState,
|
|
253
340
|
});
|
|
254
341
|
this.page = await this.context.newPage();
|
|
255
|
-
this.
|
|
256
|
-
this.page.setDefaultNavigationTimeout(this.config.navigationTimeout);
|
|
257
|
-
// Capture console messages on the new page
|
|
258
|
-
this.page.on('console', (msg) => {
|
|
259
|
-
this.consoleMessages.push(`[${msg.type()}] ${msg.text()}`);
|
|
260
|
-
if (this.consoleMessages.length > 100) {
|
|
261
|
-
this.consoleMessages.shift();
|
|
262
|
-
}
|
|
263
|
-
});
|
|
342
|
+
this.setupPageHandlers(this.page);
|
|
264
343
|
// Navigate back to the previous URL if there was one
|
|
265
344
|
if (previousUrl) {
|
|
266
345
|
await this.page.goto(previousUrl);
|
|
267
346
|
}
|
|
347
|
+
// Restore sessionStorage after navigation (cookies + localStorage handled by storageState)
|
|
348
|
+
if (savedState) {
|
|
349
|
+
await this.restoreSessionStorage(savedState);
|
|
350
|
+
}
|
|
268
351
|
this._recording = true;
|
|
269
352
|
return { previousUrl };
|
|
270
353
|
}
|
|
@@ -272,6 +355,8 @@ export class PlaywrightClient {
|
|
|
272
355
|
if (!this._recording || !this.page) {
|
|
273
356
|
throw new Error('Not currently recording');
|
|
274
357
|
}
|
|
358
|
+
// Capture session state BEFORE closing the recording context
|
|
359
|
+
const savedState = await this.captureSessionState();
|
|
275
360
|
// Capture current page state before closing the context
|
|
276
361
|
let pageUrl;
|
|
277
362
|
let pageTitle;
|
|
@@ -296,22 +381,20 @@ export class PlaywrightClient {
|
|
|
296
381
|
await this.context.close();
|
|
297
382
|
this.context = null;
|
|
298
383
|
this._recording = false;
|
|
299
|
-
// Create a new context WITHOUT recording
|
|
300
|
-
await this.createContext(
|
|
301
|
-
|
|
302
|
-
this.page.setDefaultTimeout(this.config.timeout);
|
|
303
|
-
this.page.setDefaultNavigationTimeout(this.config.navigationTimeout);
|
|
304
|
-
// Capture console messages on the new page
|
|
305
|
-
this.page.on('console', (msg) => {
|
|
306
|
-
this.consoleMessages.push(`[${msg.type()}] ${msg.text()}`);
|
|
307
|
-
if (this.consoleMessages.length > 100) {
|
|
308
|
-
this.consoleMessages.shift();
|
|
309
|
-
}
|
|
384
|
+
// Create a new context WITHOUT recording, restoring session state
|
|
385
|
+
await this.createContext({
|
|
386
|
+
storageState: savedState?.storageState,
|
|
310
387
|
});
|
|
388
|
+
this.page = await this.context.newPage();
|
|
389
|
+
this.setupPageHandlers(this.page);
|
|
311
390
|
// Navigate back to the previous URL
|
|
312
391
|
if (pageUrl) {
|
|
313
392
|
await this.page.goto(pageUrl);
|
|
314
393
|
}
|
|
394
|
+
// Restore sessionStorage after navigation
|
|
395
|
+
if (savedState) {
|
|
396
|
+
await this.restoreSessionStorage(savedState);
|
|
397
|
+
}
|
|
315
398
|
return { videoPath, pageUrl, pageTitle };
|
|
316
399
|
}
|
|
317
400
|
}
|
|
@@ -6,6 +6,13 @@ export interface ScreenshotMetadata {
|
|
|
6
6
|
pageUrl?: string;
|
|
7
7
|
pageTitle?: string;
|
|
8
8
|
fullPage: boolean;
|
|
9
|
+
selector?: string;
|
|
10
|
+
clip?: {
|
|
11
|
+
x: number;
|
|
12
|
+
y: number;
|
|
13
|
+
width: number;
|
|
14
|
+
height: number;
|
|
15
|
+
};
|
|
9
16
|
}
|
|
10
17
|
export interface ScreenshotResourceData {
|
|
11
18
|
uri: string;
|
package/shared/tools.js
CHANGED
|
@@ -8,13 +8,31 @@ const ExecuteSchema = z.object({
|
|
|
8
8
|
code: z.string().describe('Playwright code to execute. The `page` object is available in scope.'),
|
|
9
9
|
timeout: z.number().optional().describe('Execution timeout in milliseconds. Default: 30000'),
|
|
10
10
|
});
|
|
11
|
-
const ScreenshotSchema = z
|
|
11
|
+
const ScreenshotSchema = z
|
|
12
|
+
.object({
|
|
12
13
|
fullPage: z.boolean().optional().describe('Capture the full scrollable page. Default: false'),
|
|
14
|
+
selector: z
|
|
15
|
+
.string()
|
|
16
|
+
.optional()
|
|
17
|
+
.describe('CSS selector of a specific element to screenshot. Mutually exclusive with fullPage and clip.'),
|
|
18
|
+
clip: z
|
|
19
|
+
.object({
|
|
20
|
+
x: z.number().nonnegative().describe('X coordinate of the top-left corner'),
|
|
21
|
+
y: z.number().nonnegative().describe('Y coordinate of the top-left corner'),
|
|
22
|
+
width: z.number().positive().describe('Width of the clip region in pixels'),
|
|
23
|
+
height: z.number().positive().describe('Height of the clip region in pixels'),
|
|
24
|
+
})
|
|
25
|
+
.optional()
|
|
26
|
+
.describe('Region of the page to screenshot as {x, y, width, height}. Mutually exclusive with fullPage and selector.'),
|
|
13
27
|
resultHandling: z
|
|
14
28
|
.enum(['saveAndReturn', 'saveOnly'])
|
|
15
29
|
.optional()
|
|
16
30
|
.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"),
|
|
17
|
-
})
|
|
31
|
+
})
|
|
32
|
+
.refine((data) => {
|
|
33
|
+
const modes = [data.fullPage, !!data.selector, !!data.clip].filter(Boolean);
|
|
34
|
+
return modes.length <= 1;
|
|
35
|
+
}, { message: 'Only one of fullPage, selector, or clip can be specified' });
|
|
18
36
|
// =============================================================================
|
|
19
37
|
// TOOL DESCRIPTIONS
|
|
20
38
|
// =============================================================================
|
|
@@ -59,27 +77,32 @@ await page.click('button[type="submit"]');
|
|
|
59
77
|
- \`consoleOutput\`: array of console messages from the page
|
|
60
78
|
|
|
61
79
|
**Note:** When STEALTH_MODE=true, the browser includes anti-detection measures to help bypass bot protection.`;
|
|
62
|
-
const SCREENSHOT_DESCRIPTION = `Take a screenshot of the current page.
|
|
80
|
+
const SCREENSHOT_DESCRIPTION = `Take a screenshot of the current page, a specific element, or a page region.
|
|
63
81
|
|
|
64
|
-
Captures the visible viewport
|
|
82
|
+
Captures the visible viewport, full page, a specific element, or a rectangular region as a PNG image. Screenshots are saved to filesystem storage and can be accessed later via MCP resources.
|
|
65
83
|
|
|
66
84
|
**Parameters:**
|
|
67
85
|
- \`fullPage\`: Whether to capture the full scrollable page (default: false)
|
|
86
|
+
- \`selector\`: CSS selector of a specific element to screenshot (e.g., '#main-content', '.hero-banner', 'table.results')
|
|
87
|
+
- \`clip\`: Region of the page to screenshot as {x, y, width, height} in pixels
|
|
68
88
|
- \`resultHandling\`: How to handle the result:
|
|
69
89
|
- \`saveAndReturn\` (default): Saves to storage AND returns inline base64 image
|
|
70
90
|
- \`saveOnly\`: Saves to storage and returns only the resource URI (more efficient for large screenshots)
|
|
71
91
|
|
|
92
|
+
**Note:** \`fullPage\`, \`selector\`, and \`clip\` are mutually exclusive. Only one can be specified per call.
|
|
93
|
+
|
|
72
94
|
**Returns:**
|
|
73
95
|
- With \`saveAndReturn\`: Inline base64 PNG image data plus a resource_link to the saved file
|
|
74
96
|
- With \`saveOnly\`: A resource_link with the \`file://\` URI to the saved screenshot
|
|
75
97
|
|
|
76
98
|
**Dimension Limits:**
|
|
77
|
-
|
|
99
|
+
Full-page screenshots are limited to 8000 pixels in any dimension. If a full-page screenshot would exceed this limit, it is automatically clipped from the top-left corner and a warning is included in the response. Element and clip screenshots are not subject to this limit.
|
|
78
100
|
|
|
79
101
|
**Use cases:**
|
|
80
102
|
- Verify page state after navigation
|
|
103
|
+
- Screenshot a specific element like a chart, table, or form
|
|
104
|
+
- Capture a region of the page by coordinates
|
|
81
105
|
- Debug automation issues
|
|
82
|
-
- Capture visual content for analysis
|
|
83
106
|
- Store screenshots for later reference via MCP resources`;
|
|
84
107
|
const GET_STATE_DESCRIPTION = `Get the current browser state.
|
|
85
108
|
|
|
@@ -100,12 +123,7 @@ const START_RECORDING_DESCRIPTION = `Start recording the browser session as a vi
|
|
|
100
123
|
|
|
101
124
|
This tool begins capturing all browser interactions as a WebM video file. It works by recycling the browser context with video recording enabled.
|
|
102
125
|
|
|
103
|
-
**
|
|
104
|
-
- Cookies are lost
|
|
105
|
-
- localStorage and sessionStorage are cleared
|
|
106
|
-
- Any authenticated sessions will be invalidated
|
|
107
|
-
|
|
108
|
-
If you need to be logged in during the recording, navigate to the login page and authenticate again after starting the recording.
|
|
126
|
+
**Session state preservation:** Cookies and localStorage are automatically saved and restored when the context is recycled. sessionStorage for the current origin is also preserved on a best-effort basis. In rare cases involving multiple origins, some state may not be restored — if you experience authentication issues, log in again.
|
|
109
127
|
|
|
110
128
|
**Behavior when already recording:** If recording is already active, the current recording will be stopped (saving the video) and a new recording will begin.
|
|
111
129
|
|
|
@@ -118,10 +136,7 @@ const STOP_RECORDING_DESCRIPTION = `Stop recording the browser session and save
|
|
|
118
136
|
|
|
119
137
|
This tool stops the active video recording, saves the video file, and returns a resource URI for the recorded video.
|
|
120
138
|
|
|
121
|
-
**
|
|
122
|
-
- Cookies are lost
|
|
123
|
-
- localStorage and sessionStorage are cleared
|
|
124
|
-
- Any authenticated sessions will be invalidated
|
|
139
|
+
**Session state preservation:** Cookies and localStorage are automatically saved and restored when the context is recycled. sessionStorage for the current origin is also preserved on a best-effort basis.
|
|
125
140
|
|
|
126
141
|
The browser automatically navigates back to the URL it was on before the recording stopped.
|
|
127
142
|
|
|
@@ -215,6 +230,21 @@ export function createRegisterTools(clientFactory) {
|
|
|
215
230
|
type: 'boolean',
|
|
216
231
|
description: 'Capture the full scrollable page. Default: false',
|
|
217
232
|
},
|
|
233
|
+
selector: {
|
|
234
|
+
type: 'string',
|
|
235
|
+
description: 'CSS selector of a specific element to screenshot. Mutually exclusive with fullPage and clip.',
|
|
236
|
+
},
|
|
237
|
+
clip: {
|
|
238
|
+
type: 'object',
|
|
239
|
+
description: 'Region of the page to screenshot. Mutually exclusive with fullPage and selector.',
|
|
240
|
+
properties: {
|
|
241
|
+
x: { type: 'number', description: 'X coordinate of the top-left corner' },
|
|
242
|
+
y: { type: 'number', description: 'Y coordinate of the top-left corner' },
|
|
243
|
+
width: { type: 'number', description: 'Width in pixels' },
|
|
244
|
+
height: { type: 'number', description: 'Height in pixels' },
|
|
245
|
+
},
|
|
246
|
+
required: ['x', 'y', 'width', 'height'],
|
|
247
|
+
},
|
|
218
248
|
resultHandling: {
|
|
219
249
|
type: 'string',
|
|
220
250
|
enum: ['saveAndReturn', 'saveOnly'],
|
|
@@ -228,6 +258,8 @@ export function createRegisterTools(clientFactory) {
|
|
|
228
258
|
const client = getClient();
|
|
229
259
|
const screenshotResult = await client.screenshot({
|
|
230
260
|
fullPage: validated.fullPage,
|
|
261
|
+
selector: validated.selector,
|
|
262
|
+
clip: validated.clip,
|
|
231
263
|
});
|
|
232
264
|
// Get page metadata for the screenshot
|
|
233
265
|
const state = await client.getState();
|
|
@@ -237,6 +269,8 @@ export function createRegisterTools(clientFactory) {
|
|
|
237
269
|
pageUrl: state.currentUrl,
|
|
238
270
|
pageTitle: state.title,
|
|
239
271
|
fullPage: validated.fullPage ?? false,
|
|
272
|
+
selector: validated.selector,
|
|
273
|
+
clip: validated.clip,
|
|
240
274
|
});
|
|
241
275
|
const resultHandling = validated.resultHandling ?? 'saveAndReturn';
|
|
242
276
|
// Generate a name from the URI for the resource link
|
|
@@ -393,7 +427,7 @@ export function createRegisterTools(clientFactory) {
|
|
|
393
427
|
if (result.previousUrl) {
|
|
394
428
|
parts.push(`Browser navigated back to: ${result.previousUrl}`);
|
|
395
429
|
}
|
|
396
|
-
parts.push('Note:
|
|
430
|
+
parts.push('Note: Session state (cookies, localStorage) has been preserved where possible.');
|
|
397
431
|
if (previousVideoUri) {
|
|
398
432
|
parts.push(`Previous recording saved to: ${previousVideoUri}`);
|
|
399
433
|
}
|
|
@@ -451,7 +485,7 @@ export function createRegisterTools(clientFactory) {
|
|
|
451
485
|
const content = [];
|
|
452
486
|
content.push({
|
|
453
487
|
type: 'text',
|
|
454
|
-
text: `Recording stopped and saved.\nNote:
|
|
488
|
+
text: `Recording stopped and saved.\nNote: Session state (cookies, localStorage) has been preserved where possible.`,
|
|
455
489
|
});
|
|
456
490
|
content.push({
|
|
457
491
|
type: 'resource_link',
|