mcp-web-inspector 0.1.2 โ†’ 0.1.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
@@ -10,6 +10,7 @@ Modern web applications are complex. Elements are hidden, layouts break, selecto
10
10
 
11
11
  - ๐Ÿ” **Understand any page structure** - Progressive DOM inspection that drills through wrapper divs to find semantic elements
12
12
  - ๐ŸŽฏ **Debug visibility issues** - Detailed diagnostics showing exactly why clicks fail (clipped, covered, scrolled out of view)
13
+ - ๐Ÿ”ผ **Trace layout constraints** - Walk up the DOM tree to find where unexpected margins, width limits, and overflow clipping come from
13
14
  - ๐Ÿ“ **Validate layouts** - Compare element positions to ensure consistent alignment and spacing
14
15
  - ๐Ÿงช **Test selector reliability** - See all matching elements with their visibility status before writing tests
15
16
  - ๐ŸŽจ **Inspect styles** - Get computed CSS to understand why elements behave unexpectedly
@@ -139,6 +140,17 @@ code-insiders --add-mcp '{"name":"web-inspector","command":"npx","args":["mcp-we
139
140
  4. MCP only works in **Agent mode** - switch to agent mode in the chat interface
140
141
  5. Open the `mcp.json` file and click the **"Start"** button next to the server
141
142
 
143
+ ### First-Time Browser Setup
144
+
145
+ When you first use the server with `npx`, Playwright browsers will be **automatically installed** on first tool use if not already present. The installation happens once and browsers are stored in your home directory, shared across all projects.
146
+
147
+ If automatic installation doesn't work (firewall, permissions, etc.), you'll see clear instructions to run:
148
+ ```bash
149
+ npx playwright install chromium firefox webkit
150
+ ```
151
+
152
+ Then restart VS Code to use the server.
153
+
142
154
  ### Note about Embedded Browser
143
155
 
144
156
  GitHub Copilot and VS Code may have an embedded browser feature. If you experience conflicts or prefer using Web Inspector MCP for all web inspection tasks, you may want to disable the built-in browser:
@@ -458,6 +470,32 @@ Compare positions and alignment of two elements. Validates if elements are align
458
470
  - Validating grid layouts
459
471
  - Checking responsive design consistency
460
472
 
473
+ #### `inspect_ancestors` โญ **DEBUG LAYOUT CONSTRAINTS**
474
+ Walk up the DOM tree to find where width constraints, margins, borders, and overflow clipping come from. Shows position, size, and layout-critical CSS for each ancestor up to `<body>`.
475
+
476
+ **Key Features:**
477
+ - Default depth: 10 levels (reaches `<body>` in most React apps)
478
+ - Only shows non-default values (omits `border:none`, `overflow:visible`)
479
+ - Auto-detects overflow:hidden clipping, width constraints, auto-margin centering
480
+ - Token-efficient compact text format with diagnostic annotations (๐ŸŽฏโš ๏ธ)
481
+
482
+ **Use Cases:**
483
+ - Finding where unexpected margins come from (auto-centering)
484
+ - Discovering parent max-width constraints
485
+ - Locating overflow:hidden containers that clip elements
486
+ - Understanding why elements have constrained widths
487
+ - Debugging deeply nested component library layouts (Material-UI, Chakra, Ant Design)
488
+
489
+ **Example:**
490
+ ```
491
+ inspect_ancestors({ selector: "testid:header" })
492
+ โ†’ Shows: [0] header (896px, margins: 160px)
493
+ [1] div (1216px, max-w constraint)
494
+ [2] body (1920px, overflow-x: hidden)
495
+ ๐ŸŽฏ WIDTH CONSTRAINT found at parent
496
+ โš  Auto margins centering (160px each side)
497
+ ```
498
+
461
499
  ### ๐ŸŽจ Style & Content Inspection
462
500
 
463
501
  #### `get_computed_styles`
@@ -785,6 +823,32 @@ These step-by-step recipes show how to chain tools together for common testing a
785
823
 
786
824
  **Why this works**: Progressive inspection + precise measurements reveal layout problems.
787
825
 
826
+ ### Recipe 2a: Finding Where Unexpected Margins Come From
827
+
828
+ ```
829
+ 1. navigate({ url: "https://app.example.com" })
830
+ 2. inspect_dom({ selector: "main" })
831
+ โ†’ Shows header element with test ID
832
+ 3. measure_element({ selector: "testid:event-mode-header" })
833
+ โ†’ @ (160,0) 896x56px
834
+ โ†’ Margin: โ†160px โ†’160px (unexpected!)
835
+ ๐Ÿ’ก Unexpected spacing detected. Check parent constraints
836
+ 4. inspect_ancestors({ selector: "testid:event-mode-header" })
837
+ โ†’ [0] <header testid:event-mode-header>
838
+ @ (160,0) 896x56px | w:896px max-w:896px m:0 160px
839
+ border-bottom: 1px solid #e5e7eb
840
+ โš  Auto margins centering (160px each side)
841
+ โ†’ [1] <div>
842
+ @ (0,0) 1216x56px | w:1216px
843
+ โ†’ [2] <div> flex max-w-[1600px]
844
+ @ (352,60) 1216x900px
845
+ max-width: 1600px
846
+ ๐ŸŽฏ WIDTH CONSTRAINT
847
+ 5. Solution: Remove mx-auto from header (centering already handled by parent)
848
+ ```
849
+
850
+ **Why this works**: `inspect_ancestors` traces the layout constraint chain to find the root cause of unexpected spacing in deeply nested React components.
851
+
788
852
  ### Recipe 3: API Response Testing
789
853
 
790
854
  ```
@@ -953,6 +1017,71 @@ These step-by-step recipes show how to chain tools together for common testing a
953
1017
 
954
1018
  **Why this works**: DOM inspection + attribute queries reveal accessibility issues.
955
1019
 
1020
+ ## Troubleshooting
1021
+
1022
+ ### Browser Installation Issues
1023
+
1024
+ **Symptom**: Error message about Playwright browsers not being installed, or browser fails to launch.
1025
+
1026
+ **How it works**: Browsers are **automatically installed on first use** when you run any navigation tool. The installation happens once (~1GB download) and browsers are stored in your home directory, shared across all projects.
1027
+
1028
+ **What you'll see on first use**:
1029
+ ```
1030
+ ๐ŸŽญ Playwright browsers not found. Installing automatically...
1031
+ โณ This will download ~1GB of browser binaries. Please wait...
1032
+ [Installation progress...]
1033
+ โœ… Browsers installed successfully! Starting browser...
1034
+ ```
1035
+
1036
+ **If automatic installation fails** (firewall, permissions, etc.):
1037
+
1038
+ ```bash
1039
+ # Manual installation - run this command:
1040
+ npx playwright install chromium firefox webkit
1041
+
1042
+ # With system dependencies (requires admin/sudo):
1043
+ npx playwright install --with-deps chromium firefox webkit
1044
+ ```
1045
+
1046
+ **For GitHub Copilot / VS Code users**:
1047
+ - First tool use will auto-install browsers (1-2 minute wait)
1048
+ - Subsequent uses are instant
1049
+ - Installation happens in the background with progress messages
1050
+ - If manual installation is needed, restart your IDE after running the command
1051
+
1052
+ ### Server Not Loading
1053
+
1054
+ **Symptom**: Tools from web-inspector are not available in your AI assistant.
1055
+
1056
+ **Solutions**:
1057
+ 1. Verify the configuration file is correct (see AI Tool Setup section above)
1058
+ 2. Restart your AI tool completely (not just reload window)
1059
+ 3. Check server logs (location depends on your AI tool)
1060
+ 4. Try removing and re-adding the server configuration
1061
+
1062
+ ### Permission Issues
1063
+
1064
+ **Symptom**: Permission denied errors when installing browsers.
1065
+
1066
+ **Solutions**:
1067
+ ```bash
1068
+ # If using global installation, you may need sudo (Linux/macOS)
1069
+ sudo npm install -g mcp-web-inspector
1070
+
1071
+ # Or use npx without global installation (recommended)
1072
+ # Just configure with "npx -y mcp-web-inspector" as shown in setup
1073
+ ```
1074
+
1075
+ ### Browser Crashes or Disconnects
1076
+
1077
+ **Symptom**: Browser becomes unresponsive or disconnects during use.
1078
+
1079
+ **The MCP server automatically handles this**:
1080
+ - Detects disconnected browsers
1081
+ - Resets state and provides clear error messages
1082
+ - Instructs you to retry the navigation/action
1083
+ - No manual intervention needed - just retry your command
1084
+
956
1085
  ## Development
957
1086
 
958
1087
  ### Testing
@@ -1,5 +1,6 @@
1
1
  import { chromium, firefox, webkit, devices } from 'playwright';
2
2
  import { BROWSER_TOOLS } from './tools.js';
3
+ import { checkBrowsersInstalled, getInstallationInstructions } from './utils/browserCheck.js';
3
4
  import { ScreenshotTool, NavigationTool, CloseBrowserTool, ConsoleLogsTool } from './tools/browser/index.js';
4
5
  import { ClickTool, FillTool, SelectTool, HoverTool, EvaluateTool, UploadFileTool } from './tools/browser/interaction.js';
5
6
  import { VisibleTextTool, VisibleHtmlTool } from './tools/browser/visiblePage.js';
@@ -12,6 +13,7 @@ import { GetComputedStylesTool } from './tools/browser/computedStyles.js';
12
13
  import { MeasureElementTool } from './tools/browser/measureElement.js';
13
14
  import { ElementExistsTool } from './tools/browser/elementExists.js';
14
15
  import { CompareElementAlignmentTool } from './tools/browser/compareElementAlignment.js';
16
+ import { InspectAncestorsTool } from './tools/browser/ancestorInspection.js';
15
17
  import { GoBackTool, GoForwardTool } from './tools/browser/navigation.js';
16
18
  import { DragTool, PressKeyTool } from './tools/browser/interaction.js';
17
19
  import { WaitForElementTool } from './tools/browser/waitForElement.js';
@@ -97,6 +99,7 @@ let getComputedStylesTool;
97
99
  let measureElementTool;
98
100
  let elementExistsTool;
99
101
  let compareElementAlignmentTool;
102
+ let inspectAncestorsTool;
100
103
  let waitForElementTool;
101
104
  let waitForNetworkIdleTool;
102
105
  let listNetworkRequestsTool;
@@ -162,13 +165,21 @@ async function registerConsoleMessage(page) {
162
165
  page.on("console", (msg) => {
163
166
  if (consoleLogsTool) {
164
167
  const type = msg.type();
165
- const text = msg.text();
168
+ let text = msg.text();
166
169
  // "Unhandled Rejection In Promise" we injected
167
170
  if (text.startsWith("[Playwright]")) {
168
171
  const payload = text.replace("[Playwright]", "");
169
172
  consoleLogsTool.registerConsoleMessage("exception", payload);
170
173
  }
171
174
  else {
175
+ // Truncate stack traces for error messages to keep output compact
176
+ if (type === 'error' && text.includes('\n')) {
177
+ const lines = text.split('\n');
178
+ // Keep first line (error message) and up to 3 stack trace lines
179
+ if (lines.length > 4) {
180
+ text = lines.slice(0, 4).join('\n') + '\n ...[stack trace truncated]';
181
+ }
182
+ }
172
183
  consoleLogsTool.registerConsoleMessage(type, text);
173
184
  }
174
185
  }
@@ -200,11 +211,66 @@ async function registerConsoleMessage(page) {
200
211
  });
201
212
  });
202
213
  }
214
+ // Track if we've checked browser installation
215
+ let browserInstallationChecked = false;
216
+ /**
217
+ * Gets the screen size using Playwright's API
218
+ */
219
+ async function getScreenSize() {
220
+ try {
221
+ // Launch a temporary browser to get screen size
222
+ const tempBrowser = await chromium.launch({ headless: true });
223
+ const tempContext = await tempBrowser.newContext();
224
+ const tempPage = await tempContext.newPage();
225
+ const screenSize = await tempPage.evaluate(() => {
226
+ return {
227
+ width: window.screen.width,
228
+ height: window.screen.height
229
+ };
230
+ });
231
+ await tempBrowser.close();
232
+ // Validate the screen size values
233
+ if (!screenSize || typeof screenSize.width !== 'number' || typeof screenSize.height !== 'number') {
234
+ console.error('Invalid screen size detected, using defaults');
235
+ return { width: 1280, height: 720 };
236
+ }
237
+ return screenSize;
238
+ }
239
+ catch (error) {
240
+ console.error('Failed to detect screen size, using defaults:', error);
241
+ return { width: 1280, height: 720 };
242
+ }
243
+ }
203
244
  /**
204
245
  * Ensures a browser is launched and returns the page
205
246
  */
206
247
  export async function ensureBrowser(browserSettings) {
207
248
  try {
249
+ // Check if browsers are installed on first launch (only once)
250
+ if (!browser && !browserInstallationChecked) {
251
+ browserInstallationChecked = true;
252
+ const browserCheck = checkBrowsersInstalled();
253
+ if (!browserCheck.installed) {
254
+ // Try to install browsers automatically
255
+ console.error('๐ŸŽญ Playwright browsers not found. Installing automatically...');
256
+ console.error('โณ This will download ~1GB of browser binaries. Please wait...');
257
+ try {
258
+ const { execSync } = await import('child_process');
259
+ execSync('npx playwright install chromium firefox webkit', {
260
+ stdio: 'inherit',
261
+ encoding: 'utf8'
262
+ });
263
+ console.error('โœ… Browsers installed successfully! Starting browser...');
264
+ // Note: browser variable is still undefined here, which is correct.
265
+ // The code below (line 342) will launch the browser after installation.
266
+ }
267
+ catch (installError) {
268
+ // If auto-install fails, show instructions
269
+ const instructions = getInstallationInstructions();
270
+ throw new Error(`Playwright browsers not installed.\n\n${instructions}`);
271
+ }
272
+ }
273
+ }
208
274
  // Check if browser exists but is disconnected
209
275
  if (browser && !browser.isConnected()) {
210
276
  console.error("Browser exists but is disconnected. Cleaning up...");
@@ -273,6 +339,23 @@ export async function ensureBrowser(browserSettings) {
273
339
  break;
274
340
  }
275
341
  const executablePath = process.env.CHROME_EXECUTABLE_PATH;
342
+ // Determine viewport size
343
+ let viewportWidth;
344
+ let viewportHeight;
345
+ if (viewport?.width !== undefined || viewport?.height !== undefined) {
346
+ // If any viewport dimension is specified, use specified values or defaults
347
+ viewportWidth = viewport?.width ?? 1280;
348
+ viewportHeight = viewport?.height ?? 720;
349
+ }
350
+ else {
351
+ // If no viewport specified, detect screen size
352
+ const screenSize = await getScreenSize();
353
+ viewportWidth = screenSize?.width ?? 1280;
354
+ viewportHeight = screenSize?.height ?? 720;
355
+ if (screenSize && screenSize.width > 0 && screenSize.height > 0) {
356
+ console.error(`No viewport specified, using screen size: ${viewportWidth}x${viewportHeight}`);
357
+ }
358
+ }
276
359
  // Prepare context options
277
360
  const contextOptions = {
278
361
  headless,
@@ -287,8 +370,8 @@ export async function ensureBrowser(browserSettings) {
287
370
  contextOptions.userAgent = userAgent;
288
371
  }
289
372
  contextOptions.viewport = {
290
- width: viewport?.width ?? 1280,
291
- height: viewport?.height ?? 720,
373
+ width: viewportWidth,
374
+ height: viewportHeight,
292
375
  };
293
376
  contextOptions.deviceScaleFactor = 1;
294
377
  }
@@ -331,8 +414,8 @@ export async function ensureBrowser(browserSettings) {
331
414
  newContextOptions.userAgent = userAgent;
332
415
  }
333
416
  newContextOptions.viewport = {
334
- width: viewport?.width ?? 1280,
335
- height: viewport?.height ?? 720,
417
+ width: viewportWidth,
418
+ height: viewportHeight,
336
419
  };
337
420
  newContextOptions.deviceScaleFactor = 1;
338
421
  }
@@ -390,6 +473,20 @@ export async function ensureBrowser(browserSettings) {
390
473
  break;
391
474
  }
392
475
  const executablePath = process.env.CHROME_EXECUTABLE_PATH;
476
+ // Determine viewport size for retry
477
+ let retryViewportWidth;
478
+ let retryViewportHeight;
479
+ if (viewport?.width !== undefined || viewport?.height !== undefined) {
480
+ // If any viewport dimension is specified, use specified values or defaults
481
+ retryViewportWidth = viewport?.width ?? 1280;
482
+ retryViewportHeight = viewport?.height ?? 720;
483
+ }
484
+ else {
485
+ // If no viewport specified, detect screen size
486
+ const screenSize = await getScreenSize();
487
+ retryViewportWidth = screenSize?.width ?? 1280;
488
+ retryViewportHeight = screenSize?.height ?? 720;
489
+ }
393
490
  // Prepare context options
394
491
  const retryContextOptions = {
395
492
  headless,
@@ -404,8 +501,8 @@ export async function ensureBrowser(browserSettings) {
404
501
  retryContextOptions.userAgent = userAgent;
405
502
  }
406
503
  retryContextOptions.viewport = {
407
- width: viewport?.width ?? 1280,
408
- height: viewport?.height ?? 720,
504
+ width: retryViewportWidth,
505
+ height: retryViewportHeight,
409
506
  };
410
507
  retryContextOptions.deviceScaleFactor = 1;
411
508
  }
@@ -444,8 +541,8 @@ export async function ensureBrowser(browserSettings) {
444
541
  retryNewContextOptions.userAgent = userAgent;
445
542
  }
446
543
  retryNewContextOptions.viewport = {
447
- width: viewport?.width ?? 1280,
448
- height: viewport?.height ?? 720,
544
+ width: retryViewportWidth,
545
+ height: retryViewportHeight,
449
546
  };
450
547
  retryNewContextOptions.deviceScaleFactor = 1;
451
548
  }
@@ -512,6 +609,8 @@ function initializeTools(server) {
512
609
  elementExistsTool = new ElementExistsTool(server);
513
610
  if (!compareElementAlignmentTool)
514
611
  compareElementAlignmentTool = new CompareElementAlignmentTool(server);
612
+ if (!inspectAncestorsTool)
613
+ inspectAncestorsTool = new InspectAncestorsTool(server);
515
614
  if (!waitForElementTool)
516
615
  waitForElementTool = new WaitForElementTool(server);
517
616
  if (!waitForNetworkIdleTool)
@@ -653,6 +752,8 @@ export async function handleToolCall(name, args, server) {
653
752
  return await elementExistsTool.execute(args, context);
654
753
  case "compare_element_alignment":
655
754
  return await compareElementAlignmentTool.execute(args, context);
755
+ case "inspect_ancestors":
756
+ return await inspectAncestorsTool.execute(args, context);
656
757
  case "wait_for_element":
657
758
  return await waitForElementTool.execute(args, context);
658
759
  case "wait_for_network_idle":
@@ -0,0 +1,16 @@
1
+ import { BrowserToolBase } from "./base.js";
2
+ import type { ToolResponse, ToolContext } from "../common/types.js";
3
+ /**
4
+ * Tool to inspect ancestor chain of an element
5
+ * Shows parent elements up the DOM tree with layout-critical CSS properties
6
+ */
7
+ export declare class InspectAncestorsTool extends BrowserToolBase {
8
+ execute(args: {
9
+ selector: string;
10
+ limit?: number;
11
+ }, context: ToolContext): Promise<ToolResponse>;
12
+ private formatAncestorChain;
13
+ private formatBorder;
14
+ private formatOverflow;
15
+ private generateDiagnostics;
16
+ }
@@ -0,0 +1,242 @@
1
+ import { BrowserToolBase } from "./base.js";
2
+ /**
3
+ * Tool to inspect ancestor chain of an element
4
+ * Shows parent elements up the DOM tree with layout-critical CSS properties
5
+ */
6
+ export class InspectAncestorsTool extends BrowserToolBase {
7
+ async execute(args, context) {
8
+ return this.safeExecute(context, async (page) => {
9
+ const limit = Math.min(args.limit ?? 10, 15); // Default 10, max 15
10
+ const normalizedSelector = this.normalizeSelector(args.selector);
11
+ const ancestors = await page.evaluate(({ sel, lim }) => {
12
+ const element = document.querySelector(sel);
13
+ if (!element) {
14
+ return null;
15
+ }
16
+ const chain = [];
17
+ let current = element;
18
+ for (let i = 0; i < lim && current; i++) {
19
+ const rect = current.getBoundingClientRect();
20
+ const computed = window.getComputedStyle(current);
21
+ chain.push({
22
+ tagName: current.tagName.toLowerCase(),
23
+ testId: current.getAttribute("data-testid"),
24
+ classes: current.className,
25
+ rect: {
26
+ x: Math.round(rect.x),
27
+ y: Math.round(rect.y),
28
+ width: Math.round(rect.width),
29
+ height: Math.round(rect.height),
30
+ },
31
+ // Layout-critical properties
32
+ width: computed.width,
33
+ maxWidth: computed.maxWidth,
34
+ minWidth: computed.minWidth,
35
+ margin: computed.margin,
36
+ padding: computed.padding,
37
+ display: computed.display,
38
+ overflow: computed.overflow,
39
+ overflowX: computed.overflowX,
40
+ overflowY: computed.overflowY,
41
+ border: computed.border,
42
+ borderTop: computed.borderTop,
43
+ borderRight: computed.borderRight,
44
+ borderBottom: computed.borderBottom,
45
+ borderLeft: computed.borderLeft,
46
+ // Flexbox
47
+ flexDirection: computed.flexDirection,
48
+ justifyContent: computed.justifyContent,
49
+ alignItems: computed.alignItems,
50
+ // Conditional
51
+ position: computed.position !== "static" ? computed.position : undefined,
52
+ zIndex: computed.zIndex !== "auto" ? computed.zIndex : undefined,
53
+ transform: computed.transform !== "none" ? computed.transform : undefined,
54
+ });
55
+ current = current.parentElement;
56
+ }
57
+ return chain;
58
+ }, { sel: normalizedSelector, lim: limit });
59
+ if (!ancestors) {
60
+ return {
61
+ content: [
62
+ {
63
+ type: "text",
64
+ text: `Error: Element not found with selector "${args.selector}"`,
65
+ },
66
+ ],
67
+ isError: true,
68
+ };
69
+ }
70
+ return {
71
+ content: [
72
+ {
73
+ type: "text",
74
+ text: this.formatAncestorChain(ancestors, args.selector),
75
+ },
76
+ ],
77
+ isError: false,
78
+ };
79
+ });
80
+ }
81
+ formatAncestorChain(ancestors, originalSelector) {
82
+ const lines = [`Ancestor Chain: ${originalSelector}\n`];
83
+ ancestors.forEach((ancestor, index) => {
84
+ const parts = [];
85
+ // Tag and identifier
86
+ let identifier = `[${index}] <${ancestor.tagName}>`;
87
+ if (ancestor.testId) {
88
+ identifier += ` | testid:${ancestor.testId}`;
89
+ }
90
+ else if (ancestor.classes) {
91
+ // Show first few classes for context
92
+ const classes = ancestor.classes.trim().split(/\s+/).slice(0, 3);
93
+ if (classes.length > 0 && classes[0]) {
94
+ identifier += ` | ${classes.join(" ")}`;
95
+ }
96
+ }
97
+ parts.push(identifier);
98
+ // Position and size (always show)
99
+ const layoutInfo = [];
100
+ parts.push(`\n @ (${ancestor.rect.x},${ancestor.rect.y}) ${ancestor.rect.width}x${ancestor.rect.height}px`);
101
+ // Width info (always show)
102
+ layoutInfo.push(`w:${ancestor.width}`);
103
+ // Display (only if not block)
104
+ if (ancestor.display !== "block") {
105
+ layoutInfo.push(`display:${ancestor.display}`);
106
+ if (ancestor.flexDirection && ancestor.flexDirection !== "row") {
107
+ layoutInfo.push(ancestor.flexDirection);
108
+ }
109
+ }
110
+ // Only show non-default values
111
+ if (ancestor.margin !== "0px") {
112
+ layoutInfo.push(`m:${ancestor.margin}`);
113
+ }
114
+ if (ancestor.padding !== "0px") {
115
+ layoutInfo.push(`p:${ancestor.padding}`);
116
+ }
117
+ if (ancestor.maxWidth !== "none") {
118
+ layoutInfo.push(`max-w:${ancestor.maxWidth}`);
119
+ }
120
+ if (ancestor.minWidth !== "0px") {
121
+ layoutInfo.push(`min-w:${ancestor.minWidth}`);
122
+ }
123
+ if (layoutInfo.length > 0) {
124
+ parts.push(` | ${layoutInfo.join(" ")}`);
125
+ }
126
+ // Border - only if set
127
+ const borderInfo = this.formatBorder(ancestor);
128
+ if (borderInfo) {
129
+ parts.push(`\n ${borderInfo}`);
130
+ }
131
+ // Overflow - only if not visible
132
+ const overflowInfo = this.formatOverflow(ancestor);
133
+ if (overflowInfo) {
134
+ parts.push(`\n ${overflowInfo}`);
135
+ }
136
+ // Position, z-index, transform (only if set)
137
+ const extraInfo = [];
138
+ if (ancestor.position) {
139
+ extraInfo.push(`position:${ancestor.position}`);
140
+ }
141
+ if (ancestor.zIndex) {
142
+ extraInfo.push(`z-index:${ancestor.zIndex}`);
143
+ }
144
+ if (ancestor.transform) {
145
+ extraInfo.push(`transform:${ancestor.transform}`);
146
+ }
147
+ if (extraInfo.length > 0) {
148
+ parts.push(`\n ${extraInfo.join(", ")}`);
149
+ }
150
+ // Add diagnostics
151
+ const diagnostics = this.generateDiagnostics(ancestor, index);
152
+ if (diagnostics) {
153
+ parts.push(`\n ${diagnostics}`);
154
+ }
155
+ lines.push(parts.join(""));
156
+ });
157
+ return lines.join("\n\n");
158
+ }
159
+ formatBorder(ancestor) {
160
+ // Check if main border is set
161
+ if (ancestor.border &&
162
+ ancestor.border !== "none" &&
163
+ ancestor.border !== "0px none" &&
164
+ !ancestor.border.startsWith("0px")) {
165
+ return `border: ${ancestor.border}`;
166
+ }
167
+ // Check directional borders
168
+ const borders = [];
169
+ if (ancestor.borderTop &&
170
+ ancestor.borderTop !== "none" &&
171
+ !ancestor.borderTop.startsWith("0px")) {
172
+ borders.push(`top:${ancestor.borderTop}`);
173
+ }
174
+ if (ancestor.borderRight &&
175
+ ancestor.borderRight !== "none" &&
176
+ !ancestor.borderRight.startsWith("0px")) {
177
+ borders.push(`right:${ancestor.borderRight}`);
178
+ }
179
+ if (ancestor.borderBottom &&
180
+ ancestor.borderBottom !== "none" &&
181
+ !ancestor.borderBottom.startsWith("0px")) {
182
+ borders.push(`bottom:${ancestor.borderBottom}`);
183
+ }
184
+ if (ancestor.borderLeft &&
185
+ ancestor.borderLeft !== "none" &&
186
+ !ancestor.borderLeft.startsWith("0px")) {
187
+ borders.push(`left:${ancestor.borderLeft}`);
188
+ }
189
+ if (borders.length > 0) {
190
+ return `border: ${borders.join(", ")}`;
191
+ }
192
+ return null;
193
+ }
194
+ formatOverflow(ancestor) {
195
+ const parts = [];
196
+ // Handle uniform overflow
197
+ if (ancestor.overflow !== "visible" &&
198
+ ancestor.overflowX === ancestor.overflow &&
199
+ ancestor.overflowY === ancestor.overflow) {
200
+ const icon = ancestor.overflow === "hidden"
201
+ ? "๐Ÿ”’"
202
+ : ancestor.overflow === "auto" || ancestor.overflow === "scroll"
203
+ ? "โ†•๏ธ"
204
+ : "";
205
+ return `overflow: ${icon} ${ancestor.overflow}`;
206
+ }
207
+ // Handle different overflow-x/y
208
+ if (ancestor.overflowX !== "visible" ||
209
+ ancestor.overflowY !== "visible") {
210
+ const xIcon = ancestor.overflowX === "hidden"
211
+ ? "๐Ÿ”’"
212
+ : ancestor.overflowX === "auto" || ancestor.overflowX === "scroll"
213
+ ? "โ†”๏ธ"
214
+ : "";
215
+ const yIcon = ancestor.overflowY === "hidden"
216
+ ? "๐Ÿ”’"
217
+ : ancestor.overflowY === "auto" || ancestor.overflowY === "scroll"
218
+ ? "โ†•๏ธ"
219
+ : "";
220
+ parts.push(`overflow-x: ${xIcon} ${ancestor.overflowX}, overflow-y: ${yIcon} ${ancestor.overflowY}`);
221
+ return parts.join(", ");
222
+ }
223
+ return null;
224
+ }
225
+ generateDiagnostics(ancestor, index) {
226
+ const diagnostics = [];
227
+ // Overflow hidden warning
228
+ if (ancestor.overflow === "hidden" || ancestor.overflowY === "hidden") {
229
+ diagnostics.push("๐ŸŽฏ CLIPPING POINT - May clip overflowing children");
230
+ }
231
+ // Width constraint detection
232
+ if (ancestor.maxWidth !== "none" && index > 0) {
233
+ diagnostics.push("๐ŸŽฏ WIDTH CONSTRAINT");
234
+ }
235
+ // Large margins (potential centering)
236
+ const marginMatch = ancestor.margin.match(/0px (\d+)px/);
237
+ if (marginMatch && parseInt(marginMatch[1]) > 100) {
238
+ diagnostics.push(`โš  Auto margins centering (${marginMatch[1]}px each side)`);
239
+ }
240
+ return diagnostics.length > 0 ? diagnostics.join("\n ") : null;
241
+ }
242
+ }
@@ -161,6 +161,15 @@ export class CompareElementAlignmentTool extends BrowserToolBase {
161
161
  ` Width: ${formatDimension(widthSame, width1, width2, widthDiff)}`,
162
162
  ` Height: ${formatDimension(heightSame, height1, height2, heightDiff)}`
163
163
  ];
164
+ // Suggest inspect_ancestors if alignment is off and differences are significant
165
+ const hasSignificantDifference = Math.abs(topDiff) > 5 || Math.abs(leftDiff) > 5 || Math.abs(rightDiff) > 5;
166
+ const isNotAligned = !topAligned && !leftAligned && !rightAligned && !bottomAligned;
167
+ if (isNotAligned && hasSignificantDifference) {
168
+ lines.push('');
169
+ lines.push('๐Ÿ’ก Alignment issue detected. Check if parent layout affects positioning:');
170
+ lines.push(` inspect_ancestors({ selector: "${args.selector1}" })`);
171
+ lines.push(` inspect_ancestors({ selector: "${args.selector2}" })`);
172
+ }
164
173
  return createSuccessResponse(lines.filter(l => l !== undefined).join('\n'));
165
174
  }
166
175
  catch (error) {
@@ -214,6 +214,11 @@ export class ElementVisibilityTool extends BrowserToolBase {
214
214
  if (suggestions.length > 0) {
215
215
  output += '\n' + suggestions.join('\n');
216
216
  }
217
+ // Suggest inspect_ancestors if element is clipped
218
+ if (visibilityData.isClipped) {
219
+ output += '\n\n๐Ÿ’ก Element clipped by parent. Find the clipping container:';
220
+ output += `\n inspect_ancestors({ selector: "${args.selector}" })`;
221
+ }
217
222
  return createSuccessResponse(output.trim());
218
223
  }
219
224
  catch (error) {
@@ -443,6 +443,12 @@ export class InspectDomTool extends BrowserToolBase {
443
443
  lines.push(`๐Ÿ’ก Tip: Some elements found, but ${stats.skippedWrappers} wrapper divs were skipped.`);
444
444
  lines.push(' Consider adding test IDs to key elements for easier selection.');
445
445
  }
446
+ // Suggest inspect_ancestors when drilling through many wrappers
447
+ if (stats.skippedWrappers >= 3) {
448
+ lines.push('');
449
+ lines.push(`๐Ÿ’ก Drilled through ${stats.skippedWrappers} wrapper levels. To see parent constraints:`);
450
+ lines.push(` inspect_ancestors({ selector: "${args.selector || 'body'}" })`);
451
+ }
446
452
  }
447
453
  return createSuccessResponse(lines.join('\n'));
448
454
  }
@@ -76,6 +76,10 @@ export declare class UploadFileTool extends BrowserToolBase {
76
76
  * Tool for executing JavaScript in the browser
77
77
  */
78
78
  export declare class EvaluateTool extends BrowserToolBase {
79
+ /**
80
+ * Detect common patterns and suggest better tools
81
+ */
82
+ private detectBetterToolSuggestions;
79
83
  /**
80
84
  * Execute the evaluate tool
81
85
  */
@@ -160,6 +160,52 @@ export class UploadFileTool extends BrowserToolBase {
160
160
  * Tool for executing JavaScript in the browser
161
161
  */
162
162
  export class EvaluateTool extends BrowserToolBase {
163
+ /**
164
+ * Detect common patterns and suggest better tools
165
+ */
166
+ detectBetterToolSuggestions(script) {
167
+ const suggestions = [];
168
+ const scriptLower = script.toLowerCase();
169
+ // Pattern: DOM inspection/querying
170
+ if (scriptLower.match(/queryselector|getelementby|getelement|innerhtml|outerhtml|children|childnodes/)) {
171
+ suggestions.push('๐Ÿ“ DOM Inspection - Use inspect_dom({ selector: "..." }) for page structure');
172
+ }
173
+ // Pattern: Getting text content
174
+ if (scriptLower.match(/textcontent|innertext/)) {
175
+ suggestions.push('๐Ÿ“ Text Content - Use get_visible_text() or find_by_text({ text: "..." })');
176
+ }
177
+ // Pattern: Getting element position/size/layout
178
+ if (scriptLower.match(/getboundingclientrect|offsetwidth|offsetheight|offsetleft|offsettop|clientwidth|clientheight/)) {
179
+ suggestions.push('๐Ÿ“ Element Measurements - Use measure_element({ selector: "..." })');
180
+ }
181
+ // Pattern: Walking up DOM tree / checking parents
182
+ if (scriptLower.match(/parentelement|parentnode|offsetparent|closest/) ||
183
+ (scriptLower.match(/while.*parent/) && scriptLower.match(/getcomputedstyle/))) {
184
+ suggestions.push('๐Ÿ”ผ Parent Chain - Use inspect_ancestors({ selector: "..." }) to see parent constraints');
185
+ }
186
+ // Pattern: Checking visibility
187
+ if (scriptLower.match(/offsetparent|visibility|display.*none|opacity/)) {
188
+ suggestions.push('๐Ÿ‘๏ธ Visibility Check - Use element_visibility({ selector: "..." })');
189
+ }
190
+ // Pattern: Getting computed styles
191
+ if (scriptLower.match(/getcomputedstyle|style\.|currentstyle/)) {
192
+ suggestions.push('๐ŸŽจ CSS Styles - Use get_computed_styles({ selector: "..." })');
193
+ }
194
+ // Pattern: Checking element existence
195
+ if (scriptLower.match(/\!=\s*null|\!==\s*null/) && scriptLower.match(/queryselector/)) {
196
+ suggestions.push('โœ“ Element Existence - Use element_exists({ selector: "..." })');
197
+ }
198
+ // Pattern: Finding test IDs
199
+ if (scriptLower.match(/data-testid|data-test|data-cy/)) {
200
+ suggestions.push('๐Ÿ” Test IDs - Use get_test_ids() to discover all test identifiers');
201
+ }
202
+ // Pattern: Comparing positions/alignment
203
+ if (scriptLower.match(/getboundingclientrect.*getboundingclientrect/) ||
204
+ (scriptLower.match(/\.left|\.top|\.right|\.bottom/) && scriptLower.match(/===|==|!==|!=/))) {
205
+ suggestions.push('โš–๏ธ Position Comparison - Use compare_positions({ selector1: "...", selector2: "..." })');
206
+ }
207
+ return suggestions;
208
+ }
163
209
  /**
164
210
  * Execute the evaluate tool
165
211
  */
@@ -175,12 +221,23 @@ export class EvaluateTool extends BrowserToolBase {
175
221
  catch (error) {
176
222
  resultStr = String(result);
177
223
  }
178
- return createSuccessResponse([
179
- `Executed JavaScript:`,
224
+ const messages = [
225
+ `โœ“ Executed JavaScript:`,
180
226
  `${args.script}`,
227
+ ``,
181
228
  `Result:`,
182
229
  `${resultStr}`
183
- ]);
230
+ ];
231
+ // Detect if specialized tools would be better
232
+ const suggestions = this.detectBetterToolSuggestions(args.script);
233
+ if (suggestions.length > 0) {
234
+ messages.push('');
235
+ messages.push('๐Ÿ’ก Consider using specialized tools instead:');
236
+ suggestions.forEach(suggestion => messages.push(` ${suggestion}`));
237
+ messages.push('');
238
+ messages.push('โ„น๏ธ Specialized tools are more reliable and token-efficient than evaluate()');
239
+ }
240
+ return createSuccessResponse(messages);
184
241
  });
185
242
  }
186
243
  }
@@ -125,6 +125,14 @@ export class MeasureElementTool extends BrowserToolBase {
125
125
  sections.push(` Margin: ${formatSpacing(measurements.marginTop, measurements.marginRight, measurements.marginBottom, measurements.marginLeft)}`);
126
126
  sections.push('');
127
127
  sections.push(`Total Space: ${totalWidth}x${totalHeight}px (with margin)`);
128
+ // Detect unusual spacing and suggest inspect_ancestors
129
+ const hasUnusualMargins = measurements.marginLeft > 100 || measurements.marginRight > 100;
130
+ const isWidthConstrained = boxWidth < 800 && (measurements.marginLeft + measurements.marginRight) > 200;
131
+ if (hasUnusualMargins || isWidthConstrained) {
132
+ sections.push('');
133
+ sections.push('๐Ÿ’ก Unexpected spacing/width detected. Check parent constraints:');
134
+ sections.push(` inspect_ancestors({ selector: "${args.selector}" })`);
135
+ }
128
136
  return {
129
137
  content: [
130
138
  {
@@ -49,7 +49,7 @@ export class ScreenshotTool extends BrowserToolBase {
49
49
  screenshotOptions.path = outputPath;
50
50
  const screenshot = await page.screenshot(screenshotOptions);
51
51
  const base64Screenshot = screenshot.toString('base64');
52
- const messages = [`Screenshot saved to: ${path.relative(process.cwd(), outputPath)}`];
52
+ const messages = [`โœ“ Screenshot saved to: ${path.relative(process.cwd(), outputPath)}`];
53
53
  // Handle base64 storage
54
54
  if (args.storeBase64 !== false) {
55
55
  this.screenshots.set(args.name || 'screenshot', base64Screenshot);
@@ -58,6 +58,18 @@ export class ScreenshotTool extends BrowserToolBase {
58
58
  });
59
59
  messages.push(`Screenshot also stored in memory with name: '${args.name || 'screenshot'}'`);
60
60
  }
61
+ // Add actionable guidance based on screenshot context
62
+ messages.push('');
63
+ messages.push('๐Ÿ’ก To debug layout issues in this screenshot:');
64
+ if (args.selector) {
65
+ messages.push(` inspect_ancestors({ selector: "${args.selector}" })`);
66
+ messages.push(' โ†’ See parent constraints (width, margins, overflow, borders)');
67
+ }
68
+ else {
69
+ messages.push(' 1. Find the element: inspect_dom({}) or get_test_ids()');
70
+ messages.push(' 2. Check parent constraints: inspect_ancestors({ selector: "..." })');
71
+ messages.push(' 3. Compare alignment: compare_element_alignment({ selector1: "...", selector2: "..." })');
72
+ }
61
73
  return createSuccessResponse(messages);
62
74
  });
63
75
  }
package/dist/tools.d.ts CHANGED
@@ -25,11 +25,11 @@ export declare function createToolDefinitions(sessionConfig?: SessionConfig): [{
25
25
  };
26
26
  readonly width: {
27
27
  readonly type: "number";
28
- readonly description: "Viewport width in pixels (default: 1280). Ignored if device is specified.";
28
+ readonly description: "Viewport width in pixels. If not specified, automatically matches screen width. Ignored if device is specified.";
29
29
  };
30
30
  readonly height: {
31
31
  readonly type: "number";
32
- readonly description: "Viewport height in pixels (default: 720). Ignored if device is specified.";
32
+ readonly description: "Viewport height in pixels. If not specified, automatically matches screen height. Ignored if device is specified.";
33
33
  };
34
34
  readonly timeout: {
35
35
  readonly type: "number";
@@ -468,6 +468,23 @@ export declare function createToolDefinitions(sessionConfig?: SessionConfig): [{
468
468
  };
469
469
  readonly required: readonly ["selector1", "selector2"];
470
470
  };
471
+ }, {
472
+ readonly name: "inspect_ancestors";
473
+ readonly description: "DEBUG LAYOUT CONSTRAINTS: Walk up the DOM tree to find where width constraints, margins, borders, and overflow clipping come from. Essential when elements have unexpected centering (large auto margins), constrained width (max-width from parent), or are clipped (overflow:hidden ancestor). Shows position, size, and layout-critical CSS for each ancestor. Default depth: 10 levels (reaches <body> in most React apps). Use after inspect_dom() when you need to understand parent layout flow.";
474
+ readonly inputSchema: {
475
+ readonly type: "object";
476
+ readonly properties: {
477
+ readonly selector: {
478
+ readonly type: "string";
479
+ readonly description: "CSS selector or testid shorthand for the element to start from (e.g., 'testid:header', '#main')";
480
+ };
481
+ readonly limit: {
482
+ readonly type: "number";
483
+ readonly description: "Maximum number of ancestors to traverse (default: 10, max: 15). Increase for deeply nested component frameworks.";
484
+ };
485
+ };
486
+ readonly required: readonly ["selector"];
487
+ };
471
488
  }, {
472
489
  readonly name: "wait_for_element";
473
490
  readonly description: "Wait for an element to reach a specific state (visible, hidden, attached, detached). Better than sleep() for waiting on dynamic content. Returns duration and current element status. Supports testid shortcuts (e.g., 'testid:submit-button').";
package/dist/tools.js CHANGED
@@ -20,8 +20,8 @@ export function createToolDefinitions(sessionConfig) {
20
20
  description: "Mobile device preset to emulate. Uses Playwright's built-in device configurations for viewport, user agent, and device scale factor. When specified, overrides width/height parameters.",
21
21
  enum: ["iphone-se", "iphone-14", "iphone-14-pro", "pixel-5", "ipad", "samsung-s21"]
22
22
  },
23
- width: { type: "number", description: "Viewport width in pixels (default: 1280). Ignored if device is specified." },
24
- height: { type: "number", description: "Viewport height in pixels (default: 720). Ignored if device is specified." },
23
+ width: { type: "number", description: "Viewport width in pixels. If not specified, automatically matches screen width. Ignored if device is specified." },
24
+ height: { type: "number", description: "Viewport height in pixels. If not specified, automatically matches screen height. Ignored if device is specified." },
25
25
  timeout: { type: "number", description: "Navigation timeout in milliseconds" },
26
26
  waitUntil: { type: "string", description: "Navigation wait condition" },
27
27
  headless: { type: "boolean", description: "Run browser in headless mode (default: false)" }
@@ -437,6 +437,24 @@ More efficient than get_html() or evaluate(). Supports testid shortcuts.`,
437
437
  required: ["selector1", "selector2"],
438
438
  },
439
439
  },
440
+ {
441
+ name: "inspect_ancestors",
442
+ description: "DEBUG LAYOUT CONSTRAINTS: Walk up the DOM tree to find where width constraints, margins, borders, and overflow clipping come from. Essential when elements have unexpected centering (large auto margins), constrained width (max-width from parent), or are clipped (overflow:hidden ancestor). Shows position, size, and layout-critical CSS for each ancestor. Default depth: 10 levels (reaches <body> in most React apps). Use after inspect_dom() when you need to understand parent layout flow.",
443
+ inputSchema: {
444
+ type: "object",
445
+ properties: {
446
+ selector: {
447
+ type: "string",
448
+ description: "CSS selector or testid shorthand for the element to start from (e.g., 'testid:header', '#main')"
449
+ },
450
+ limit: {
451
+ type: "number",
452
+ description: "Maximum number of ancestors to traverse (default: 10, max: 15). Increase for deeply nested component frameworks."
453
+ }
454
+ },
455
+ required: ["selector"],
456
+ },
457
+ },
440
458
  {
441
459
  name: "wait_for_element",
442
460
  description: "Wait for an element to reach a specific state (visible, hidden, attached, detached). Better than sleep() for waiting on dynamic content. Returns duration and current element status. Supports testid shortcuts (e.g., 'testid:submit-button').",
@@ -523,6 +541,7 @@ export const BROWSER_TOOLS = [
523
541
  // Visibility & Position
524
542
  "check_visibility",
525
543
  "compare_element_alignment",
544
+ "inspect_ancestors",
526
545
  "element_exists",
527
546
  "wait_for_element",
528
547
  "wait_for_network_idle",
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Check if Playwright browsers are installed
3
+ * Returns true if browsers are available, false otherwise
4
+ */
5
+ export declare function checkBrowsersInstalled(): {
6
+ installed: boolean;
7
+ message?: string;
8
+ };
9
+ /**
10
+ * Get installation instructions for the current context
11
+ */
12
+ export declare function getInstallationInstructions(): string;
@@ -0,0 +1,67 @@
1
+ import { execSync } from 'child_process';
2
+ import { existsSync } from 'fs';
3
+ import { join } from 'path';
4
+ /**
5
+ * Check if Playwright browsers are installed
6
+ * Returns true if browsers are available, false otherwise
7
+ */
8
+ export function checkBrowsersInstalled() {
9
+ try {
10
+ // Check if playwright is available
11
+ const result = execSync('npx playwright --version', {
12
+ encoding: 'utf8',
13
+ stdio: 'pipe'
14
+ });
15
+ // If we got here, playwright CLI is available
16
+ // Now check if browsers are actually installed
17
+ // Playwright stores browsers in a cache directory
18
+ const homeDir = process.env.HOME || process.env.USERPROFILE;
19
+ if (!homeDir) {
20
+ return {
21
+ installed: false,
22
+ message: 'Could not determine home directory to check for browsers'
23
+ };
24
+ }
25
+ // Common browser cache locations
26
+ const possibleCacheDirs = [
27
+ join(homeDir, '.cache', 'ms-playwright'), // Linux
28
+ join(homeDir, 'Library', 'Caches', 'ms-playwright'), // macOS
29
+ join(homeDir, 'AppData', 'Local', 'ms-playwright'), // Windows
30
+ ];
31
+ const cacheExists = possibleCacheDirs.some(dir => existsSync(dir));
32
+ if (!cacheExists) {
33
+ return {
34
+ installed: false,
35
+ message: 'Playwright browsers not found in cache directories'
36
+ };
37
+ }
38
+ return { installed: true };
39
+ }
40
+ catch (error) {
41
+ return {
42
+ installed: false,
43
+ message: `Playwright check failed: ${error.message}`
44
+ };
45
+ }
46
+ }
47
+ /**
48
+ * Get installation instructions for the current context
49
+ */
50
+ export function getInstallationInstructions() {
51
+ return `
52
+ Playwright browsers are not installed. To fix this, run one of the following commands:
53
+
54
+ 1. In your project directory:
55
+ npx playwright install chromium firefox webkit
56
+
57
+ 2. If you installed mcp-web-inspector globally:
58
+ cd $(npm root -g)/mcp-web-inspector && npx playwright install chromium firefox webkit
59
+
60
+ 3. Or install system-wide with dependencies (requires admin/sudo):
61
+ npx playwright install --with-deps chromium firefox webkit
62
+
63
+ For GitHub Copilot or VS Code users, you may need to:
64
+ - Close and reopen your IDE after installation
65
+ - Or run: npx playwright install chromium
66
+ `.trim();
67
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcp-web-inspector",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
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",