mcp-web-inspector 0.5.3 → 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 +14 -17
- package/dist/index.js +12 -1
- package/dist/toolHandler.d.ts +8 -0
- package/dist/toolHandler.js +61 -23
- package/dist/tools/browser/base.d.ts +6 -0
- package/dist/tools/browser/base.js +96 -8
- package/dist/tools/browser/common/postAction.d.ts +7 -0
- package/dist/tools/browser/common/postAction.js +46 -0
- package/dist/tools/browser/console/__tests__/console.test.js +9 -0
- package/dist/tools/browser/console/get_console_logs.d.ts +4 -0
- package/dist/tools/browser/console/get_console_logs.js +14 -2
- 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/interaction/__tests__/duplicateClickErrorFormatting.test.d.ts +1 -0
- package/dist/tools/browser/interaction/__tests__/duplicateClickErrorFormatting.test.js +97 -0
- package/dist/tools/browser/interaction/click.js +46 -2
- package/dist/tools/browser/navigation/__tests__/goNavigation.test.js +21 -33
- package/dist/tools/browser/navigation/history.d.ts +9 -0
- package/dist/tools/browser/navigation/history.js +86 -0
- package/dist/tools/browser/navigation/index.d.ts +1 -2
- package/dist/tools/browser/navigation/index.js +1 -2
- package/dist/tools/browser/navigation/navigate.js +40 -1
- 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/browser/register.js +2 -4
- package/dist/tools/common/types.d.ts +1 -0
- package/dist/utils/browserCheck.js +6 -1
- package/package.json +12 -13
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>
|
|
@@ -911,11 +910,11 @@ Quick check if an element exists on the page. Ultra-lightweight alternative to q
|
|
|
911
910
|
|
|
912
911
|
### Navigation
|
|
913
912
|
|
|
914
|
-
#### `
|
|
915
|
-
Navigate back in browser history
|
|
913
|
+
#### `go_history`
|
|
914
|
+
Navigate browser history (back/forward). Returns: 'Navigated <direction> in browser history', a quick network-idle note if available, 'URL: <current>', and 'Title: <current>' when set. If console errors occur after the navigation, returns an error like 'Console error after history navigation: <message>' including Title when available.
|
|
916
915
|
|
|
917
|
-
|
|
918
|
-
|
|
916
|
+
- Parameters:
|
|
917
|
+
- direction (string, required): History direction to navigate
|
|
919
918
|
|
|
920
919
|
#### `navigate`
|
|
921
920
|
Navigate to a URL. Browser sessions (cookies, localStorage, sessionStorage) are automatically saved in ./.mcp-web-inspector/user-data directory and persist across restarts. To clear saved sessions, delete the directory.
|
|
@@ -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`
|
|
@@ -1048,7 +1045,7 @@ Clears captured console logs and returns the number of entries cleared.
|
|
|
1048
1045
|
Retrieve console logs with filtering and token‑efficient output. Defaults: since='last-interaction', limit=20, format='grouped'. Grouped output deduplicates identical lines and shows counts. Use format='raw' for chronological, ungrouped lines. Large outputs return a preview and a one-time token to fetch the full payload.
|
|
1049
1046
|
|
|
1050
1047
|
- Parameters:
|
|
1051
|
-
- type (string, optional): Type
|
|
1048
|
+
- type (string, optional): Type filter (all, error, warning, log, info, debug, exception). Note: 'error' also includes 'exception' entries for convenience.
|
|
1052
1049
|
- search (string, optional): Text to search for in logs (handles text with square brackets)
|
|
1053
1050
|
- limit (number, optional): Maximum entries to return (groups when grouped, lines when raw). Default: 20
|
|
1054
1051
|
- since (string, optional): Filter logs since a specific event: 'last-call' (since last get_console_logs call), 'last-navigation' (since last page navigation), or 'last-interaction' (since last user interaction like click, fill, etc.). Default: 'last-interaction'
|
|
@@ -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
|
*/
|
|
@@ -79,6 +83,10 @@ export declare function getConsoleLogs(): string[];
|
|
|
79
83
|
* Get console logs captured after the last navigation
|
|
80
84
|
*/
|
|
81
85
|
export declare function getConsoleLogsSinceLastNavigation(): string[];
|
|
86
|
+
/**
|
|
87
|
+
* Get console logs captured after the last interaction
|
|
88
|
+
*/
|
|
89
|
+
export declare function getConsoleLogsSinceLastInteraction(): string[];
|
|
82
90
|
/**
|
|
83
91
|
* Get screenshots
|
|
84
92
|
*/
|
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,21 +78,36 @@ 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;
|
|
73
85
|
}
|
|
74
86
|
async function applyColorScheme(targetPage) {
|
|
75
|
-
if (!targetPage)
|
|
87
|
+
if (!targetPage)
|
|
76
88
|
return;
|
|
77
|
-
|
|
89
|
+
const scheme = getColorSchemeValue();
|
|
78
90
|
try {
|
|
79
|
-
|
|
80
|
-
|
|
91
|
+
// Some test environments or mocks may not implement emulateMedia
|
|
92
|
+
const anyPage = targetPage;
|
|
93
|
+
if (typeof anyPage.emulateMedia === 'function') {
|
|
94
|
+
await anyPage.emulateMedia({ colorScheme: scheme ?? null });
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
// Fallback: if emulateMedia is unavailable, do a best-effort hint via CSS.
|
|
98
|
+
// This won't fully emulate prefers-color-scheme but avoids throwing in tests.
|
|
99
|
+
if (scheme) {
|
|
100
|
+
const css = scheme === 'dark' ? ':root{color-scheme: dark;}'
|
|
101
|
+
: scheme === 'light' ? ':root{color-scheme: light;}'
|
|
102
|
+
: ':root{color-scheme: light dark;}';
|
|
103
|
+
if (typeof anyPage.addStyleTag === 'function') {
|
|
104
|
+
await anyPage.addStyleTag({ content: css });
|
|
105
|
+
}
|
|
106
|
+
}
|
|
81
107
|
}
|
|
82
108
|
catch (error) {
|
|
83
|
-
|
|
109
|
+
// Swallow errors to keep color scheme application non-fatal
|
|
110
|
+
console.warn("Failed to apply color scheme (non-fatal):", error);
|
|
84
111
|
}
|
|
85
112
|
}
|
|
86
113
|
export async function setColorSchemeOverride(scheme) {
|
|
@@ -258,13 +285,13 @@ async function getScreenSize() {
|
|
|
258
285
|
await tempBrowser.close();
|
|
259
286
|
// Validate the screen size values
|
|
260
287
|
if (!screenSize || typeof screenSize.width !== 'number' || typeof screenSize.height !== 'number') {
|
|
261
|
-
console.
|
|
288
|
+
console.warn('Invalid screen size detected, using defaults');
|
|
262
289
|
return { width: 1280, height: 720 };
|
|
263
290
|
}
|
|
264
291
|
return screenSize;
|
|
265
292
|
}
|
|
266
293
|
catch (error) {
|
|
267
|
-
console.
|
|
294
|
+
console.warn('Failed to detect screen size, using defaults:', error);
|
|
268
295
|
return { width: 1280, height: 720 };
|
|
269
296
|
}
|
|
270
297
|
}
|
|
@@ -279,13 +306,14 @@ export async function ensureBrowser(browserSettings) {
|
|
|
279
306
|
const browserCheck = checkBrowsersInstalled();
|
|
280
307
|
if (!browserCheck.installed) {
|
|
281
308
|
// Try to install browsers automatically
|
|
282
|
-
console.
|
|
283
|
-
console.
|
|
309
|
+
console.warn('🎭 Playwright browsers not found. Installing automatically...');
|
|
310
|
+
console.warn('⏳ This will download ~1GB of browser binaries. Please wait...');
|
|
284
311
|
try {
|
|
285
312
|
const { execSync } = await import('child_process');
|
|
286
313
|
execSync('npx playwright install chromium firefox webkit', {
|
|
287
314
|
stdio: 'inherit',
|
|
288
|
-
encoding: 'utf8'
|
|
315
|
+
encoding: 'utf8',
|
|
316
|
+
cwd: PACKAGE_ROOT,
|
|
289
317
|
});
|
|
290
318
|
console.error('✅ Browsers installed successfully! Starting browser...');
|
|
291
319
|
// Note: browser variable is still undefined here, which is correct.
|
|
@@ -300,7 +328,7 @@ export async function ensureBrowser(browserSettings) {
|
|
|
300
328
|
}
|
|
301
329
|
// Check if browser exists but is disconnected
|
|
302
330
|
if (browser && !browser.isConnected()) {
|
|
303
|
-
console.
|
|
331
|
+
console.warn("Browser exists but is disconnected. Cleaning up...");
|
|
304
332
|
try {
|
|
305
333
|
await browser.close().catch(err => console.error("Error closing disconnected browser:", err));
|
|
306
334
|
}
|
|
@@ -312,7 +340,7 @@ export async function ensureBrowser(browserSettings) {
|
|
|
312
340
|
}
|
|
313
341
|
// Check if device preset has changed (requires browser restart)
|
|
314
342
|
if (browser && browserSettings?.device && browserSettings.device !== currentDevice) {
|
|
315
|
-
console.
|
|
343
|
+
console.warn(`Device preset changed from ${currentDevice || 'none'} to ${browserSettings.device}. Restarting browser...`);
|
|
316
344
|
try {
|
|
317
345
|
await browser.close().catch(err => console.error("Error closing browser on device change:", err));
|
|
318
346
|
}
|
|
@@ -360,14 +388,14 @@ export async function ensureBrowser(browserSettings) {
|
|
|
360
388
|
currentDevice = device;
|
|
361
389
|
}
|
|
362
390
|
else {
|
|
363
|
-
console.
|
|
391
|
+
console.warn(`Warning: Device preset ${playwrightDeviceName} not found`);
|
|
364
392
|
currentDevice = undefined;
|
|
365
393
|
}
|
|
366
394
|
}
|
|
367
395
|
else {
|
|
368
396
|
currentDevice = undefined;
|
|
369
397
|
}
|
|
370
|
-
console.
|
|
398
|
+
console.warn(`Launching new ${browserType} browser instance...`);
|
|
371
399
|
// Use the appropriate browser engine
|
|
372
400
|
let browserInstance;
|
|
373
401
|
switch (browserType) {
|
|
@@ -421,14 +449,14 @@ export async function ensureBrowser(browserSettings) {
|
|
|
421
449
|
}
|
|
422
450
|
// Use persistent context if session saving is enabled
|
|
423
451
|
if (sessionConfig.saveSession) {
|
|
424
|
-
console.
|
|
452
|
+
console.warn(`Launching ${browserType} with persistent context at ${sessionConfig.userDataDir}...`);
|
|
425
453
|
const context = await browserInstance.launchPersistentContext(sessionConfig.userDataDir, contextOptions);
|
|
426
454
|
// Get the browser instance from the context
|
|
427
455
|
browser = context.browser();
|
|
428
456
|
currentBrowserType = browserType;
|
|
429
457
|
// Add cleanup logic when browser is disconnected
|
|
430
458
|
browser.on('disconnected', () => {
|
|
431
|
-
console.
|
|
459
|
+
console.warn("Browser disconnected event triggered");
|
|
432
460
|
browser = undefined;
|
|
433
461
|
page = undefined;
|
|
434
462
|
});
|
|
@@ -444,7 +472,7 @@ export async function ensureBrowser(browserSettings) {
|
|
|
444
472
|
currentBrowserType = browserType;
|
|
445
473
|
// Add cleanup logic when browser is disconnected
|
|
446
474
|
browser.on('disconnected', () => {
|
|
447
|
-
console.
|
|
475
|
+
console.warn("Browser disconnected event triggered");
|
|
448
476
|
browser = undefined;
|
|
449
477
|
page = undefined;
|
|
450
478
|
});
|
|
@@ -473,7 +501,7 @@ export async function ensureBrowser(browserSettings) {
|
|
|
473
501
|
}
|
|
474
502
|
// Verify page is still valid
|
|
475
503
|
if (!page || page.isClosed()) {
|
|
476
|
-
console.
|
|
504
|
+
console.warn("Page is closed or invalid. Creating new page...");
|
|
477
505
|
// Create a new page if the current one is invalid
|
|
478
506
|
const context = browser.contexts()[0] || await browser.newContext();
|
|
479
507
|
page = await context.newPage();
|
|
@@ -565,12 +593,12 @@ export async function ensureBrowser(browserSettings) {
|
|
|
565
593
|
}
|
|
566
594
|
// Use persistent context if session saving is enabled
|
|
567
595
|
if (sessionConfig.saveSession) {
|
|
568
|
-
console.
|
|
596
|
+
console.warn(`Launching ${browserType} with persistent context at ${sessionConfig.userDataDir} (retry)...`);
|
|
569
597
|
const context = await browserInstance.launchPersistentContext(sessionConfig.userDataDir, retryContextOptions);
|
|
570
598
|
browser = context.browser();
|
|
571
599
|
currentBrowserType = browserType;
|
|
572
600
|
browser.on('disconnected', () => {
|
|
573
|
-
console.
|
|
601
|
+
console.warn("Browser disconnected event triggered (retry)");
|
|
574
602
|
browser = undefined;
|
|
575
603
|
page = undefined;
|
|
576
604
|
});
|
|
@@ -584,7 +612,7 @@ export async function ensureBrowser(browserSettings) {
|
|
|
584
612
|
});
|
|
585
613
|
currentBrowserType = browserType;
|
|
586
614
|
browser.on('disconnected', () => {
|
|
587
|
-
console.
|
|
615
|
+
console.warn("Browser disconnected event triggered (retry)");
|
|
588
616
|
browser = undefined;
|
|
589
617
|
page = undefined;
|
|
590
618
|
});
|
|
@@ -650,7 +678,7 @@ export async function handleToolCall(name, args, server) {
|
|
|
650
678
|
const requiresBrowser = isBrowserTool(name);
|
|
651
679
|
// Check if we have a disconnected browser that needs cleanup
|
|
652
680
|
if (browser && !browser.isConnected() && requiresBrowser) {
|
|
653
|
-
console.
|
|
681
|
+
console.warn("Detected disconnected browser before tool execution, cleaning up...");
|
|
654
682
|
try {
|
|
655
683
|
await browser.close().catch(() => { }); // Ignore errors
|
|
656
684
|
}
|
|
@@ -739,6 +767,16 @@ export function getConsoleLogsSinceLastNavigation() {
|
|
|
739
767
|
return [];
|
|
740
768
|
return consoleLogsTool.getLogsSinceLastNavigation();
|
|
741
769
|
}
|
|
770
|
+
/**
|
|
771
|
+
* Get console logs captured after the last interaction
|
|
772
|
+
*/
|
|
773
|
+
export function getConsoleLogsSinceLastInteraction() {
|
|
774
|
+
const consoleLogsTool = getToolInstance("get_console_logs", null);
|
|
775
|
+
if (!consoleLogsTool)
|
|
776
|
+
return [];
|
|
777
|
+
// Expose a compact accessor mirroring navigation-based retrieval
|
|
778
|
+
return consoleLogsTool.getLogsSinceLastInteraction?.() ?? [];
|
|
779
|
+
}
|
|
742
780
|
/**
|
|
743
781
|
* Get screenshots
|
|
744
782
|
*/
|
|
@@ -121,4 +121,10 @@ export declare abstract class BrowserToolBase implements ToolHandler {
|
|
|
121
121
|
* @param totalCount Total number of matches
|
|
122
122
|
*/
|
|
123
123
|
protected buildNthSelectorHint(selector: string, totalCount: number): string;
|
|
124
|
+
/**
|
|
125
|
+
* Describe matched elements in a compact, copyable format for disambiguation errors.
|
|
126
|
+
* Shows: index, tag, trimmed text, nearest parent marker, and a suggested selector.
|
|
127
|
+
* Suggests testid:VALUE when present; otherwise falls back to id=VALUE or original >> nth=i.
|
|
128
|
+
*/
|
|
129
|
+
protected describeMatchedElements(locator: any, originalSelector: string, count: number): Promise<string>;
|
|
124
130
|
}
|
|
@@ -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
|
/**
|
|
@@ -233,8 +249,8 @@ export class BrowserToolBase {
|
|
|
233
249
|
// Check for multiple elements with errorOnMultiple flag
|
|
234
250
|
if (options?.errorOnMultiple && count > 1) {
|
|
235
251
|
const selector = options.originalSelector || 'selector';
|
|
236
|
-
const nthHint =
|
|
237
|
-
const warning =
|
|
252
|
+
const nthHint = ''.trimEnd();
|
|
253
|
+
const warning = ''.trimEnd();
|
|
238
254
|
let message = `Selector "${selector}" matched ${count} elements. Please use a more specific selector.`;
|
|
239
255
|
if (nthHint) {
|
|
240
256
|
message += `\n${nthHint}`;
|
|
@@ -242,7 +258,15 @@ export class BrowserToolBase {
|
|
|
242
258
|
if (warning) {
|
|
243
259
|
message += `\n${warning}`;
|
|
244
260
|
}
|
|
245
|
-
|
|
261
|
+
{
|
|
262
|
+
const guidance = [
|
|
263
|
+
`1) Preferred: add a unique data-testid and select it directly (e.g., testid:submit).`,
|
|
264
|
+
`2) If you cannot change markup: append \`>> nth=<index>\` to target a specific match.`,
|
|
265
|
+
];
|
|
266
|
+
const matchesDetails = await this.describeMatchedElements(locator, selector, count);
|
|
267
|
+
message += `\n${guidance.join('\n')}\n\nMatches:\n${matchesDetails}`;
|
|
268
|
+
throw new Error(message);
|
|
269
|
+
}
|
|
246
270
|
}
|
|
247
271
|
// Handle explicit element index (1-based)
|
|
248
272
|
if (options?.elementIndex !== undefined) {
|
|
@@ -358,4 +382,68 @@ export class BrowserToolBase {
|
|
|
358
382
|
`Note: nth selectors are brittle and may break with layout/content changes.\n` +
|
|
359
383
|
`Prefer unique data-testid attributes for long-term stability.`);
|
|
360
384
|
}
|
|
385
|
+
/**
|
|
386
|
+
* Describe matched elements in a compact, copyable format for disambiguation errors.
|
|
387
|
+
* Shows: index, tag, trimmed text, nearest parent marker, and a suggested selector.
|
|
388
|
+
* Suggests testid:VALUE when present; otherwise falls back to id=VALUE or original >> nth=i.
|
|
389
|
+
*/
|
|
390
|
+
async describeMatchedElements(locator, originalSelector, count) {
|
|
391
|
+
const maxItems = Math.min(count, 5);
|
|
392
|
+
const lines = [];
|
|
393
|
+
for (let i = 0; i < maxItems; i++) {
|
|
394
|
+
const nth = locator.nth(i);
|
|
395
|
+
try {
|
|
396
|
+
const info = await nth.evaluate((el) => {
|
|
397
|
+
const tag = (el.tagName || '').toLowerCase();
|
|
398
|
+
let text = el.innerText || el.textContent || '';
|
|
399
|
+
text = (text || '').replace(/\s+/g, ' ').trim();
|
|
400
|
+
const testid = el.getAttribute?.('data-testid') || el.getAttribute?.('data-test') || el.getAttribute?.('data-cy') || null;
|
|
401
|
+
const id = el.id || null;
|
|
402
|
+
let parentLabel = null;
|
|
403
|
+
let p = el.parentElement;
|
|
404
|
+
while (p && !parentLabel) {
|
|
405
|
+
const ptid = p.getAttribute?.('data-testid');
|
|
406
|
+
const ptest = p.getAttribute?.('data-test');
|
|
407
|
+
const pcy = p.getAttribute?.('data-cy');
|
|
408
|
+
const pid = p.id || null;
|
|
409
|
+
if (ptid)
|
|
410
|
+
parentLabel = `[data-testid="${ptid}"]`;
|
|
411
|
+
else if (ptest)
|
|
412
|
+
parentLabel = `[data-test="${ptest}"]`;
|
|
413
|
+
else if (pcy)
|
|
414
|
+
parentLabel = `[data-cy="${pcy}"]`;
|
|
415
|
+
else if (pid)
|
|
416
|
+
parentLabel = `#${pid}`;
|
|
417
|
+
p = p.parentElement;
|
|
418
|
+
}
|
|
419
|
+
return { tag, text, testid, id, parentLabel };
|
|
420
|
+
});
|
|
421
|
+
const truncatedText = info.text && info.text.length > 80 ? `${info.text.slice(0, 77)}...` : info.text;
|
|
422
|
+
let selectorSuggestion = `${originalSelector} >> nth=${i}`;
|
|
423
|
+
let altSuggestion;
|
|
424
|
+
if (info?.testid) {
|
|
425
|
+
selectorSuggestion = `testid:${info.testid}`;
|
|
426
|
+
altSuggestion = `${originalSelector} >> nth=${i}`;
|
|
427
|
+
}
|
|
428
|
+
else if (info?.id) {
|
|
429
|
+
selectorSuggestion = `id=${info.id}`;
|
|
430
|
+
altSuggestion = `${originalSelector} >> nth=${i}`;
|
|
431
|
+
}
|
|
432
|
+
const parts = [
|
|
433
|
+
`[${i}] <${info.tag}>${truncatedText ? ` "${truncatedText}"` : ''}`,
|
|
434
|
+
info.parentLabel ? ` parent: ${info.parentLabel}` : undefined,
|
|
435
|
+
` selector: ${selectorSuggestion}`,
|
|
436
|
+
altSuggestion ? ` alt: ${altSuggestion}` : undefined,
|
|
437
|
+
].filter(Boolean);
|
|
438
|
+
lines.push(parts.join('\n'));
|
|
439
|
+
}
|
|
440
|
+
catch {
|
|
441
|
+
lines.push(`[${i}] (element)\n selector: ${originalSelector} >> nth=${i}`);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
if (count > maxItems) {
|
|
445
|
+
lines.push(`… and ${count - maxItems} more matches (use >> nth=<index> to target).`);
|
|
446
|
+
}
|
|
447
|
+
return lines.join('\n');
|
|
448
|
+
}
|
|
361
449
|
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Page } from 'playwright';
|
|
2
|
+
export declare function gatherConsoleErrorsSince(since: 'navigation' | 'interaction'): Promise<string[]>;
|
|
3
|
+
export declare function quickNetworkIdleNote(page: Page): Promise<string>;
|
|
4
|
+
export declare function titleUrlChangeLines(page: Page, initial?: {
|
|
5
|
+
url?: string;
|
|
6
|
+
title?: string;
|
|
7
|
+
}): Promise<string[]>;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// Gather console error/exception logs since a baseline event
|
|
2
|
+
export async function gatherConsoleErrorsSince(since) {
|
|
3
|
+
const { getConsoleLogsSinceLastNavigation, getConsoleLogsSinceLastInteraction } = await import('../../../toolHandler.js');
|
|
4
|
+
const logs = since === 'navigation'
|
|
5
|
+
? getConsoleLogsSinceLastNavigation()
|
|
6
|
+
: getConsoleLogsSinceLastInteraction();
|
|
7
|
+
return logs.filter(l => l.startsWith('[error]') || l.startsWith('[exception]'));
|
|
8
|
+
}
|
|
9
|
+
// Provide a compact, best-effort network idle note
|
|
10
|
+
export async function quickNetworkIdleNote(page) {
|
|
11
|
+
try {
|
|
12
|
+
const start = Date.now();
|
|
13
|
+
const anyPage = page;
|
|
14
|
+
const wait = anyPage?.waitForLoadState?.bind(page);
|
|
15
|
+
if (typeof wait === 'function') {
|
|
16
|
+
await wait('networkidle', { timeout: 500 });
|
|
17
|
+
const ms = Date.now() - start;
|
|
18
|
+
return `✓ Network idle after ${ms}ms, 0 pending requests`;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
// fall through to no-activity note
|
|
23
|
+
}
|
|
24
|
+
return 'No new network activity detected (quick check)';
|
|
25
|
+
}
|
|
26
|
+
// Compute concise lines for title/URL when changed
|
|
27
|
+
export async function titleUrlChangeLines(page, initial = {}) {
|
|
28
|
+
const lines = [];
|
|
29
|
+
let newUrl = '';
|
|
30
|
+
let newTitle = '';
|
|
31
|
+
try {
|
|
32
|
+
newUrl = page.url();
|
|
33
|
+
}
|
|
34
|
+
catch { }
|
|
35
|
+
try {
|
|
36
|
+
newTitle = await page.title();
|
|
37
|
+
}
|
|
38
|
+
catch { }
|
|
39
|
+
if (initial.url && newUrl && initial.url !== newUrl) {
|
|
40
|
+
lines.push(`URL changed: ${newUrl}`);
|
|
41
|
+
}
|
|
42
|
+
if (initial.title && newTitle && initial.title !== newTitle) {
|
|
43
|
+
lines.push(`Title changed: ${newTitle}`);
|
|
44
|
+
}
|
|
45
|
+
return lines;
|
|
46
|
+
}
|
|
@@ -237,4 +237,13 @@ describe('GetConsoleLogsTool', () => {
|
|
|
237
237
|
// Should include one of the first 20 grouped messages
|
|
238
238
|
expect(fullText).toContain('GROUP-LONG-0');
|
|
239
239
|
});
|
|
240
|
+
test('type: error should include exception entries', async () => {
|
|
241
|
+
consoleLogsTool.registerConsoleMessage('exception', 'Hook failed');
|
|
242
|
+
consoleLogsTool.registerConsoleMessage('error', 'Console error');
|
|
243
|
+
const result = await consoleLogsTool.execute({ type: 'error' }, mockContext);
|
|
244
|
+
expect(result.isError).toBe(false);
|
|
245
|
+
const text = result.content.map(c => c.text).join('\n');
|
|
246
|
+
expect(text).toContain('[exception] Hook failed');
|
|
247
|
+
expect(text).toContain('[error] Console error');
|
|
248
|
+
});
|
|
240
249
|
});
|