mcp-web-inspector 0.1.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/LICENSE +21 -0
- package/README.md +1017 -0
- package/dist/evals/evals.d.ts +5 -0
- package/dist/evals/evals.js +41 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +62 -0
- package/dist/requestHandler.d.ts +3 -0
- package/dist/requestHandler.js +53 -0
- package/dist/toolHandler.d.ts +91 -0
- package/dist/toolHandler.js +725 -0
- package/dist/tools/api/base.d.ts +33 -0
- package/dist/tools/api/base.js +49 -0
- package/dist/tools/api/index.d.ts +2 -0
- package/dist/tools/api/index.js +3 -0
- package/dist/tools/api/requests.d.ts +47 -0
- package/dist/tools/api/requests.js +168 -0
- package/dist/tools/browser/base.d.ts +51 -0
- package/dist/tools/browser/base.js +111 -0
- package/dist/tools/browser/cleanSession.d.ts +10 -0
- package/dist/tools/browser/cleanSession.js +42 -0
- package/dist/tools/browser/comparePositions.d.ts +11 -0
- package/dist/tools/browser/comparePositions.js +149 -0
- package/dist/tools/browser/computedStyles.d.ts +11 -0
- package/dist/tools/browser/computedStyles.js +128 -0
- package/dist/tools/browser/console.d.ts +37 -0
- package/dist/tools/browser/console.js +106 -0
- package/dist/tools/browser/elementExists.d.ts +9 -0
- package/dist/tools/browser/elementExists.js +57 -0
- package/dist/tools/browser/elementInspection.d.ts +21 -0
- package/dist/tools/browser/elementInspection.js +151 -0
- package/dist/tools/browser/elementPosition.d.ts +11 -0
- package/dist/tools/browser/elementPosition.js +107 -0
- package/dist/tools/browser/elementVisibility.d.ts +12 -0
- package/dist/tools/browser/elementVisibility.js +224 -0
- package/dist/tools/browser/findByText.d.ts +13 -0
- package/dist/tools/browser/findByText.js +207 -0
- package/dist/tools/browser/getRequestDetails.d.ts +9 -0
- package/dist/tools/browser/getRequestDetails.js +137 -0
- package/dist/tools/browser/getTestIds.d.ts +12 -0
- package/dist/tools/browser/getTestIds.js +148 -0
- package/dist/tools/browser/index.d.ts +7 -0
- package/dist/tools/browser/index.js +7 -0
- package/dist/tools/browser/inspectDom.d.ts +12 -0
- package/dist/tools/browser/inspectDom.js +447 -0
- package/dist/tools/browser/interaction.d.ts +104 -0
- package/dist/tools/browser/interaction.js +259 -0
- package/dist/tools/browser/listNetworkRequests.d.ts +10 -0
- package/dist/tools/browser/listNetworkRequests.js +74 -0
- package/dist/tools/browser/measureElement.d.ts +9 -0
- package/dist/tools/browser/measureElement.js +139 -0
- package/dist/tools/browser/navigation.d.ts +38 -0
- package/dist/tools/browser/navigation.js +109 -0
- package/dist/tools/browser/output.d.ts +11 -0
- package/dist/tools/browser/output.js +29 -0
- package/dist/tools/browser/querySelectorAll.d.ts +12 -0
- package/dist/tools/browser/querySelectorAll.js +201 -0
- package/dist/tools/browser/response.d.ts +29 -0
- package/dist/tools/browser/response.js +67 -0
- package/dist/tools/browser/screenshot.d.ts +16 -0
- package/dist/tools/browser/screenshot.js +70 -0
- package/dist/tools/browser/useragent.d.ts +15 -0
- package/dist/tools/browser/useragent.js +32 -0
- package/dist/tools/browser/visiblePage.d.ts +20 -0
- package/dist/tools/browser/visiblePage.js +170 -0
- package/dist/tools/browser/waitForElement.d.ts +10 -0
- package/dist/tools/browser/waitForElement.js +38 -0
- package/dist/tools/browser/waitForNetworkIdle.d.ts +8 -0
- package/dist/tools/browser/waitForNetworkIdle.js +32 -0
- package/dist/tools/codegen/generator.d.ts +21 -0
- package/dist/tools/codegen/generator.js +158 -0
- package/dist/tools/codegen/index.d.ts +11 -0
- package/dist/tools/codegen/index.js +187 -0
- package/dist/tools/codegen/recorder.d.ts +14 -0
- package/dist/tools/codegen/recorder.js +62 -0
- package/dist/tools/codegen/types.d.ts +28 -0
- package/dist/tools/codegen/types.js +1 -0
- package/dist/tools/common/types.d.ts +17 -0
- package/dist/tools/common/types.js +20 -0
- package/dist/tools/index.d.ts +2 -0
- package/dist/tools/index.js +2 -0
- package/dist/tools.d.ts +557 -0
- package/dist/tools.js +554 -0
- package/dist/types.d.ts +16 -0
- package/dist/types.js +1 -0
- package/package.json +60 -0
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { BrowserToolBase } from './base.js';
|
|
2
|
+
import { ToolContext, ToolResponse } from '../common/types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Tool for testing a selector and returning information about all matched elements
|
|
5
|
+
* Useful for selector debugging and finding the right element to interact with
|
|
6
|
+
*/
|
|
7
|
+
export declare class QuerySelectorAllTool extends BrowserToolBase {
|
|
8
|
+
/**
|
|
9
|
+
* Execute the query selector all tool
|
|
10
|
+
*/
|
|
11
|
+
execute(args: any, context: ToolContext): Promise<ToolResponse>;
|
|
12
|
+
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { BrowserToolBase } from './base.js';
|
|
2
|
+
import { createSuccessResponse, createErrorResponse } from '../common/types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Tool for testing a selector and returning information about all matched elements
|
|
5
|
+
* Useful for selector debugging and finding the right element to interact with
|
|
6
|
+
*/
|
|
7
|
+
export class QuerySelectorAllTool extends BrowserToolBase {
|
|
8
|
+
/**
|
|
9
|
+
* Execute the query selector all tool
|
|
10
|
+
*/
|
|
11
|
+
async execute(args, context) {
|
|
12
|
+
return this.safeExecute(context, async (page) => {
|
|
13
|
+
const selector = this.normalizeSelector(args.selector);
|
|
14
|
+
const limit = args.limit ?? 10;
|
|
15
|
+
const onlyVisible = args.onlyVisible; // true = visible only, false = hidden only, undefined = all
|
|
16
|
+
const showAttributes = args.showAttributes
|
|
17
|
+
? args.showAttributes.split(',').map((a) => a.trim())
|
|
18
|
+
: undefined;
|
|
19
|
+
try {
|
|
20
|
+
// Query all elements matching the selector
|
|
21
|
+
const elements = await page.locator(selector).all();
|
|
22
|
+
const totalMatches = elements.length;
|
|
23
|
+
if (totalMatches === 0) {
|
|
24
|
+
return createSuccessResponse(`No elements found matching "${args.selector}"\n\nTip: Try using inspect_dom to explore the page structure.`);
|
|
25
|
+
}
|
|
26
|
+
// Get detailed information for each element (all elements to allow filtering)
|
|
27
|
+
const matchData = [];
|
|
28
|
+
for (let i = 0; i < elements.length; i++) {
|
|
29
|
+
const element = elements[i];
|
|
30
|
+
try {
|
|
31
|
+
const elementInfo = await element.evaluate((el, attrList) => {
|
|
32
|
+
const rect = el.getBoundingClientRect();
|
|
33
|
+
const styles = window.getComputedStyle(el);
|
|
34
|
+
const isVisible = styles.display !== 'none' &&
|
|
35
|
+
styles.visibility !== 'hidden' &&
|
|
36
|
+
parseFloat(styles.opacity) > 0 &&
|
|
37
|
+
rect.width > 0 &&
|
|
38
|
+
rect.height > 0;
|
|
39
|
+
// Get test ID
|
|
40
|
+
const testId = el.getAttribute('data-testid') ||
|
|
41
|
+
el.getAttribute('data-test') ||
|
|
42
|
+
el.getAttribute('data-cy') ||
|
|
43
|
+
undefined;
|
|
44
|
+
// Get selector representation
|
|
45
|
+
let selectorRepr = '';
|
|
46
|
+
if (testId) {
|
|
47
|
+
const attrName = el.hasAttribute('data-testid')
|
|
48
|
+
? 'data-testid'
|
|
49
|
+
: el.hasAttribute('data-test')
|
|
50
|
+
? 'data-test'
|
|
51
|
+
: 'data-cy';
|
|
52
|
+
selectorRepr = `${attrName}="${testId}"`;
|
|
53
|
+
}
|
|
54
|
+
else if (el.id) {
|
|
55
|
+
selectorRepr = `#${el.id}`;
|
|
56
|
+
}
|
|
57
|
+
else if (el.className && typeof el.className === 'string') {
|
|
58
|
+
const classes = el.className.trim().split(/\s+/).slice(0, 3).join('.');
|
|
59
|
+
if (classes) {
|
|
60
|
+
selectorRepr = `class="${classes}"`;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
// Check if interactive
|
|
64
|
+
const tag = el.tagName.toLowerCase();
|
|
65
|
+
const interactiveTags = new Set(['button', 'a', 'input', 'select', 'textarea']);
|
|
66
|
+
const isInteractive = interactiveTags.has(tag) ||
|
|
67
|
+
el.hasAttribute('onclick') ||
|
|
68
|
+
el.hasAttribute('contenteditable') ||
|
|
69
|
+
el.getAttribute('role') === 'button';
|
|
70
|
+
// Get text content (trimmed and limited)
|
|
71
|
+
const text = el.textContent?.trim().slice(0, 100) || '';
|
|
72
|
+
// Get requested attributes if specified
|
|
73
|
+
let attributes;
|
|
74
|
+
if (attrList && attrList.length > 0) {
|
|
75
|
+
attributes = {};
|
|
76
|
+
attrList.forEach((attr) => {
|
|
77
|
+
const value = el.getAttribute(attr);
|
|
78
|
+
if (value !== null) {
|
|
79
|
+
attributes[attr] = value;
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
tag: tag,
|
|
85
|
+
selector: selectorRepr,
|
|
86
|
+
testId,
|
|
87
|
+
classes: Array.from(el.classList).join('.'),
|
|
88
|
+
text,
|
|
89
|
+
position: {
|
|
90
|
+
x: Math.round(rect.x),
|
|
91
|
+
y: Math.round(rect.y),
|
|
92
|
+
width: Math.round(rect.width),
|
|
93
|
+
height: Math.round(rect.height),
|
|
94
|
+
},
|
|
95
|
+
isVisible,
|
|
96
|
+
isInteractive,
|
|
97
|
+
opacity: parseFloat(styles.opacity),
|
|
98
|
+
display: styles.display,
|
|
99
|
+
attributes,
|
|
100
|
+
};
|
|
101
|
+
}, showAttributes);
|
|
102
|
+
matchData.push(elementInfo);
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
// Skip elements that fail evaluation (e.g., detached from DOM)
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
// Filter by visibility if requested
|
|
110
|
+
let filteredMatches = matchData;
|
|
111
|
+
if (onlyVisible !== undefined) {
|
|
112
|
+
filteredMatches = matchData.filter((match) => onlyVisible ? match.isVisible : !match.isVisible);
|
|
113
|
+
}
|
|
114
|
+
// Apply limit after filtering
|
|
115
|
+
const displayMatches = filteredMatches.slice(0, limit);
|
|
116
|
+
// Format compact text output
|
|
117
|
+
const lines = [];
|
|
118
|
+
// Header with counts
|
|
119
|
+
if (onlyVisible === true) {
|
|
120
|
+
const visibleCount = filteredMatches.length;
|
|
121
|
+
lines.push(`Found ${totalMatches} element${totalMatches > 1 ? 's' : ''} matching "${args.selector}" (${visibleCount} visible):`);
|
|
122
|
+
}
|
|
123
|
+
else if (onlyVisible === false) {
|
|
124
|
+
const hiddenCount = filteredMatches.length;
|
|
125
|
+
lines.push(`Found ${totalMatches} element${totalMatches > 1 ? 's' : ''} matching "${args.selector}" (${hiddenCount} hidden):`);
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
lines.push(`Found ${totalMatches} element${totalMatches > 1 ? 's' : ''} matching "${args.selector}":`);
|
|
129
|
+
}
|
|
130
|
+
lines.push('');
|
|
131
|
+
displayMatches.forEach((match, index) => {
|
|
132
|
+
const prefix = `[${index}]`;
|
|
133
|
+
// Element tag with selector info
|
|
134
|
+
let tagInfo = `<${match.tag}`;
|
|
135
|
+
if (match.selector) {
|
|
136
|
+
tagInfo += ` ${match.selector}`;
|
|
137
|
+
}
|
|
138
|
+
tagInfo += '>';
|
|
139
|
+
lines.push(`${prefix} ${tagInfo}`);
|
|
140
|
+
// Position
|
|
141
|
+
lines.push(` @ (${match.position.x},${match.position.y}) ${match.position.width}x${match.position.height}px`);
|
|
142
|
+
// Text content
|
|
143
|
+
if (match.text) {
|
|
144
|
+
const displayText = match.text.length > 50 ? match.text.slice(0, 47) + '...' : match.text;
|
|
145
|
+
lines.push(` "${displayText}"`);
|
|
146
|
+
}
|
|
147
|
+
// Attributes (if requested)
|
|
148
|
+
if (match.attributes && Object.keys(match.attributes).length > 0) {
|
|
149
|
+
Object.entries(match.attributes).forEach(([attr, value]) => {
|
|
150
|
+
const displayValue = value.length > 50 ? value.slice(0, 47) + '...' : value;
|
|
151
|
+
lines.push(` ${attr}: "${displayValue}"`);
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
// Status symbols
|
|
155
|
+
const statusParts = [];
|
|
156
|
+
statusParts.push(match.isVisible ? '✓ visible' : '✗ hidden');
|
|
157
|
+
if (!match.isVisible) {
|
|
158
|
+
// Add reason for being hidden
|
|
159
|
+
if (match.display === 'none') {
|
|
160
|
+
statusParts.push('display: none');
|
|
161
|
+
}
|
|
162
|
+
else if (match.opacity === 0) {
|
|
163
|
+
statusParts.push('opacity: 0');
|
|
164
|
+
}
|
|
165
|
+
else if (match.position.width === 0 || match.position.height === 0) {
|
|
166
|
+
statusParts.push('zero size');
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
if (match.isInteractive) {
|
|
170
|
+
statusParts.push('⚡ interactive');
|
|
171
|
+
}
|
|
172
|
+
lines.push(` ${statusParts.join(', ')}`);
|
|
173
|
+
lines.push('');
|
|
174
|
+
});
|
|
175
|
+
// Show omitted count and summary
|
|
176
|
+
if (filteredMatches.length > limit) {
|
|
177
|
+
const omitted = filteredMatches.length - limit;
|
|
178
|
+
const matchType = onlyVisible === true ? 'visible ' : onlyVisible === false ? 'hidden ' : '';
|
|
179
|
+
lines.push(`Showing ${limit} of ${filteredMatches.length} ${matchType}matches (${omitted} omitted)`);
|
|
180
|
+
lines.push(`Use limit parameter to show more: { selector: "${args.selector}", limit: ${Math.min(filteredMatches.length, 50)} }`);
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
const matchWord = filteredMatches.length === 1 ? 'match' : 'matches';
|
|
184
|
+
if (onlyVisible === true) {
|
|
185
|
+
lines.push(`Showing ${filteredMatches.length} visible ${matchWord}`);
|
|
186
|
+
}
|
|
187
|
+
else if (onlyVisible === false) {
|
|
188
|
+
lines.push(`Showing ${filteredMatches.length} hidden ${matchWord}`);
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
lines.push(`Showing all ${filteredMatches.length} ${matchWord}`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return createSuccessResponse(lines.join('\n'));
|
|
195
|
+
}
|
|
196
|
+
catch (error) {
|
|
197
|
+
return createErrorResponse(`Failed to query selector: ${error.message}`);
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { BrowserToolBase } from './base.js';
|
|
2
|
+
import type { ToolContext, ToolResponse } from '../common/types.js';
|
|
3
|
+
interface ExpectResponseArgs {
|
|
4
|
+
id: string;
|
|
5
|
+
url: string;
|
|
6
|
+
}
|
|
7
|
+
interface AssertResponseArgs {
|
|
8
|
+
id: string;
|
|
9
|
+
value?: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Tool for setting up response wait operations
|
|
13
|
+
*/
|
|
14
|
+
export declare class ExpectResponseTool extends BrowserToolBase {
|
|
15
|
+
/**
|
|
16
|
+
* Execute the expect response tool
|
|
17
|
+
*/
|
|
18
|
+
execute(args: ExpectResponseArgs, context: ToolContext): Promise<ToolResponse>;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Tool for asserting and validating responses
|
|
22
|
+
*/
|
|
23
|
+
export declare class AssertResponseTool extends BrowserToolBase {
|
|
24
|
+
/**
|
|
25
|
+
* Execute the assert response tool
|
|
26
|
+
*/
|
|
27
|
+
execute(args: AssertResponseArgs, context: ToolContext): Promise<ToolResponse>;
|
|
28
|
+
}
|
|
29
|
+
export {};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { BrowserToolBase } from './base.js';
|
|
2
|
+
import { createSuccessResponse, createErrorResponse } from '../common/types.js';
|
|
3
|
+
const responsePromises = new Map();
|
|
4
|
+
/**
|
|
5
|
+
* Tool for setting up response wait operations
|
|
6
|
+
*/
|
|
7
|
+
export class ExpectResponseTool extends BrowserToolBase {
|
|
8
|
+
/**
|
|
9
|
+
* Execute the expect response tool
|
|
10
|
+
*/
|
|
11
|
+
async execute(args, context) {
|
|
12
|
+
return this.safeExecute(context, async (page) => {
|
|
13
|
+
if (!args.id || !args.url) {
|
|
14
|
+
return createErrorResponse("Missing required parameters: id and url must be provided");
|
|
15
|
+
}
|
|
16
|
+
const responsePromise = page.waitForResponse(args.url);
|
|
17
|
+
responsePromises.set(args.id, responsePromise);
|
|
18
|
+
return createSuccessResponse(`Started waiting for response with ID ${args.id}`);
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Tool for asserting and validating responses
|
|
24
|
+
*/
|
|
25
|
+
export class AssertResponseTool extends BrowserToolBase {
|
|
26
|
+
/**
|
|
27
|
+
* Execute the assert response tool
|
|
28
|
+
*/
|
|
29
|
+
async execute(args, context) {
|
|
30
|
+
return this.safeExecute(context, async () => {
|
|
31
|
+
if (!args.id) {
|
|
32
|
+
return createErrorResponse("Missing required parameter: id must be provided");
|
|
33
|
+
}
|
|
34
|
+
const responsePromise = responsePromises.get(args.id);
|
|
35
|
+
if (!responsePromise) {
|
|
36
|
+
return createErrorResponse(`No response wait operation found with ID: ${args.id}`);
|
|
37
|
+
}
|
|
38
|
+
try {
|
|
39
|
+
const response = await responsePromise;
|
|
40
|
+
const body = await response.json();
|
|
41
|
+
if (args.value) {
|
|
42
|
+
const bodyStr = JSON.stringify(body);
|
|
43
|
+
if (!bodyStr.includes(args.value)) {
|
|
44
|
+
const messages = [
|
|
45
|
+
`Response body does not contain expected value: ${args.value}`,
|
|
46
|
+
`Actual body: ${bodyStr}`
|
|
47
|
+
];
|
|
48
|
+
return createErrorResponse(messages.join('\n'));
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
const messages = [
|
|
52
|
+
`Response assertion for ID ${args.id} successful`,
|
|
53
|
+
`URL: ${response.url()}`,
|
|
54
|
+
`Status: ${response.status()}`,
|
|
55
|
+
`Body: ${JSON.stringify(body, null, 2)}`
|
|
56
|
+
];
|
|
57
|
+
return createSuccessResponse(messages.join('\n'));
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
return createErrorResponse(`Failed to assert response: ${error.message}`);
|
|
61
|
+
}
|
|
62
|
+
finally {
|
|
63
|
+
responsePromises.delete(args.id);
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { BrowserToolBase } from './base.js';
|
|
2
|
+
import { ToolContext, ToolResponse } from '../common/types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Tool for taking screenshots of pages or elements
|
|
5
|
+
*/
|
|
6
|
+
export declare class ScreenshotTool extends BrowserToolBase {
|
|
7
|
+
private screenshots;
|
|
8
|
+
/**
|
|
9
|
+
* Execute the screenshot tool
|
|
10
|
+
*/
|
|
11
|
+
execute(args: any, context: ToolContext): Promise<ToolResponse>;
|
|
12
|
+
/**
|
|
13
|
+
* Get all stored screenshots
|
|
14
|
+
*/
|
|
15
|
+
getScreenshots(): Map<string, string>;
|
|
16
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import * as os from 'node:os';
|
|
4
|
+
import { BrowserToolBase } from './base.js';
|
|
5
|
+
import { createSuccessResponse } from '../common/types.js';
|
|
6
|
+
import { getScreenshotsDir } from '../../toolHandler.js';
|
|
7
|
+
const defaultDownloadsPath = path.join(os.homedir(), 'Downloads');
|
|
8
|
+
/**
|
|
9
|
+
* Tool for taking screenshots of pages or elements
|
|
10
|
+
*/
|
|
11
|
+
export class ScreenshotTool extends BrowserToolBase {
|
|
12
|
+
constructor() {
|
|
13
|
+
super(...arguments);
|
|
14
|
+
this.screenshots = new Map();
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Execute the screenshot tool
|
|
18
|
+
*/
|
|
19
|
+
async execute(args, context) {
|
|
20
|
+
return this.safeExecute(context, async (page) => {
|
|
21
|
+
const screenshotOptions = {
|
|
22
|
+
type: args.type || "png",
|
|
23
|
+
fullPage: !!args.fullPage
|
|
24
|
+
};
|
|
25
|
+
if (args.selector) {
|
|
26
|
+
// Normalize selector (support testid: shorthand)
|
|
27
|
+
const selector = this.normalizeSelector(args.selector);
|
|
28
|
+
const element = await page.$(selector);
|
|
29
|
+
if (!element) {
|
|
30
|
+
return {
|
|
31
|
+
content: [{
|
|
32
|
+
type: "text",
|
|
33
|
+
text: `Element not found: ${selector}`,
|
|
34
|
+
}],
|
|
35
|
+
isError: true
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
screenshotOptions.element = element;
|
|
39
|
+
}
|
|
40
|
+
// Generate output path
|
|
41
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
42
|
+
const filename = `${args.name || 'screenshot'}-${timestamp}.png`;
|
|
43
|
+
// Use screenshots directory from config, fall back to downloadsDir arg, then default Downloads
|
|
44
|
+
const downloadsDir = args.downloadsDir || getScreenshotsDir();
|
|
45
|
+
if (!fs.existsSync(downloadsDir)) {
|
|
46
|
+
fs.mkdirSync(downloadsDir, { recursive: true });
|
|
47
|
+
}
|
|
48
|
+
const outputPath = path.join(downloadsDir, filename);
|
|
49
|
+
screenshotOptions.path = outputPath;
|
|
50
|
+
const screenshot = await page.screenshot(screenshotOptions);
|
|
51
|
+
const base64Screenshot = screenshot.toString('base64');
|
|
52
|
+
const messages = [`Screenshot saved to: ${path.relative(process.cwd(), outputPath)}`];
|
|
53
|
+
// Handle base64 storage
|
|
54
|
+
if (args.storeBase64 !== false) {
|
|
55
|
+
this.screenshots.set(args.name || 'screenshot', base64Screenshot);
|
|
56
|
+
this.server.notification({
|
|
57
|
+
method: "notifications/resources/list_changed",
|
|
58
|
+
});
|
|
59
|
+
messages.push(`Screenshot also stored in memory with name: '${args.name || 'screenshot'}'`);
|
|
60
|
+
}
|
|
61
|
+
return createSuccessResponse(messages);
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Get all stored screenshots
|
|
66
|
+
*/
|
|
67
|
+
getScreenshots() {
|
|
68
|
+
return this.screenshots;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { BrowserToolBase } from './base.js';
|
|
2
|
+
import type { ToolContext, ToolResponse } from '../common/types.js';
|
|
3
|
+
interface CustomUserAgentArgs {
|
|
4
|
+
userAgent: string;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Tool for validating custom User Agent settings
|
|
8
|
+
*/
|
|
9
|
+
export declare class CustomUserAgentTool extends BrowserToolBase {
|
|
10
|
+
/**
|
|
11
|
+
* Execute the custom user agent tool
|
|
12
|
+
*/
|
|
13
|
+
execute(args: CustomUserAgentArgs, context: ToolContext): Promise<ToolResponse>;
|
|
14
|
+
}
|
|
15
|
+
export {};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { BrowserToolBase } from './base.js';
|
|
2
|
+
import { createSuccessResponse, createErrorResponse } from '../common/types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Tool for validating custom User Agent settings
|
|
5
|
+
*/
|
|
6
|
+
export class CustomUserAgentTool extends BrowserToolBase {
|
|
7
|
+
/**
|
|
8
|
+
* Execute the custom user agent tool
|
|
9
|
+
*/
|
|
10
|
+
async execute(args, context) {
|
|
11
|
+
return this.safeExecute(context, async (page) => {
|
|
12
|
+
if (!args.userAgent) {
|
|
13
|
+
return createErrorResponse("Missing required parameter: userAgent must be provided");
|
|
14
|
+
}
|
|
15
|
+
try {
|
|
16
|
+
const currentUserAgent = await page.evaluate(() => navigator.userAgent);
|
|
17
|
+
if (currentUserAgent !== args.userAgent) {
|
|
18
|
+
const messages = [
|
|
19
|
+
"Page was already initialized with a different User Agent.",
|
|
20
|
+
`Requested: ${args.userAgent}`,
|
|
21
|
+
`Current: ${currentUserAgent}`
|
|
22
|
+
];
|
|
23
|
+
return createErrorResponse(messages.join('\n'));
|
|
24
|
+
}
|
|
25
|
+
return createSuccessResponse("User Agent validation successful");
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
return createErrorResponse(`Failed to validate User Agent: ${error.message}`);
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { ToolContext, ToolResponse } from "../common/types.js";
|
|
2
|
+
import { BrowserToolBase } from "./base.js";
|
|
3
|
+
/**
|
|
4
|
+
* Tool for getting the visible text content of the current page
|
|
5
|
+
*/
|
|
6
|
+
export declare class VisibleTextTool extends BrowserToolBase {
|
|
7
|
+
/**
|
|
8
|
+
* Execute the visible text page tool
|
|
9
|
+
*/
|
|
10
|
+
execute(args: any, context: ToolContext): Promise<ToolResponse>;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Tool for getting the visible HTML content of the current page
|
|
14
|
+
*/
|
|
15
|
+
export declare class VisibleHtmlTool extends BrowserToolBase {
|
|
16
|
+
/**
|
|
17
|
+
* Execute the visible HTML page tool
|
|
18
|
+
*/
|
|
19
|
+
execute(args: any, context: ToolContext): Promise<ToolResponse>;
|
|
20
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { resetBrowserState } from "../../toolHandler.js";
|
|
2
|
+
import { createErrorResponse, createSuccessResponse } from "../common/types.js";
|
|
3
|
+
import { BrowserToolBase } from "./base.js";
|
|
4
|
+
/**
|
|
5
|
+
* Tool for getting the visible text content of the current page
|
|
6
|
+
*/
|
|
7
|
+
export class VisibleTextTool extends BrowserToolBase {
|
|
8
|
+
/**
|
|
9
|
+
* Execute the visible text page tool
|
|
10
|
+
*/
|
|
11
|
+
async execute(args, context) {
|
|
12
|
+
// Check if browser is available
|
|
13
|
+
if (!context.browser || !context.browser.isConnected()) {
|
|
14
|
+
// If browser is not connected, we need to reset the state to force recreation
|
|
15
|
+
resetBrowserState();
|
|
16
|
+
return createErrorResponse("Browser is not connected. The connection has been reset - please retry your navigation.");
|
|
17
|
+
}
|
|
18
|
+
// Check if page is available and not closed
|
|
19
|
+
if (!context.page || context.page.isClosed()) {
|
|
20
|
+
return createErrorResponse("Page is not available or has been closed. Please retry your navigation.");
|
|
21
|
+
}
|
|
22
|
+
return this.safeExecute(context, async (page) => {
|
|
23
|
+
try {
|
|
24
|
+
const visibleText = await page.evaluate(() => {
|
|
25
|
+
const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, {
|
|
26
|
+
acceptNode: (node) => {
|
|
27
|
+
const style = window.getComputedStyle(node.parentElement);
|
|
28
|
+
return (style.display !== "none" && style.visibility !== "hidden")
|
|
29
|
+
? NodeFilter.FILTER_ACCEPT
|
|
30
|
+
: NodeFilter.FILTER_REJECT;
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
let text = "";
|
|
34
|
+
let node;
|
|
35
|
+
while ((node = walker.nextNode())) {
|
|
36
|
+
const trimmedText = node.textContent?.trim();
|
|
37
|
+
if (trimmedText) {
|
|
38
|
+
text += trimmedText + "\n";
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return text.trim();
|
|
42
|
+
});
|
|
43
|
+
// Truncate logic
|
|
44
|
+
const maxLength = typeof args.maxLength === 'number' ? args.maxLength : 20000;
|
|
45
|
+
let output = visibleText;
|
|
46
|
+
let truncated = false;
|
|
47
|
+
if (output.length > maxLength) {
|
|
48
|
+
output = output.slice(0, maxLength) + '\n[Output truncated due to size limits]';
|
|
49
|
+
truncated = true;
|
|
50
|
+
}
|
|
51
|
+
return createSuccessResponse(`Visible text content:\n${output}`);
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
return createErrorResponse(`Failed to get visible text content: ${error.message}`);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Tool for getting the visible HTML content of the current page
|
|
61
|
+
*/
|
|
62
|
+
export class VisibleHtmlTool extends BrowserToolBase {
|
|
63
|
+
/**
|
|
64
|
+
* Execute the visible HTML page tool
|
|
65
|
+
*/
|
|
66
|
+
async execute(args, context) {
|
|
67
|
+
// Check if browser is available
|
|
68
|
+
if (!context.browser || !context.browser.isConnected()) {
|
|
69
|
+
// If browser is not connected, we need to reset the state to force recreation
|
|
70
|
+
resetBrowserState();
|
|
71
|
+
return createErrorResponse("Browser is not connected. The connection has been reset - please retry your navigation.");
|
|
72
|
+
}
|
|
73
|
+
// Check if page is available and not closed
|
|
74
|
+
if (!context.page || context.page.isClosed()) {
|
|
75
|
+
return createErrorResponse("Page is not available or has been closed. Please retry your navigation.");
|
|
76
|
+
}
|
|
77
|
+
return this.safeExecute(context, async (page) => {
|
|
78
|
+
try {
|
|
79
|
+
const { removeComments, removeStyles, removeMeta, minify, cleanHtml } = args;
|
|
80
|
+
// Default removeScripts to true unless explicitly set to false
|
|
81
|
+
const removeScripts = args.removeScripts === false ? false : true;
|
|
82
|
+
// Normalize selector (support testid: shorthand)
|
|
83
|
+
const selector = args.selector ? this.normalizeSelector(args.selector) : undefined;
|
|
84
|
+
// Get the HTML content
|
|
85
|
+
let htmlContent;
|
|
86
|
+
if (selector) {
|
|
87
|
+
// If a selector is provided, get only the HTML for that element
|
|
88
|
+
const element = await page.$(selector);
|
|
89
|
+
if (!element) {
|
|
90
|
+
return createErrorResponse(`Element with selector "${selector}" not found`);
|
|
91
|
+
}
|
|
92
|
+
htmlContent = await page.evaluate((el) => el.outerHTML, element);
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
// Otherwise get the full page HTML
|
|
96
|
+
htmlContent = await page.content();
|
|
97
|
+
}
|
|
98
|
+
// Determine if we need to apply filters
|
|
99
|
+
const shouldRemoveScripts = removeScripts || cleanHtml;
|
|
100
|
+
const shouldRemoveComments = removeComments || cleanHtml;
|
|
101
|
+
const shouldRemoveStyles = removeStyles || cleanHtml;
|
|
102
|
+
const shouldRemoveMeta = removeMeta || cleanHtml;
|
|
103
|
+
// Apply filters in the browser context
|
|
104
|
+
if (shouldRemoveScripts || shouldRemoveComments || shouldRemoveStyles || shouldRemoveMeta || minify) {
|
|
105
|
+
htmlContent = await page.evaluate(({ html, removeScripts, removeComments, removeStyles, removeMeta, minify }) => {
|
|
106
|
+
// Create a DOM parser to work with the HTML
|
|
107
|
+
const parser = new DOMParser();
|
|
108
|
+
const doc = parser.parseFromString(html, 'text/html');
|
|
109
|
+
// Remove script tags if requested
|
|
110
|
+
if (removeScripts) {
|
|
111
|
+
const scripts = doc.querySelectorAll('script');
|
|
112
|
+
scripts.forEach(script => script.remove());
|
|
113
|
+
}
|
|
114
|
+
// Remove style tags if requested
|
|
115
|
+
if (removeStyles) {
|
|
116
|
+
const styles = doc.querySelectorAll('style');
|
|
117
|
+
styles.forEach(style => style.remove());
|
|
118
|
+
}
|
|
119
|
+
// Remove meta tags if requested
|
|
120
|
+
if (removeMeta) {
|
|
121
|
+
const metaTags = doc.querySelectorAll('meta');
|
|
122
|
+
metaTags.forEach(meta => meta.remove());
|
|
123
|
+
}
|
|
124
|
+
// Remove HTML comments if requested
|
|
125
|
+
if (removeComments) {
|
|
126
|
+
const removeComments = (node) => {
|
|
127
|
+
const childNodes = node.childNodes;
|
|
128
|
+
for (let i = childNodes.length - 1; i >= 0; i--) {
|
|
129
|
+
const child = childNodes[i];
|
|
130
|
+
if (child.nodeType === 8) { // 8 is for comment nodes
|
|
131
|
+
node.removeChild(child);
|
|
132
|
+
}
|
|
133
|
+
else if (child.nodeType === 1) { // 1 is for element nodes
|
|
134
|
+
removeComments(child);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
removeComments(doc.documentElement);
|
|
139
|
+
}
|
|
140
|
+
// Get the processed HTML
|
|
141
|
+
let result = doc.documentElement.outerHTML;
|
|
142
|
+
// Minify if requested
|
|
143
|
+
if (minify) {
|
|
144
|
+
// Simple minification: remove extra whitespace
|
|
145
|
+
result = result.replace(/>\s+</g, '><').trim();
|
|
146
|
+
}
|
|
147
|
+
return result;
|
|
148
|
+
}, {
|
|
149
|
+
html: htmlContent,
|
|
150
|
+
removeScripts: shouldRemoveScripts,
|
|
151
|
+
removeComments: shouldRemoveComments,
|
|
152
|
+
removeStyles: shouldRemoveStyles,
|
|
153
|
+
removeMeta: shouldRemoveMeta,
|
|
154
|
+
minify
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
// Truncate logic
|
|
158
|
+
const maxLength = typeof args.maxLength === 'number' ? args.maxLength : 20000;
|
|
159
|
+
let output = htmlContent;
|
|
160
|
+
if (output.length > maxLength) {
|
|
161
|
+
output = output.slice(0, maxLength) + '\n<!-- Output truncated due to size limits -->';
|
|
162
|
+
}
|
|
163
|
+
return createSuccessResponse(`HTML content:\n${output}`);
|
|
164
|
+
}
|
|
165
|
+
catch (error) {
|
|
166
|
+
return createErrorResponse(`Failed to get visible HTML content: ${error.message}`);
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { BrowserToolBase } from './base.js';
|
|
2
|
+
import { ToolContext, ToolResponse } from '../common/types.js';
|
|
3
|
+
export interface WaitForElementArgs {
|
|
4
|
+
selector: string;
|
|
5
|
+
state?: 'visible' | 'hidden' | 'attached' | 'detached';
|
|
6
|
+
timeout?: number;
|
|
7
|
+
}
|
|
8
|
+
export declare class WaitForElementTool extends BrowserToolBase {
|
|
9
|
+
execute(args: WaitForElementArgs, context: ToolContext): Promise<ToolResponse>;
|
|
10
|
+
}
|