mcp-web-inspector 0.3.0 → 0.4.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.
@@ -19,6 +19,7 @@ export interface NetworkRequest {
19
19
  body: string | null;
20
20
  };
21
21
  }
22
+ type ColorSchemeOverride = 'light' | 'dark' | 'no-preference';
22
23
  /**
23
24
  * Sets the session configuration
24
25
  */
@@ -49,6 +50,8 @@ export declare function clearNetworkLog(): void;
49
50
  * @param newPage The Page object to set as the global page
50
51
  */
51
52
  export declare function setGlobalPage(newPage: Page): Promise<void>;
53
+ export declare function setColorSchemeOverride(scheme: ColorSchemeOverride | null): Promise<void>;
54
+ export declare function getColorSchemeOverride(): ColorSchemeOverride | null;
52
55
  interface BrowserSettings {
53
56
  viewport?: {
54
57
  width?: number;
@@ -5,6 +5,7 @@ import { getToolInstance, isBrowserTool, executeTool } from './tools/common/regi
5
5
  let browser;
6
6
  let page;
7
7
  let currentBrowserType = 'chromium';
8
+ let currentDevice;
8
9
  let networkLog = [];
9
10
  let sessionConfig = {
10
11
  saveSession: false,
@@ -12,6 +13,7 @@ let sessionConfig = {
12
13
  screenshotsDir: './.mcp-web-inspector/screenshots',
13
14
  headlessDefault: false,
14
15
  };
16
+ let colorSchemeOverride = null;
15
17
  /**
16
18
  * Sets the session configuration
17
19
  */
@@ -38,6 +40,7 @@ export function resetBrowserState() {
38
40
  browser = undefined;
39
41
  page = undefined;
40
42
  currentBrowserType = 'chromium';
43
+ currentDevice = undefined;
41
44
  networkLog = [];
42
45
  }
43
46
  /**
@@ -61,9 +64,34 @@ export async function setGlobalPage(newPage) {
61
64
  // Register console message handlers and network listeners for the new page
62
65
  await registerConsoleMessage(page);
63
66
  await registerNetworkListeners(page);
67
+ await applyColorScheme(page);
64
68
  page.bringToFront(); // Bring the new tab to the front
65
69
  console.log("Global page has been updated with listeners registered.");
66
70
  }
71
+ function getColorSchemeValue() {
72
+ return colorSchemeOverride;
73
+ }
74
+ async function applyColorScheme(targetPage) {
75
+ if (!targetPage) {
76
+ return;
77
+ }
78
+ try {
79
+ const scheme = getColorSchemeValue();
80
+ await targetPage.emulateMedia({ colorScheme: scheme ?? null });
81
+ }
82
+ catch (error) {
83
+ console.error("Failed to apply color scheme:", error);
84
+ }
85
+ }
86
+ export async function setColorSchemeOverride(scheme) {
87
+ colorSchemeOverride = scheme;
88
+ if (page && !page.isClosed()) {
89
+ await applyColorScheme(page);
90
+ }
91
+ }
92
+ export function getColorSchemeOverride() {
93
+ return colorSchemeOverride;
94
+ }
67
95
  /**
68
96
  * Device preset mapping to Playwright device descriptors
69
97
  */
@@ -282,6 +310,17 @@ export async function ensureBrowser(browserSettings) {
282
310
  // Reset browser and page references
283
311
  resetBrowserState();
284
312
  }
313
+ // Check if device preset has changed (requires browser restart)
314
+ if (browser && browserSettings?.device && browserSettings.device !== currentDevice) {
315
+ console.error(`Device preset changed from ${currentDevice || 'none'} to ${browserSettings.device}. Restarting browser...`);
316
+ try {
317
+ await browser.close().catch(err => console.error("Error closing browser on device change:", err));
318
+ }
319
+ catch (e) {
320
+ // Ignore errors when closing browser
321
+ }
322
+ resetBrowserState();
323
+ }
285
324
  // If browser exists and viewport settings changed, resize the viewport
286
325
  if (browser && page && !page.isClosed() && browserSettings?.viewport) {
287
326
  const { width, height } = browserSettings.viewport;
@@ -318,11 +357,16 @@ export async function ensureBrowser(browserSettings) {
318
357
  deviceConfig = CUSTOM_DEVICE_CONFIGS[playwrightDeviceName] || devices[playwrightDeviceName];
319
358
  if (deviceConfig) {
320
359
  console.error(`Using device preset: ${device} (${playwrightDeviceName})`);
360
+ currentDevice = device;
321
361
  }
322
362
  else {
323
363
  console.error(`Warning: Device preset ${playwrightDeviceName} not found`);
364
+ currentDevice = undefined;
324
365
  }
325
366
  }
367
+ else {
368
+ currentDevice = undefined;
369
+ }
326
370
  console.error(`Launching new ${browserType} browser instance...`);
327
371
  // Use the appropriate browser engine
328
372
  let browserInstance;
@@ -425,6 +469,7 @@ export async function ensureBrowser(browserSettings) {
425
469
  // Register console message handler and network listeners
426
470
  await registerConsoleMessage(page);
427
471
  await registerNetworkListeners(page);
472
+ await applyColorScheme(page);
428
473
  }
429
474
  // Verify page is still valid
430
475
  if (!page || page.isClosed()) {
@@ -435,7 +480,9 @@ export async function ensureBrowser(browserSettings) {
435
480
  // Re-register console message handler and network listeners
436
481
  await registerConsoleMessage(page);
437
482
  await registerNetworkListeners(page);
483
+ await applyColorScheme(page);
438
484
  }
485
+ await applyColorScheme(page);
439
486
  return page;
440
487
  }
441
488
  catch (error) {
@@ -458,6 +505,15 @@ export async function ensureBrowser(browserSettings) {
458
505
  const playwrightDeviceName = DEVICE_PRESETS[device];
459
506
  // Check custom configs first, then Playwright's built-in devices
460
507
  deviceConfig = CUSTOM_DEVICE_CONFIGS[playwrightDeviceName] || devices[playwrightDeviceName];
508
+ if (deviceConfig) {
509
+ currentDevice = device;
510
+ }
511
+ else {
512
+ currentDevice = undefined;
513
+ }
514
+ }
515
+ else {
516
+ currentDevice = undefined;
461
517
  }
462
518
  // Use the appropriate browser engine
463
519
  let browserInstance;
@@ -552,6 +608,7 @@ export async function ensureBrowser(browserSettings) {
552
608
  }
553
609
  await registerConsoleMessage(page);
554
610
  await registerNetworkListeners(page);
611
+ await applyColorScheme(page);
555
612
  return page;
556
613
  }
557
614
  }
@@ -12,11 +12,14 @@ export declare abstract class BrowserToolBase implements ToolHandler {
12
12
  */
13
13
  abstract execute(args: any, context: ToolContext): Promise<ToolResponse>;
14
14
  /**
15
- * Normalize selector shortcuts to full Playwright selectors
15
+ * Normalize selector shortcuts to full Playwright selectors and clean common escaping mistakes
16
16
  * - "testid:foo" → "[data-testid='foo']"
17
17
  * - "data-test:bar" → "[data-test='bar']"
18
18
  * - "data-cy:baz" → "[data-cy='baz']"
19
- * - Everything else pass through
19
+ * - Removes unnecessary backslash escapes from CSS selectors that LLMs often add
20
+ * - ".dark\\:bg-gray-700" → ".dark:bg-gray-700"
21
+ * - ".top-\\[36px\\]" → ".top-[36px]"
22
+ * - ".top-\\\\[36px\\\\]" → ".top-[36px]" (double-escaped)
20
23
  * @param selector The selector string
21
24
  * @returns Normalized selector
22
25
  */
@@ -8,11 +8,14 @@ export class BrowserToolBase {
8
8
  this.server = server;
9
9
  }
10
10
  /**
11
- * Normalize selector shortcuts to full Playwright selectors
11
+ * Normalize selector shortcuts to full Playwright selectors and clean common escaping mistakes
12
12
  * - "testid:foo" → "[data-testid='foo']"
13
13
  * - "data-test:bar" → "[data-test='bar']"
14
14
  * - "data-cy:baz" → "[data-cy='baz']"
15
- * - Everything else pass through
15
+ * - Removes unnecessary backslash escapes from CSS selectors that LLMs often add
16
+ * - ".dark\\:bg-gray-700" → ".dark:bg-gray-700"
17
+ * - ".top-\\[36px\\]" → ".top-[36px]"
18
+ * - ".top-\\\\[36px\\\\]" → ".top-[36px]" (double-escaped)
16
19
  * @param selector The selector string
17
20
  * @returns Normalized selector
18
21
  */
@@ -22,13 +25,22 @@ export class BrowserToolBase {
22
25
  'data-test:': 'data-test',
23
26
  'data-cy:': 'data-cy',
24
27
  };
28
+ // Handle testid shortcuts first
25
29
  for (const [prefix, attr] of Object.entries(prefixMap)) {
26
30
  if (selector.startsWith(prefix)) {
27
31
  const value = selector.slice(prefix.length);
28
32
  return `[${attr}="${value}"]`;
29
33
  }
30
34
  }
31
- return selector; // CSS, text=, etc. pass through
35
+ // Clean up common escaping mistakes that LLMs make in CSS selectors
36
+ // These characters don't need to be escaped in CSS selectors: [ ] :
37
+ let cleaned = selector;
38
+ // Remove backslash escapes before brackets and colons
39
+ // Handle both single escapes (\[) and double escapes (\\[)
40
+ cleaned = cleaned.replace(/\\+\[/g, '[');
41
+ cleaned = cleaned.replace(/\\+\]/g, ']');
42
+ cleaned = cleaned.replace(/\\+:/g, ':');
43
+ return cleaned;
32
44
  }
33
45
  /**
34
46
  * Ensures a page is available and returns it
@@ -140,7 +152,36 @@ export class BrowserToolBase {
140
152
  * @returns Object with { element: Locator, elementIndex: number, totalCount: number }
141
153
  */
142
154
  async selectPreferredLocator(locator, options) {
143
- const count = await locator.count();
155
+ let count;
156
+ try {
157
+ count = await locator.count();
158
+ }
159
+ catch (error) {
160
+ // Catch selector syntax errors and provide helpful guidance
161
+ const errorMsg = error.message;
162
+ const selector = options?.originalSelector || 'selector';
163
+ if (errorMsg.includes('Unexpected token') ||
164
+ errorMsg.includes('Invalid selector') ||
165
+ errorMsg.includes('SyntaxError') ||
166
+ errorMsg.includes('selector')) {
167
+ throw new Error(`Invalid CSS selector: "${selector}"\n\n` +
168
+ `Selector syntax error: ${errorMsg}\n\n` +
169
+ `💡 Tips:\n` +
170
+ ` • CSS selectors don't need backslash escapes for [ ] or :\n` +
171
+ ` • Use .class-name or #id without escaping\n` +
172
+ ` • For data attributes, use [data-attr="value"]\n` +
173
+ ` • For testid shortcuts, use testid:name\n\n` +
174
+ `Examples:\n` +
175
+ ` ✓ .dark:bg-gray-700\n` +
176
+ ` ✓ .top-[36px]\n` +
177
+ ` ✓ testid:submit-button\n` +
178
+ ` ✓ #login-form\n` +
179
+ ` ✗ .dark\\:bg-gray-700 (unnecessary escape)\n` +
180
+ ` ✗ .top-\\[36px\\] (unnecessary escape)`);
181
+ }
182
+ // Re-throw other errors as-is
183
+ throw error;
184
+ }
144
185
  if (count === 0) {
145
186
  throw new Error('No elements found');
146
187
  }
@@ -300,10 +300,11 @@ describe('GetHtmlTool', () => {
300
300
  expect(result.isError).toBe(false);
301
301
  expect(result.content[0].text).toContain('HTML content');
302
302
  });
303
- test('should respect maxLength parameter', async () => {
303
+ test('should respect maxLength parameter for small HTML', async () => {
304
304
  const args = { maxLength: 50 };
305
- const longHtml = '<div>' + 'A'.repeat(100) + '</div>';
306
- mockEvaluate.mockImplementationOnce(() => Promise.resolve(longHtml));
305
+ // Small HTML that won't trigger preview threshold
306
+ const smallHtml = '<div>' + 'A'.repeat(100) + '</div>';
307
+ mockEvaluate.mockImplementationOnce(() => Promise.resolve(smallHtml));
307
308
  const result = await visibleHtmlTool.execute(args, mockContext);
308
309
  expect(result.isError).toBe(false);
309
310
  expect(result.content[0].text).toContain('Output truncated due to size limits');
@@ -378,4 +379,107 @@ describe('GetHtmlTool', () => {
378
379
  expect(result.content[0].text).toContain('Failed to get visible HTML content');
379
380
  expect(result.content[0].text).toContain('Content retrieval failed');
380
381
  });
382
+ test('should return preview with token for large HTML (≥2000 chars)', async () => {
383
+ const args = {};
384
+ // Large HTML that exceeds preview threshold
385
+ const largeHtml = '<div>' + 'X'.repeat(3000) + '</div>';
386
+ mockEvaluate.mockImplementationOnce(() => Promise.resolve(largeHtml));
387
+ const result = await visibleHtmlTool.execute(args, mockContext);
388
+ expect(result.isError).toBe(false);
389
+ expect(result.content[0].text).toContain('HTML size:');
390
+ expect(result.content[0].text).toContain('exceeds 2000 char threshold');
391
+ expect(result.content[0].text).toContain('Preview (first 500 chars)');
392
+ expect(result.content[0].text).toContain('⚠️ Full HTML not returned to save tokens');
393
+ expect(result.content[0].text).toContain('💡 RECOMMENDED: Use token-efficient alternatives');
394
+ expect(result.content[0].text).toContain('inspect_dom()');
395
+ expect(result.content[0].text).toMatch(/confirmToken: "[\w\d]{8}"/);
396
+ });
397
+ test('should return full HTML with valid confirmToken', async () => {
398
+ const args = {};
399
+ // Large HTML
400
+ const largeHtml = '<div>' + 'Y'.repeat(3000) + '</div>';
401
+ mockEvaluate.mockImplementation(() => Promise.resolve(largeHtml));
402
+ // First call - get preview and token
403
+ const previewResult = await visibleHtmlTool.execute(args, mockContext);
404
+ expect(previewResult.isError).toBe(false);
405
+ // Extract token from preview
406
+ const tokenMatch = previewResult.content[0].text.match(/confirmToken: "([\w\d]{8})"/);
407
+ expect(tokenMatch).toBeTruthy();
408
+ const token = tokenMatch[1];
409
+ // Second call - with token
410
+ const fullResult = await visibleHtmlTool.execute({ confirmToken: token }, mockContext);
411
+ expect(fullResult.isError).toBe(false);
412
+ expect(fullResult.content[0].text).toContain(largeHtml);
413
+ expect(fullResult.content[0].text).not.toContain('Preview (first 500 chars)');
414
+ expect(fullResult.content[0].text).toContain('💡 TIP: If you need structured inspection');
415
+ });
416
+ test('should generate new token for invalid confirmToken', async () => {
417
+ const args = { confirmToken: 'invalid123' };
418
+ // Large HTML
419
+ const largeHtml = '<div>' + 'Z'.repeat(3000) + '</div>';
420
+ mockEvaluate.mockImplementationOnce(() => Promise.resolve(largeHtml));
421
+ const result = await visibleHtmlTool.execute(args, mockContext);
422
+ expect(result.isError).toBe(false);
423
+ expect(result.content[0].text).toContain('HTML size:');
424
+ expect(result.content[0].text).toContain('Preview (first 500 chars)');
425
+ expect(result.content[0].text).toMatch(/confirmToken: "[\w\d]{8}"/);
426
+ // Should not return full HTML
427
+ expect(result.content[0].text).not.toContain('Z'.repeat(3000));
428
+ });
429
+ test('should return small HTML directly without token requirement', async () => {
430
+ const args = {};
431
+ // Small HTML below threshold
432
+ const smallHtml = '<div>Small content with 1500 chars: ' + 'A'.repeat(1400) + '</div>';
433
+ mockEvaluate.mockImplementationOnce(() => Promise.resolve(smallHtml));
434
+ const result = await visibleHtmlTool.execute(args, mockContext);
435
+ expect(result.isError).toBe(false);
436
+ expect(result.content[0].text).toContain(smallHtml);
437
+ expect(result.content[0].text).not.toContain('confirmToken:');
438
+ expect(result.content[0].text).not.toContain('Preview (first 500 chars)');
439
+ expect(result.content[0].text).toContain('💡 TIP: If you need structured inspection');
440
+ });
441
+ test('should consume token only once (one-time use)', async () => {
442
+ const args = {};
443
+ // Large HTML
444
+ const largeHtml = '<div>' + 'M'.repeat(3000) + '</div>';
445
+ mockEvaluate.mockImplementation(() => Promise.resolve(largeHtml));
446
+ // First call - get token
447
+ const previewResult = await visibleHtmlTool.execute(args, mockContext);
448
+ const tokenMatch = previewResult.content[0].text.match(/confirmToken: "([\w\d]{8})"/);
449
+ const token = tokenMatch[1];
450
+ // Second call - use token (should work)
451
+ const fullResult = await visibleHtmlTool.execute({ confirmToken: token }, mockContext);
452
+ expect(fullResult.isError).toBe(false);
453
+ expect(fullResult.content[0].text).toContain(largeHtml);
454
+ // Third call - try to reuse same token (should fail, get new preview)
455
+ const retryResult = await visibleHtmlTool.execute({ confirmToken: token }, mockContext);
456
+ expect(retryResult.isError).toBe(false);
457
+ expect(retryResult.content[0].text).toContain('Preview (first 500 chars)');
458
+ expect(retryResult.content[0].text).toMatch(/confirmToken: "[\w\d]{8}"/);
459
+ // Should have different token
460
+ const newTokenMatch = retryResult.content[0].text.match(/confirmToken: "([\w\d]{8})"/);
461
+ expect(newTokenMatch[1]).not.toBe(token);
462
+ });
463
+ test('should handle large HTML with selector and confirmToken', async () => {
464
+ const largeHtml = '<section>' + 'S'.repeat(3000) + '</section>';
465
+ const args = { selector: 'testid:large-section' };
466
+ mockLocator.mockImplementation(() => ({}));
467
+ const mockElement = {
468
+ evaluate: jest.fn(async () => largeHtml),
469
+ };
470
+ const selectSpy = jest
471
+ .spyOn(visibleHtmlTool, 'selectPreferredLocator')
472
+ .mockResolvedValue({ element: mockElement, elementIndex: 0, totalCount: 1 });
473
+ mockEvaluate.mockImplementation(() => Promise.resolve(largeHtml));
474
+ // First call - get preview
475
+ const previewResult = await visibleHtmlTool.execute(args, mockContext);
476
+ expect(previewResult.content[0].text).toContain('(from "testid:large-section")');
477
+ const tokenMatch = previewResult.content[0].text.match(/confirmToken: "([\w\d]{8})"/);
478
+ const token = tokenMatch[1];
479
+ // Second call - with token
480
+ const fullResult = await visibleHtmlTool.execute({ selector: 'testid:large-section', confirmToken: token }, mockContext);
481
+ expect(fullResult.isError).toBe(false);
482
+ expect(fullResult.content[0].text).toContain(largeHtml);
483
+ selectSpy.mockRestore();
484
+ });
381
485
  });
@@ -4,6 +4,7 @@ import { ToolContext, ToolResponse, ToolMetadata, SessionConfig } from '../../co
4
4
  * Tool for getting HTML from the page
5
5
  */
6
6
  export declare class GetHtmlTool extends BrowserToolBase {
7
+ private confirmTokens;
7
8
  static getMetadata(sessionConfig?: SessionConfig): ToolMetadata;
8
9
  execute(args: any, context: ToolContext): Promise<ToolResponse>;
9
10
  }
@@ -4,10 +4,14 @@ import { createSuccessResponse, createErrorResponse, } from '../../common/types.
4
4
  * Tool for getting HTML from the page
5
5
  */
6
6
  export class GetHtmlTool extends BrowserToolBase {
7
+ constructor() {
8
+ super(...arguments);
9
+ this.confirmTokens = new Map(); // Stores tokens for two-step confirmation
10
+ }
7
11
  static getMetadata(sessionConfig) {
8
12
  return {
9
13
  name: "get_html",
10
- description: "⚠️ RARELY NEEDED: Get raw HTML markup from the page (no rendering, just source code). Most tasks need structured inspection instead. ONLY use get_html for: (1) checking specific HTML attributes or element nesting, (2) analyzing markup structure, (3) debugging SSR/HTML issues. For structured tasks, use: inspect_dom() to understand page structure with positions, query_selector() to find and inspect elements, get_computed_styles() for CSS values. Returns HTML up to 20000 chars (truncated if longer), scripts removed by default for security/size. Supports testid shortcuts.",
14
+ description: "⚠️ RARELY NEEDED: Get raw HTML markup from the page (no rendering, just source code). Most tasks need structured inspection instead. ONLY use get_html for: (1) checking specific HTML attributes or element nesting, (2) analyzing markup structure, (3) debugging SSR/HTML issues. For structured tasks, use: inspect_dom() to understand page structure with positions, query_selector() to find and inspect elements, get_computed_styles() for CSS values. Auto-returns HTML if <2000 chars (small elements), shows preview with token-based confirmation if larger. Scripts removed by default for security/size. Supports testid shortcuts.",
11
15
  inputSchema: {
12
16
  type: "object",
13
17
  properties: {
@@ -26,6 +30,10 @@ export class GetHtmlTool extends BrowserToolBase {
26
30
  maxLength: {
27
31
  type: "number",
28
32
  description: "Maximum number of characters to return (default: 20000)"
33
+ },
34
+ confirmToken: {
35
+ type: "string",
36
+ description: "Confirmation token from preview response (required to retrieve large HTML). Get this token by calling without confirmToken first - the preview will include the token to use."
29
37
  }
30
38
  },
31
39
  required: [],
@@ -37,6 +45,8 @@ export class GetHtmlTool extends BrowserToolBase {
37
45
  ? Math.floor(args.maxLength)
38
46
  : 20000;
39
47
  const clean = args.clean ?? false;
48
+ const confirmToken = args.confirmToken;
49
+ const PREVIEW_THRESHOLD = 2000;
40
50
  if (!context.page) {
41
51
  return createErrorResponse('Page is not available');
42
52
  }
@@ -58,6 +68,7 @@ export class GetHtmlTool extends BrowserToolBase {
58
68
  const locator = page.locator(normalizedSelector);
59
69
  const { element, elementIndex, totalCount } = await this.selectPreferredLocator(locator, {
60
70
  elementIndex: args.elementIndex,
71
+ originalSelector: args.selector,
61
72
  });
62
73
  selectionWarning = this.formatElementSelectionInfo(args.selector, elementIndex, totalCount, true);
63
74
  rawHtml = await element.evaluate((target) => {
@@ -99,6 +110,40 @@ export class GetHtmlTool extends BrowserToolBase {
99
110
  const safeMaxLength = requestedMaxLength > 0 ? requestedMaxLength : 20000;
100
111
  const processedHtml = sanitizedHtml ?? '';
101
112
  const originalLength = processedHtml.length;
113
+ // Generate key for this HTML request
114
+ const tokenKey = `${args.selector || 'page'}:${originalLength}`;
115
+ // Check if HTML is too large
116
+ if (originalLength >= PREVIEW_THRESHOLD) {
117
+ // Verify if confirmToken matches
118
+ const expectedToken = this.confirmTokens.get(tokenKey);
119
+ if (confirmToken && expectedToken && confirmToken === expectedToken) {
120
+ // Valid token - delete it (one-time use) and proceed to return HTML
121
+ this.confirmTokens.delete(tokenKey);
122
+ }
123
+ else {
124
+ // No token or invalid token - show preview and generate new token
125
+ const newToken = Math.random().toString(36).substring(2, 10);
126
+ this.confirmTokens.set(tokenKey, newToken);
127
+ lines.push(`HTML size: ${originalLength.toLocaleString()} characters (exceeds ${PREVIEW_THRESHOLD} char threshold)`);
128
+ lines.push('');
129
+ lines.push('Preview (first 500 chars):');
130
+ lines.push(processedHtml.slice(0, 500));
131
+ if (originalLength > 500) {
132
+ lines.push('...');
133
+ }
134
+ lines.push('');
135
+ lines.push('⚠️ Full HTML not returned to save tokens (~' + Math.round(originalLength / 3) + ' tokens)');
136
+ lines.push('');
137
+ lines.push('💡 RECOMMENDED: Use token-efficient alternatives:');
138
+ lines.push(' • inspect_dom() - structured view with positions and layout');
139
+ lines.push(' • query_selector_all() - find specific elements');
140
+ lines.push(' • get_computed_styles() - CSS values for debugging');
141
+ lines.push('');
142
+ lines.push(`To get full HTML anyway, call again with: confirmToken: "${newToken}"`);
143
+ return createSuccessResponse(lines.join('\n'));
144
+ }
145
+ }
146
+ // Return full HTML (either small or explicitly requested)
102
147
  let displayHtml = processedHtml;
103
148
  const truncated = displayHtml.length > safeMaxLength;
104
149
  if (truncated) {
@@ -53,6 +53,7 @@ export class GetTextTool extends BrowserToolBase {
53
53
  const locator = page.locator(normalizedSelector);
54
54
  const { element, elementIndex, totalCount } = await this.selectPreferredLocator(locator, {
55
55
  elementIndex: args.elementIndex,
56
+ originalSelector: args.selector,
56
57
  });
57
58
  selectionWarning = this.formatElementSelectionInfo(args.selector, elementIndex, totalCount, true);
58
59
  textContent = await element.evaluate((target) => {
@@ -14,7 +14,7 @@ export class ScreenshotTool extends BrowserToolBase {
14
14
  const screenshotsDir = sessionConfig?.screenshotsDir || './.mcp-web-inspector/screenshots';
15
15
  return {
16
16
  name: "screenshot",
17
- description: `⚠️ RARELY NEEDED: Screenshots are NOT useful for LLMs to analyze layouts, margins, or alignment issues. Use inspect_dom(), compare_positions(), or measure_element() instead - they provide precise numerical data. Only take screenshots for: (1) sharing with humans for visual confirmation, (2) documenting test results, or (3) verifying colors/images. Screenshots are saved to ${screenshotsDir}. Example: { name: "login-page", fullPage: true } or { name: "submit-btn", selector: "testid:submit" }`,
17
+ description: `📸 VISUAL OUTPUT TOOL - Captures page/element appearance and saves to file. Essential for: visual regression testing, sharing with humans, confirming UI appearance (colors/fonts/images). ⚠️ NOT for layout debugging (positions/sizes/alignment/margins) - use inspect_dom/compare_positions/inspect_ancestors/get_computed_styles instead (structural data is token-efficient, screenshots require ~1,500 tokens to read). Screenshots saved to ${screenshotsDir}. Example: { name: "login-page", fullPage: true } or { name: "submit-btn", selector: "testid:submit" }`,
18
18
  inputSchema: {
19
19
  type: "object",
20
20
  properties: {
@@ -82,9 +82,13 @@ export class ScreenshotTool extends BrowserToolBase {
82
82
  });
83
83
  messages.push(`Screenshot also stored in memory with name: '${args.name || 'screenshot'}'`);
84
84
  }
85
+ // Add token cost warning
86
+ messages.push('');
87
+ messages.push('📸 Open the file in your IDE to view the screenshot');
88
+ messages.push('⚠️ Reading the image file consumes ~1,500 tokens - only use Read tool if visual analysis is essential');
85
89
  // Add actionable guidance based on screenshot context
86
90
  messages.push('');
87
- messages.push('💡 To debug layout issues in this screenshot:');
91
+ messages.push('💡 To debug layout issues without reading the screenshot:');
88
92
  if (args.selector) {
89
93
  messages.push(` inspect_ancestors({ selector: "${args.selector}" })`);
90
94
  messages.push(' → See parent constraints (width, margins, overflow, borders)');
@@ -36,6 +36,7 @@ export class CheckVisibilityTool extends BrowserToolBase {
36
36
  // Use standard element selection with visibility preference
37
37
  const { element, elementIndex, totalCount } = await this.selectPreferredLocator(locator, {
38
38
  elementIndex: args.elementIndex,
39
+ originalSelector: args.selector,
39
40
  });
40
41
  // Format selection warning if multiple elements matched
41
42
  const multipleMatchWarning = this.formatElementSelectionInfo(args.selector, elementIndex, totalCount);
@@ -44,6 +44,7 @@ export class GetComputedStylesTool extends BrowserToolBase {
44
44
  const locator = page.locator(normalizedSelector);
45
45
  const { element, elementIndex, totalCount } = await this.selectPreferredLocator(locator, {
46
46
  elementIndex: args.elementIndex,
47
+ originalSelector: args.selector,
47
48
  });
48
49
  // Format selection warning if multiple elements matched
49
50
  const warning = this.formatElementSelectionInfo(args.selector, elementIndex, totalCount);
@@ -46,7 +46,9 @@ export class InspectAncestorsTool extends BrowserToolBase {
46
46
  isError: true,
47
47
  };
48
48
  }
49
- const { element, elementIndex, totalCount } = await this.selectPreferredLocator(locator);
49
+ const { element, elementIndex, totalCount } = await this.selectPreferredLocator(locator, {
50
+ originalSelector: args.selector,
51
+ });
50
52
  // Use the selected element for ancestor traversal
51
53
  const ancestors = await element.evaluate((el, lim) => {
52
54
  const chain = [];
@@ -78,7 +78,9 @@ More efficient than get_html() or evaluate(). Supports testid shortcuts.`,
78
78
  if (count === 0) {
79
79
  return createErrorResponse(`Element not found: ${args.selector || 'body'}`);
80
80
  }
81
- const { element, elementIndex, totalCount } = await this.selectPreferredLocator(locator);
81
+ const { element, elementIndex, totalCount } = await this.selectPreferredLocator(locator, {
82
+ originalSelector: args.selector || 'body',
83
+ });
82
84
  // Get the target element and its semantic children
83
85
  const inspectionData = await element.evaluate((target, { hidden, max, maxDepth }) => {
84
86
  // Helper to check if element is visible
@@ -27,6 +27,7 @@ export class MeasureElementTool extends BrowserToolBase {
27
27
  const locator = page.locator(normalizedSelector);
28
28
  const { element, elementIndex, totalCount } = await this.selectPreferredLocator(locator, {
29
29
  elementIndex: args.elementIndex,
30
+ originalSelector: args.selector,
30
31
  });
31
32
  // Format selection warning if multiple elements matched
32
33
  const warning = this.formatElementSelectionInfo(args.selector, elementIndex, totalCount);
@@ -1 +1,2 @@
1
1
  export { CloseTool } from './close.js';
2
+ export { SetColorSchemeTool } from './set_color_scheme.js';
@@ -1 +1,2 @@
1
1
  export { CloseTool } from './close.js';
2
+ export { SetColorSchemeTool } from './set_color_scheme.js';
@@ -0,0 +1,9 @@
1
+ import { BrowserToolBase } from '../base.js';
2
+ import { ToolContext, ToolMetadata, ToolResponse, SessionConfig } from '../../common/types.js';
3
+ /**
4
+ * Tool for setting prefers-color-scheme emulation
5
+ */
6
+ export declare class SetColorSchemeTool extends BrowserToolBase {
7
+ static getMetadata(_sessionConfig?: SessionConfig): ToolMetadata;
8
+ execute(args: any, context: ToolContext): Promise<ToolResponse>;
9
+ }
@@ -0,0 +1,52 @@
1
+ import { BrowserToolBase } from '../base.js';
2
+ import { createErrorResponse, createSuccessResponse, } from '../../common/types.js';
3
+ function normalizeScheme(rawValue) {
4
+ if (typeof rawValue !== 'string') {
5
+ return null;
6
+ }
7
+ const normalized = rawValue.trim().toLowerCase();
8
+ if (normalized === 'dark' ||
9
+ normalized === 'light' ||
10
+ normalized === 'system' ||
11
+ normalized === 'no-preference') {
12
+ return normalized;
13
+ }
14
+ return null;
15
+ }
16
+ /**
17
+ * Tool for setting prefers-color-scheme emulation
18
+ */
19
+ export class SetColorSchemeTool extends BrowserToolBase {
20
+ static getMetadata(_sessionConfig) {
21
+ return {
22
+ name: "set_color_scheme",
23
+ description: "Set the browser color scheme that controls CSS prefers-color-scheme. Defaults to system appearance. Use before inspecting colors or taking screenshots. Options: system (clear override to follow OS/browser setting), dark, light, no-preference (simulate agents with no declared preference). Returns confirmation of the active scheme.",
24
+ inputSchema: {
25
+ type: "object",
26
+ properties: {
27
+ scheme: {
28
+ type: "string",
29
+ description: "Color scheme to emulate: 'system', 'dark', 'light', or 'no-preference'. Example: { scheme: 'dark' }",
30
+ },
31
+ },
32
+ required: ["scheme"],
33
+ },
34
+ };
35
+ }
36
+ async execute(args, context) {
37
+ const schemeInput = normalizeScheme(args.scheme);
38
+ if (!schemeInput) {
39
+ return createErrorResponse(`Invalid scheme "${args.scheme}". Use one of: system, dark, light, no-preference.`);
40
+ }
41
+ return this.safeExecute(context, async () => {
42
+ const { setColorSchemeOverride, getColorSchemeOverride } = await import('../../../toolHandler.js');
43
+ const override = schemeInput === 'system'
44
+ ? null
45
+ : schemeInput;
46
+ await setColorSchemeOverride(override);
47
+ const active = getColorSchemeOverride();
48
+ const label = active ?? 'system';
49
+ return createSuccessResponse(`Color scheme set to ${label}.`);
50
+ });
51
+ }
52
+ }
@@ -6,6 +6,7 @@ import { ScrollToElementTool } from './navigation/scroll_to_element.js';
6
6
  import { ScrollByTool } from './navigation/scroll_by.js';
7
7
  // Lifecycle
8
8
  import { CloseTool } from './lifecycle/close.js';
9
+ import { SetColorSchemeTool } from './lifecycle/set_color_scheme.js';
9
10
  // Interaction
10
11
  import { ClickTool } from './interaction/click.js';
11
12
  import { FillTool } from './interaction/fill.js';
@@ -40,13 +41,15 @@ import { GetRequestDetailsTool } from './network/get_request_details.js';
40
41
  import { WaitForElementTool } from './waiting/wait_for_element.js';
41
42
  import { WaitForNetworkIdleTool } from './waiting/wait_for_network_idle.js';
42
43
  export const BROWSER_TOOL_CLASSES = [
43
- // Navigation (6)
44
+ // Navigation (5)
44
45
  NavigateTool,
45
46
  GoBackTool,
46
47
  GoForwardTool,
47
48
  ScrollToElementTool,
48
49
  ScrollByTool,
50
+ // Lifecycle (2)
49
51
  CloseTool,
52
+ SetColorSchemeTool,
50
53
  // Interaction (7)
51
54
  ClickTool,
52
55
  FillTool,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcp-web-inspector",
3
- "version": "0.3.0",
3
+ "version": "0.4.1",
4
4
  "description": "Web Inspector MCP: Give LLMs visual superpowers to see, debug, and test any web page.",
5
5
  "license": "MIT",
6
6
  "author": "Anton",