mcp-web-inspector 0.3.0 → 0.4.1
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/dist/toolHandler.d.ts +3 -0
- package/dist/toolHandler.js +57 -0
- package/dist/tools/browser/base.d.ts +5 -2
- package/dist/tools/browser/base.js +45 -4
- package/dist/tools/browser/content/__tests__/visiblePage.test.js +107 -3
- package/dist/tools/browser/content/get_html.d.ts +1 -0
- package/dist/tools/browser/content/get_html.js +46 -1
- package/dist/tools/browser/content/get_text.js +1 -0
- package/dist/tools/browser/content/screenshot.js +6 -2
- package/dist/tools/browser/inspection/check_visibility.js +1 -0
- package/dist/tools/browser/inspection/get_computed_styles.js +1 -0
- package/dist/tools/browser/inspection/inspect_ancestors.js +3 -1
- package/dist/tools/browser/inspection/inspect_dom.js +3 -1
- package/dist/tools/browser/inspection/measure_element.js +1 -0
- package/dist/tools/browser/lifecycle/index.d.ts +1 -0
- package/dist/tools/browser/lifecycle/index.js +1 -0
- package/dist/tools/browser/lifecycle/set_color_scheme.d.ts +9 -0
- package/dist/tools/browser/lifecycle/set_color_scheme.js +52 -0
- package/dist/tools/browser/register.js +4 -1
- package/package.json +1 -1
package/dist/toolHandler.d.ts
CHANGED
|
@@ -19,6 +19,7 @@ export interface NetworkRequest {
|
|
|
19
19
|
body: string | null;
|
|
20
20
|
};
|
|
21
21
|
}
|
|
22
|
+
type ColorSchemeOverride = 'light' | 'dark' | 'no-preference';
|
|
22
23
|
/**
|
|
23
24
|
* Sets the session configuration
|
|
24
25
|
*/
|
|
@@ -49,6 +50,8 @@ export declare function clearNetworkLog(): void;
|
|
|
49
50
|
* @param newPage The Page object to set as the global page
|
|
50
51
|
*/
|
|
51
52
|
export declare function setGlobalPage(newPage: Page): Promise<void>;
|
|
53
|
+
export declare function setColorSchemeOverride(scheme: ColorSchemeOverride | null): Promise<void>;
|
|
54
|
+
export declare function getColorSchemeOverride(): ColorSchemeOverride | null;
|
|
52
55
|
interface BrowserSettings {
|
|
53
56
|
viewport?: {
|
|
54
57
|
width?: number;
|
package/dist/toolHandler.js
CHANGED
|
@@ -5,6 +5,7 @@ import { getToolInstance, isBrowserTool, executeTool } from './tools/common/regi
|
|
|
5
5
|
let browser;
|
|
6
6
|
let page;
|
|
7
7
|
let currentBrowserType = 'chromium';
|
|
8
|
+
let currentDevice;
|
|
8
9
|
let networkLog = [];
|
|
9
10
|
let sessionConfig = {
|
|
10
11
|
saveSession: false,
|
|
@@ -12,6 +13,7 @@ let sessionConfig = {
|
|
|
12
13
|
screenshotsDir: './.mcp-web-inspector/screenshots',
|
|
13
14
|
headlessDefault: false,
|
|
14
15
|
};
|
|
16
|
+
let colorSchemeOverride = null;
|
|
15
17
|
/**
|
|
16
18
|
* Sets the session configuration
|
|
17
19
|
*/
|
|
@@ -38,6 +40,7 @@ export function resetBrowserState() {
|
|
|
38
40
|
browser = undefined;
|
|
39
41
|
page = undefined;
|
|
40
42
|
currentBrowserType = 'chromium';
|
|
43
|
+
currentDevice = undefined;
|
|
41
44
|
networkLog = [];
|
|
42
45
|
}
|
|
43
46
|
/**
|
|
@@ -61,9 +64,34 @@ export async function setGlobalPage(newPage) {
|
|
|
61
64
|
// Register console message handlers and network listeners for the new page
|
|
62
65
|
await registerConsoleMessage(page);
|
|
63
66
|
await registerNetworkListeners(page);
|
|
67
|
+
await applyColorScheme(page);
|
|
64
68
|
page.bringToFront(); // Bring the new tab to the front
|
|
65
69
|
console.log("Global page has been updated with listeners registered.");
|
|
66
70
|
}
|
|
71
|
+
function getColorSchemeValue() {
|
|
72
|
+
return colorSchemeOverride;
|
|
73
|
+
}
|
|
74
|
+
async function applyColorScheme(targetPage) {
|
|
75
|
+
if (!targetPage) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
try {
|
|
79
|
+
const scheme = getColorSchemeValue();
|
|
80
|
+
await targetPage.emulateMedia({ colorScheme: scheme ?? null });
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
console.error("Failed to apply color scheme:", error);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
export async function setColorSchemeOverride(scheme) {
|
|
87
|
+
colorSchemeOverride = scheme;
|
|
88
|
+
if (page && !page.isClosed()) {
|
|
89
|
+
await applyColorScheme(page);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
export function getColorSchemeOverride() {
|
|
93
|
+
return colorSchemeOverride;
|
|
94
|
+
}
|
|
67
95
|
/**
|
|
68
96
|
* Device preset mapping to Playwright device descriptors
|
|
69
97
|
*/
|
|
@@ -282,6 +310,17 @@ export async function ensureBrowser(browserSettings) {
|
|
|
282
310
|
// Reset browser and page references
|
|
283
311
|
resetBrowserState();
|
|
284
312
|
}
|
|
313
|
+
// Check if device preset has changed (requires browser restart)
|
|
314
|
+
if (browser && browserSettings?.device && browserSettings.device !== currentDevice) {
|
|
315
|
+
console.error(`Device preset changed from ${currentDevice || 'none'} to ${browserSettings.device}. Restarting browser...`);
|
|
316
|
+
try {
|
|
317
|
+
await browser.close().catch(err => console.error("Error closing browser on device change:", err));
|
|
318
|
+
}
|
|
319
|
+
catch (e) {
|
|
320
|
+
// Ignore errors when closing browser
|
|
321
|
+
}
|
|
322
|
+
resetBrowserState();
|
|
323
|
+
}
|
|
285
324
|
// If browser exists and viewport settings changed, resize the viewport
|
|
286
325
|
if (browser && page && !page.isClosed() && browserSettings?.viewport) {
|
|
287
326
|
const { width, height } = browserSettings.viewport;
|
|
@@ -318,11 +357,16 @@ export async function ensureBrowser(browserSettings) {
|
|
|
318
357
|
deviceConfig = CUSTOM_DEVICE_CONFIGS[playwrightDeviceName] || devices[playwrightDeviceName];
|
|
319
358
|
if (deviceConfig) {
|
|
320
359
|
console.error(`Using device preset: ${device} (${playwrightDeviceName})`);
|
|
360
|
+
currentDevice = device;
|
|
321
361
|
}
|
|
322
362
|
else {
|
|
323
363
|
console.error(`Warning: Device preset ${playwrightDeviceName} not found`);
|
|
364
|
+
currentDevice = undefined;
|
|
324
365
|
}
|
|
325
366
|
}
|
|
367
|
+
else {
|
|
368
|
+
currentDevice = undefined;
|
|
369
|
+
}
|
|
326
370
|
console.error(`Launching new ${browserType} browser instance...`);
|
|
327
371
|
// Use the appropriate browser engine
|
|
328
372
|
let browserInstance;
|
|
@@ -425,6 +469,7 @@ export async function ensureBrowser(browserSettings) {
|
|
|
425
469
|
// Register console message handler and network listeners
|
|
426
470
|
await registerConsoleMessage(page);
|
|
427
471
|
await registerNetworkListeners(page);
|
|
472
|
+
await applyColorScheme(page);
|
|
428
473
|
}
|
|
429
474
|
// Verify page is still valid
|
|
430
475
|
if (!page || page.isClosed()) {
|
|
@@ -435,7 +480,9 @@ export async function ensureBrowser(browserSettings) {
|
|
|
435
480
|
// Re-register console message handler and network listeners
|
|
436
481
|
await registerConsoleMessage(page);
|
|
437
482
|
await registerNetworkListeners(page);
|
|
483
|
+
await applyColorScheme(page);
|
|
438
484
|
}
|
|
485
|
+
await applyColorScheme(page);
|
|
439
486
|
return page;
|
|
440
487
|
}
|
|
441
488
|
catch (error) {
|
|
@@ -458,6 +505,15 @@ export async function ensureBrowser(browserSettings) {
|
|
|
458
505
|
const playwrightDeviceName = DEVICE_PRESETS[device];
|
|
459
506
|
// Check custom configs first, then Playwright's built-in devices
|
|
460
507
|
deviceConfig = CUSTOM_DEVICE_CONFIGS[playwrightDeviceName] || devices[playwrightDeviceName];
|
|
508
|
+
if (deviceConfig) {
|
|
509
|
+
currentDevice = device;
|
|
510
|
+
}
|
|
511
|
+
else {
|
|
512
|
+
currentDevice = undefined;
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
else {
|
|
516
|
+
currentDevice = undefined;
|
|
461
517
|
}
|
|
462
518
|
// Use the appropriate browser engine
|
|
463
519
|
let browserInstance;
|
|
@@ -552,6 +608,7 @@ export async function ensureBrowser(browserSettings) {
|
|
|
552
608
|
}
|
|
553
609
|
await registerConsoleMessage(page);
|
|
554
610
|
await registerNetworkListeners(page);
|
|
611
|
+
await applyColorScheme(page);
|
|
555
612
|
return page;
|
|
556
613
|
}
|
|
557
614
|
}
|
|
@@ -12,11 +12,14 @@ export declare abstract class BrowserToolBase implements ToolHandler {
|
|
|
12
12
|
*/
|
|
13
13
|
abstract execute(args: any, context: ToolContext): Promise<ToolResponse>;
|
|
14
14
|
/**
|
|
15
|
-
* Normalize selector shortcuts to full Playwright selectors
|
|
15
|
+
* Normalize selector shortcuts to full Playwright selectors and clean common escaping mistakes
|
|
16
16
|
* - "testid:foo" → "[data-testid='foo']"
|
|
17
17
|
* - "data-test:bar" → "[data-test='bar']"
|
|
18
18
|
* - "data-cy:baz" → "[data-cy='baz']"
|
|
19
|
-
* -
|
|
19
|
+
* - Removes unnecessary backslash escapes from CSS selectors that LLMs often add
|
|
20
|
+
* - ".dark\\:bg-gray-700" → ".dark:bg-gray-700"
|
|
21
|
+
* - ".top-\\[36px\\]" → ".top-[36px]"
|
|
22
|
+
* - ".top-\\\\[36px\\\\]" → ".top-[36px]" (double-escaped)
|
|
20
23
|
* @param selector The selector string
|
|
21
24
|
* @returns Normalized selector
|
|
22
25
|
*/
|
|
@@ -8,11 +8,14 @@ export class BrowserToolBase {
|
|
|
8
8
|
this.server = server;
|
|
9
9
|
}
|
|
10
10
|
/**
|
|
11
|
-
* Normalize selector shortcuts to full Playwright selectors
|
|
11
|
+
* Normalize selector shortcuts to full Playwright selectors and clean common escaping mistakes
|
|
12
12
|
* - "testid:foo" → "[data-testid='foo']"
|
|
13
13
|
* - "data-test:bar" → "[data-test='bar']"
|
|
14
14
|
* - "data-cy:baz" → "[data-cy='baz']"
|
|
15
|
-
* -
|
|
15
|
+
* - Removes unnecessary backslash escapes from CSS selectors that LLMs often add
|
|
16
|
+
* - ".dark\\:bg-gray-700" → ".dark:bg-gray-700"
|
|
17
|
+
* - ".top-\\[36px\\]" → ".top-[36px]"
|
|
18
|
+
* - ".top-\\\\[36px\\\\]" → ".top-[36px]" (double-escaped)
|
|
16
19
|
* @param selector The selector string
|
|
17
20
|
* @returns Normalized selector
|
|
18
21
|
*/
|
|
@@ -22,13 +25,22 @@ export class BrowserToolBase {
|
|
|
22
25
|
'data-test:': 'data-test',
|
|
23
26
|
'data-cy:': 'data-cy',
|
|
24
27
|
};
|
|
28
|
+
// Handle testid shortcuts first
|
|
25
29
|
for (const [prefix, attr] of Object.entries(prefixMap)) {
|
|
26
30
|
if (selector.startsWith(prefix)) {
|
|
27
31
|
const value = selector.slice(prefix.length);
|
|
28
32
|
return `[${attr}="${value}"]`;
|
|
29
33
|
}
|
|
30
34
|
}
|
|
31
|
-
|
|
35
|
+
// Clean up common escaping mistakes that LLMs make in CSS selectors
|
|
36
|
+
// These characters don't need to be escaped in CSS selectors: [ ] :
|
|
37
|
+
let cleaned = selector;
|
|
38
|
+
// Remove backslash escapes before brackets and colons
|
|
39
|
+
// Handle both single escapes (\[) and double escapes (\\[)
|
|
40
|
+
cleaned = cleaned.replace(/\\+\[/g, '[');
|
|
41
|
+
cleaned = cleaned.replace(/\\+\]/g, ']');
|
|
42
|
+
cleaned = cleaned.replace(/\\+:/g, ':');
|
|
43
|
+
return cleaned;
|
|
32
44
|
}
|
|
33
45
|
/**
|
|
34
46
|
* Ensures a page is available and returns it
|
|
@@ -140,7 +152,36 @@ export class BrowserToolBase {
|
|
|
140
152
|
* @returns Object with { element: Locator, elementIndex: number, totalCount: number }
|
|
141
153
|
*/
|
|
142
154
|
async selectPreferredLocator(locator, options) {
|
|
143
|
-
|
|
155
|
+
let count;
|
|
156
|
+
try {
|
|
157
|
+
count = await locator.count();
|
|
158
|
+
}
|
|
159
|
+
catch (error) {
|
|
160
|
+
// Catch selector syntax errors and provide helpful guidance
|
|
161
|
+
const errorMsg = error.message;
|
|
162
|
+
const selector = options?.originalSelector || 'selector';
|
|
163
|
+
if (errorMsg.includes('Unexpected token') ||
|
|
164
|
+
errorMsg.includes('Invalid selector') ||
|
|
165
|
+
errorMsg.includes('SyntaxError') ||
|
|
166
|
+
errorMsg.includes('selector')) {
|
|
167
|
+
throw new Error(`Invalid CSS selector: "${selector}"\n\n` +
|
|
168
|
+
`Selector syntax error: ${errorMsg}\n\n` +
|
|
169
|
+
`💡 Tips:\n` +
|
|
170
|
+
` • CSS selectors don't need backslash escapes for [ ] or :\n` +
|
|
171
|
+
` • Use .class-name or #id without escaping\n` +
|
|
172
|
+
` • For data attributes, use [data-attr="value"]\n` +
|
|
173
|
+
` • For testid shortcuts, use testid:name\n\n` +
|
|
174
|
+
`Examples:\n` +
|
|
175
|
+
` ✓ .dark:bg-gray-700\n` +
|
|
176
|
+
` ✓ .top-[36px]\n` +
|
|
177
|
+
` ✓ testid:submit-button\n` +
|
|
178
|
+
` ✓ #login-form\n` +
|
|
179
|
+
` ✗ .dark\\:bg-gray-700 (unnecessary escape)\n` +
|
|
180
|
+
` ✗ .top-\\[36px\\] (unnecessary escape)`);
|
|
181
|
+
}
|
|
182
|
+
// Re-throw other errors as-is
|
|
183
|
+
throw error;
|
|
184
|
+
}
|
|
144
185
|
if (count === 0) {
|
|
145
186
|
throw new Error('No elements found');
|
|
146
187
|
}
|
|
@@ -300,10 +300,11 @@ describe('GetHtmlTool', () => {
|
|
|
300
300
|
expect(result.isError).toBe(false);
|
|
301
301
|
expect(result.content[0].text).toContain('HTML content');
|
|
302
302
|
});
|
|
303
|
-
test('should respect maxLength parameter', async () => {
|
|
303
|
+
test('should respect maxLength parameter for small HTML', async () => {
|
|
304
304
|
const args = { maxLength: 50 };
|
|
305
|
-
|
|
306
|
-
|
|
305
|
+
// Small HTML that won't trigger preview threshold
|
|
306
|
+
const smallHtml = '<div>' + 'A'.repeat(100) + '</div>';
|
|
307
|
+
mockEvaluate.mockImplementationOnce(() => Promise.resolve(smallHtml));
|
|
307
308
|
const result = await visibleHtmlTool.execute(args, mockContext);
|
|
308
309
|
expect(result.isError).toBe(false);
|
|
309
310
|
expect(result.content[0].text).toContain('Output truncated due to size limits');
|
|
@@ -378,4 +379,107 @@ describe('GetHtmlTool', () => {
|
|
|
378
379
|
expect(result.content[0].text).toContain('Failed to get visible HTML content');
|
|
379
380
|
expect(result.content[0].text).toContain('Content retrieval failed');
|
|
380
381
|
});
|
|
382
|
+
test('should return preview with token for large HTML (≥2000 chars)', async () => {
|
|
383
|
+
const args = {};
|
|
384
|
+
// Large HTML that exceeds preview threshold
|
|
385
|
+
const largeHtml = '<div>' + 'X'.repeat(3000) + '</div>';
|
|
386
|
+
mockEvaluate.mockImplementationOnce(() => Promise.resolve(largeHtml));
|
|
387
|
+
const result = await visibleHtmlTool.execute(args, mockContext);
|
|
388
|
+
expect(result.isError).toBe(false);
|
|
389
|
+
expect(result.content[0].text).toContain('HTML size:');
|
|
390
|
+
expect(result.content[0].text).toContain('exceeds 2000 char threshold');
|
|
391
|
+
expect(result.content[0].text).toContain('Preview (first 500 chars)');
|
|
392
|
+
expect(result.content[0].text).toContain('⚠️ Full HTML not returned to save tokens');
|
|
393
|
+
expect(result.content[0].text).toContain('💡 RECOMMENDED: Use token-efficient alternatives');
|
|
394
|
+
expect(result.content[0].text).toContain('inspect_dom()');
|
|
395
|
+
expect(result.content[0].text).toMatch(/confirmToken: "[\w\d]{8}"/);
|
|
396
|
+
});
|
|
397
|
+
test('should return full HTML with valid confirmToken', async () => {
|
|
398
|
+
const args = {};
|
|
399
|
+
// Large HTML
|
|
400
|
+
const largeHtml = '<div>' + 'Y'.repeat(3000) + '</div>';
|
|
401
|
+
mockEvaluate.mockImplementation(() => Promise.resolve(largeHtml));
|
|
402
|
+
// First call - get preview and token
|
|
403
|
+
const previewResult = await visibleHtmlTool.execute(args, mockContext);
|
|
404
|
+
expect(previewResult.isError).toBe(false);
|
|
405
|
+
// Extract token from preview
|
|
406
|
+
const tokenMatch = previewResult.content[0].text.match(/confirmToken: "([\w\d]{8})"/);
|
|
407
|
+
expect(tokenMatch).toBeTruthy();
|
|
408
|
+
const token = tokenMatch[1];
|
|
409
|
+
// Second call - with token
|
|
410
|
+
const fullResult = await visibleHtmlTool.execute({ confirmToken: token }, mockContext);
|
|
411
|
+
expect(fullResult.isError).toBe(false);
|
|
412
|
+
expect(fullResult.content[0].text).toContain(largeHtml);
|
|
413
|
+
expect(fullResult.content[0].text).not.toContain('Preview (first 500 chars)');
|
|
414
|
+
expect(fullResult.content[0].text).toContain('💡 TIP: If you need structured inspection');
|
|
415
|
+
});
|
|
416
|
+
test('should generate new token for invalid confirmToken', async () => {
|
|
417
|
+
const args = { confirmToken: 'invalid123' };
|
|
418
|
+
// Large HTML
|
|
419
|
+
const largeHtml = '<div>' + 'Z'.repeat(3000) + '</div>';
|
|
420
|
+
mockEvaluate.mockImplementationOnce(() => Promise.resolve(largeHtml));
|
|
421
|
+
const result = await visibleHtmlTool.execute(args, mockContext);
|
|
422
|
+
expect(result.isError).toBe(false);
|
|
423
|
+
expect(result.content[0].text).toContain('HTML size:');
|
|
424
|
+
expect(result.content[0].text).toContain('Preview (first 500 chars)');
|
|
425
|
+
expect(result.content[0].text).toMatch(/confirmToken: "[\w\d]{8}"/);
|
|
426
|
+
// Should not return full HTML
|
|
427
|
+
expect(result.content[0].text).not.toContain('Z'.repeat(3000));
|
|
428
|
+
});
|
|
429
|
+
test('should return small HTML directly without token requirement', async () => {
|
|
430
|
+
const args = {};
|
|
431
|
+
// Small HTML below threshold
|
|
432
|
+
const smallHtml = '<div>Small content with 1500 chars: ' + 'A'.repeat(1400) + '</div>';
|
|
433
|
+
mockEvaluate.mockImplementationOnce(() => Promise.resolve(smallHtml));
|
|
434
|
+
const result = await visibleHtmlTool.execute(args, mockContext);
|
|
435
|
+
expect(result.isError).toBe(false);
|
|
436
|
+
expect(result.content[0].text).toContain(smallHtml);
|
|
437
|
+
expect(result.content[0].text).not.toContain('confirmToken:');
|
|
438
|
+
expect(result.content[0].text).not.toContain('Preview (first 500 chars)');
|
|
439
|
+
expect(result.content[0].text).toContain('💡 TIP: If you need structured inspection');
|
|
440
|
+
});
|
|
441
|
+
test('should consume token only once (one-time use)', async () => {
|
|
442
|
+
const args = {};
|
|
443
|
+
// Large HTML
|
|
444
|
+
const largeHtml = '<div>' + 'M'.repeat(3000) + '</div>';
|
|
445
|
+
mockEvaluate.mockImplementation(() => Promise.resolve(largeHtml));
|
|
446
|
+
// First call - get token
|
|
447
|
+
const previewResult = await visibleHtmlTool.execute(args, mockContext);
|
|
448
|
+
const tokenMatch = previewResult.content[0].text.match(/confirmToken: "([\w\d]{8})"/);
|
|
449
|
+
const token = tokenMatch[1];
|
|
450
|
+
// Second call - use token (should work)
|
|
451
|
+
const fullResult = await visibleHtmlTool.execute({ confirmToken: token }, mockContext);
|
|
452
|
+
expect(fullResult.isError).toBe(false);
|
|
453
|
+
expect(fullResult.content[0].text).toContain(largeHtml);
|
|
454
|
+
// Third call - try to reuse same token (should fail, get new preview)
|
|
455
|
+
const retryResult = await visibleHtmlTool.execute({ confirmToken: token }, mockContext);
|
|
456
|
+
expect(retryResult.isError).toBe(false);
|
|
457
|
+
expect(retryResult.content[0].text).toContain('Preview (first 500 chars)');
|
|
458
|
+
expect(retryResult.content[0].text).toMatch(/confirmToken: "[\w\d]{8}"/);
|
|
459
|
+
// Should have different token
|
|
460
|
+
const newTokenMatch = retryResult.content[0].text.match(/confirmToken: "([\w\d]{8})"/);
|
|
461
|
+
expect(newTokenMatch[1]).not.toBe(token);
|
|
462
|
+
});
|
|
463
|
+
test('should handle large HTML with selector and confirmToken', async () => {
|
|
464
|
+
const largeHtml = '<section>' + 'S'.repeat(3000) + '</section>';
|
|
465
|
+
const args = { selector: 'testid:large-section' };
|
|
466
|
+
mockLocator.mockImplementation(() => ({}));
|
|
467
|
+
const mockElement = {
|
|
468
|
+
evaluate: jest.fn(async () => largeHtml),
|
|
469
|
+
};
|
|
470
|
+
const selectSpy = jest
|
|
471
|
+
.spyOn(visibleHtmlTool, 'selectPreferredLocator')
|
|
472
|
+
.mockResolvedValue({ element: mockElement, elementIndex: 0, totalCount: 1 });
|
|
473
|
+
mockEvaluate.mockImplementation(() => Promise.resolve(largeHtml));
|
|
474
|
+
// First call - get preview
|
|
475
|
+
const previewResult = await visibleHtmlTool.execute(args, mockContext);
|
|
476
|
+
expect(previewResult.content[0].text).toContain('(from "testid:large-section")');
|
|
477
|
+
const tokenMatch = previewResult.content[0].text.match(/confirmToken: "([\w\d]{8})"/);
|
|
478
|
+
const token = tokenMatch[1];
|
|
479
|
+
// Second call - with token
|
|
480
|
+
const fullResult = await visibleHtmlTool.execute({ selector: 'testid:large-section', confirmToken: token }, mockContext);
|
|
481
|
+
expect(fullResult.isError).toBe(false);
|
|
482
|
+
expect(fullResult.content[0].text).toContain(largeHtml);
|
|
483
|
+
selectSpy.mockRestore();
|
|
484
|
+
});
|
|
381
485
|
});
|
|
@@ -4,6 +4,7 @@ import { ToolContext, ToolResponse, ToolMetadata, SessionConfig } from '../../co
|
|
|
4
4
|
* Tool for getting HTML from the page
|
|
5
5
|
*/
|
|
6
6
|
export declare class GetHtmlTool extends BrowserToolBase {
|
|
7
|
+
private confirmTokens;
|
|
7
8
|
static getMetadata(sessionConfig?: SessionConfig): ToolMetadata;
|
|
8
9
|
execute(args: any, context: ToolContext): Promise<ToolResponse>;
|
|
9
10
|
}
|
|
@@ -4,10 +4,14 @@ import { createSuccessResponse, createErrorResponse, } from '../../common/types.
|
|
|
4
4
|
* Tool for getting HTML from the page
|
|
5
5
|
*/
|
|
6
6
|
export class GetHtmlTool extends BrowserToolBase {
|
|
7
|
+
constructor() {
|
|
8
|
+
super(...arguments);
|
|
9
|
+
this.confirmTokens = new Map(); // Stores tokens for two-step confirmation
|
|
10
|
+
}
|
|
7
11
|
static getMetadata(sessionConfig) {
|
|
8
12
|
return {
|
|
9
13
|
name: "get_html",
|
|
10
|
-
description: "⚠️ RARELY NEEDED: Get raw HTML markup from the page (no rendering, just source code). Most tasks need structured inspection instead. ONLY use get_html for: (1) checking specific HTML attributes or element nesting, (2) analyzing markup structure, (3) debugging SSR/HTML issues. For structured tasks, use: inspect_dom() to understand page structure with positions, query_selector() to find and inspect elements, get_computed_styles() for CSS values.
|
|
14
|
+
description: "⚠️ RARELY NEEDED: Get raw HTML markup from the page (no rendering, just source code). Most tasks need structured inspection instead. ONLY use get_html for: (1) checking specific HTML attributes or element nesting, (2) analyzing markup structure, (3) debugging SSR/HTML issues. For structured tasks, use: inspect_dom() to understand page structure with positions, query_selector() to find and inspect elements, get_computed_styles() for CSS values. Auto-returns HTML if <2000 chars (small elements), shows preview with token-based confirmation if larger. Scripts removed by default for security/size. Supports testid shortcuts.",
|
|
11
15
|
inputSchema: {
|
|
12
16
|
type: "object",
|
|
13
17
|
properties: {
|
|
@@ -26,6 +30,10 @@ export class GetHtmlTool extends BrowserToolBase {
|
|
|
26
30
|
maxLength: {
|
|
27
31
|
type: "number",
|
|
28
32
|
description: "Maximum number of characters to return (default: 20000)"
|
|
33
|
+
},
|
|
34
|
+
confirmToken: {
|
|
35
|
+
type: "string",
|
|
36
|
+
description: "Confirmation token from preview response (required to retrieve large HTML). Get this token by calling without confirmToken first - the preview will include the token to use."
|
|
29
37
|
}
|
|
30
38
|
},
|
|
31
39
|
required: [],
|
|
@@ -37,6 +45,8 @@ export class GetHtmlTool extends BrowserToolBase {
|
|
|
37
45
|
? Math.floor(args.maxLength)
|
|
38
46
|
: 20000;
|
|
39
47
|
const clean = args.clean ?? false;
|
|
48
|
+
const confirmToken = args.confirmToken;
|
|
49
|
+
const PREVIEW_THRESHOLD = 2000;
|
|
40
50
|
if (!context.page) {
|
|
41
51
|
return createErrorResponse('Page is not available');
|
|
42
52
|
}
|
|
@@ -58,6 +68,7 @@ export class GetHtmlTool extends BrowserToolBase {
|
|
|
58
68
|
const locator = page.locator(normalizedSelector);
|
|
59
69
|
const { element, elementIndex, totalCount } = await this.selectPreferredLocator(locator, {
|
|
60
70
|
elementIndex: args.elementIndex,
|
|
71
|
+
originalSelector: args.selector,
|
|
61
72
|
});
|
|
62
73
|
selectionWarning = this.formatElementSelectionInfo(args.selector, elementIndex, totalCount, true);
|
|
63
74
|
rawHtml = await element.evaluate((target) => {
|
|
@@ -99,6 +110,40 @@ export class GetHtmlTool extends BrowserToolBase {
|
|
|
99
110
|
const safeMaxLength = requestedMaxLength > 0 ? requestedMaxLength : 20000;
|
|
100
111
|
const processedHtml = sanitizedHtml ?? '';
|
|
101
112
|
const originalLength = processedHtml.length;
|
|
113
|
+
// Generate key for this HTML request
|
|
114
|
+
const tokenKey = `${args.selector || 'page'}:${originalLength}`;
|
|
115
|
+
// Check if HTML is too large
|
|
116
|
+
if (originalLength >= PREVIEW_THRESHOLD) {
|
|
117
|
+
// Verify if confirmToken matches
|
|
118
|
+
const expectedToken = this.confirmTokens.get(tokenKey);
|
|
119
|
+
if (confirmToken && expectedToken && confirmToken === expectedToken) {
|
|
120
|
+
// Valid token - delete it (one-time use) and proceed to return HTML
|
|
121
|
+
this.confirmTokens.delete(tokenKey);
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
// No token or invalid token - show preview and generate new token
|
|
125
|
+
const newToken = Math.random().toString(36).substring(2, 10);
|
|
126
|
+
this.confirmTokens.set(tokenKey, newToken);
|
|
127
|
+
lines.push(`HTML size: ${originalLength.toLocaleString()} characters (exceeds ${PREVIEW_THRESHOLD} char threshold)`);
|
|
128
|
+
lines.push('');
|
|
129
|
+
lines.push('Preview (first 500 chars):');
|
|
130
|
+
lines.push(processedHtml.slice(0, 500));
|
|
131
|
+
if (originalLength > 500) {
|
|
132
|
+
lines.push('...');
|
|
133
|
+
}
|
|
134
|
+
lines.push('');
|
|
135
|
+
lines.push('⚠️ Full HTML not returned to save tokens (~' + Math.round(originalLength / 3) + ' tokens)');
|
|
136
|
+
lines.push('');
|
|
137
|
+
lines.push('💡 RECOMMENDED: Use token-efficient alternatives:');
|
|
138
|
+
lines.push(' • inspect_dom() - structured view with positions and layout');
|
|
139
|
+
lines.push(' • query_selector_all() - find specific elements');
|
|
140
|
+
lines.push(' • get_computed_styles() - CSS values for debugging');
|
|
141
|
+
lines.push('');
|
|
142
|
+
lines.push(`To get full HTML anyway, call again with: confirmToken: "${newToken}"`);
|
|
143
|
+
return createSuccessResponse(lines.join('\n'));
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
// Return full HTML (either small or explicitly requested)
|
|
102
147
|
let displayHtml = processedHtml;
|
|
103
148
|
const truncated = displayHtml.length > safeMaxLength;
|
|
104
149
|
if (truncated) {
|
|
@@ -53,6 +53,7 @@ export class GetTextTool extends BrowserToolBase {
|
|
|
53
53
|
const locator = page.locator(normalizedSelector);
|
|
54
54
|
const { element, elementIndex, totalCount } = await this.selectPreferredLocator(locator, {
|
|
55
55
|
elementIndex: args.elementIndex,
|
|
56
|
+
originalSelector: args.selector,
|
|
56
57
|
});
|
|
57
58
|
selectionWarning = this.formatElementSelectionInfo(args.selector, elementIndex, totalCount, true);
|
|
58
59
|
textContent = await element.evaluate((target) => {
|
|
@@ -14,7 +14,7 @@ export class ScreenshotTool extends BrowserToolBase {
|
|
|
14
14
|
const screenshotsDir = sessionConfig?.screenshotsDir || './.mcp-web-inspector/screenshots';
|
|
15
15
|
return {
|
|
16
16
|
name: "screenshot",
|
|
17
|
-
description:
|
|
17
|
+
description: `📸 VISUAL OUTPUT TOOL - Captures page/element appearance and saves to file. Essential for: visual regression testing, sharing with humans, confirming UI appearance (colors/fonts/images). ⚠️ NOT for layout debugging (positions/sizes/alignment/margins) - use inspect_dom/compare_positions/inspect_ancestors/get_computed_styles instead (structural data is token-efficient, screenshots require ~1,500 tokens to read). Screenshots saved to ${screenshotsDir}. Example: { name: "login-page", fullPage: true } or { name: "submit-btn", selector: "testid:submit" }`,
|
|
18
18
|
inputSchema: {
|
|
19
19
|
type: "object",
|
|
20
20
|
properties: {
|
|
@@ -82,9 +82,13 @@ export class ScreenshotTool extends BrowserToolBase {
|
|
|
82
82
|
});
|
|
83
83
|
messages.push(`Screenshot also stored in memory with name: '${args.name || 'screenshot'}'`);
|
|
84
84
|
}
|
|
85
|
+
// Add token cost warning
|
|
86
|
+
messages.push('');
|
|
87
|
+
messages.push('📸 Open the file in your IDE to view the screenshot');
|
|
88
|
+
messages.push('⚠️ Reading the image file consumes ~1,500 tokens - only use Read tool if visual analysis is essential');
|
|
85
89
|
// Add actionable guidance based on screenshot context
|
|
86
90
|
messages.push('');
|
|
87
|
-
messages.push('💡 To debug layout issues
|
|
91
|
+
messages.push('💡 To debug layout issues without reading the screenshot:');
|
|
88
92
|
if (args.selector) {
|
|
89
93
|
messages.push(` inspect_ancestors({ selector: "${args.selector}" })`);
|
|
90
94
|
messages.push(' → See parent constraints (width, margins, overflow, borders)');
|
|
@@ -36,6 +36,7 @@ export class CheckVisibilityTool extends BrowserToolBase {
|
|
|
36
36
|
// Use standard element selection with visibility preference
|
|
37
37
|
const { element, elementIndex, totalCount } = await this.selectPreferredLocator(locator, {
|
|
38
38
|
elementIndex: args.elementIndex,
|
|
39
|
+
originalSelector: args.selector,
|
|
39
40
|
});
|
|
40
41
|
// Format selection warning if multiple elements matched
|
|
41
42
|
const multipleMatchWarning = this.formatElementSelectionInfo(args.selector, elementIndex, totalCount);
|
|
@@ -44,6 +44,7 @@ export class GetComputedStylesTool extends BrowserToolBase {
|
|
|
44
44
|
const locator = page.locator(normalizedSelector);
|
|
45
45
|
const { element, elementIndex, totalCount } = await this.selectPreferredLocator(locator, {
|
|
46
46
|
elementIndex: args.elementIndex,
|
|
47
|
+
originalSelector: args.selector,
|
|
47
48
|
});
|
|
48
49
|
// Format selection warning if multiple elements matched
|
|
49
50
|
const warning = this.formatElementSelectionInfo(args.selector, elementIndex, totalCount);
|
|
@@ -46,7 +46,9 @@ export class InspectAncestorsTool extends BrowserToolBase {
|
|
|
46
46
|
isError: true,
|
|
47
47
|
};
|
|
48
48
|
}
|
|
49
|
-
const { element, elementIndex, totalCount } = await this.selectPreferredLocator(locator
|
|
49
|
+
const { element, elementIndex, totalCount } = await this.selectPreferredLocator(locator, {
|
|
50
|
+
originalSelector: args.selector,
|
|
51
|
+
});
|
|
50
52
|
// Use the selected element for ancestor traversal
|
|
51
53
|
const ancestors = await element.evaluate((el, lim) => {
|
|
52
54
|
const chain = [];
|
|
@@ -78,7 +78,9 @@ More efficient than get_html() or evaluate(). Supports testid shortcuts.`,
|
|
|
78
78
|
if (count === 0) {
|
|
79
79
|
return createErrorResponse(`Element not found: ${args.selector || 'body'}`);
|
|
80
80
|
}
|
|
81
|
-
const { element, elementIndex, totalCount } = await this.selectPreferredLocator(locator
|
|
81
|
+
const { element, elementIndex, totalCount } = await this.selectPreferredLocator(locator, {
|
|
82
|
+
originalSelector: args.selector || 'body',
|
|
83
|
+
});
|
|
82
84
|
// Get the target element and its semantic children
|
|
83
85
|
const inspectionData = await element.evaluate((target, { hidden, max, maxDepth }) => {
|
|
84
86
|
// Helper to check if element is visible
|
|
@@ -27,6 +27,7 @@ export class MeasureElementTool extends BrowserToolBase {
|
|
|
27
27
|
const locator = page.locator(normalizedSelector);
|
|
28
28
|
const { element, elementIndex, totalCount } = await this.selectPreferredLocator(locator, {
|
|
29
29
|
elementIndex: args.elementIndex,
|
|
30
|
+
originalSelector: args.selector,
|
|
30
31
|
});
|
|
31
32
|
// Format selection warning if multiple elements matched
|
|
32
33
|
const warning = this.formatElementSelectionInfo(args.selector, elementIndex, totalCount);
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { BrowserToolBase } from '../base.js';
|
|
2
|
+
import { ToolContext, ToolMetadata, ToolResponse, SessionConfig } from '../../common/types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Tool for setting prefers-color-scheme emulation
|
|
5
|
+
*/
|
|
6
|
+
export declare class SetColorSchemeTool extends BrowserToolBase {
|
|
7
|
+
static getMetadata(_sessionConfig?: SessionConfig): ToolMetadata;
|
|
8
|
+
execute(args: any, context: ToolContext): Promise<ToolResponse>;
|
|
9
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { BrowserToolBase } from '../base.js';
|
|
2
|
+
import { createErrorResponse, createSuccessResponse, } from '../../common/types.js';
|
|
3
|
+
function normalizeScheme(rawValue) {
|
|
4
|
+
if (typeof rawValue !== 'string') {
|
|
5
|
+
return null;
|
|
6
|
+
}
|
|
7
|
+
const normalized = rawValue.trim().toLowerCase();
|
|
8
|
+
if (normalized === 'dark' ||
|
|
9
|
+
normalized === 'light' ||
|
|
10
|
+
normalized === 'system' ||
|
|
11
|
+
normalized === 'no-preference') {
|
|
12
|
+
return normalized;
|
|
13
|
+
}
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Tool for setting prefers-color-scheme emulation
|
|
18
|
+
*/
|
|
19
|
+
export class SetColorSchemeTool extends BrowserToolBase {
|
|
20
|
+
static getMetadata(_sessionConfig) {
|
|
21
|
+
return {
|
|
22
|
+
name: "set_color_scheme",
|
|
23
|
+
description: "Set the browser color scheme that controls CSS prefers-color-scheme. Defaults to system appearance. Use before inspecting colors or taking screenshots. Options: system (clear override to follow OS/browser setting), dark, light, no-preference (simulate agents with no declared preference). Returns confirmation of the active scheme.",
|
|
24
|
+
inputSchema: {
|
|
25
|
+
type: "object",
|
|
26
|
+
properties: {
|
|
27
|
+
scheme: {
|
|
28
|
+
type: "string",
|
|
29
|
+
description: "Color scheme to emulate: 'system', 'dark', 'light', or 'no-preference'. Example: { scheme: 'dark' }",
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
required: ["scheme"],
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
async execute(args, context) {
|
|
37
|
+
const schemeInput = normalizeScheme(args.scheme);
|
|
38
|
+
if (!schemeInput) {
|
|
39
|
+
return createErrorResponse(`Invalid scheme "${args.scheme}". Use one of: system, dark, light, no-preference.`);
|
|
40
|
+
}
|
|
41
|
+
return this.safeExecute(context, async () => {
|
|
42
|
+
const { setColorSchemeOverride, getColorSchemeOverride } = await import('../../../toolHandler.js');
|
|
43
|
+
const override = schemeInput === 'system'
|
|
44
|
+
? null
|
|
45
|
+
: schemeInput;
|
|
46
|
+
await setColorSchemeOverride(override);
|
|
47
|
+
const active = getColorSchemeOverride();
|
|
48
|
+
const label = active ?? 'system';
|
|
49
|
+
return createSuccessResponse(`Color scheme set to ${label}.`);
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -6,6 +6,7 @@ import { ScrollToElementTool } from './navigation/scroll_to_element.js';
|
|
|
6
6
|
import { ScrollByTool } from './navigation/scroll_by.js';
|
|
7
7
|
// Lifecycle
|
|
8
8
|
import { CloseTool } from './lifecycle/close.js';
|
|
9
|
+
import { SetColorSchemeTool } from './lifecycle/set_color_scheme.js';
|
|
9
10
|
// Interaction
|
|
10
11
|
import { ClickTool } from './interaction/click.js';
|
|
11
12
|
import { FillTool } from './interaction/fill.js';
|
|
@@ -40,13 +41,15 @@ import { GetRequestDetailsTool } from './network/get_request_details.js';
|
|
|
40
41
|
import { WaitForElementTool } from './waiting/wait_for_element.js';
|
|
41
42
|
import { WaitForNetworkIdleTool } from './waiting/wait_for_network_idle.js';
|
|
42
43
|
export const BROWSER_TOOL_CLASSES = [
|
|
43
|
-
// Navigation (
|
|
44
|
+
// Navigation (5)
|
|
44
45
|
NavigateTool,
|
|
45
46
|
GoBackTool,
|
|
46
47
|
GoForwardTool,
|
|
47
48
|
ScrollToElementTool,
|
|
48
49
|
ScrollByTool,
|
|
50
|
+
// Lifecycle (2)
|
|
49
51
|
CloseTool,
|
|
52
|
+
SetColorSchemeTool,
|
|
50
53
|
// Interaction (7)
|
|
51
54
|
ClickTool,
|
|
52
55
|
FillTool,
|