mcp-web-inspector 0.6.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -319,6 +319,7 @@ Customize server behavior with command line flags:
319
319
  - **`--no-save-session`** - Disable automatic session persistence (start with fresh browser state each time)
320
320
  - **`--user-data-dir <path>`** - Custom directory for session data (default: `./.mcp-web-inspector`)
321
321
  - **`--headless`** - Run browser in headless mode by default (no visible window)
322
+ - **`--expose-sensitive-network-data`** - Loosen redaction for sensitive network headers (e.g., show truncated auth/cookie values). Disabled by default for safety.
322
323
 
323
324
  **Example usage:**
324
325
  ```json
@@ -350,7 +351,7 @@ Customize server behavior with command line flags:
350
351
  "mcpServers": {
351
352
  "web-inspector": {
352
353
  "command": "npx",
353
- "args": ["-y", "mcp-web-inspector", "--headless", "--no-save-session"]
354
+ "args": ["-y", "mcp-web-inspector", "--headless", "--no-save-session", "--user-data-dir", "./.mcp-web-inspector"]
354
355
  }
355
356
  }
356
357
  }
@@ -495,7 +496,6 @@ RELATED TOOLS: For comparing TWO elements' alignment (not parent-child), use com
495
496
  - includeHidden (boolean, optional): Include hidden elements in results (default: false)
496
497
  - maxChildren (number, optional): Maximum number of children to show (default: 20)
497
498
  - maxDepth (number, optional): Maximum depth to drill through non-semantic wrapper elements when looking for semantic children (default: 5). Increase for extremely deeply nested components, decrease to 1 to see only immediate children without drilling.
498
- - elementIndex (number, optional): When selector matches multiple elements, use this 1-based index to select a specific one (e.g., 2 = second element). Default: first visible element.
499
499
 
500
500
  - Output Format:
501
501
  - Optional selection header when multiple matches (with chosen index).
@@ -542,7 +542,6 @@ DEBUG LAYOUT CONSTRAINTS: Walk up the DOM tree to find where width constraints,
542
542
  - Parameters:
543
543
  - selector (string, required): CSS selector or testid shorthand for the element to start from (e.g., 'testid:header', '#main')
544
544
  - limit (number, optional): Maximum number of ancestors to traverse (default: 10, max: 15). Increase for deeply nested component frameworks.
545
- - elementIndex (number, optional): When selector matches multiple elements, use this 1-based index to select a specific one (e.g., 2 = second element). Default: first visible element.
546
545
 
547
546
  - Output Format:
548
547
  - Header showing selected element index when selector matched multiple.
@@ -634,7 +633,6 @@ INSPECT CSS PROPERTIES: Get computed CSS values for specific properties (display
634
633
  - Parameters:
635
634
  - selector (string, required): CSS selector, text selector, or testid shorthand (e.g., 'testid:submit-button', '#main')
636
635
  - properties (string, optional): Comma-separated list of CSS properties to retrieve (e.g., 'display,width,color'). If not specified, returns common layout properties: display, position, width, height, opacity, visibility, z-index, overflow, margin, padding, font-size, font-weight, color, background-color
637
- - elementIndex (number, optional): When selector matches multiple elements, use this 1-based index to select a specific one (e.g., 2 = second element). Default: first visible element.
638
636
 
639
637
  - Output Format:
640
638
  - Optional selection header when multiple elements matched.
@@ -648,7 +646,10 @@ INSPECT CSS PROPERTIES: Get computed CSS values for specific properties (display
648
646
 
649
647
  - Example Output (get_computed_styles({ selector: 'testid:login-form' })):
650
648
  ```
651
- Warning: Selector matched 2 elements, showing 1 (use elementIndex to target a specific one)
649
+ Found 2 elements matching "testid:login-form", using element 1 (first visible)
650
+ 💡 Tip: Consider adding a unique data-testid attribute for more reliable selection.
651
+ Primary fix: add data-testid and target it (e.g., testid:submit).
652
+ Workaround: use '>> nth=<index>' only when you can't add test IDs.
652
653
 
653
654
  Computed Styles: <form data-testid="login-form">
654
655
 
@@ -679,7 +680,6 @@ Check if an element is visible to the user. CRITICAL for debugging click/interac
679
680
 
680
681
  - Parameters:
681
682
  - selector (string, required): CSS selector, text selector, or testid shorthand (e.g., 'testid:login-button', '#submit', 'text=Click here')
682
- - elementIndex (number, optional): When selector matches multiple elements, use this 1-based index to select a specific one (e.g., 2 = second element). Default: first visible element.
683
683
 
684
684
  - Output Format:
685
685
  - Header: Visibility: <tag id/class/testid>
@@ -692,7 +692,7 @@ Check if an element is visible to the user. CRITICAL for debugging click/interac
692
692
 
693
693
  - Examples:
694
694
  - check_visibility({ selector: 'testid:submit' })
695
- - check_visibility({ selector: '#login button', elementIndex: 2 })
695
+ - check_visibility({ selector: '#login button' })
696
696
 
697
697
  - Example Output (check_visibility({ selector: 'testid:submit' })):
698
698
  ```
@@ -808,7 +808,6 @@ data-cy (2):
808
808
 
809
809
  - Parameters:
810
810
  - selector (string, required): CSS selector or testid shorthand (e.g., 'testid:submit', '#login-button')
811
- - elementIndex (number, optional): When selector matches multiple elements, use this 1-based index to select a specific one (e.g., 2 = second element). Default: first visible element.
812
811
 
813
812
  - Output Format:
814
813
  - Header: Element: <tag id/class/testid>
@@ -1001,16 +1000,14 @@ Upload a file to an input[type='file'] element on the page
1001
1000
 
1002
1001
  - Parameters:
1003
1002
  - selector (string, optional): 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'
1004
- - elementIndex (number, optional): When selector matches multiple elements, use this 1-based index to select a specific one (e.g., 2 = second element). Default: first visible element.
1005
1003
  - clean (boolean, optional): Remove noise from HTML: false (default) = remove scripts only, true = remove scripts + styles + comments + meta tags for minimal markup
1006
1004
  - maxLength (number, optional): Maximum number of characters to return (default: 20000)
1007
1005
 
1008
1006
  #### `get_text`
1009
- ⚠️ 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.
1007
+ ⚠️ 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. Auto-returns text if <2000 chars (small elements); if larger, returns a preview and a one-time token to fetch the full output via confirm_output. Supports testid shortcuts.
1010
1008
 
1011
1009
  - Parameters:
1012
1010
  - selector (string, optional): 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'
1013
- - elementIndex (number, optional): When selector matches multiple elements, use this 1-based index to select a specific one (e.g., 2 = second element). Default: first visible element.
1014
1011
  - maxLength (number, optional): Maximum number of characters to return (default: 20000)
1015
1012
 
1016
1013
  #### `visual_screenshot_for_humans`
@@ -1078,7 +1075,7 @@ Retrieve console logs with filtering and token‑efficient output. Defaults: sin
1078
1075
  ### Network
1079
1076
 
1080
1077
  #### `get_request_details`
1081
- Get detailed information about a specific network request by index (from list_network_requests). Returns request/response headers, body (truncated at 500 chars), timing, and size. Request bodies with passwords are automatically masked. Essential for debugging API responses and investigating failed requests.
1078
+ Get detailed information about a specific network request by index (from list_network_requests). Returns request/response headers, body (truncated at 500 chars), timing, and size. Request bodies with passwords are automatically masked. If a request or response body exceeds 500 chars, includes a preview and a one-time confirm_output token that, when called, saves the full body to disk under ./.mcp-web-inspector/network-bodies/ and returns the file path(s). Essential for debugging API responses and investigating failed requests.
1082
1079
 
1083
1080
  - Parameters:
1084
1081
  - index (number, required): Index of the request from list_network_requests output (e.g., [0], [1], etc.)
package/dist/index.js CHANGED
@@ -10,6 +10,12 @@ import { fileURLToPath } from "node:url";
10
10
  import { dirname, join } from "node:path";
11
11
  // Get package.json version
12
12
  const __dirname = dirname(fileURLToPath(import.meta.url));
13
+ const PACKAGE_ROOT = join(__dirname, "..");
14
+ // Expose package root for tools that need to spawn child processes (e.g., npx playwright)
15
+ // Tests and non-CLI environments can override or ignore this.
16
+ if (!process.env.MCP_WEB_INSPECTOR_PACKAGE_ROOT) {
17
+ process.env.MCP_WEB_INSPECTOR_PACKAGE_ROOT = PACKAGE_ROOT;
18
+ }
13
19
  const packageJson = JSON.parse(readFileSync(join(__dirname, "../package.json"), "utf-8"));
14
20
  const VERSION = packageJson.version;
15
21
  // Parse command line arguments
@@ -19,6 +25,10 @@ const { values } = parseArgs({
19
25
  type: 'boolean',
20
26
  default: false,
21
27
  },
28
+ 'expose-sensitive-network-data': {
29
+ type: 'boolean',
30
+ default: false,
31
+ },
22
32
  'user-data-dir': {
23
33
  type: 'string',
24
34
  default: './.mcp-web-inspector',
@@ -45,6 +55,7 @@ const sessionConfig = {
45
55
  userDataDir: `${baseDir}/user-data`,
46
56
  screenshotsDir: `${baseDir}/screenshots`,
47
57
  headlessDefault: Boolean(values['headless']),
58
+ exposeSensitiveNetworkData: Boolean(values['expose-sensitive-network-data']),
48
59
  };
49
60
  setSessionConfig(sessionConfig);
50
61
  async function runServer() {
@@ -74,7 +85,7 @@ async function runServer() {
74
85
  setupRequestHandlers(server, TOOLS);
75
86
  // Graceful shutdown logic
76
87
  function shutdown() {
77
- console.log('Shutdown signal received');
88
+ console.error('Shutdown signal received');
78
89
  process.exit(0);
79
90
  }
80
91
  process.on('SIGINT', shutdown);
@@ -24,6 +24,10 @@ type ColorSchemeOverride = 'light' | 'dark' | 'no-preference';
24
24
  * Sets the session configuration
25
25
  */
26
26
  export declare function setSessionConfig(config: Partial<SessionConfig>): void;
27
+ /**
28
+ * Gets the current session configuration
29
+ */
30
+ export declare function getSessionConfig(): SessionConfig;
27
31
  /**
28
32
  * Gets the screenshots directory
29
33
  */
@@ -12,14 +12,25 @@ let sessionConfig = {
12
12
  userDataDir: './.mcp-web-inspector/user-data',
13
13
  screenshotsDir: './.mcp-web-inspector/screenshots',
14
14
  headlessDefault: false,
15
+ exposeSensitiveNetworkData: false,
15
16
  };
16
17
  let colorSchemeOverride = null;
18
+ // Resolve package root for child processes (like npx). Entry point sets
19
+ // MCP_WEB_INSPECTOR_PACKAGE_ROOT using import.meta.url; tests and other
20
+ // environments fall back to process.cwd().
21
+ const PACKAGE_ROOT = process.env.MCP_WEB_INSPECTOR_PACKAGE_ROOT || process.cwd();
17
22
  /**
18
23
  * Sets the session configuration
19
24
  */
20
25
  export function setSessionConfig(config) {
21
26
  sessionConfig = { ...sessionConfig, ...config };
22
27
  }
28
+ /**
29
+ * Gets the current session configuration
30
+ */
31
+ export function getSessionConfig() {
32
+ return sessionConfig;
33
+ }
23
34
  /**
24
35
  * Gets the screenshots directory
25
36
  */
@@ -42,6 +53,7 @@ export function resetBrowserState() {
42
53
  currentBrowserType = 'chromium';
43
54
  currentDevice = undefined;
44
55
  networkLog = [];
56
+ clearConsoleLogs();
45
57
  }
46
58
  /**
47
59
  * Gets the network log
@@ -66,7 +78,7 @@ export async function setGlobalPage(newPage) {
66
78
  await registerNetworkListeners(page);
67
79
  await applyColorScheme(page);
68
80
  page.bringToFront(); // Bring the new tab to the front
69
- console.log("Global page has been updated with listeners registered.");
81
+ console.error("Global page has been updated with listeners registered.");
70
82
  }
71
83
  function getColorSchemeValue() {
72
84
  return colorSchemeOverride;
@@ -300,9 +312,10 @@ export async function ensureBrowser(browserSettings) {
300
312
  const { execSync } = await import('child_process');
301
313
  execSync('npx playwright install chromium firefox webkit', {
302
314
  stdio: 'inherit',
303
- encoding: 'utf8'
315
+ encoding: 'utf8',
316
+ cwd: PACKAGE_ROOT,
304
317
  });
305
- console.log('✅ Browsers installed successfully! Starting browser...');
318
+ console.error('✅ Browsers installed successfully! Starting browser...');
306
319
  // Note: browser variable is still undefined here, which is correct.
307
320
  // The code below (line 342) will launch the browser after installation.
308
321
  }
@@ -346,7 +359,7 @@ export async function ensureBrowser(browserSettings) {
346
359
  const targetHeight = height ?? currentViewport?.height ?? 720;
347
360
  // Check if viewport size actually changed
348
361
  if (!currentViewport || currentViewport.width !== targetWidth || currentViewport.height !== targetHeight) {
349
- console.log(`Resizing viewport to ${targetWidth}x${targetHeight}`);
362
+ console.error(`Resizing viewport to ${targetWidth}x${targetHeight}`);
350
363
  await page.setViewportSize({ width: targetWidth, height: targetHeight });
351
364
  }
352
365
  }
@@ -371,7 +384,7 @@ export async function ensureBrowser(browserSettings) {
371
384
  // Check custom configs first, then Playwright's built-in devices
372
385
  deviceConfig = CUSTOM_DEVICE_CONFIGS[playwrightDeviceName] || devices[playwrightDeviceName];
373
386
  if (deviceConfig) {
374
- console.log(`Using device preset: ${device} (${playwrightDeviceName})`);
387
+ console.error(`Using device preset: ${device} (${playwrightDeviceName})`);
375
388
  currentDevice = device;
376
389
  }
377
390
  else {
@@ -412,7 +425,7 @@ export async function ensureBrowser(browserSettings) {
412
425
  viewportWidth = screenSize?.width ?? 1280;
413
426
  viewportHeight = screenSize?.height ?? 720;
414
427
  if (screenSize && screenSize.width > 0 && screenSize.height > 0) {
415
- console.log(`No viewport specified, using screen size: ${viewportWidth}x${viewportHeight}`);
428
+ console.error(`No viewport specified, using screen size: ${viewportWidth}x${viewportHeight}`);
416
429
  }
417
430
  }
418
431
  // Prepare context options
@@ -20,6 +20,10 @@ export class BrowserToolBase {
20
20
  * @returns Normalized selector
21
21
  */
22
22
  normalizeSelector(selector) {
23
+ const raw = selector.trim();
24
+ if (!raw) {
25
+ return '';
26
+ }
23
27
  const prefixMap = {
24
28
  'testid:': 'data-testid',
25
29
  'data-test:': 'data-test',
@@ -27,12 +31,23 @@ export class BrowserToolBase {
27
31
  };
28
32
  // Handle testid shortcuts first
29
33
  for (const [prefix, attr] of Object.entries(prefixMap)) {
30
- if (selector.startsWith(prefix)) {
31
- const value = selector.slice(prefix.length);
32
- return `[${attr}="${value}"]`;
34
+ if (raw.startsWith(prefix)) {
35
+ const rest = raw.slice(prefix.length);
36
+ // Allow combined selectors like:
37
+ // "testid:chat-buttons button:first-child"
38
+ // "testid:chat-buttons\n button:first-child"
39
+ // We treat the portion immediately after the prefix up to the first
40
+ // whitespace or combinator as the attribute value, and append the tail.
41
+ const splitIndex = rest.search(/[\s>+~,]/);
42
+ if (splitIndex === -1) {
43
+ return `[${attr}="${rest}"]`;
44
+ }
45
+ const attrValue = rest.slice(0, splitIndex);
46
+ const tail = rest.slice(splitIndex);
47
+ return `[${attr}="${attrValue}"]${tail}`;
33
48
  }
34
49
  }
35
- const trimmed = selector.trim();
50
+ const trimmed = raw;
36
51
  // Helper: unescape simple backslash-escapes used inside IDs (e.g., \:, \[, \])
37
52
  const unescapeCssIdentifier = (s) => {
38
53
  // Collapse multiple backslashes before a single char to the char itself
@@ -158,8 +173,9 @@ export class BrowserToolBase {
158
173
  * Record that a navigation occurred (for console log filtering)
159
174
  */
160
175
  recordNavigation() {
161
- import('../../toolHandler.js').then(({ updateLastNavigationTimestamp }) => {
176
+ import('../../toolHandler.js').then(({ updateLastNavigationTimestamp, updateLastInteractionTimestamp }) => {
162
177
  updateLastNavigationTimestamp();
178
+ updateLastInteractionTimestamp();
163
179
  });
164
180
  }
165
181
  /**
@@ -16,10 +16,6 @@ export class GetHtmlTool extends BrowserToolBase {
16
16
  type: "string",
17
17
  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'"
18
18
  },
19
- elementIndex: {
20
- type: "number",
21
- description: "When selector matches multiple elements, use this 1-based index to select a specific one (e.g., 2 = second element). Default: first visible element."
22
- },
23
19
  clean: {
24
20
  type: "boolean",
25
21
  description: "Remove noise from HTML: false (default) = remove scripts only, true = remove scripts + styles + comments + meta tags for minimal markup"
@@ -59,7 +55,6 @@ export class GetHtmlTool extends BrowserToolBase {
59
55
  const normalizedSelector = this.normalizeSelector(args.selector);
60
56
  const locator = page.locator(normalizedSelector);
61
57
  const { element, elementIndex, totalCount } = await this.selectPreferredLocator(locator, {
62
- elementIndex: args.elementIndex,
63
58
  originalSelector: args.selector,
64
59
  });
65
60
  selectionWarning = this.formatElementSelectionInfo(args.selector, elementIndex, totalCount, true);
@@ -1,5 +1,6 @@
1
1
  import { BrowserToolBase } from '../base.js';
2
2
  import { createSuccessResponse, createErrorResponse, } from '../../common/types.js';
3
+ import { makeConfirmPreview } from '../../common/confirm_output.js';
3
4
  /**
4
5
  * Tool for getting visible text from the page
5
6
  */
@@ -7,7 +8,7 @@ export class GetTextTool extends BrowserToolBase {
7
8
  static getMetadata(sessionConfig) {
8
9
  return {
9
10
  name: "get_text",
10
- 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.",
11
+ 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. Auto-returns text if <2000 chars (small elements); if larger, returns a preview and a one-time token to fetch the full output via confirm_output. Supports testid shortcuts.",
11
12
  inputSchema: {
12
13
  type: "object",
13
14
  properties: {
@@ -15,10 +16,6 @@ export class GetTextTool extends BrowserToolBase {
15
16
  type: "string",
16
17
  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'"
17
18
  },
18
- elementIndex: {
19
- type: "number",
20
- description: "When selector matches multiple elements, use this 1-based index to select a specific one (e.g., 2 = second element). Default: first visible element."
21
- },
22
19
  maxLength: {
23
20
  type: "number",
24
21
  description: "Maximum number of characters to return (default: 20000)"
@@ -32,6 +29,7 @@ export class GetTextTool extends BrowserToolBase {
32
29
  const requestedMaxLength = typeof args.maxLength === 'number' && Number.isFinite(args.maxLength) && args.maxLength > 0
33
30
  ? Math.floor(args.maxLength)
34
31
  : 20000;
32
+ const PREVIEW_THRESHOLD = 2000;
35
33
  if (!context.page) {
36
34
  return createErrorResponse('Page is not available');
37
35
  }
@@ -52,7 +50,6 @@ export class GetTextTool extends BrowserToolBase {
52
50
  const normalizedSelector = this.normalizeSelector(args.selector);
53
51
  const locator = page.locator(normalizedSelector);
54
52
  const { element, elementIndex, totalCount } = await this.selectPreferredLocator(locator, {
55
- elementIndex: args.elementIndex,
56
53
  originalSelector: args.selector,
57
54
  });
58
55
  selectionWarning = this.formatElementSelectionInfo(args.selector, elementIndex, totalCount, true);
@@ -73,8 +70,35 @@ export class GetTextTool extends BrowserToolBase {
73
70
  if (selectionWarning) {
74
71
  lines.push(selectionWarning.trimEnd());
75
72
  }
76
- lines.push('');
77
73
  const safeMaxLength = requestedMaxLength > 0 ? requestedMaxLength : 20000;
74
+ const totalLength = textContent.length;
75
+ // Large-output guard: return preview + confirm_output token when text is big
76
+ if (totalLength >= PREVIEW_THRESHOLD) {
77
+ const preview = makeConfirmPreview(() => textContent, {
78
+ counts: {
79
+ totalLength,
80
+ shownLength: Math.min(500, totalLength),
81
+ truncated: true,
82
+ },
83
+ previewLines: [
84
+ 'Preview (first 500 chars):',
85
+ textContent.slice(0, 500),
86
+ ...(totalLength > 500 ? ['...'] : []),
87
+ '',
88
+ '⚠️ Full text not returned to save tokens',
89
+ '',
90
+ '💡 RECOMMENDED: Use token-efficient alternatives:',
91
+ ' • inspect_dom() - structured view with positions and layout',
92
+ ' • find_by_text() - locate specific text with context',
93
+ ' • query_selector() - inspect specific elements',
94
+ ],
95
+ });
96
+ lines.push(`Text size: ${totalLength.toLocaleString()} characters (exceeds ${PREVIEW_THRESHOLD} char threshold)`);
97
+ lines.push('');
98
+ lines.push(...preview.lines);
99
+ return createSuccessResponse(lines.join('\n'));
100
+ }
101
+ lines.push('');
78
102
  let displayText = textContent;
79
103
  const truncated = displayText.length > safeMaxLength;
80
104
  if (truncated) {
@@ -1,6 +1,7 @@
1
1
  import { BrowserToolBase } from '../base.js';
2
2
  import { createSuccessResponse, createErrorResponse, } from '../../common/types.js';
3
3
  import { makeConfirmPreview } from '../../common/confirm_output.js';
4
+ import { gatherConsoleErrorsSince, quickNetworkIdleNote } from '../common/postAction.js';
4
5
  /**
5
6
  * Tool for executing JavaScript in the browser
6
7
  */
@@ -113,6 +114,21 @@ export class EvaluateTool extends BrowserToolBase {
113
114
  ' Why: Playwright auto-scrolls before interactions, but these tools help with\n' +
114
115
  ' testing scroll behavior, lazy-loading, and scroll-triggered content');
115
116
  }
117
+ // Pattern: Navigation (top-level or SPA routing)
118
+ // Detect common navigation attempts: window.location / document.location assignments,
119
+ // location.assign|replace, history.pushState|replaceState, and href changes.
120
+ if (scriptLower.match(/\blocation\s*\./) ||
121
+ scriptLower.match(/window\s*\.\s*location/) ||
122
+ scriptLower.match(/document\s*\.\s*location/) ||
123
+ scriptLower.match(/history\s*\.\s*pushstate|history\s*\.\s*replacestate/) ||
124
+ scriptLower.match(/location\s*=(?!\s*location)/) ||
125
+ scriptLower.match(/location\s*\.\s*href\s*=|location\s*\.\s*assign|location\s*\.\s*replace/)) {
126
+ suggestions.push('🌐 Navigation\n' +
127
+ ' • navigate({ url: "..." }) — full document navigation with proper waits\n' +
128
+ ' • SPA-only: click a router link or evaluate history.pushState(...), then wait_for_element\n' +
129
+ ' • go_history for back/forward\n' +
130
+ ' Note: setting window.location.href causes a reload; prefer navigate for reliability');
131
+ }
116
132
  return suggestions;
117
133
  }
118
134
  async execute(args, context) {
@@ -267,6 +283,43 @@ export class EvaluateTool extends BrowserToolBase {
267
283
  resultStr = String(evalReturn);
268
284
  }
269
285
  }
286
+ // Detect navigation patterns in the script for post-action waits
287
+ const scriptLower = (args.script || '').toLowerCase();
288
+ const navDetected = (/\blocation\s*\./.test(scriptLower) ||
289
+ /window\s*\.\s*location/.test(scriptLower) ||
290
+ /document\s*\.\s*location/.test(scriptLower) ||
291
+ /history\s*\.\s*pushstate|history\s*\.\s*replacestate/.test(scriptLower) ||
292
+ /location\s*=(?!\s*location)/.test(scriptLower) ||
293
+ /location\s*\.\s*href\s*=|location\s*\.\s*assign|location\s*\.\s*replace/.test(scriptLower));
294
+ // Optional quick network-idle note when navigation is detected
295
+ let netIdleNote = null;
296
+ if (navDetected) {
297
+ try {
298
+ const note = await quickNetworkIdleNote(page);
299
+ if (note)
300
+ netIdleNote = note;
301
+ }
302
+ catch {
303
+ // ignore
304
+ }
305
+ }
306
+ // After script execution (and any quick wait), surface console errors since this interaction
307
+ try {
308
+ const errs = await gatherConsoleErrorsSince('interaction');
309
+ if (errs.length > 0) {
310
+ let titleInfo = '';
311
+ try {
312
+ const t = await page.title();
313
+ if (t)
314
+ titleInfo = `\nTitle: ${t}`;
315
+ }
316
+ catch { }
317
+ return createErrorResponse(`Console error after evaluate: ${errs[0]}${titleInfo}`);
318
+ }
319
+ }
320
+ catch {
321
+ // Best-effort; continue on failure
322
+ }
270
323
  // Guard for large outputs: preview + confirm
271
324
  const totalLength = resultStr.length;
272
325
  const lines = [];
@@ -285,6 +338,10 @@ export class EvaluateTool extends BrowserToolBase {
285
338
  extraTips: ['Tip: Prefer specialized tools or narrow the script when possible.'],
286
339
  });
287
340
  lines.push(...previewBlock.lines);
341
+ if (netIdleNote) {
342
+ lines.push('');
343
+ lines.push(netIdleNote);
344
+ }
288
345
  if (suggestions.length > 0) {
289
346
  lines.push('');
290
347
  lines.push('💡 Consider specialized tools:');
@@ -293,6 +350,10 @@ export class EvaluateTool extends BrowserToolBase {
293
350
  return createSuccessResponse(lines);
294
351
  }
295
352
  const messages = [`✓ JavaScript execution result:`, resultStr];
353
+ if (netIdleNote) {
354
+ messages.push('');
355
+ messages.push(netIdleNote);
356
+ }
296
357
  // Detect if specialized tools would be better
297
358
  if (suggestions.length > 0) {
298
359
  messages.push('');
@@ -21,7 +21,7 @@ export class CheckVisibilityTool extends BrowserToolBase {
21
21
  ],
22
22
  examples: [
23
23
  "check_visibility({ selector: 'testid:submit' })",
24
- "check_visibility({ selector: '#login button', elementIndex: 2 })",
24
+ "check_visibility({ selector: '#login button' })",
25
25
  ],
26
26
  exampleOutputs: [
27
27
  {
@@ -39,10 +39,6 @@ export class CheckVisibilityTool extends BrowserToolBase {
39
39
  selector: {
40
40
  type: "string",
41
41
  description: "CSS selector, text selector, or testid shorthand (e.g., 'testid:login-button', '#submit', 'text=Click here')"
42
- },
43
- elementIndex: {
44
- type: "number",
45
- description: "When selector matches multiple elements, use this 1-based index to select a specific one (e.g., 2 = second element). Default: first visible element."
46
42
  }
47
43
  },
48
44
  required: ["selector"],
@@ -59,7 +55,6 @@ export class CheckVisibilityTool extends BrowserToolBase {
59
55
  try {
60
56
  // Use standard element selection with visibility preference
61
57
  const { element, elementIndex, totalCount } = await this.selectPreferredLocator(locator, {
62
- elementIndex: args.elementIndex,
63
58
  originalSelector: args.selector,
64
59
  });
65
60
  // Format selection warning if multiple elements matched
@@ -1,13 +1,11 @@
1
1
  import { ToolHandler, ToolMetadata, SessionConfig } from '../../common/types.js';
2
2
  import { BrowserToolBase } from '../base.js';
3
3
  import type { ToolContext, ToolResponse } from '../../common/types.js';
4
- export interface GetComputedStylesArgs {
5
- selector: string;
6
- properties?: string;
7
- elementIndex?: number;
8
- }
9
4
  export declare class GetComputedStylesTool extends BrowserToolBase implements ToolHandler {
10
5
  static getMetadata(sessionConfig?: SessionConfig): ToolMetadata;
11
6
  private readonly DEFAULT_PROPERTIES;
12
- execute(args: GetComputedStylesArgs, context: ToolContext): Promise<ToolResponse>;
7
+ execute(args: {
8
+ selector: string;
9
+ properties?: string;
10
+ }, context: ToolContext): Promise<ToolResponse>;
13
11
  }
@@ -27,7 +27,7 @@ export class GetComputedStylesTool extends BrowserToolBase {
27
27
  exampleOutputs: [
28
28
  {
29
29
  call: "get_computed_styles({ selector: 'testid:login-form' })",
30
- output: `⚠ Warning: Selector matched 2 elements, showing 1 (use elementIndex to target a specific one)\n\nComputed Styles: <form data-testid=\"login-form\">\n\nLayout:\n display: block\n position: static\n width: 560px\n height: 480px\n\nVisibility:\n opacity: 1\n visibility: visible\n z-index: auto\n overflow: visible\n\nSpacing:\n margin: 0px\n padding: 24px\n\nTypography:\n font-size: 16px\n font-weight: 400\n color: rgb(33, 37, 41)`
30
+ output: `⚠ Found 2 elements matching \"testid:login-form\", using element 1 (first visible)\n💡 Tip: Consider adding a unique data-testid attribute for more reliable selection.\n Primary fix: add data-testid and target it (e.g., testid:submit).\n Workaround: use '>> nth=<index>' only when you can't add test IDs.\n\nComputed Styles: <form data-testid=\"login-form\">\n\nLayout:\n display: block\n position: static\n width: 560px\n height: 480px\n\nVisibility:\n opacity: 1\n visibility: visible\n z-index: auto\n overflow: visible\n\nSpacing:\n margin: 0px\n padding: 24px\n\nTypography:\n font-size: 16px\n font-weight: 400\n color: rgb(33, 37, 41)`
31
31
  }
32
32
  ],
33
33
  inputSchema: {
@@ -40,10 +40,6 @@ export class GetComputedStylesTool extends BrowserToolBase {
40
40
  properties: {
41
41
  type: "string",
42
42
  description: "Comma-separated list of CSS properties to retrieve (e.g., 'display,width,color'). If not specified, returns common layout properties: display, position, width, height, opacity, visibility, z-index, overflow, margin, padding, font-size, font-weight, color, background-color"
43
- },
44
- elementIndex: {
45
- type: "number",
46
- description: "When selector matches multiple elements, use this 1-based index to select a specific one (e.g., 2 = second element). Default: first visible element."
47
43
  }
48
44
  },
49
45
  required: ["selector"],
@@ -60,7 +56,6 @@ export class GetComputedStylesTool extends BrowserToolBase {
60
56
  // Use standard element selection with visibility preference
61
57
  const locator = page.locator(normalizedSelector);
62
58
  const { element, elementIndex, totalCount } = await this.selectPreferredLocator(locator, {
63
- elementIndex: args.elementIndex,
64
59
  originalSelector: args.selector,
65
60
  });
66
61
  // Format selection warning if multiple elements matched
@@ -42,10 +42,6 @@ export class InspectAncestorsTool extends BrowserToolBase {
42
42
  limit: {
43
43
  type: "number",
44
44
  description: "Maximum number of ancestors to traverse (default: 10, max: 15). Increase for deeply nested component frameworks."
45
- },
46
- elementIndex: {
47
- type: "number",
48
- description: "When selector matches multiple elements, use this 1-based index to select a specific one (e.g., 2 = second element). Default: first visible element."
49
45
  }
50
46
  },
51
47
  required: ["selector"],
@@ -83,10 +83,6 @@ RELATED TOOLS: For comparing TWO elements' alignment (not parent-child), use com
83
83
  maxDepth: {
84
84
  type: "number",
85
85
  description: "Maximum depth to drill through non-semantic wrapper elements when looking for semantic children (default: 5). Increase for extremely deeply nested components, decrease to 1 to see only immediate children without drilling."
86
- },
87
- elementIndex: {
88
- type: "number",
89
- description: "When selector matches multiple elements, use this 1-based index to select a specific one (e.g., 2 = second element). Default: first visible element."
90
86
  }
91
87
  },
92
88
  required: [],
@@ -1,11 +1,9 @@
1
1
  import { ToolHandler, ToolMetadata, SessionConfig } from '../../common/types.js';
2
2
  import { BrowserToolBase } from '../base.js';
3
3
  import type { ToolContext, ToolResponse } from '../../common/types.js';
4
- export interface MeasureElementArgs {
5
- selector: string;
6
- elementIndex?: number;
7
- }
8
4
  export declare class MeasureElementTool extends BrowserToolBase implements ToolHandler {
9
5
  static getMetadata(sessionConfig?: SessionConfig): ToolMetadata;
10
- execute(args: MeasureElementArgs, context: ToolContext): Promise<ToolResponse>;
6
+ execute(args: {
7
+ selector: string;
8
+ }, context: ToolContext): Promise<ToolResponse>;
11
9
  }
@@ -28,10 +28,6 @@ export class MeasureElementTool extends BrowserToolBase {
28
28
  selector: {
29
29
  type: "string",
30
30
  description: "CSS selector or testid shorthand (e.g., 'testid:submit', '#login-button')"
31
- },
32
- elementIndex: {
33
- type: "number",
34
- description: "When selector matches multiple elements, use this 1-based index to select a specific one (e.g., 2 = second element). Default: first visible element."
35
31
  }
36
32
  },
37
33
  required: ["selector"],
@@ -44,7 +40,6 @@ export class MeasureElementTool extends BrowserToolBase {
44
40
  // Use standard element selection with visibility preference
45
41
  const locator = page.locator(normalizedSelector);
46
42
  const { element, elementIndex, totalCount } = await this.selectPreferredLocator(locator, {
47
- elementIndex: args.elementIndex,
48
43
  originalSelector: args.selector,
49
44
  });
50
45
  // Format selection warning if multiple elements matched
@@ -1,9 +1,12 @@
1
+ import fs from 'node:fs';
2
+ import * as path from 'node:path';
1
3
  import { BrowserToolBase } from '../base.js';
4
+ import { makeConfirmPreview } from '../../common/confirm_output.js';
2
5
  export class GetRequestDetailsTool extends BrowserToolBase {
3
6
  static getMetadata(sessionConfig) {
4
7
  return {
5
8
  name: "get_request_details",
6
- description: "Get detailed information about a specific network request by index (from list_network_requests). Returns request/response headers, body (truncated at 500 chars), timing, and size. Request bodies with passwords are automatically masked. Essential for debugging API responses and investigating failed requests.",
9
+ description: "Get detailed information about a specific network request by index (from list_network_requests). Returns request/response headers, body (truncated at 500 chars), timing, and size. Request bodies with passwords are automatically masked. If a request or response body exceeds 500 chars, includes a preview and a one-time confirm_output token that, when called, saves the full body to disk under ./.mcp-web-inspector/network-bodies/ and returns the file path(s). Essential for debugging API responses and investigating failed requests.",
7
10
  inputSchema: {
8
11
  type: "object",
9
12
  properties: {
@@ -19,8 +22,10 @@ export class GetRequestDetailsTool extends BrowserToolBase {
19
22
  async execute(args, context) {
20
23
  return this.safeExecute(context, async () => {
21
24
  const { index } = args;
22
- const { getNetworkLog } = await import('../../../toolHandler.js');
25
+ const { getNetworkLog, getSessionConfig } = await import('../../../toolHandler.js');
23
26
  const networkLog = getNetworkLog();
27
+ const sessionConfig = getSessionConfig();
28
+ const exposeSensitive = Boolean(sessionConfig?.exposeSensitiveNetworkData);
24
29
  if (index < 0 || index >= networkLog.length) {
25
30
  return {
26
31
  content: [{
@@ -31,6 +36,15 @@ export class GetRequestDetailsTool extends BrowserToolBase {
31
36
  };
32
37
  }
33
38
  const req = networkLog[index];
39
+ // Helper to look up headers case-insensitively
40
+ const getHeader = (headers, key) => {
41
+ if (!headers)
42
+ return undefined;
43
+ const found = Object.entries(headers).find(([k]) => k.toLowerCase() === key.toLowerCase());
44
+ return found ? String(found[1]) : undefined;
45
+ };
46
+ const reqContentType = getHeader(req.requestData.headers, 'content-type') || '';
47
+ const respContentType = getHeader(req.responseData?.headers || {}, 'content-type') || '';
34
48
  // Build compact text response
35
49
  const lines = [];
36
50
  lines.push(`Request Details [${index}]:\n`);
@@ -55,11 +69,10 @@ export class GetRequestDetailsTool extends BrowserToolBase {
55
69
  return `${bytes} bytes`;
56
70
  return `${(bytes / 1024).toFixed(1)}KB`;
57
71
  };
58
- if (responseSize > 0) {
59
- lines.push(`Size: ${formatBytes(requestSize)} ${formatBytes(responseSize)}`);
60
- }
61
- else if (requestSize > 0) {
62
- lines.push(`Size: ${formatBytes(requestSize)} →`);
72
+ if (responseSize > 0 || requestSize > 0) {
73
+ const reqPart = requestSize > 0 ? formatBytes(requestSize) : '0 bytes';
74
+ const respPart = responseSize > 0 ? formatBytes(responseSize) : '0 bytes';
75
+ lines.push(`Size: requestBody=${reqPart}, responseBody≈${respPart}`);
63
76
  }
64
77
  // Request headers (show important ones)
65
78
  const importantRequestHeaders = ['content-type', 'authorization', 'cookie', 'user-agent', 'accept'];
@@ -67,13 +80,36 @@ export class GetRequestDetailsTool extends BrowserToolBase {
67
80
  .filter(([key]) => importantRequestHeaders.includes(key.toLowerCase()));
68
81
  if (reqHeaders.length > 0) {
69
82
  lines.push('\nRequest Headers:');
70
- reqHeaders.forEach(([key, value]) => {
71
- // Truncate sensitive values
72
- if (key.toLowerCase() === 'authorization' || key.toLowerCase() === 'cookie') {
73
- const truncated = value.length > 20
74
- ? value.substring(0, 17) + '...'
75
- : value;
76
- lines.push(` ${key}: ${truncated}`);
83
+ const order = (name) => {
84
+ const idx = importantRequestHeaders.indexOf(name.toLowerCase());
85
+ return idx === -1 ? importantRequestHeaders.length : idx;
86
+ };
87
+ reqHeaders
88
+ .sort(([a], [b]) => {
89
+ const oa = order(a);
90
+ const ob = order(b);
91
+ if (oa !== ob)
92
+ return oa - ob;
93
+ return a.localeCompare(b);
94
+ })
95
+ .forEach(([key, value]) => {
96
+ const keyLower = key.toLowerCase();
97
+ if (keyLower === 'authorization' || keyLower === 'cookie') {
98
+ if (!exposeSensitive) {
99
+ if (keyLower === 'authorization') {
100
+ const scheme = value.split(' ')[0] || '';
101
+ lines.push(` ${key}: ${scheme ? `${scheme} <redacted>` : '<redacted>'}`);
102
+ }
103
+ else {
104
+ lines.push(` ${key}: <redacted>`);
105
+ }
106
+ }
107
+ else {
108
+ const truncated = value.length > 60
109
+ ? value.substring(0, 57) + '...'
110
+ : value;
111
+ lines.push(` ${key}: ${truncated}`);
112
+ }
77
113
  }
78
114
  else {
79
115
  lines.push(` ${key}: ${value}`);
@@ -91,15 +127,21 @@ export class GetRequestDetailsTool extends BrowserToolBase {
91
127
  parsed.password = '***';
92
128
  if (parsed.pass)
93
129
  parsed.pass = '***';
94
- displayData = JSON.stringify(parsed);
130
+ const compact = JSON.stringify(parsed);
131
+ const pretty = JSON.stringify(parsed, null, 2);
132
+ // Pretty-print small JSON bodies for readability; keep large ones compact
133
+ displayData = pretty.length <= 500 ? pretty : compact;
95
134
  }
96
135
  catch (e) {
97
136
  // Not JSON, use as is
98
137
  }
99
138
  // Truncate at 500 chars
100
139
  if (displayData.length > 500) {
101
- lines.push(` ${displayData.substring(0, 500)}`);
102
- lines.push(` ... [${displayData.length - 500} more chars]`);
140
+ const shown = 500;
141
+ const remaining = displayData.length - shown;
142
+ const coverage = Math.round((shown / displayData.length) * 1000) / 10;
143
+ lines.push(` ${displayData.substring(0, shown)}`);
144
+ lines.push(` ... [${remaining} more chars] (previewCoverage≈${coverage}%)`);
103
145
  }
104
146
  else {
105
147
  lines.push(` ${displayData}`);
@@ -113,13 +155,30 @@ export class GetRequestDetailsTool extends BrowserToolBase {
113
155
  : [];
114
156
  if (respHeaders.length > 0) {
115
157
  lines.push('\nResponse Headers:');
116
- respHeaders.forEach(([key, value]) => {
117
- // Truncate cookies
118
- if (key.toLowerCase() === 'set-cookie') {
119
- const truncated = value.length > 60
120
- ? value.substring(0, 57) + '...'
121
- : value;
122
- lines.push(` ${key}: ${truncated}`);
158
+ const order = (name) => {
159
+ const idx = importantResponseHeaders.indexOf(name.toLowerCase());
160
+ return idx === -1 ? importantResponseHeaders.length : idx;
161
+ };
162
+ respHeaders
163
+ .sort(([a], [b]) => {
164
+ const oa = order(a);
165
+ const ob = order(b);
166
+ if (oa !== ob)
167
+ return oa - ob;
168
+ return a.localeCompare(b);
169
+ })
170
+ .forEach(([key, value]) => {
171
+ const keyLower = key.toLowerCase();
172
+ if (keyLower === 'set-cookie') {
173
+ if (!exposeSensitive) {
174
+ lines.push(` ${key}: <redacted>`);
175
+ }
176
+ else {
177
+ const truncated = value.length > 60
178
+ ? value.substring(0, 57) + '...'
179
+ : value;
180
+ lines.push(` ${key}: ${truncated}`);
181
+ }
123
182
  }
124
183
  else {
125
184
  lines.push(` ${key}: ${value}`);
@@ -129,18 +188,104 @@ export class GetRequestDetailsTool extends BrowserToolBase {
129
188
  // Response body
130
189
  if (req.responseData?.body) {
131
190
  lines.push('\nResponse Body (truncated at 500 chars):');
132
- const body = req.responseData.body;
133
- if (body.length > 500) {
134
- lines.push(` ${body.substring(0, 500)}`);
135
- lines.push(` ... [${body.length - 500} more chars]`);
191
+ const rawBody = req.responseData.body;
192
+ let displayBody = rawBody;
193
+ // Pretty-print small JSON responses for readability; keep large ones compact
194
+ if (respContentType.toLowerCase().includes('application/json')) {
195
+ try {
196
+ const parsed = JSON.parse(rawBody);
197
+ const compact = JSON.stringify(parsed);
198
+ const pretty = JSON.stringify(parsed, null, 2);
199
+ displayBody = pretty.length <= 500 ? pretty : compact;
200
+ }
201
+ catch {
202
+ // Not valid JSON, fall back to raw body
203
+ }
204
+ }
205
+ if (displayBody.length > 500) {
206
+ const shown = 500;
207
+ const remaining = displayBody.length - shown;
208
+ const coverage = Math.round((shown / displayBody.length) * 1000) / 10;
209
+ lines.push(` ${displayBody.substring(0, shown)}`);
210
+ lines.push(` ... [${remaining} more chars] (previewCoverage≈${coverage}%)`);
136
211
  }
137
212
  else {
138
- lines.push(` ${body}`);
213
+ lines.push(` ${displayBody}`);
139
214
  }
140
215
  }
141
216
  else if (req.status) {
142
217
  lines.push('\nResponse Body: (none or binary data)');
143
218
  }
219
+ // If either request or response body was truncated, provide a confirm_output token
220
+ const reqBody = req.requestData.postData || '';
221
+ const respBody = req.responseData?.body || '';
222
+ const reqTruncated = typeof reqBody === 'string' && reqBody.length > 500;
223
+ const respTruncated = typeof respBody === 'string' && respBody.length > 500;
224
+ if (reqTruncated || respTruncated) {
225
+ const inferExt = (ct) => {
226
+ const c = (ct || '').toLowerCase();
227
+ if (c.includes('application/json'))
228
+ return 'json';
229
+ if (c.includes('text/html'))
230
+ return 'html';
231
+ if (c.startsWith('text/'))
232
+ return 'txt';
233
+ if (c.includes('xml'))
234
+ return 'xml';
235
+ return 'txt';
236
+ };
237
+ const { getScreenshotsDir } = await import('../../../toolHandler.js');
238
+ const baseInspectorDir = path.dirname(getScreenshotsDir());
239
+ const outDir = path.join(baseInspectorDir, 'network-bodies');
240
+ const makeSafe = (s) => s.replace(/[^a-zA-Z0-9._-]/g, '-');
241
+ const urlObj = (() => { try {
242
+ return new URL(req.url);
243
+ }
244
+ catch {
245
+ return null;
246
+ } })();
247
+ const host = urlObj?.hostname ? makeSafe(urlObj.hostname) : 'unknown-host';
248
+ const ts = new Date().toISOString().replace(/[:.]/g, '-');
249
+ const thunk = async () => {
250
+ if (!fs.existsSync(outDir)) {
251
+ fs.mkdirSync(outDir, { recursive: true });
252
+ }
253
+ const messages = [];
254
+ if (respTruncated) {
255
+ const ext = inferExt(respContentType);
256
+ const respFile = path.join(outDir, `${ts}-${index}-${makeSafe(req.method)}-${host}.response.${ext}`);
257
+ fs.writeFileSync(respFile, respBody, 'utf8');
258
+ messages.push(`✓ Saved full response body to: ${path.relative(process.cwd(), respFile)} (${respBody.length} bytes${respContentType ? `, content-type: ${respContentType}` : ''})`);
259
+ }
260
+ if (reqTruncated) {
261
+ const ext = inferExt(reqContentType);
262
+ const reqFile = path.join(outDir, `${ts}-${index}-${makeSafe(req.method)}-${host}.request.${ext}`);
263
+ fs.writeFileSync(reqFile, reqBody, 'utf8');
264
+ messages.push(`✓ Saved full request body to: ${path.relative(process.cwd(), reqFile)} (${reqBody.length} bytes${reqContentType ? `, content-type: ${reqContentType}` : ''})`);
265
+ }
266
+ if (messages.length === 0) {
267
+ messages.push('Nothing to save (no truncated text bodies).');
268
+ }
269
+ else {
270
+ messages.push('');
271
+ messages.push(`Paths above are relative to the current working directory: ${process.cwd()}`);
272
+ }
273
+ messages.push('');
274
+ messages.push('Note: .mcp-web-inspector/ is recommended in .gitignore to avoid committing sensitive data.');
275
+ return messages.join('\n');
276
+ };
277
+ const totalLen = (respTruncated ? respBody.length : 0) + (reqTruncated ? reqBody.length : 0);
278
+ const preview = makeConfirmPreview(thunk, {
279
+ counts: { totalLength: totalLen, shownLength: 500, truncated: true },
280
+ previewLines: [
281
+ 'Large network body detected — preview shown above.',
282
+ 'Confirm to save full body to disk (token-efficient).',
283
+ `Output directory: ${path.relative(process.cwd(), outDir)}`,
284
+ ],
285
+ });
286
+ lines.push('');
287
+ lines.push(...preview.lines);
288
+ }
144
289
  return {
145
290
  content: [{
146
291
  type: "text",
@@ -81,7 +81,7 @@ export class ListNetworkRequestsTool extends BrowserToolBase {
81
81
  parts.push('|', cached);
82
82
  lines.push(parts.join(' '));
83
83
  });
84
- lines.push('\nUse get_request_details(index) for full info');
84
+ lines.push('\nUse get_request_details(index) for full info (indices are 0-based from this list)');
85
85
  return {
86
86
  content: [{
87
87
  type: "text",
@@ -5,6 +5,7 @@ export interface SessionConfig {
5
5
  userDataDir: string;
6
6
  screenshotsDir: string;
7
7
  headlessDefault: boolean;
8
+ exposeSensitiveNetworkData?: boolean;
8
9
  }
9
10
  export interface ToolContext {
10
11
  page?: Page;
@@ -1,6 +1,10 @@
1
1
  import { execSync } from 'child_process';
2
2
  import { existsSync } from 'fs';
3
3
  import { join } from 'path';
4
+ // Resolve package root so npx uses this package's Playwright.
5
+ // Entry point sets MCP_WEB_INSPECTOR_PACKAGE_ROOT using import.meta.url;
6
+ // tests and other environments fall back to process.cwd().
7
+ const PACKAGE_ROOT = process.env.MCP_WEB_INSPECTOR_PACKAGE_ROOT || process.cwd();
4
8
  /**
5
9
  * Check if Playwright browsers are installed
6
10
  * Returns true if browsers are available, false otherwise
@@ -10,7 +14,8 @@ export function checkBrowsersInstalled() {
10
14
  // Check if playwright is available
11
15
  const result = execSync('npx playwright --version', {
12
16
  encoding: 'utf8',
13
- stdio: 'pipe'
17
+ stdio: 'pipe',
18
+ cwd: PACKAGE_ROOT,
14
19
  });
15
20
  // If we got here, playwright CLI is available
16
21
  // Now check if browsers are actually installed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcp-web-inspector",
3
- "version": "0.6.0",
3
+ "version": "0.8.0",
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",