mcp-web-inspector 0.1.3 → 0.1.4

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
@@ -289,6 +289,7 @@ Customize server behavior with command line flags:
289
289
 
290
290
  - **`--no-save-session`** - Disable automatic session persistence (start with fresh browser state each time)
291
291
  - **`--user-data-dir <path>`** - Custom directory for session data (default: `./.mcp-web-inspector`)
292
+ - **`--headless`** - Run browser in headless mode by default (no visible window)
292
293
 
293
294
  **Example usage:**
294
295
  ```json
@@ -302,6 +303,30 @@ Customize server behavior with command line flags:
302
303
  }
303
304
  ```
304
305
 
306
+ **Run in headless mode for automation/CI:**
307
+ ```json
308
+ {
309
+ "mcpServers": {
310
+ "web-inspector": {
311
+ "command": "npx",
312
+ "args": ["-y", "mcp-web-inspector", "--headless"]
313
+ }
314
+ }
315
+ }
316
+ ```
317
+
318
+ **Combine multiple flags:**
319
+ ```json
320
+ {
321
+ "mcpServers": {
322
+ "web-inspector": {
323
+ "command": "npx",
324
+ "args": ["-y", "mcp-web-inspector", "--headless", "--no-save-session"]
325
+ }
326
+ }
327
+ }
328
+ ```
329
+
305
330
  ---
306
331
 
307
332
  ## Session Persistence & Data Storage
@@ -395,8 +420,8 @@ rm -rf ./.mcp-web-inspector/screenshots # Clear screenshots only
395
420
  - Session files can be large and bloat your git history
396
421
 
397
422
  **Best practices:**
398
- - Use `headless: true` for automation and CI/CD environments
399
- - Use `headless: false` only when debugging interactively
423
+ - **Default is visible browser** (`headless: false`) for interactive debugging
424
+ - Use `headless: true` explicitly for automation and CI/CD environments
400
425
  - Clear session data after testing sensitive applications
401
426
  - Use `--no-save-session` flag when testing on shared/public sites
402
427
 
@@ -570,11 +595,16 @@ Ultra-lightweight existence check. Returns simple ✓ exists or ✗ not found st
570
595
  Navigate to a URL with full browser configuration options.
571
596
 
572
597
  **Parameters:**
573
- - `browserType` - chromium, firefox, or webkit
574
- - `width`, `height` - Viewport dimensions
575
- - `headless` - Run in headless mode
576
- - `timeout` - Navigation timeout
577
- - `waitUntil` - Navigation wait condition
598
+ - `browserType` - chromium, firefox, or webkit (default: chromium)
599
+ - `width`, `height` - Viewport dimensions (default: auto-detected screen size)
600
+ - `headless` - Run in headless mode (default: **false** - browser window visible)
601
+ - `timeout` - Navigation timeout in ms (default: 30000)
602
+ - `waitUntil` - Navigation wait condition (default: "load")
603
+
604
+ **Default Behavior:**
605
+ - Browser window is **visible by default** for interactive debugging
606
+ - Use `headless: true` for automation, CI/CD, or when you don't need visual feedback
607
+ - Use `headless: false` (or omit) when debugging interactively
578
608
 
579
609
  #### `go_back`
580
610
  Navigate back in browser history. Essential for testing navigation flows and multi-step forms.
package/dist/index.js CHANGED
@@ -16,6 +16,10 @@ const { values } = parseArgs({
16
16
  type: 'string',
17
17
  default: './.mcp-web-inspector',
18
18
  },
19
+ 'headless': {
20
+ type: 'boolean',
21
+ default: false,
22
+ },
19
23
  },
20
24
  strict: false,
21
25
  });
@@ -25,6 +29,7 @@ const sessionConfig = {
25
29
  saveSession: !Boolean(values['no-save-session']),
26
30
  userDataDir: `${baseDir}/user-data`,
27
31
  screenshotsDir: `${baseDir}/screenshots`,
32
+ headlessDefault: Boolean(values['headless']),
28
33
  };
29
34
  setSessionConfig(sessionConfig);
30
35
  async function runServer() {
@@ -22,6 +22,7 @@ interface SessionConfig {
22
22
  saveSession: boolean;
23
23
  userDataDir: string;
24
24
  screenshotsDir: string;
25
+ headlessDefault: boolean;
25
26
  }
26
27
  /**
27
28
  * Sets the session configuration
@@ -31,6 +32,10 @@ export declare function setSessionConfig(config: Partial<SessionConfig>): void;
31
32
  * Gets the screenshots directory
32
33
  */
33
34
  export declare function getScreenshotsDir(): string;
35
+ /**
36
+ * Gets the default headless setting
37
+ */
38
+ export declare function getHeadlessDefault(): boolean;
34
39
  /**
35
40
  * Resets browser and page variables
36
41
  * Used when browser is closed
@@ -48,7 +53,7 @@ export declare function clearNetworkLog(): void;
48
53
  * Sets the provided page to the global page variable
49
54
  * @param newPage The Page object to set as the global page
50
55
  */
51
- export declare function setGlobalPage(newPage: Page): void;
56
+ export declare function setGlobalPage(newPage: Page): Promise<void>;
52
57
  interface BrowserSettings {
53
58
  viewport?: {
54
59
  width?: number;
@@ -29,6 +29,7 @@ let sessionConfig = {
29
29
  saveSession: false,
30
30
  userDataDir: './.mcp-web-inspector/user-data',
31
31
  screenshotsDir: './.mcp-web-inspector/screenshots',
32
+ headlessDefault: false,
32
33
  };
33
34
  /**
34
35
  * Sets the session configuration
@@ -42,6 +43,12 @@ export function setSessionConfig(config) {
42
43
  export function getScreenshotsDir() {
43
44
  return sessionConfig.screenshotsDir;
44
45
  }
46
+ /**
47
+ * Gets the default headless setting
48
+ */
49
+ export function getHeadlessDefault() {
50
+ return sessionConfig.headlessDefault;
51
+ }
45
52
  /**
46
53
  * Resets browser and page variables
47
54
  * Used when browser is closed
@@ -68,10 +75,13 @@ export function clearNetworkLog() {
68
75
  * Sets the provided page to the global page variable
69
76
  * @param newPage The Page object to set as the global page
70
77
  */
71
- export function setGlobalPage(newPage) {
78
+ export async function setGlobalPage(newPage) {
72
79
  page = newPage;
80
+ // Register console message handlers and network listeners for the new page
81
+ await registerConsoleMessage(page);
82
+ await registerNetworkListeners(page);
73
83
  page.bringToFront(); // Bring the new tab to the front
74
- console.log("Global page has been updated.");
84
+ console.log("Global page has been updated with listeners registered.");
75
85
  }
76
86
  // Tool instances
77
87
  let screenshotTool;
@@ -300,7 +310,7 @@ export async function ensureBrowser(browserSettings) {
300
310
  }
301
311
  // Launch new browser if needed
302
312
  if (!browser) {
303
- const { viewport, userAgent, headless = false, browserType = 'chromium', device } = browserSettings ?? {};
313
+ const { viewport, userAgent, headless = sessionConfig.headlessDefault, browserType = 'chromium', device } = browserSettings ?? {};
304
314
  // If browser type is changing, force a new browser instance
305
315
  if (browser && currentBrowserType !== browserType) {
306
316
  try {
@@ -451,7 +461,7 @@ export async function ensureBrowser(browserSettings) {
451
461
  }
452
462
  resetBrowserState();
453
463
  // Try one more time from scratch
454
- const { viewport, userAgent, headless = false, browserType = 'chromium', device } = browserSettings ?? {};
464
+ const { viewport, userAgent, headless = sessionConfig.headlessDefault, browserType = 'chromium', device } = browserSettings ?? {};
455
465
  // Get device configuration if device preset is specified
456
466
  let deviceConfig = null;
457
467
  if (device && DEVICE_PRESETS[device]) {
@@ -12,5 +12,7 @@ export declare class InspectAncestorsTool extends BrowserToolBase {
12
12
  private formatAncestorChain;
13
13
  private formatBorder;
14
14
  private formatOverflow;
15
+ private formatLayoutContext;
16
+ private formatMarginDetails;
15
17
  private generateDiagnostics;
16
18
  }
@@ -33,6 +33,10 @@ export class InspectAncestorsTool extends BrowserToolBase {
33
33
  maxWidth: computed.maxWidth,
34
34
  minWidth: computed.minWidth,
35
35
  margin: computed.margin,
36
+ marginTop: computed.marginTop,
37
+ marginRight: computed.marginRight,
38
+ marginBottom: computed.marginBottom,
39
+ marginLeft: computed.marginLeft,
36
40
  padding: computed.padding,
37
41
  display: computed.display,
38
42
  overflow: computed.overflow,
@@ -47,6 +51,10 @@ export class InspectAncestorsTool extends BrowserToolBase {
47
51
  flexDirection: computed.flexDirection,
48
52
  justifyContent: computed.justifyContent,
49
53
  alignItems: computed.alignItems,
54
+ gap: computed.gap,
55
+ // Grid
56
+ gridTemplateColumns: computed.gridTemplateColumns,
57
+ gridTemplateRows: computed.gridTemplateRows,
50
58
  // Conditional
51
59
  position: computed.position !== "static" ? computed.position : undefined,
52
60
  zIndex: computed.zIndex !== "auto" ? computed.zIndex : undefined,
@@ -103,9 +111,6 @@ export class InspectAncestorsTool extends BrowserToolBase {
103
111
  // Display (only if not block)
104
112
  if (ancestor.display !== "block") {
105
113
  layoutInfo.push(`display:${ancestor.display}`);
106
- if (ancestor.flexDirection && ancestor.flexDirection !== "row") {
107
- layoutInfo.push(ancestor.flexDirection);
108
- }
109
114
  }
110
115
  // Only show non-default values
111
116
  if (ancestor.margin !== "0px") {
@@ -123,6 +128,16 @@ export class InspectAncestorsTool extends BrowserToolBase {
123
128
  if (layoutInfo.length > 0) {
124
129
  parts.push(` | ${layoutInfo.join(" ")}`);
125
130
  }
131
+ // Flexbox/Grid context (on separate line for clarity)
132
+ const layoutContext = this.formatLayoutContext(ancestor);
133
+ if (layoutContext) {
134
+ parts.push(`\n ${layoutContext}`);
135
+ }
136
+ // Margin details (only if non-zero or has auto)
137
+ const marginDetails = this.formatMarginDetails(ancestor);
138
+ if (marginDetails) {
139
+ parts.push(`\n ${marginDetails}`);
140
+ }
126
141
  // Border - only if set
127
142
  const borderInfo = this.formatBorder(ancestor);
128
143
  if (borderInfo) {
@@ -222,6 +237,116 @@ export class InspectAncestorsTool extends BrowserToolBase {
222
237
  }
223
238
  return null;
224
239
  }
240
+ formatLayoutContext(ancestor) {
241
+ const parts = [];
242
+ // Flexbox
243
+ if (ancestor.display === "flex" || ancestor.display === "inline-flex") {
244
+ const flexParts = ["flex"];
245
+ if (ancestor.flexDirection && ancestor.flexDirection !== "row") {
246
+ flexParts.push(ancestor.flexDirection);
247
+ }
248
+ if (ancestor.justifyContent && ancestor.justifyContent !== "normal" && ancestor.justifyContent !== "flex-start") {
249
+ flexParts.push(`justify:${ancestor.justifyContent}`);
250
+ }
251
+ if (ancestor.alignItems && ancestor.alignItems !== "normal" && ancestor.alignItems !== "stretch") {
252
+ flexParts.push(`items:${ancestor.alignItems}`);
253
+ }
254
+ if (ancestor.gap && ancestor.gap !== "0px" && ancestor.gap !== "normal") {
255
+ flexParts.push(`gap:${ancestor.gap}`);
256
+ }
257
+ parts.push(flexParts.join(" "));
258
+ }
259
+ // Grid
260
+ if (ancestor.display === "grid" || ancestor.display === "inline-grid") {
261
+ const gridParts = ["grid"];
262
+ if (ancestor.gridTemplateColumns && ancestor.gridTemplateColumns !== "none") {
263
+ gridParts.push(`cols:${ancestor.gridTemplateColumns}`);
264
+ }
265
+ if (ancestor.gridTemplateRows && ancestor.gridTemplateRows !== "none") {
266
+ gridParts.push(`rows:${ancestor.gridTemplateRows}`);
267
+ }
268
+ if (ancestor.gap && ancestor.gap !== "0px" && ancestor.gap !== "normal") {
269
+ gridParts.push(`gap:${ancestor.gap}`);
270
+ }
271
+ parts.push(gridParts.join(" "));
272
+ }
273
+ return parts.length > 0 ? parts.join(" | ") : null;
274
+ }
275
+ formatMarginDetails(ancestor) {
276
+ // Check if any margin is "auto" (CSS value)
277
+ // Note: computed styles show actual values, not "auto"
278
+ const hasAuto = ancestor.margin.includes("auto") ||
279
+ ancestor.marginTop === "auto" ||
280
+ ancestor.marginRight === "auto" ||
281
+ ancestor.marginBottom === "auto" ||
282
+ ancestor.marginLeft === "auto";
283
+ // Check if margins are non-uniform (can't be represented by shorthand)
284
+ const isNonUniform = ancestor.marginTop !== ancestor.marginBottom ||
285
+ ancestor.marginLeft !== ancestor.marginRight ||
286
+ ancestor.marginTop !== ancestor.marginLeft;
287
+ // Parse margin values to detect large symmetric margins (likely auto-centered)
288
+ const parseMarginValue = (val) => {
289
+ const match = val.match(/^([\d.]+)px$/);
290
+ return match ? parseFloat(match[1]) : 0;
291
+ };
292
+ const marginLeftPx = parseMarginValue(ancestor.marginLeft);
293
+ const marginRightPx = parseMarginValue(ancestor.marginRight);
294
+ const marginTopPx = parseMarginValue(ancestor.marginTop);
295
+ const marginBottomPx = parseMarginValue(ancestor.marginBottom);
296
+ // Detect horizontal centering: large equal left/right margins, small top/bottom
297
+ const isHorizontallyCentered = marginLeftPx > 100 &&
298
+ marginRightPx > 100 &&
299
+ Math.abs(marginLeftPx - marginRightPx) < 2 && // Allow 1px rounding
300
+ marginTopPx === 0 &&
301
+ marginBottomPx === 0;
302
+ if (!hasAuto && !isHorizontallyCentered && ancestor.margin === "0px") {
303
+ return null; // All zeros, skip
304
+ }
305
+ // If has auto, always show detailed breakdown with arrows
306
+ if (hasAuto) {
307
+ const parts = [];
308
+ if (ancestor.marginTop !== "0px") {
309
+ parts.push(`↑${ancestor.marginTop}`);
310
+ }
311
+ if (ancestor.marginRight === "auto" || ancestor.marginRight !== "0px") {
312
+ parts.push(`→${ancestor.marginRight}`);
313
+ }
314
+ if (ancestor.marginBottom !== "0px") {
315
+ parts.push(`↓${ancestor.marginBottom}`);
316
+ }
317
+ if (ancestor.marginLeft === "auto" || ancestor.marginLeft !== "0px") {
318
+ parts.push(`←${ancestor.marginLeft}`);
319
+ }
320
+ const marginStr = `margin: ${parts.join(" ")}`;
321
+ // Add diagnostic if horizontally centered
322
+ if (ancestor.marginLeft === "auto" && ancestor.marginRight === "auto") {
323
+ return `${marginStr} ← Horizontally centered by auto margins`;
324
+ }
325
+ return marginStr;
326
+ }
327
+ // Show horizontal centering diagnostic
328
+ if (isHorizontallyCentered) {
329
+ return `margin: →${ancestor.marginRight} ←${ancestor.marginLeft} ← Horizontally centered (likely margin:0 auto)`;
330
+ }
331
+ // If non-uniform and non-zero, show with arrows
332
+ if (isNonUniform && ancestor.margin !== "0px") {
333
+ const parts = [];
334
+ if (ancestor.marginTop !== "0px") {
335
+ parts.push(`↑${ancestor.marginTop}`);
336
+ }
337
+ if (ancestor.marginRight !== "0px") {
338
+ parts.push(`→${ancestor.marginRight}`);
339
+ }
340
+ if (ancestor.marginBottom !== "0px") {
341
+ parts.push(`↓${ancestor.marginBottom}`);
342
+ }
343
+ if (ancestor.marginLeft !== "0px") {
344
+ parts.push(`←${ancestor.marginLeft}`);
345
+ }
346
+ return `margin: ${parts.join(" ")}`;
347
+ }
348
+ return null;
349
+ }
225
350
  generateDiagnostics(ancestor, index) {
226
351
  const diagnostics = [];
227
352
  // Overflow hidden warning
@@ -232,11 +357,6 @@ export class InspectAncestorsTool extends BrowserToolBase {
232
357
  if (ancestor.maxWidth !== "none" && index > 0) {
233
358
  diagnostics.push("🎯 WIDTH CONSTRAINT");
234
359
  }
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
360
  return diagnostics.length > 0 ? diagnostics.join("\n ") : null;
241
361
  }
242
362
  }
@@ -36,11 +36,8 @@ export class ClickAndSwitchTabTool extends BrowserToolBase {
36
36
  ]);
37
37
  // Wait for the new page to load
38
38
  await newPage.waitForLoadState('domcontentloaded');
39
- // Switch control to the new tab
40
- setGlobalPage(newPage);
41
- //page= newPage; // Update the current page to the new tab
42
- //context.page = newPage;
43
- //context.page.bringToFront(); // Bring the new tab to the front
39
+ // Switch control to the new tab and register listeners
40
+ await setGlobalPage(newPage);
44
41
  return createSuccessResponse(`Clicked link and switched to new tab: ${newPage.url()}`);
45
42
  //return createSuccessResponse(`Clicked link and switched to new tab: ${context.page.url()}`);
46
43
  });
@@ -21,8 +21,25 @@ export class VisibleTextTool extends BrowserToolBase {
21
21
  }
22
22
  return this.safeExecute(context, async (page) => {
23
23
  try {
24
- const visibleText = await page.evaluate(() => {
25
- const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, {
24
+ // Normalize selector (support testid: shorthand)
25
+ const selector = args.selector ? this.normalizeSelector(args.selector) : undefined;
26
+ // If selector provided, validate element exists
27
+ if (selector) {
28
+ const element = await page.$(selector);
29
+ if (!element) {
30
+ return createErrorResponse(`Element with selector "${args.selector}" not found`);
31
+ }
32
+ }
33
+ const visibleText = await page.evaluate((sel) => {
34
+ // Find root element - either selected element or body
35
+ let rootElement = document.body;
36
+ if (sel) {
37
+ const element = document.querySelector(sel);
38
+ if (!element)
39
+ return ""; // Should not happen due to earlier check, but be safe
40
+ rootElement = element;
41
+ }
42
+ const walker = document.createTreeWalker(rootElement, NodeFilter.SHOW_TEXT, {
26
43
  acceptNode: (node) => {
27
44
  const style = window.getComputedStyle(node.parentElement);
28
45
  return (style.display !== "none" && style.visibility !== "hidden")
@@ -39,7 +56,7 @@ export class VisibleTextTool extends BrowserToolBase {
39
56
  }
40
57
  }
41
58
  return text.trim();
42
- });
59
+ }, selector);
43
60
  // Truncate logic
44
61
  const maxLength = typeof args.maxLength === 'number' ? args.maxLength : 20000;
45
62
  let output = visibleText;
@@ -48,7 +65,13 @@ export class VisibleTextTool extends BrowserToolBase {
48
65
  output = output.slice(0, maxLength) + '\n[Output truncated due to size limits]';
49
66
  truncated = true;
50
67
  }
51
- return createSuccessResponse(`Visible text content:\n${output}`);
68
+ // Add guidance footer
69
+ const scopeInfo = selector ? ` (from "${args.selector}")` : " (entire page)";
70
+ const guidance = `\n\n💡 TIP: If you need structured inspection rather than raw text:
71
+ • inspect_dom() - See page structure with positions and relationships
72
+ • find_by_text("text") - Locate specific text with element context
73
+ • query_selector("selector") - Find and inspect specific elements`;
74
+ return createSuccessResponse(`Visible text content${scopeInfo}:\n${output}${guidance}`);
52
75
  }
53
76
  catch (error) {
54
77
  return createErrorResponse(`Failed to get visible text content: ${error.message}`);
@@ -76,91 +99,80 @@ export class VisibleHtmlTool extends BrowserToolBase {
76
99
  }
77
100
  return this.safeExecute(context, async (page) => {
78
101
  try {
79
- const { removeComments, removeStyles, removeMeta, minify, cleanHtml } = args;
80
- // Default removeScripts to true unless explicitly set to false
81
- const removeScripts = args.removeScripts === false ? false : true;
82
102
  // Normalize selector (support testid: shorthand)
83
103
  const selector = args.selector ? this.normalizeSelector(args.selector) : undefined;
104
+ // If selector provided, validate element exists
105
+ if (selector) {
106
+ const element = await page.$(selector);
107
+ if (!element) {
108
+ return createErrorResponse(`Element with selector "${args.selector}" not found`);
109
+ }
110
+ }
84
111
  // Get the HTML content
85
112
  let htmlContent;
86
113
  if (selector) {
87
114
  // If a selector is provided, get only the HTML for that element
88
115
  const element = await page.$(selector);
89
- if (!element) {
90
- return createErrorResponse(`Element with selector "${selector}" not found`);
91
- }
92
116
  htmlContent = await page.evaluate((el) => el.outerHTML, element);
93
117
  }
94
118
  else {
95
119
  // Otherwise get the full page HTML
96
120
  htmlContent = await page.content();
97
121
  }
98
- // Determine if we need to apply filters
99
- const shouldRemoveScripts = removeScripts || cleanHtml;
100
- const shouldRemoveComments = removeComments || cleanHtml;
101
- const shouldRemoveStyles = removeStyles || cleanHtml;
102
- const shouldRemoveMeta = removeMeta || cleanHtml;
122
+ // Determine cleanup level
123
+ // Default: remove scripts only (security/size)
124
+ // clean=true: remove scripts + styles + comments + meta
125
+ const clean = args.clean === true;
103
126
  // Apply filters in the browser context
104
- if (shouldRemoveScripts || shouldRemoveComments || shouldRemoveStyles || shouldRemoveMeta || minify) {
105
- htmlContent = await page.evaluate(({ html, removeScripts, removeComments, removeStyles, removeMeta, minify }) => {
106
- // Create a DOM parser to work with the HTML
107
- const parser = new DOMParser();
108
- const doc = parser.parseFromString(html, 'text/html');
109
- // Remove script tags if requested
110
- if (removeScripts) {
111
- const scripts = doc.querySelectorAll('script');
112
- scripts.forEach(script => script.remove());
113
- }
114
- // Remove style tags if requested
115
- if (removeStyles) {
116
- const styles = doc.querySelectorAll('style');
117
- styles.forEach(style => style.remove());
118
- }
119
- // Remove meta tags if requested
120
- if (removeMeta) {
121
- const metaTags = doc.querySelectorAll('meta');
122
- metaTags.forEach(meta => meta.remove());
123
- }
124
- // Remove HTML comments if requested
125
- if (removeComments) {
126
- const removeComments = (node) => {
127
- const childNodes = node.childNodes;
128
- for (let i = childNodes.length - 1; i >= 0; i--) {
129
- const child = childNodes[i];
130
- if (child.nodeType === 8) { // 8 is for comment nodes
131
- node.removeChild(child);
132
- }
133
- else if (child.nodeType === 1) { // 1 is for element nodes
134
- removeComments(child);
135
- }
127
+ htmlContent = await page.evaluate(({ html, clean }) => {
128
+ // Create a DOM parser to work with the HTML
129
+ const parser = new DOMParser();
130
+ const doc = parser.parseFromString(html, 'text/html');
131
+ // Always remove scripts (default behavior)
132
+ const scripts = doc.querySelectorAll('script');
133
+ scripts.forEach(script => script.remove());
134
+ // If clean=true, remove additional noise
135
+ if (clean) {
136
+ // Remove style tags
137
+ const styles = doc.querySelectorAll('style');
138
+ styles.forEach(style => style.remove());
139
+ // Remove meta tags
140
+ const metaTags = doc.querySelectorAll('meta');
141
+ metaTags.forEach(meta => meta.remove());
142
+ // Remove HTML comments
143
+ const removeCommentsRecursive = (node) => {
144
+ const childNodes = node.childNodes;
145
+ for (let i = childNodes.length - 1; i >= 0; i--) {
146
+ const child = childNodes[i];
147
+ if (child.nodeType === 8) { // 8 is for comment nodes
148
+ node.removeChild(child);
136
149
  }
137
- };
138
- removeComments(doc.documentElement);
139
- }
140
- // Get the processed HTML
141
- let result = doc.documentElement.outerHTML;
142
- // Minify if requested
143
- if (minify) {
144
- // Simple minification: remove extra whitespace
145
- result = result.replace(/>\s+</g, '><').trim();
146
- }
147
- return result;
148
- }, {
149
- html: htmlContent,
150
- removeScripts: shouldRemoveScripts,
151
- removeComments: shouldRemoveComments,
152
- removeStyles: shouldRemoveStyles,
153
- removeMeta: shouldRemoveMeta,
154
- minify
155
- });
156
- }
150
+ else if (child.nodeType === 1) { // 1 is for element nodes
151
+ removeCommentsRecursive(child);
152
+ }
153
+ }
154
+ };
155
+ removeCommentsRecursive(doc.documentElement);
156
+ }
157
+ // Get the processed HTML
158
+ return doc.documentElement.outerHTML;
159
+ }, { html: htmlContent, clean });
157
160
  // Truncate logic
158
161
  const maxLength = typeof args.maxLength === 'number' ? args.maxLength : 20000;
159
162
  let output = htmlContent;
163
+ let truncated = false;
160
164
  if (output.length > maxLength) {
161
165
  output = output.slice(0, maxLength) + '\n<!-- Output truncated due to size limits -->';
166
+ truncated = true;
162
167
  }
163
- return createSuccessResponse(`HTML content:\n${output}`);
168
+ // Add guidance footer
169
+ const scopeInfo = selector ? ` (from "${args.selector}")` : " (entire page)";
170
+ const cleanInfo = clean ? ", clean mode" : ", scripts removed";
171
+ const guidance = `\n\n💡 TIP: If you need structured inspection rather than raw HTML:
172
+ • inspect_dom() - See page structure with positions and relationships
173
+ • query_selector("selector") - Find and inspect specific elements
174
+ • get_computed_styles("selector") - Get CSS values for elements`;
175
+ return createSuccessResponse(`HTML content${scopeInfo}${cleanInfo}:\n${output}${guidance}`);
164
176
  }
165
177
  catch (error) {
166
178
  return createErrorResponse(`Failed to get visible HTML content: ${error.message}`);
package/dist/tools.d.ts CHANGED
@@ -2,6 +2,7 @@ interface SessionConfig {
2
2
  saveSession: boolean;
3
3
  userDataDir: string;
4
4
  screenshotsDir: string;
5
+ headlessDefault: boolean;
5
6
  }
6
7
  export declare function createToolDefinitions(sessionConfig?: SessionConfig): [{
7
8
  readonly name: "navigate";
@@ -41,7 +42,7 @@ export declare function createToolDefinitions(sessionConfig?: SessionConfig): [{
41
42
  };
42
43
  readonly headless: {
43
44
  readonly type: "boolean";
44
- readonly description: "Run browser in headless mode (default: false)";
45
+ readonly description: "Run browser in headless mode (default: true - no window shown)" | "Run browser in headless mode (default: false - browser window visible)";
45
46
  };
46
47
  };
47
48
  readonly required: readonly ["url"];
@@ -202,45 +203,34 @@ export declare function createToolDefinitions(sessionConfig?: SessionConfig): [{
202
203
  };
203
204
  }, {
204
205
  readonly name: "get_text";
205
- readonly description: "Get the visible text content of the current page";
206
+ readonly description: "⚠️ RARELY NEEDED: Get ALL visible text content from the entire page (no structure, just raw text). Most tasks need structured inspection instead. ONLY use get_text for: (1) extracting text for content analysis (word count, language detection), (2) searching for text when location is completely unknown, (3) text-only snapshots for comparison. For structured tasks, use: inspect_dom() to understand page structure, find_by_text() to locate specific text with context, query_selector() to find elements. Returns plain text up to 20000 chars (truncated if longer). Supports testid shortcuts.";
206
207
  readonly inputSchema: {
207
208
  readonly type: "object";
208
- readonly properties: {};
209
+ readonly properties: {
210
+ readonly selector: {
211
+ readonly type: "string";
212
+ readonly description: "CSS selector, text selector, or testid shorthand to limit text extraction to a specific container. Omit to get text from entire page. Example: 'testid:article-body' or '#main-content'";
213
+ };
214
+ readonly maxLength: {
215
+ readonly type: "number";
216
+ readonly description: "Maximum number of characters to return (default: 20000)";
217
+ };
218
+ };
209
219
  readonly required: readonly [];
210
220
  };
211
221
  }, {
212
222
  readonly name: "get_html";
213
- readonly description: "Get raw HTML content of the page or specific element. ⚠️ For understanding page structure, use inspect_dom() instead - it's more efficient and filters out noise. Use get_html() only when you need the actual HTML markup (e.g., to check specific attributes or element nesting).";
223
+ readonly 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.";
214
224
  readonly inputSchema: {
215
225
  readonly type: "object";
216
226
  readonly properties: {
217
227
  readonly selector: {
218
228
  readonly type: "string";
219
- readonly description: "CSS selector to limit the HTML to a specific container";
220
- };
221
- readonly removeScripts: {
222
- readonly type: "boolean";
223
- readonly description: "Remove all script tags from the HTML (default: true)";
224
- };
225
- readonly removeComments: {
226
- readonly type: "boolean";
227
- readonly description: "Remove all HTML comments (default: false)";
228
- };
229
- readonly removeStyles: {
230
- readonly type: "boolean";
231
- readonly description: "Remove all style tags from the HTML (default: false)";
232
- };
233
- readonly removeMeta: {
234
- readonly type: "boolean";
235
- readonly description: "Remove all meta tags from the HTML (default: false)";
236
- };
237
- readonly cleanHtml: {
238
- readonly type: "boolean";
239
- readonly description: "Perform comprehensive HTML cleaning (default: false)";
229
+ readonly description: "CSS selector, text selector, or testid shorthand to limit HTML extraction to a specific container. Omit to get entire page HTML. Example: 'testid:main-content' or '#app'";
240
230
  };
241
- readonly minify: {
231
+ readonly clean: {
242
232
  readonly type: "boolean";
243
- readonly description: "Minify the HTML output (default: false)";
233
+ readonly description: "Remove noise from HTML: false (default) = remove scripts only, true = remove scripts + styles + comments + meta tags for minimal markup";
244
234
  };
245
235
  readonly maxLength: {
246
236
  readonly type: "number";
@@ -470,7 +460,7 @@ export declare function createToolDefinitions(sessionConfig?: SessionConfig): [{
470
460
  };
471
461
  }, {
472
462
  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.";
463
+ readonly description: "DEBUG LAYOUT CONSTRAINTS: Walk up the DOM tree to find where width constraints, margins, borders, and overflow clipping come from. Shows for each ancestor: position/size, width constraints (w, max-w, min-w), margins with directional arrows (↑↓←→ format), padding, display type, borders (directional if non-uniform), overflow (🔒=hidden, ↕️=scroll), flexbox context (flex direction justify items gap), grid context (cols rows gap), position/z-index/transform when set. Automatically detects horizontal centering via auto margins and flags clipping points (🎯). Essential for debugging unexpected centering, constrained width, or clipped content. Default: 10 ancestors (reaches <body> in most React apps), max: 15. Use after inspect_dom() to understand parent layout constraints.";
474
464
  readonly inputSchema: {
475
465
  readonly type: "object";
476
466
  readonly properties: {
package/dist/tools.js CHANGED
@@ -3,6 +3,7 @@ export function createToolDefinitions(sessionConfig) {
3
3
  const sessionEnabled = sessionConfig?.saveSession ?? true;
4
4
  const userDataDir = sessionConfig?.userDataDir || './.mcp-web-inspector/user-data';
5
5
  const screenshotsDir = sessionConfig?.screenshotsDir || './.mcp-web-inspector/screenshots';
6
+ const headlessDefault = sessionConfig?.headlessDefault ?? false;
6
7
  const navigateDescription = sessionEnabled
7
8
  ? `Navigate to a URL. Browser sessions (cookies, localStorage, sessionStorage) are automatically saved in ${userDataDir} directory and persist across restarts. To clear saved sessions, delete the directory.`
8
9
  : "Navigate to a URL. Browser starts fresh each time with no persistent session state (started with --no-save-session flag).";
@@ -24,7 +25,7 @@ export function createToolDefinitions(sessionConfig) {
24
25
  height: { type: "number", description: "Viewport height in pixels. If not specified, automatically matches screen height. Ignored if device is specified." },
25
26
  timeout: { type: "number", description: "Navigation timeout in milliseconds" },
26
27
  waitUntil: { type: "string", description: "Navigation wait condition" },
27
- headless: { type: "boolean", description: "Run browser in headless mode (default: false)" }
28
+ headless: { type: "boolean", description: `Run browser in headless mode (default: ${headlessDefault ? 'true - no window shown' : 'false - browser window visible'})` }
28
29
  },
29
30
  required: ["url"],
30
31
  },
@@ -167,27 +168,40 @@ export function createToolDefinitions(sessionConfig) {
167
168
  },
168
169
  {
169
170
  name: "get_text",
170
- description: "Get the visible text content of the current page",
171
+ description: "⚠️ RARELY NEEDED: Get ALL visible text content from the entire page (no structure, just raw text). Most tasks need structured inspection instead. ONLY use get_text for: (1) extracting text for content analysis (word count, language detection), (2) searching for text when location is completely unknown, (3) text-only snapshots for comparison. For structured tasks, use: inspect_dom() to understand page structure, find_by_text() to locate specific text with context, query_selector() to find elements. Returns plain text up to 20000 chars (truncated if longer). Supports testid shortcuts.",
171
172
  inputSchema: {
172
173
  type: "object",
173
- properties: {},
174
+ properties: {
175
+ selector: {
176
+ type: "string",
177
+ description: "CSS selector, text selector, or testid shorthand to limit text extraction to a specific container. Omit to get text from entire page. Example: 'testid:article-body' or '#main-content'"
178
+ },
179
+ maxLength: {
180
+ type: "number",
181
+ description: "Maximum number of characters to return (default: 20000)"
182
+ }
183
+ },
174
184
  required: [],
175
185
  },
176
186
  },
177
187
  {
178
188
  name: "get_html",
179
- description: "Get raw HTML content of the page or specific element. ⚠️ For understanding page structure, use inspect_dom() instead - it's more efficient and filters out noise. Use get_html() only when you need the actual HTML markup (e.g., to check specific attributes or element nesting).",
189
+ 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.",
180
190
  inputSchema: {
181
191
  type: "object",
182
192
  properties: {
183
- selector: { type: "string", description: "CSS selector to limit the HTML to a specific container" },
184
- removeScripts: { type: "boolean", description: "Remove all script tags from the HTML (default: true)" },
185
- removeComments: { type: "boolean", description: "Remove all HTML comments (default: false)" },
186
- removeStyles: { type: "boolean", description: "Remove all style tags from the HTML (default: false)" },
187
- removeMeta: { type: "boolean", description: "Remove all meta tags from the HTML (default: false)" },
188
- cleanHtml: { type: "boolean", description: "Perform comprehensive HTML cleaning (default: false)" },
189
- minify: { type: "boolean", description: "Minify the HTML output (default: false)" },
190
- maxLength: { type: "number", description: "Maximum number of characters to return (default: 20000)" }
193
+ selector: {
194
+ type: "string",
195
+ description: "CSS selector, text selector, or testid shorthand to limit HTML extraction to a specific container. Omit to get entire page HTML. Example: 'testid:main-content' or '#app'"
196
+ },
197
+ clean: {
198
+ type: "boolean",
199
+ description: "Remove noise from HTML: false (default) = remove scripts only, true = remove scripts + styles + comments + meta tags for minimal markup"
200
+ },
201
+ maxLength: {
202
+ type: "number",
203
+ description: "Maximum number of characters to return (default: 20000)"
204
+ }
191
205
  },
192
206
  required: [],
193
207
  },
@@ -439,7 +453,7 @@ More efficient than get_html() or evaluate(). Supports testid shortcuts.`,
439
453
  },
440
454
  {
441
455
  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.",
456
+ description: "DEBUG LAYOUT CONSTRAINTS: Walk up the DOM tree to find where width constraints, margins, borders, and overflow clipping come from. Shows for each ancestor: position/size, width constraints (w, max-w, min-w), margins with directional arrows (↑↓←→ format), padding, display type, borders (directional if non-uniform), overflow (🔒=hidden, ↕️=scroll), flexbox context (flex direction justify items gap), grid context (cols rows gap), position/z-index/transform when set. Automatically detects horizontal centering via auto margins and flags clipping points (🎯). Essential for debugging unexpected centering, constrained width, or clipped content. Default: 10 ancestors (reaches <body> in most React apps), max: 15. Use after inspect_dom() to understand parent layout constraints.",
443
457
  inputSchema: {
444
458
  type: "object",
445
459
  properties: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcp-web-inspector",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
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",