mcp-web-inspector 0.5.3 → 0.6.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 +5 -5
- package/dist/toolHandler.d.ts +4 -0
- package/dist/toolHandler.js +50 -25
- package/dist/tools/browser/base.d.ts +6 -0
- package/dist/tools/browser/base.js +75 -3
- 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/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/register.js +2 -4
- package/package.json +12 -13
package/README.md
CHANGED
|
@@ -911,11 +911,11 @@ Quick check if an element exists on the page. Ultra-lightweight alternative to q
|
|
|
911
911
|
|
|
912
912
|
### Navigation
|
|
913
913
|
|
|
914
|
-
#### `
|
|
915
|
-
Navigate back in browser history
|
|
914
|
+
#### `go_history`
|
|
915
|
+
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
916
|
|
|
917
|
-
|
|
918
|
-
|
|
917
|
+
- Parameters:
|
|
918
|
+
- direction (string, required): History direction to navigate
|
|
919
919
|
|
|
920
920
|
#### `navigate`
|
|
921
921
|
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.
|
|
@@ -1048,7 +1048,7 @@ Clears captured console logs and returns the number of entries cleared.
|
|
|
1048
1048
|
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
1049
|
|
|
1050
1050
|
- Parameters:
|
|
1051
|
-
- type (string, optional): Type
|
|
1051
|
+
- type (string, optional): Type filter (all, error, warning, log, info, debug, exception). Note: 'error' also includes 'exception' entries for convenience.
|
|
1052
1052
|
- search (string, optional): Text to search for in logs (handles text with square brackets)
|
|
1053
1053
|
- limit (number, optional): Maximum entries to return (groups when grouped, lines when raw). Default: 20
|
|
1054
1054
|
- 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'
|
package/dist/toolHandler.d.ts
CHANGED
|
@@ -79,6 +79,10 @@ export declare function getConsoleLogs(): string[];
|
|
|
79
79
|
* Get console logs captured after the last navigation
|
|
80
80
|
*/
|
|
81
81
|
export declare function getConsoleLogsSinceLastNavigation(): string[];
|
|
82
|
+
/**
|
|
83
|
+
* Get console logs captured after the last interaction
|
|
84
|
+
*/
|
|
85
|
+
export declare function getConsoleLogsSinceLastInteraction(): string[];
|
|
82
86
|
/**
|
|
83
87
|
* Get screenshots
|
|
84
88
|
*/
|
package/dist/toolHandler.js
CHANGED
|
@@ -72,15 +72,30 @@ function getColorSchemeValue() {
|
|
|
72
72
|
return colorSchemeOverride;
|
|
73
73
|
}
|
|
74
74
|
async function applyColorScheme(targetPage) {
|
|
75
|
-
if (!targetPage)
|
|
75
|
+
if (!targetPage)
|
|
76
76
|
return;
|
|
77
|
-
|
|
77
|
+
const scheme = getColorSchemeValue();
|
|
78
78
|
try {
|
|
79
|
-
|
|
80
|
-
|
|
79
|
+
// Some test environments or mocks may not implement emulateMedia
|
|
80
|
+
const anyPage = targetPage;
|
|
81
|
+
if (typeof anyPage.emulateMedia === 'function') {
|
|
82
|
+
await anyPage.emulateMedia({ colorScheme: scheme ?? null });
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
// Fallback: if emulateMedia is unavailable, do a best-effort hint via CSS.
|
|
86
|
+
// This won't fully emulate prefers-color-scheme but avoids throwing in tests.
|
|
87
|
+
if (scheme) {
|
|
88
|
+
const css = scheme === 'dark' ? ':root{color-scheme: dark;}'
|
|
89
|
+
: scheme === 'light' ? ':root{color-scheme: light;}'
|
|
90
|
+
: ':root{color-scheme: light dark;}';
|
|
91
|
+
if (typeof anyPage.addStyleTag === 'function') {
|
|
92
|
+
await anyPage.addStyleTag({ content: css });
|
|
93
|
+
}
|
|
94
|
+
}
|
|
81
95
|
}
|
|
82
96
|
catch (error) {
|
|
83
|
-
|
|
97
|
+
// Swallow errors to keep color scheme application non-fatal
|
|
98
|
+
console.warn("Failed to apply color scheme (non-fatal):", error);
|
|
84
99
|
}
|
|
85
100
|
}
|
|
86
101
|
export async function setColorSchemeOverride(scheme) {
|
|
@@ -258,13 +273,13 @@ async function getScreenSize() {
|
|
|
258
273
|
await tempBrowser.close();
|
|
259
274
|
// Validate the screen size values
|
|
260
275
|
if (!screenSize || typeof screenSize.width !== 'number' || typeof screenSize.height !== 'number') {
|
|
261
|
-
console.
|
|
276
|
+
console.warn('Invalid screen size detected, using defaults');
|
|
262
277
|
return { width: 1280, height: 720 };
|
|
263
278
|
}
|
|
264
279
|
return screenSize;
|
|
265
280
|
}
|
|
266
281
|
catch (error) {
|
|
267
|
-
console.
|
|
282
|
+
console.warn('Failed to detect screen size, using defaults:', error);
|
|
268
283
|
return { width: 1280, height: 720 };
|
|
269
284
|
}
|
|
270
285
|
}
|
|
@@ -279,15 +294,15 @@ export async function ensureBrowser(browserSettings) {
|
|
|
279
294
|
const browserCheck = checkBrowsersInstalled();
|
|
280
295
|
if (!browserCheck.installed) {
|
|
281
296
|
// Try to install browsers automatically
|
|
282
|
-
console.
|
|
283
|
-
console.
|
|
297
|
+
console.warn('🎭 Playwright browsers not found. Installing automatically...');
|
|
298
|
+
console.warn('⏳ This will download ~1GB of browser binaries. Please wait...');
|
|
284
299
|
try {
|
|
285
300
|
const { execSync } = await import('child_process');
|
|
286
301
|
execSync('npx playwright install chromium firefox webkit', {
|
|
287
302
|
stdio: 'inherit',
|
|
288
303
|
encoding: 'utf8'
|
|
289
304
|
});
|
|
290
|
-
console.
|
|
305
|
+
console.log('✅ Browsers installed successfully! Starting browser...');
|
|
291
306
|
// Note: browser variable is still undefined here, which is correct.
|
|
292
307
|
// The code below (line 342) will launch the browser after installation.
|
|
293
308
|
}
|
|
@@ -300,7 +315,7 @@ export async function ensureBrowser(browserSettings) {
|
|
|
300
315
|
}
|
|
301
316
|
// Check if browser exists but is disconnected
|
|
302
317
|
if (browser && !browser.isConnected()) {
|
|
303
|
-
console.
|
|
318
|
+
console.warn("Browser exists but is disconnected. Cleaning up...");
|
|
304
319
|
try {
|
|
305
320
|
await browser.close().catch(err => console.error("Error closing disconnected browser:", err));
|
|
306
321
|
}
|
|
@@ -312,7 +327,7 @@ export async function ensureBrowser(browserSettings) {
|
|
|
312
327
|
}
|
|
313
328
|
// Check if device preset has changed (requires browser restart)
|
|
314
329
|
if (browser && browserSettings?.device && browserSettings.device !== currentDevice) {
|
|
315
|
-
console.
|
|
330
|
+
console.warn(`Device preset changed from ${currentDevice || 'none'} to ${browserSettings.device}. Restarting browser...`);
|
|
316
331
|
try {
|
|
317
332
|
await browser.close().catch(err => console.error("Error closing browser on device change:", err));
|
|
318
333
|
}
|
|
@@ -331,7 +346,7 @@ export async function ensureBrowser(browserSettings) {
|
|
|
331
346
|
const targetHeight = height ?? currentViewport?.height ?? 720;
|
|
332
347
|
// Check if viewport size actually changed
|
|
333
348
|
if (!currentViewport || currentViewport.width !== targetWidth || currentViewport.height !== targetHeight) {
|
|
334
|
-
console.
|
|
349
|
+
console.log(`Resizing viewport to ${targetWidth}x${targetHeight}`);
|
|
335
350
|
await page.setViewportSize({ width: targetWidth, height: targetHeight });
|
|
336
351
|
}
|
|
337
352
|
}
|
|
@@ -356,18 +371,18 @@ export async function ensureBrowser(browserSettings) {
|
|
|
356
371
|
// Check custom configs first, then Playwright's built-in devices
|
|
357
372
|
deviceConfig = CUSTOM_DEVICE_CONFIGS[playwrightDeviceName] || devices[playwrightDeviceName];
|
|
358
373
|
if (deviceConfig) {
|
|
359
|
-
console.
|
|
374
|
+
console.log(`Using device preset: ${device} (${playwrightDeviceName})`);
|
|
360
375
|
currentDevice = device;
|
|
361
376
|
}
|
|
362
377
|
else {
|
|
363
|
-
console.
|
|
378
|
+
console.warn(`Warning: Device preset ${playwrightDeviceName} not found`);
|
|
364
379
|
currentDevice = undefined;
|
|
365
380
|
}
|
|
366
381
|
}
|
|
367
382
|
else {
|
|
368
383
|
currentDevice = undefined;
|
|
369
384
|
}
|
|
370
|
-
console.
|
|
385
|
+
console.warn(`Launching new ${browserType} browser instance...`);
|
|
371
386
|
// Use the appropriate browser engine
|
|
372
387
|
let browserInstance;
|
|
373
388
|
switch (browserType) {
|
|
@@ -397,7 +412,7 @@ export async function ensureBrowser(browserSettings) {
|
|
|
397
412
|
viewportWidth = screenSize?.width ?? 1280;
|
|
398
413
|
viewportHeight = screenSize?.height ?? 720;
|
|
399
414
|
if (screenSize && screenSize.width > 0 && screenSize.height > 0) {
|
|
400
|
-
console.
|
|
415
|
+
console.log(`No viewport specified, using screen size: ${viewportWidth}x${viewportHeight}`);
|
|
401
416
|
}
|
|
402
417
|
}
|
|
403
418
|
// Prepare context options
|
|
@@ -421,14 +436,14 @@ export async function ensureBrowser(browserSettings) {
|
|
|
421
436
|
}
|
|
422
437
|
// Use persistent context if session saving is enabled
|
|
423
438
|
if (sessionConfig.saveSession) {
|
|
424
|
-
console.
|
|
439
|
+
console.warn(`Launching ${browserType} with persistent context at ${sessionConfig.userDataDir}...`);
|
|
425
440
|
const context = await browserInstance.launchPersistentContext(sessionConfig.userDataDir, contextOptions);
|
|
426
441
|
// Get the browser instance from the context
|
|
427
442
|
browser = context.browser();
|
|
428
443
|
currentBrowserType = browserType;
|
|
429
444
|
// Add cleanup logic when browser is disconnected
|
|
430
445
|
browser.on('disconnected', () => {
|
|
431
|
-
console.
|
|
446
|
+
console.warn("Browser disconnected event triggered");
|
|
432
447
|
browser = undefined;
|
|
433
448
|
page = undefined;
|
|
434
449
|
});
|
|
@@ -444,7 +459,7 @@ export async function ensureBrowser(browserSettings) {
|
|
|
444
459
|
currentBrowserType = browserType;
|
|
445
460
|
// Add cleanup logic when browser is disconnected
|
|
446
461
|
browser.on('disconnected', () => {
|
|
447
|
-
console.
|
|
462
|
+
console.warn("Browser disconnected event triggered");
|
|
448
463
|
browser = undefined;
|
|
449
464
|
page = undefined;
|
|
450
465
|
});
|
|
@@ -473,7 +488,7 @@ export async function ensureBrowser(browserSettings) {
|
|
|
473
488
|
}
|
|
474
489
|
// Verify page is still valid
|
|
475
490
|
if (!page || page.isClosed()) {
|
|
476
|
-
console.
|
|
491
|
+
console.warn("Page is closed or invalid. Creating new page...");
|
|
477
492
|
// Create a new page if the current one is invalid
|
|
478
493
|
const context = browser.contexts()[0] || await browser.newContext();
|
|
479
494
|
page = await context.newPage();
|
|
@@ -565,12 +580,12 @@ export async function ensureBrowser(browserSettings) {
|
|
|
565
580
|
}
|
|
566
581
|
// Use persistent context if session saving is enabled
|
|
567
582
|
if (sessionConfig.saveSession) {
|
|
568
|
-
console.
|
|
583
|
+
console.warn(`Launching ${browserType} with persistent context at ${sessionConfig.userDataDir} (retry)...`);
|
|
569
584
|
const context = await browserInstance.launchPersistentContext(sessionConfig.userDataDir, retryContextOptions);
|
|
570
585
|
browser = context.browser();
|
|
571
586
|
currentBrowserType = browserType;
|
|
572
587
|
browser.on('disconnected', () => {
|
|
573
|
-
console.
|
|
588
|
+
console.warn("Browser disconnected event triggered (retry)");
|
|
574
589
|
browser = undefined;
|
|
575
590
|
page = undefined;
|
|
576
591
|
});
|
|
@@ -584,7 +599,7 @@ export async function ensureBrowser(browserSettings) {
|
|
|
584
599
|
});
|
|
585
600
|
currentBrowserType = browserType;
|
|
586
601
|
browser.on('disconnected', () => {
|
|
587
|
-
console.
|
|
602
|
+
console.warn("Browser disconnected event triggered (retry)");
|
|
588
603
|
browser = undefined;
|
|
589
604
|
page = undefined;
|
|
590
605
|
});
|
|
@@ -650,7 +665,7 @@ export async function handleToolCall(name, args, server) {
|
|
|
650
665
|
const requiresBrowser = isBrowserTool(name);
|
|
651
666
|
// Check if we have a disconnected browser that needs cleanup
|
|
652
667
|
if (browser && !browser.isConnected() && requiresBrowser) {
|
|
653
|
-
console.
|
|
668
|
+
console.warn("Detected disconnected browser before tool execution, cleaning up...");
|
|
654
669
|
try {
|
|
655
670
|
await browser.close().catch(() => { }); // Ignore errors
|
|
656
671
|
}
|
|
@@ -739,6 +754,16 @@ export function getConsoleLogsSinceLastNavigation() {
|
|
|
739
754
|
return [];
|
|
740
755
|
return consoleLogsTool.getLogsSinceLastNavigation();
|
|
741
756
|
}
|
|
757
|
+
/**
|
|
758
|
+
* Get console logs captured after the last interaction
|
|
759
|
+
*/
|
|
760
|
+
export function getConsoleLogsSinceLastInteraction() {
|
|
761
|
+
const consoleLogsTool = getToolInstance("get_console_logs", null);
|
|
762
|
+
if (!consoleLogsTool)
|
|
763
|
+
return [];
|
|
764
|
+
// Expose a compact accessor mirroring navigation-based retrieval
|
|
765
|
+
return consoleLogsTool.getLogsSinceLastInteraction?.() ?? [];
|
|
766
|
+
}
|
|
742
767
|
/**
|
|
743
768
|
* Get screenshots
|
|
744
769
|
*/
|
|
@@ -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
|
}
|
|
@@ -233,8 +233,8 @@ export class BrowserToolBase {
|
|
|
233
233
|
// Check for multiple elements with errorOnMultiple flag
|
|
234
234
|
if (options?.errorOnMultiple && count > 1) {
|
|
235
235
|
const selector = options.originalSelector || 'selector';
|
|
236
|
-
const nthHint =
|
|
237
|
-
const warning =
|
|
236
|
+
const nthHint = ''.trimEnd();
|
|
237
|
+
const warning = ''.trimEnd();
|
|
238
238
|
let message = `Selector "${selector}" matched ${count} elements. Please use a more specific selector.`;
|
|
239
239
|
if (nthHint) {
|
|
240
240
|
message += `\n${nthHint}`;
|
|
@@ -242,7 +242,15 @@ export class BrowserToolBase {
|
|
|
242
242
|
if (warning) {
|
|
243
243
|
message += `\n${warning}`;
|
|
244
244
|
}
|
|
245
|
-
|
|
245
|
+
{
|
|
246
|
+
const guidance = [
|
|
247
|
+
`1) Preferred: add a unique data-testid and select it directly (e.g., testid:submit).`,
|
|
248
|
+
`2) If you cannot change markup: append \`>> nth=<index>\` to target a specific match.`,
|
|
249
|
+
];
|
|
250
|
+
const matchesDetails = await this.describeMatchedElements(locator, selector, count);
|
|
251
|
+
message += `\n${guidance.join('\n')}\n\nMatches:\n${matchesDetails}`;
|
|
252
|
+
throw new Error(message);
|
|
253
|
+
}
|
|
246
254
|
}
|
|
247
255
|
// Handle explicit element index (1-based)
|
|
248
256
|
if (options?.elementIndex !== undefined) {
|
|
@@ -358,4 +366,68 @@ export class BrowserToolBase {
|
|
|
358
366
|
`Note: nth selectors are brittle and may break with layout/content changes.\n` +
|
|
359
367
|
`Prefer unique data-testid attributes for long-term stability.`);
|
|
360
368
|
}
|
|
369
|
+
/**
|
|
370
|
+
* Describe matched elements in a compact, copyable format for disambiguation errors.
|
|
371
|
+
* Shows: index, tag, trimmed text, nearest parent marker, and a suggested selector.
|
|
372
|
+
* Suggests testid:VALUE when present; otherwise falls back to id=VALUE or original >> nth=i.
|
|
373
|
+
*/
|
|
374
|
+
async describeMatchedElements(locator, originalSelector, count) {
|
|
375
|
+
const maxItems = Math.min(count, 5);
|
|
376
|
+
const lines = [];
|
|
377
|
+
for (let i = 0; i < maxItems; i++) {
|
|
378
|
+
const nth = locator.nth(i);
|
|
379
|
+
try {
|
|
380
|
+
const info = await nth.evaluate((el) => {
|
|
381
|
+
const tag = (el.tagName || '').toLowerCase();
|
|
382
|
+
let text = el.innerText || el.textContent || '';
|
|
383
|
+
text = (text || '').replace(/\s+/g, ' ').trim();
|
|
384
|
+
const testid = el.getAttribute?.('data-testid') || el.getAttribute?.('data-test') || el.getAttribute?.('data-cy') || null;
|
|
385
|
+
const id = el.id || null;
|
|
386
|
+
let parentLabel = null;
|
|
387
|
+
let p = el.parentElement;
|
|
388
|
+
while (p && !parentLabel) {
|
|
389
|
+
const ptid = p.getAttribute?.('data-testid');
|
|
390
|
+
const ptest = p.getAttribute?.('data-test');
|
|
391
|
+
const pcy = p.getAttribute?.('data-cy');
|
|
392
|
+
const pid = p.id || null;
|
|
393
|
+
if (ptid)
|
|
394
|
+
parentLabel = `[data-testid="${ptid}"]`;
|
|
395
|
+
else if (ptest)
|
|
396
|
+
parentLabel = `[data-test="${ptest}"]`;
|
|
397
|
+
else if (pcy)
|
|
398
|
+
parentLabel = `[data-cy="${pcy}"]`;
|
|
399
|
+
else if (pid)
|
|
400
|
+
parentLabel = `#${pid}`;
|
|
401
|
+
p = p.parentElement;
|
|
402
|
+
}
|
|
403
|
+
return { tag, text, testid, id, parentLabel };
|
|
404
|
+
});
|
|
405
|
+
const truncatedText = info.text && info.text.length > 80 ? `${info.text.slice(0, 77)}...` : info.text;
|
|
406
|
+
let selectorSuggestion = `${originalSelector} >> nth=${i}`;
|
|
407
|
+
let altSuggestion;
|
|
408
|
+
if (info?.testid) {
|
|
409
|
+
selectorSuggestion = `testid:${info.testid}`;
|
|
410
|
+
altSuggestion = `${originalSelector} >> nth=${i}`;
|
|
411
|
+
}
|
|
412
|
+
else if (info?.id) {
|
|
413
|
+
selectorSuggestion = `id=${info.id}`;
|
|
414
|
+
altSuggestion = `${originalSelector} >> nth=${i}`;
|
|
415
|
+
}
|
|
416
|
+
const parts = [
|
|
417
|
+
`[${i}] <${info.tag}>${truncatedText ? ` "${truncatedText}"` : ''}`,
|
|
418
|
+
info.parentLabel ? ` parent: ${info.parentLabel}` : undefined,
|
|
419
|
+
` selector: ${selectorSuggestion}`,
|
|
420
|
+
altSuggestion ? ` alt: ${altSuggestion}` : undefined,
|
|
421
|
+
].filter(Boolean);
|
|
422
|
+
lines.push(parts.join('\n'));
|
|
423
|
+
}
|
|
424
|
+
catch {
|
|
425
|
+
lines.push(`[${i}] (element)\n selector: ${originalSelector} >> nth=${i}`);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
if (count > maxItems) {
|
|
429
|
+
lines.push(`… and ${count - maxItems} more matches (use >> nth=<index> to target).`);
|
|
430
|
+
}
|
|
431
|
+
return lines.join('\n');
|
|
432
|
+
}
|
|
361
433
|
}
|
|
@@ -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
|
});
|
|
@@ -36,6 +36,10 @@ export declare class GetConsoleLogsTool extends BrowserToolBase {
|
|
|
36
36
|
* Return messages for logs captured after the last recorded navigation
|
|
37
37
|
*/
|
|
38
38
|
getLogsSinceLastNavigation(): string[];
|
|
39
|
+
/**
|
|
40
|
+
* Return messages for logs captured after the last recorded interaction
|
|
41
|
+
*/
|
|
42
|
+
getLogsSinceLastInteraction(): string[];
|
|
39
43
|
}
|
|
40
44
|
/**
|
|
41
45
|
* Tool for clearing console logs (atomic operation)
|
|
@@ -23,7 +23,7 @@ export class GetConsoleLogsTool extends BrowserToolBase {
|
|
|
23
23
|
properties: {
|
|
24
24
|
type: {
|
|
25
25
|
type: "string",
|
|
26
|
-
description: "Type
|
|
26
|
+
description: "Type filter (all, error, warning, log, info, debug, exception). Note: 'error' also includes 'exception' entries for convenience.",
|
|
27
27
|
enum: ["all", "error", "warning", "log", "info", "debug", "exception"]
|
|
28
28
|
},
|
|
29
29
|
search: {
|
|
@@ -100,7 +100,10 @@ export class GetConsoleLogsTool extends BrowserToolBase {
|
|
|
100
100
|
this.lastCallTimestamp = Date.now();
|
|
101
101
|
// Filter by type if specified
|
|
102
102
|
if (args.type && args.type !== 'all') {
|
|
103
|
-
|
|
103
|
+
const wanted = args.type;
|
|
104
|
+
// Treat 'error' as including both console errors and exceptions captured via pageerror/unhandledrejection
|
|
105
|
+
const prefixes = wanted === 'error' ? ['[error]', '[exception]'] : [`[${wanted}]`];
|
|
106
|
+
logs = logs.filter(log => prefixes.some(p => log.message.startsWith(p)));
|
|
104
107
|
}
|
|
105
108
|
// Filter by search text if specified
|
|
106
109
|
if (args.search) {
|
|
@@ -193,6 +196,15 @@ export class GetConsoleLogsTool extends BrowserToolBase {
|
|
|
193
196
|
.filter(log => log.timestamp > since)
|
|
194
197
|
.map(log => log.message);
|
|
195
198
|
}
|
|
199
|
+
/**
|
|
200
|
+
* Return messages for logs captured after the last recorded interaction
|
|
201
|
+
*/
|
|
202
|
+
getLogsSinceLastInteraction() {
|
|
203
|
+
const since = this.lastInteractionTimestamp;
|
|
204
|
+
return this.consoleLogs
|
|
205
|
+
.filter(log => log.timestamp > since)
|
|
206
|
+
.map(log => log.message);
|
|
207
|
+
}
|
|
196
208
|
}
|
|
197
209
|
// Track latest instance for sibling tool access (module-level singleton pattern)
|
|
198
210
|
GetConsoleLogsTool.latestInstance = null;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { describe, expect, test, beforeEach, jest } from '@jest/globals';
|
|
2
|
+
import { ClickTool } from '../click.js';
|
|
3
|
+
function makeFakeLocator() {
|
|
4
|
+
const elements = [
|
|
5
|
+
{
|
|
6
|
+
tagName: 'BUTTON',
|
|
7
|
+
innerText: 'Cancel',
|
|
8
|
+
textContent: 'Cancel',
|
|
9
|
+
id: '',
|
|
10
|
+
attrs: { 'data-testid': 'modal-cancel' },
|
|
11
|
+
parent: { tagName: 'DIV', id: 'modal-footer', attrs: {}, parent: null },
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
tagName: 'BUTTON',
|
|
15
|
+
innerText: 'Cancel',
|
|
16
|
+
textContent: 'Cancel',
|
|
17
|
+
id: 'toolbar-cancel',
|
|
18
|
+
attrs: {},
|
|
19
|
+
parent: { tagName: 'DIV', id: 'toolbar', attrs: {}, parent: null },
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
tagName: 'BUTTON',
|
|
23
|
+
innerText: 'Cancel third',
|
|
24
|
+
textContent: 'Cancel third',
|
|
25
|
+
id: '',
|
|
26
|
+
attrs: {},
|
|
27
|
+
parent: { tagName: 'DIV', id: '', attrs: { 'data-test': 'footer' }, parent: null },
|
|
28
|
+
},
|
|
29
|
+
];
|
|
30
|
+
const wrap = (el) => ({
|
|
31
|
+
evaluate: (fn) => {
|
|
32
|
+
const toDomEl = (src) => ({
|
|
33
|
+
tagName: src.tagName,
|
|
34
|
+
innerText: src.innerText,
|
|
35
|
+
textContent: src.textContent,
|
|
36
|
+
id: src.id,
|
|
37
|
+
getAttribute: (name) => src.attrs[name] ?? null,
|
|
38
|
+
parentElement: src.parent
|
|
39
|
+
? {
|
|
40
|
+
tagName: src.parent.tagName,
|
|
41
|
+
id: src.parent.id,
|
|
42
|
+
getAttribute: (name) => src.parent.attrs[name] ?? null,
|
|
43
|
+
parentElement: src.parent.parent,
|
|
44
|
+
}
|
|
45
|
+
: null,
|
|
46
|
+
});
|
|
47
|
+
return fn(toDomEl(el));
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
return {
|
|
51
|
+
count: async () => elements.length,
|
|
52
|
+
nth: (i) => wrap(elements[i]),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
describe('ClickTool duplicate selection error formatting (integration)', () => {
|
|
56
|
+
let clickTool;
|
|
57
|
+
let mockPage;
|
|
58
|
+
let mockBrowser;
|
|
59
|
+
let mockContext;
|
|
60
|
+
const mockIsClosed = jest.fn().mockReturnValue(false);
|
|
61
|
+
const mockIsConnected = jest.fn().mockReturnValue(true);
|
|
62
|
+
const mockPageLocator = jest.fn();
|
|
63
|
+
beforeEach(() => {
|
|
64
|
+
jest.clearAllMocks();
|
|
65
|
+
clickTool = new ClickTool({});
|
|
66
|
+
mockPage = {
|
|
67
|
+
locator: mockPageLocator,
|
|
68
|
+
isClosed: mockIsClosed,
|
|
69
|
+
};
|
|
70
|
+
mockBrowser = {
|
|
71
|
+
isConnected: mockIsConnected,
|
|
72
|
+
};
|
|
73
|
+
mockContext = {
|
|
74
|
+
page: mockPage,
|
|
75
|
+
browser: mockBrowser,
|
|
76
|
+
server: {},
|
|
77
|
+
};
|
|
78
|
+
});
|
|
79
|
+
test('returns concise guidance and match options when multiple elements match', async () => {
|
|
80
|
+
const fakeLocator = makeFakeLocator();
|
|
81
|
+
mockPageLocator.mockReturnValue(fakeLocator);
|
|
82
|
+
const res = await clickTool.execute({ selector: 'text=Cancel' }, mockContext);
|
|
83
|
+
expect(res.isError).toBe(true);
|
|
84
|
+
const text = res.content.map(c => c.text).join('\n');
|
|
85
|
+
// Prefixed by "Operation failed: ..."
|
|
86
|
+
expect(text).toContain('matched 3 elements');
|
|
87
|
+
expect(text).toContain('1) Preferred: add a unique data-testid');
|
|
88
|
+
expect(text).toContain('2) If you cannot change markup');
|
|
89
|
+
expect(text).toContain('Matches:');
|
|
90
|
+
expect(text).toContain('selector: testid:modal-cancel');
|
|
91
|
+
expect(text).toContain('selector: id=toolbar-cancel');
|
|
92
|
+
expect(text).toContain('selector: text=Cancel >> nth=2');
|
|
93
|
+
// Ensure old verbose hints are not duplicated here
|
|
94
|
+
expect(text).not.toContain('Primary fix: add a unique data-testid');
|
|
95
|
+
expect(text).not.toContain('Workaround: Append');
|
|
96
|
+
});
|
|
97
|
+
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { BrowserToolBase } from '../base.js';
|
|
2
|
-
import { createSuccessResponse } from '../../common/types.js';
|
|
2
|
+
import { createSuccessResponse, createErrorResponse } from '../../common/types.js';
|
|
3
|
+
import { gatherConsoleErrorsSince, quickNetworkIdleNote, titleUrlChangeLines } from '../common/postAction.js';
|
|
3
4
|
/**
|
|
4
5
|
* Tool for clicking elements on the page
|
|
5
6
|
*/
|
|
@@ -27,8 +28,51 @@ export class ClickTool extends BrowserToolBase {
|
|
|
27
28
|
errorOnMultiple: true,
|
|
28
29
|
originalSelector: args.selector,
|
|
29
30
|
});
|
|
31
|
+
// Capture initial state for change detection
|
|
32
|
+
let initialUrl = '';
|
|
33
|
+
let initialTitle = '';
|
|
34
|
+
try {
|
|
35
|
+
initialUrl = page.url();
|
|
36
|
+
}
|
|
37
|
+
catch { }
|
|
38
|
+
try {
|
|
39
|
+
initialTitle = await page.title();
|
|
40
|
+
}
|
|
41
|
+
catch { }
|
|
30
42
|
await element.click();
|
|
31
|
-
|
|
43
|
+
const lines = [`Clicked element: ${args.selector}`];
|
|
44
|
+
// First, a quick network-idle hint to allow errors to flush
|
|
45
|
+
try {
|
|
46
|
+
const note = await quickNetworkIdleNote(page);
|
|
47
|
+
if (note)
|
|
48
|
+
lines.push(note);
|
|
49
|
+
}
|
|
50
|
+
catch { }
|
|
51
|
+
// Then, surface console errors triggered by the interaction
|
|
52
|
+
try {
|
|
53
|
+
const errs = await gatherConsoleErrorsSince('interaction');
|
|
54
|
+
if (errs.length > 0) {
|
|
55
|
+
let titleInfo = '';
|
|
56
|
+
try {
|
|
57
|
+
const t = await page.title();
|
|
58
|
+
if (t)
|
|
59
|
+
titleInfo = `\nTitle: ${t}`;
|
|
60
|
+
}
|
|
61
|
+
catch { }
|
|
62
|
+
return createErrorResponse(`Console error after click: ${errs[0]}${titleInfo}`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
// ignore log retrieval errors
|
|
67
|
+
}
|
|
68
|
+
// Title / URL changes
|
|
69
|
+
try {
|
|
70
|
+
const changeLines = await titleUrlChangeLines(page, { url: initialUrl, title: initialTitle });
|
|
71
|
+
if (changeLines.length > 0)
|
|
72
|
+
lines.push(...changeLines);
|
|
73
|
+
}
|
|
74
|
+
catch { }
|
|
75
|
+
return createSuccessResponse(lines);
|
|
32
76
|
});
|
|
33
77
|
}
|
|
34
78
|
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { GoForwardTool } from '../go_forward.js';
|
|
1
|
+
import { GoHistoryTool } from '../history.js';
|
|
3
2
|
import { jest } from '@jest/globals';
|
|
4
3
|
// Mock page functions
|
|
5
4
|
const mockGoBack = jest.fn().mockImplementation(() => Promise.resolve());
|
|
@@ -26,63 +25,52 @@ const mockContext = {
|
|
|
26
25
|
browser: mockBrowser,
|
|
27
26
|
server: mockServer
|
|
28
27
|
};
|
|
29
|
-
describe('Browser Navigation History
|
|
30
|
-
let
|
|
31
|
-
let goForwardTool;
|
|
28
|
+
describe('Browser Navigation History Tool', () => {
|
|
29
|
+
let historyTool;
|
|
32
30
|
beforeEach(() => {
|
|
33
31
|
jest.clearAllMocks();
|
|
34
|
-
|
|
35
|
-
goForwardTool = new GoForwardTool(mockServer);
|
|
32
|
+
historyTool = new GoHistoryTool(mockServer);
|
|
36
33
|
// Reset browser and page mocks
|
|
37
34
|
mockIsConnected.mockReturnValue(true);
|
|
38
35
|
mockIsClosed.mockReturnValue(false);
|
|
39
36
|
});
|
|
40
|
-
describe('
|
|
37
|
+
describe('HistoryTool', () => {
|
|
41
38
|
test('should navigate back in browser history', async () => {
|
|
42
|
-
const args = {};
|
|
43
|
-
const result = await
|
|
39
|
+
const args = { direction: 'back' };
|
|
40
|
+
const result = await historyTool.execute(args, mockContext);
|
|
44
41
|
expect(mockGoBack).toHaveBeenCalled();
|
|
45
42
|
expect(result.isError).toBe(false);
|
|
46
43
|
expect(result.content[0].text).toContain('Navigated back');
|
|
47
44
|
});
|
|
45
|
+
test('should navigate forward in browser history', async () => {
|
|
46
|
+
const args = { direction: 'forward' };
|
|
47
|
+
const result = await historyTool.execute(args, mockContext);
|
|
48
|
+
expect(mockGoForward).toHaveBeenCalled();
|
|
49
|
+
expect(result.isError).toBe(false);
|
|
50
|
+
expect(result.content[0].text).toContain('Navigated forward');
|
|
51
|
+
});
|
|
48
52
|
test('should handle navigation back errors', async () => {
|
|
49
|
-
const args = {};
|
|
53
|
+
const args = { direction: 'back' };
|
|
50
54
|
// Mock a navigation error
|
|
51
55
|
mockGoBack.mockImplementationOnce(() => Promise.reject(new Error('Navigation back failed')));
|
|
52
|
-
const result = await
|
|
56
|
+
const result = await historyTool.execute(args, mockContext);
|
|
53
57
|
expect(mockGoBack).toHaveBeenCalled();
|
|
54
58
|
expect(result.isError).toBe(true);
|
|
55
59
|
expect(result.content[0].text).toContain('Operation failed');
|
|
56
60
|
});
|
|
57
|
-
test('should handle missing page', async () => {
|
|
58
|
-
const args = {};
|
|
59
|
-
const result = await goBackTool.execute(args, { server: mockServer });
|
|
60
|
-
expect(mockGoBack).not.toHaveBeenCalled();
|
|
61
|
-
expect(result.isError).toBe(true);
|
|
62
|
-
expect(result.content[0].text).toContain('Browser page not initialized');
|
|
63
|
-
});
|
|
64
|
-
});
|
|
65
|
-
describe('GoForwardTool', () => {
|
|
66
|
-
test('should navigate forward in browser history', async () => {
|
|
67
|
-
const args = {};
|
|
68
|
-
const result = await goForwardTool.execute(args, mockContext);
|
|
69
|
-
expect(mockGoForward).toHaveBeenCalled();
|
|
70
|
-
expect(result.isError).toBe(false);
|
|
71
|
-
expect(result.content[0].text).toContain('Navigated forward');
|
|
72
|
-
});
|
|
73
61
|
test('should handle navigation forward errors', async () => {
|
|
74
|
-
const args = {};
|
|
62
|
+
const args = { direction: 'forward' };
|
|
75
63
|
// Mock a navigation error
|
|
76
64
|
mockGoForward.mockImplementationOnce(() => Promise.reject(new Error('Navigation forward failed')));
|
|
77
|
-
const result = await
|
|
65
|
+
const result = await historyTool.execute(args, mockContext);
|
|
78
66
|
expect(mockGoForward).toHaveBeenCalled();
|
|
79
67
|
expect(result.isError).toBe(true);
|
|
80
68
|
expect(result.content[0].text).toContain('Operation failed');
|
|
81
69
|
});
|
|
82
70
|
test('should handle missing page', async () => {
|
|
83
|
-
const args = {};
|
|
84
|
-
const result = await
|
|
85
|
-
expect(
|
|
71
|
+
const args = { direction: 'back' };
|
|
72
|
+
const result = await historyTool.execute(args, { server: mockServer });
|
|
73
|
+
expect(mockGoBack).not.toHaveBeenCalled();
|
|
86
74
|
expect(result.isError).toBe(true);
|
|
87
75
|
expect(result.content[0].text).toContain('Browser page not initialized');
|
|
88
76
|
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { BrowserToolBase } from '../base.js';
|
|
2
|
+
import { ToolContext, ToolResponse, ToolMetadata, SessionConfig } from '../../common/types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Tool for navigating browser history (back/forward)
|
|
5
|
+
*/
|
|
6
|
+
export declare class GoHistoryTool extends BrowserToolBase {
|
|
7
|
+
static getMetadata(sessionConfig?: SessionConfig): ToolMetadata;
|
|
8
|
+
execute(args: any, context: ToolContext): Promise<ToolResponse>;
|
|
9
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { BrowserToolBase } from '../base.js';
|
|
2
|
+
import { createSuccessResponse, createErrorResponse } from '../../common/types.js';
|
|
3
|
+
import { gatherConsoleErrorsSince, quickNetworkIdleNote } from '../common/postAction.js';
|
|
4
|
+
/**
|
|
5
|
+
* Tool for navigating browser history (back/forward)
|
|
6
|
+
*/
|
|
7
|
+
export class GoHistoryTool extends BrowserToolBase {
|
|
8
|
+
static getMetadata(sessionConfig) {
|
|
9
|
+
return {
|
|
10
|
+
name: 'go_history',
|
|
11
|
+
description: "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.",
|
|
12
|
+
inputSchema: {
|
|
13
|
+
type: 'object',
|
|
14
|
+
properties: {
|
|
15
|
+
direction: {
|
|
16
|
+
type: 'string',
|
|
17
|
+
description: "History direction to navigate",
|
|
18
|
+
enum: ['back', 'forward']
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
required: ['direction'],
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
async execute(args, context) {
|
|
26
|
+
this.recordNavigation();
|
|
27
|
+
return this.safeExecute(context, async (page) => {
|
|
28
|
+
const dir = args.direction === 'forward' ? 'forward' : 'back';
|
|
29
|
+
// Capture initial state
|
|
30
|
+
let initialUrl = '';
|
|
31
|
+
let initialTitle = '';
|
|
32
|
+
try {
|
|
33
|
+
initialUrl = page.url();
|
|
34
|
+
}
|
|
35
|
+
catch { }
|
|
36
|
+
try {
|
|
37
|
+
initialTitle = await page.title();
|
|
38
|
+
}
|
|
39
|
+
catch { }
|
|
40
|
+
// Perform history navigation
|
|
41
|
+
if (dir === 'back') {
|
|
42
|
+
await page.goBack();
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
await page.goForward();
|
|
46
|
+
}
|
|
47
|
+
const verb = dir === 'back' ? 'back' : 'forward';
|
|
48
|
+
const lines = [`Navigated ${verb} in browser history`];
|
|
49
|
+
// Allow network to settle briefly first (best-effort)
|
|
50
|
+
try {
|
|
51
|
+
const note = await quickNetworkIdleNote(page);
|
|
52
|
+
if (note)
|
|
53
|
+
lines.push(note);
|
|
54
|
+
}
|
|
55
|
+
catch { }
|
|
56
|
+
// After the brief wait, surface console errors since navigation
|
|
57
|
+
try {
|
|
58
|
+
const errs = await gatherConsoleErrorsSince('navigation');
|
|
59
|
+
if (errs.length > 0) {
|
|
60
|
+
let titleInfo = '';
|
|
61
|
+
try {
|
|
62
|
+
const t = await page.title();
|
|
63
|
+
if (t)
|
|
64
|
+
titleInfo = `\nTitle: ${t}`;
|
|
65
|
+
}
|
|
66
|
+
catch { }
|
|
67
|
+
return createErrorResponse(`Console error after history navigation: ${errs[0]}${titleInfo}`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
catch { }
|
|
71
|
+
// Report new URL and Title explicitly
|
|
72
|
+
try {
|
|
73
|
+
const newUrl = page.url();
|
|
74
|
+
lines.push(`URL: ${newUrl}`);
|
|
75
|
+
}
|
|
76
|
+
catch { }
|
|
77
|
+
try {
|
|
78
|
+
const newTitle = await page.title();
|
|
79
|
+
if (newTitle)
|
|
80
|
+
lines.push(`Title: ${newTitle}`);
|
|
81
|
+
}
|
|
82
|
+
catch { }
|
|
83
|
+
return createSuccessResponse(lines);
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
export { NavigateTool } from './navigate.js';
|
|
2
|
-
export {
|
|
3
|
-
export { GoForwardTool } from './go_forward.js';
|
|
2
|
+
export { GoHistoryTool } from './history.js';
|
|
4
3
|
export { ScrollToElementTool } from './scroll_to_element.js';
|
|
5
4
|
export { ScrollByTool } from './scroll_by.js';
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
export { NavigateTool } from './navigate.js';
|
|
2
|
-
export {
|
|
3
|
-
export { GoForwardTool } from './go_forward.js';
|
|
2
|
+
export { GoHistoryTool } from './history.js';
|
|
4
3
|
export { ScrollToElementTool } from './scroll_to_element.js';
|
|
5
4
|
export { ScrollByTool } from './scroll_by.js';
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { BrowserToolBase } from '../base.js';
|
|
2
2
|
import { createSuccessResponse, createErrorResponse } from '../../common/types.js';
|
|
3
|
+
import { gatherConsoleErrorsSince, quickNetworkIdleNote } from '../common/postAction.js';
|
|
3
4
|
async function resetState() {
|
|
4
5
|
const { resetBrowserState } = await import('../../../toolHandler.js');
|
|
5
6
|
resetBrowserState();
|
|
@@ -105,7 +106,45 @@ export class NavigateTool extends BrowserToolBase {
|
|
|
105
106
|
catch {
|
|
106
107
|
// Best-effort detection; ignore and proceed
|
|
107
108
|
}
|
|
108
|
-
|
|
109
|
+
// First, perform a quick network-idle check to allow any errors to flush
|
|
110
|
+
// before we examine console errors. Keep it best-effort with small timeout.
|
|
111
|
+
const messages = [`Navigated to ${args.url}`];
|
|
112
|
+
try {
|
|
113
|
+
const note = await quickNetworkIdleNote(page);
|
|
114
|
+
if (note)
|
|
115
|
+
messages.push(note);
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
// Ignore failures in the quick check
|
|
119
|
+
}
|
|
120
|
+
// After waiting briefly, surface hard page errors if any
|
|
121
|
+
try {
|
|
122
|
+
const errs = await gatherConsoleErrorsSince('navigation');
|
|
123
|
+
if (errs.length > 0) {
|
|
124
|
+
// Include page title (best-effort) to aid debugging
|
|
125
|
+
let titleInfo = '';
|
|
126
|
+
try {
|
|
127
|
+
const t = await page.title();
|
|
128
|
+
if (t)
|
|
129
|
+
titleInfo = `\nTitle: ${t}`;
|
|
130
|
+
}
|
|
131
|
+
catch { }
|
|
132
|
+
return createErrorResponse(`Console error after navigation: ${errs[0]}${titleInfo}`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
// If log retrieval fails, continue normally
|
|
137
|
+
}
|
|
138
|
+
// Add page title to help the agent orient itself
|
|
139
|
+
try {
|
|
140
|
+
const title = await page.title();
|
|
141
|
+
if (title)
|
|
142
|
+
messages.push(`Title: ${title}`);
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
// Ignore title failures
|
|
146
|
+
}
|
|
147
|
+
return createSuccessResponse(messages);
|
|
109
148
|
}
|
|
110
149
|
catch (error) {
|
|
111
150
|
const errorMessage = error.message;
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
// Navigation
|
|
2
2
|
import { NavigateTool } from './navigation/navigate.js';
|
|
3
|
-
import {
|
|
4
|
-
import { GoForwardTool } from './navigation/go_forward.js';
|
|
3
|
+
import { GoHistoryTool } from './navigation/history.js';
|
|
5
4
|
import { ScrollToElementTool } from './navigation/scroll_to_element.js';
|
|
6
5
|
import { ScrollByTool } from './navigation/scroll_by.js';
|
|
7
6
|
// Lifecycle
|
|
@@ -43,8 +42,7 @@ import { WaitForNetworkIdleTool } from './waiting/wait_for_network_idle.js';
|
|
|
43
42
|
export const BROWSER_TOOL_CLASSES = [
|
|
44
43
|
// Navigation (5)
|
|
45
44
|
NavigateTool,
|
|
46
|
-
|
|
47
|
-
GoForwardTool,
|
|
45
|
+
GoHistoryTool,
|
|
48
46
|
ScrollToElementTool,
|
|
49
47
|
ScrollByTool,
|
|
50
48
|
// Lifecycle (2)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mcp-web-inspector",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.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",
|
|
@@ -28,14 +28,13 @@
|
|
|
28
28
|
"url": "https://github.com/antonzherdev/mcp-web-inspector.git"
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@modelcontextprotocol/sdk": "1.
|
|
32
|
-
"@playwright/browser-chromium": "1.
|
|
33
|
-
"@playwright/browser-firefox": "1.
|
|
34
|
-
"@playwright/browser-webkit": "1.
|
|
35
|
-
"@playwright/test": "^1.
|
|
36
|
-
"
|
|
37
|
-
"
|
|
38
|
-
"uuid": "11.1.0"
|
|
31
|
+
"@modelcontextprotocol/sdk": "1.21.0",
|
|
32
|
+
"@playwright/browser-chromium": "1.56.1",
|
|
33
|
+
"@playwright/browser-firefox": "1.56.1",
|
|
34
|
+
"@playwright/browser-webkit": "1.56.1",
|
|
35
|
+
"@playwright/test": "^1.56.1",
|
|
36
|
+
"playwright": "1.56.1",
|
|
37
|
+
"uuid": "13.0.0"
|
|
39
38
|
},
|
|
40
39
|
"keywords": [
|
|
41
40
|
"mcp",
|
|
@@ -51,11 +50,11 @@
|
|
|
51
50
|
],
|
|
52
51
|
"devDependencies": {
|
|
53
52
|
"@types/jest": "^29.5.14",
|
|
54
|
-
"@types/node": "^
|
|
53
|
+
"@types/node": "^24.10.0",
|
|
55
54
|
"jest": "^29.7.0",
|
|
56
55
|
"jest-playwright-preset": "4.0.0",
|
|
57
|
-
"shx": "^0.
|
|
58
|
-
"ts-jest": "^29.
|
|
59
|
-
"typescript": "^5.
|
|
56
|
+
"shx": "^0.4.0",
|
|
57
|
+
"ts-jest": "^29.4.5",
|
|
58
|
+
"typescript": "^5.9.3"
|
|
60
59
|
}
|
|
61
60
|
}
|