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 +9 -12
- package/dist/index.js +12 -1
- package/dist/toolHandler.d.ts +4 -0
- package/dist/toolHandler.js +19 -6
- package/dist/tools/browser/base.js +21 -5
- package/dist/tools/browser/content/get_html.js +0 -5
- package/dist/tools/browser/content/get_text.js +31 -7
- package/dist/tools/browser/evaluation/evaluate.js +61 -0
- package/dist/tools/browser/inspection/check_visibility.js +1 -6
- package/dist/tools/browser/inspection/get_computed_styles.d.ts +4 -6
- package/dist/tools/browser/inspection/get_computed_styles.js +1 -6
- package/dist/tools/browser/inspection/inspect_ancestors.js +0 -4
- package/dist/tools/browser/inspection/inspect_dom.js +0 -4
- package/dist/tools/browser/inspection/measure_element.d.ts +3 -5
- package/dist/tools/browser/inspection/measure_element.js +0 -5
- package/dist/tools/browser/network/get_request_details.js +174 -29
- package/dist/tools/browser/network/list_network_requests.js +1 -1
- package/dist/tools/common/types.d.ts +1 -0
- package/dist/utils/browserCheck.js +6 -1
- package/package.json +1 -1
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
|
-
⚠
|
|
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'
|
|
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.
|
|
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.
|
|
88
|
+
console.error('Shutdown signal received');
|
|
78
89
|
process.exit(0);
|
|
79
90
|
}
|
|
80
91
|
process.on('SIGINT', shutdown);
|
package/dist/toolHandler.d.ts
CHANGED
|
@@ -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
|
*/
|
package/dist/toolHandler.js
CHANGED
|
@@ -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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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 (
|
|
31
|
-
const
|
|
32
|
-
|
|
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 =
|
|
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.
|
|
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'
|
|
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:
|
|
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: `⚠
|
|
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:
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
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
|
-
|
|
102
|
-
|
|
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
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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(` ${
|
|
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",
|
|
@@ -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
|