playwright-stealth-mcp-server 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md 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. **Browser state (cookies, localStorage, sessionStorage) is lost** when recording starts. If you need to be logged in during the recording, authenticate again after starting.
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). **Browser state is lost** when recording stops — the browser navigates back to the previous URL automatically.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "playwright-stealth-mcp-server",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
4
4
  "description": "Local implementation of Playwright Stealth MCP server",
5
5
  "mcpName": "com.pulsemcp.servers/playwright-stealth",
6
6
  "main": "build/index.js",
@@ -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(): Promise<ScreenshotResult>;
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==',
@@ -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
- * Note: Cookies, localStorage, and sessionStorage are lost when the context is recycled.
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
- * Note: Cookies, localStorage, and sessionStorage are lost when the context is recycled.
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
- // Apply timeout configuration to Playwright
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.page.setDefaultTimeout(this.config.timeout);
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
- this.page = await this.context.newPage();
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.object({
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 or full page as a PNG image. Screenshots are saved to filesystem storage and can be accessed later via MCP resources.
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
- 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.
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
- **Important: Browser state is lost when recording starts.** Starting a recording closes the current browser context and creates a new one. This means:
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
- **Important: Browser state is lost when recording stops.** Stopping a recording closes the recording context and creates a new one. This means:
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: Cookies and session storage have been cleared. Log in again if needed.');
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: Cookies and session storage have been cleared. Log in again if needed.`,
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',