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.
Files changed (85) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1017 -0
  3. package/dist/evals/evals.d.ts +5 -0
  4. package/dist/evals/evals.js +41 -0
  5. package/dist/index.d.ts +2 -0
  6. package/dist/index.js +62 -0
  7. package/dist/requestHandler.d.ts +3 -0
  8. package/dist/requestHandler.js +53 -0
  9. package/dist/toolHandler.d.ts +91 -0
  10. package/dist/toolHandler.js +725 -0
  11. package/dist/tools/api/base.d.ts +33 -0
  12. package/dist/tools/api/base.js +49 -0
  13. package/dist/tools/api/index.d.ts +2 -0
  14. package/dist/tools/api/index.js +3 -0
  15. package/dist/tools/api/requests.d.ts +47 -0
  16. package/dist/tools/api/requests.js +168 -0
  17. package/dist/tools/browser/base.d.ts +51 -0
  18. package/dist/tools/browser/base.js +111 -0
  19. package/dist/tools/browser/cleanSession.d.ts +10 -0
  20. package/dist/tools/browser/cleanSession.js +42 -0
  21. package/dist/tools/browser/comparePositions.d.ts +11 -0
  22. package/dist/tools/browser/comparePositions.js +149 -0
  23. package/dist/tools/browser/computedStyles.d.ts +11 -0
  24. package/dist/tools/browser/computedStyles.js +128 -0
  25. package/dist/tools/browser/console.d.ts +37 -0
  26. package/dist/tools/browser/console.js +106 -0
  27. package/dist/tools/browser/elementExists.d.ts +9 -0
  28. package/dist/tools/browser/elementExists.js +57 -0
  29. package/dist/tools/browser/elementInspection.d.ts +21 -0
  30. package/dist/tools/browser/elementInspection.js +151 -0
  31. package/dist/tools/browser/elementPosition.d.ts +11 -0
  32. package/dist/tools/browser/elementPosition.js +107 -0
  33. package/dist/tools/browser/elementVisibility.d.ts +12 -0
  34. package/dist/tools/browser/elementVisibility.js +224 -0
  35. package/dist/tools/browser/findByText.d.ts +13 -0
  36. package/dist/tools/browser/findByText.js +207 -0
  37. package/dist/tools/browser/getRequestDetails.d.ts +9 -0
  38. package/dist/tools/browser/getRequestDetails.js +137 -0
  39. package/dist/tools/browser/getTestIds.d.ts +12 -0
  40. package/dist/tools/browser/getTestIds.js +148 -0
  41. package/dist/tools/browser/index.d.ts +7 -0
  42. package/dist/tools/browser/index.js +7 -0
  43. package/dist/tools/browser/inspectDom.d.ts +12 -0
  44. package/dist/tools/browser/inspectDom.js +447 -0
  45. package/dist/tools/browser/interaction.d.ts +104 -0
  46. package/dist/tools/browser/interaction.js +259 -0
  47. package/dist/tools/browser/listNetworkRequests.d.ts +10 -0
  48. package/dist/tools/browser/listNetworkRequests.js +74 -0
  49. package/dist/tools/browser/measureElement.d.ts +9 -0
  50. package/dist/tools/browser/measureElement.js +139 -0
  51. package/dist/tools/browser/navigation.d.ts +38 -0
  52. package/dist/tools/browser/navigation.js +109 -0
  53. package/dist/tools/browser/output.d.ts +11 -0
  54. package/dist/tools/browser/output.js +29 -0
  55. package/dist/tools/browser/querySelectorAll.d.ts +12 -0
  56. package/dist/tools/browser/querySelectorAll.js +201 -0
  57. package/dist/tools/browser/response.d.ts +29 -0
  58. package/dist/tools/browser/response.js +67 -0
  59. package/dist/tools/browser/screenshot.d.ts +16 -0
  60. package/dist/tools/browser/screenshot.js +70 -0
  61. package/dist/tools/browser/useragent.d.ts +15 -0
  62. package/dist/tools/browser/useragent.js +32 -0
  63. package/dist/tools/browser/visiblePage.d.ts +20 -0
  64. package/dist/tools/browser/visiblePage.js +170 -0
  65. package/dist/tools/browser/waitForElement.d.ts +10 -0
  66. package/dist/tools/browser/waitForElement.js +38 -0
  67. package/dist/tools/browser/waitForNetworkIdle.d.ts +8 -0
  68. package/dist/tools/browser/waitForNetworkIdle.js +32 -0
  69. package/dist/tools/codegen/generator.d.ts +21 -0
  70. package/dist/tools/codegen/generator.js +158 -0
  71. package/dist/tools/codegen/index.d.ts +11 -0
  72. package/dist/tools/codegen/index.js +187 -0
  73. package/dist/tools/codegen/recorder.d.ts +14 -0
  74. package/dist/tools/codegen/recorder.js +62 -0
  75. package/dist/tools/codegen/types.d.ts +28 -0
  76. package/dist/tools/codegen/types.js +1 -0
  77. package/dist/tools/common/types.d.ts +17 -0
  78. package/dist/tools/common/types.js +20 -0
  79. package/dist/tools/index.d.ts +2 -0
  80. package/dist/tools/index.js +2 -0
  81. package/dist/tools.d.ts +557 -0
  82. package/dist/tools.js +554 -0
  83. package/dist/types.d.ts +16 -0
  84. package/dist/types.js +1 -0
  85. package/package.json +60 -0
@@ -0,0 +1,11 @@
1
+ import { BrowserToolBase } from './base.js';
2
+ import { ToolContext, ToolResponse } from '../common/types.js';
3
+ /**
4
+ * Tool for getting element position and size
5
+ */
6
+ export declare class ElementPositionTool extends BrowserToolBase {
7
+ /**
8
+ * Execute the element position tool
9
+ */
10
+ execute(args: any, context: ToolContext): Promise<ToolResponse>;
11
+ }
@@ -0,0 +1,107 @@
1
+ import { BrowserToolBase } from './base.js';
2
+ import { createSuccessResponse, createErrorResponse } from '../common/types.js';
3
+ /**
4
+ * Tool for getting element position and size
5
+ */
6
+ export class ElementPositionTool extends BrowserToolBase {
7
+ /**
8
+ * Execute the element position tool
9
+ */
10
+ async execute(args, context) {
11
+ return this.safeExecute(context, async (page) => {
12
+ const selector = this.normalizeSelector(args.selector);
13
+ const locator = page.locator(selector);
14
+ try {
15
+ // Check if element exists
16
+ const count = await locator.count();
17
+ if (count === 0) {
18
+ return createErrorResponse(`Element not found: ${args.selector}`);
19
+ }
20
+ // Handle multiple matches by using first() - show warning
21
+ const targetLocator = count > 1 ? locator.first() : locator;
22
+ const multipleMatchWarning = count > 1
23
+ ? `⚠ Warning: Selector matched ${count} elements, showing first:\n\n`
24
+ : '';
25
+ // Get bounding box
26
+ const boundingBox = await targetLocator.boundingBox();
27
+ if (!boundingBox) {
28
+ // Get element info for better error message
29
+ const hiddenInfo = await targetLocator.evaluate((element) => {
30
+ const styles = window.getComputedStyle(element);
31
+ const tagName = element.tagName.toLowerCase();
32
+ const testId = element.getAttribute('data-testid') || element.getAttribute('data-test') || element.getAttribute('data-cy');
33
+ let descriptor = `<${tagName}`;
34
+ if (testId)
35
+ descriptor += ` data-testid="${testId}"`;
36
+ descriptor += '>';
37
+ return {
38
+ descriptor,
39
+ display: styles.display,
40
+ opacity: styles.opacity,
41
+ visibility: styles.visibility,
42
+ width: parseFloat(styles.width) || 0,
43
+ height: parseFloat(styles.height) || 0
44
+ };
45
+ });
46
+ // Return structured response instead of error
47
+ let reason = 'unknown reason';
48
+ if (hiddenInfo.display === 'none') {
49
+ reason = 'display: none';
50
+ }
51
+ else if (hiddenInfo.width === 0 || hiddenInfo.height === 0) {
52
+ reason = 'zero size';
53
+ }
54
+ else if (parseFloat(hiddenInfo.opacity) === 0) {
55
+ reason = 'opacity: 0';
56
+ }
57
+ else if (hiddenInfo.visibility === 'hidden') {
58
+ reason = 'visibility: hidden';
59
+ }
60
+ const output = multipleMatchWarning +
61
+ `Position: ${hiddenInfo.descriptor}\n` +
62
+ `@ null (element hidden: ${reason})\n` +
63
+ `display: ${hiddenInfo.display}, opacity: ${hiddenInfo.opacity}, visibility: ${hiddenInfo.visibility}`;
64
+ return createSuccessResponse(output);
65
+ }
66
+ // Check if in viewport and get element tag info
67
+ const elementData = await targetLocator.evaluate((element) => {
68
+ const rect = element.getBoundingClientRect();
69
+ const viewportHeight = window.innerHeight;
70
+ const viewportWidth = window.innerWidth;
71
+ // Element is in viewport if any part is visible
72
+ const inViewport = (rect.bottom > 0 &&
73
+ rect.right > 0 &&
74
+ rect.top < viewportHeight &&
75
+ rect.left < viewportWidth);
76
+ // Build element descriptor
77
+ const tagName = element.tagName.toLowerCase();
78
+ const testId = element.getAttribute('data-testid') || element.getAttribute('data-test') || element.getAttribute('data-cy');
79
+ const id = element.id ? `#${element.id}` : '';
80
+ const classes = element.className && typeof element.className === 'string' ? `.${element.className.split(' ').filter(c => c).join('.')}` : '';
81
+ let descriptor = `<${tagName}`;
82
+ if (testId)
83
+ descriptor += ` data-testid="${testId}"`;
84
+ else if (id)
85
+ descriptor += id;
86
+ else if (classes)
87
+ descriptor += classes;
88
+ descriptor += '>';
89
+ return { inViewport, descriptor };
90
+ });
91
+ // Build compact text format
92
+ const x = Math.round(boundingBox.x);
93
+ const y = Math.round(boundingBox.y);
94
+ const width = Math.round(boundingBox.width);
95
+ const height = Math.round(boundingBox.height);
96
+ const viewportSymbol = elementData.inViewport ? '✓' : '✗';
97
+ const viewportStatus = elementData.inViewport ? 'in viewport' : 'outside viewport';
98
+ const output = multipleMatchWarning +
99
+ `Position: ${elementData.descriptor}\n@ (${x},${y}) ${width}x${height}px, ${viewportSymbol} ${viewportStatus}`;
100
+ return createSuccessResponse(output);
101
+ }
102
+ catch (error) {
103
+ return createErrorResponse(`Failed to get position: ${error.message}`);
104
+ }
105
+ });
106
+ }
107
+ }
@@ -0,0 +1,12 @@
1
+ import { BrowserToolBase } from './base.js';
2
+ import { ToolContext, ToolResponse } from '../common/types.js';
3
+ /**
4
+ * Tool for checking element visibility with detailed diagnostics
5
+ * Addresses the #1 debugging pain point: "Why won't it click?"
6
+ */
7
+ export declare class ElementVisibilityTool extends BrowserToolBase {
8
+ /**
9
+ * Execute the element visibility tool
10
+ */
11
+ execute(args: any, context: ToolContext): Promise<ToolResponse>;
12
+ }
@@ -0,0 +1,224 @@
1
+ import { BrowserToolBase } from './base.js';
2
+ import { createSuccessResponse, createErrorResponse } from '../common/types.js';
3
+ /**
4
+ * Tool for checking element visibility with detailed diagnostics
5
+ * Addresses the #1 debugging pain point: "Why won't it click?"
6
+ */
7
+ export class ElementVisibilityTool extends BrowserToolBase {
8
+ /**
9
+ * Execute the element visibility tool
10
+ */
11
+ async execute(args, context) {
12
+ return this.safeExecute(context, async (page) => {
13
+ const selector = this.normalizeSelector(args.selector);
14
+ const locator = page.locator(selector);
15
+ try {
16
+ // Check if element exists
17
+ const count = await locator.count();
18
+ if (count === 0) {
19
+ return createErrorResponse(`Element not found: ${args.selector}`);
20
+ }
21
+ // Handle multiple matches by using first() - show warning
22
+ const targetLocator = count > 1 ? locator.first() : locator;
23
+ const multipleMatchWarning = count > 1
24
+ ? `⚠ Warning: Selector matched ${count} elements, showing first:\n\n`
25
+ : '';
26
+ // Get basic visibility (Playwright's isVisible)
27
+ const isVisible = await targetLocator.isVisible();
28
+ // Evaluate detailed visibility information in browser context
29
+ const visibilityData = await targetLocator.evaluate((element) => {
30
+ const rect = element.getBoundingClientRect();
31
+ const viewportHeight = window.innerHeight;
32
+ const viewportWidth = window.innerWidth;
33
+ // Calculate viewport intersection ratio
34
+ const visibleTop = Math.max(0, rect.top);
35
+ const visibleBottom = Math.min(viewportHeight, rect.bottom);
36
+ const visibleLeft = Math.max(0, rect.left);
37
+ const visibleRight = Math.min(viewportWidth, rect.right);
38
+ const visibleHeight = Math.max(0, visibleBottom - visibleTop);
39
+ const visibleWidth = Math.max(0, visibleRight - visibleLeft);
40
+ const visibleArea = visibleHeight * visibleWidth;
41
+ const totalArea = rect.height * rect.width;
42
+ const viewportRatio = totalArea > 0 ? visibleArea / totalArea : 0;
43
+ // Check if element is in viewport
44
+ const isInViewport = viewportRatio > 0;
45
+ // Get computed styles
46
+ const styles = window.getComputedStyle(element);
47
+ const opacity = parseFloat(styles.opacity);
48
+ const display = styles.display;
49
+ const visibility = styles.visibility;
50
+ // Check if clipped by overflow:hidden
51
+ let isClipped = false;
52
+ let parent = element.parentElement;
53
+ while (parent) {
54
+ const parentStyle = window.getComputedStyle(parent);
55
+ if (parentStyle.overflow === 'hidden' ||
56
+ parentStyle.overflowX === 'hidden' ||
57
+ parentStyle.overflowY === 'hidden') {
58
+ const parentRect = parent.getBoundingClientRect();
59
+ // Check if element is outside parent bounds
60
+ if (rect.right < parentRect.left ||
61
+ rect.left > parentRect.right ||
62
+ rect.bottom < parentRect.top ||
63
+ rect.top > parentRect.bottom) {
64
+ isClipped = true;
65
+ break;
66
+ }
67
+ }
68
+ parent = parent.parentElement;
69
+ }
70
+ // Check if covered by another element (check center point and corners)
71
+ const centerX = rect.left + rect.width / 2;
72
+ const centerY = rect.top + rect.height / 2;
73
+ const topElement = document.elementFromPoint(centerX, centerY);
74
+ const isCovered = topElement !== element && !element.contains(topElement);
75
+ // Get covering element info if covered
76
+ let coveringElementInfo = '';
77
+ let coveragePercent = 0;
78
+ if (isCovered && topElement) {
79
+ const coveringTagName = topElement.tagName.toLowerCase();
80
+ const coveringTestId = topElement.getAttribute('data-testid');
81
+ const coveringId = topElement.id ? `#${topElement.id}` : '';
82
+ const coveringClasses = topElement.className && typeof topElement.className === 'string'
83
+ ? `.${topElement.className.split(' ').filter((c) => c).slice(0, 2).join('.')}`
84
+ : '';
85
+ const coveringStyles = window.getComputedStyle(topElement);
86
+ const zIndex = coveringStyles.zIndex;
87
+ let descriptor = `<${coveringTagName}`;
88
+ if (coveringTestId)
89
+ descriptor += ` data-testid="${coveringTestId}"`;
90
+ else if (coveringId)
91
+ descriptor += coveringId;
92
+ else if (coveringClasses)
93
+ descriptor += coveringClasses;
94
+ descriptor += `> (z-index: ${zIndex})`;
95
+ coveringElementInfo = descriptor;
96
+ // Calculate approximate coverage by checking multiple points
97
+ const samplePoints = [
98
+ [centerX, centerY],
99
+ [rect.left + rect.width * 0.25, rect.top + rect.height * 0.25],
100
+ [rect.left + rect.width * 0.75, rect.top + rect.height * 0.25],
101
+ [rect.left + rect.width * 0.25, rect.top + rect.height * 0.75],
102
+ [rect.left + rect.width * 0.75, rect.top + rect.height * 0.75],
103
+ ];
104
+ let coveredPoints = 0;
105
+ samplePoints.forEach(([x, y]) => {
106
+ const pointElement = document.elementFromPoint(x, y);
107
+ if (pointElement !== element && !element.contains(pointElement)) {
108
+ coveredPoints++;
109
+ }
110
+ });
111
+ coveragePercent = Math.round((coveredPoints / samplePoints.length) * 100);
112
+ }
113
+ // Check interactability
114
+ const computedStyles = window.getComputedStyle(element);
115
+ const pointerEvents = computedStyles.pointerEvents;
116
+ const isDisabled = element.disabled || false;
117
+ const isReadonly = element.readOnly || false;
118
+ const ariaDisabled = element.getAttribute('aria-disabled') === 'true';
119
+ return {
120
+ viewportRatio,
121
+ isInViewport,
122
+ opacity,
123
+ display,
124
+ visibility,
125
+ isClipped,
126
+ isCovered,
127
+ coveringElementInfo,
128
+ coveragePercent,
129
+ pointerEvents,
130
+ isDisabled,
131
+ isReadonly,
132
+ ariaDisabled,
133
+ };
134
+ });
135
+ // Determine if scroll is needed
136
+ const needsScroll = isVisible && !visibilityData.isInViewport;
137
+ // Get element tag name for output
138
+ const tagInfo = await targetLocator.evaluate((el) => {
139
+ const tagName = el.tagName.toLowerCase();
140
+ const testId = el.getAttribute('data-testid') || el.getAttribute('data-test') || el.getAttribute('data-cy');
141
+ const id = el.id ? `#${el.id}` : '';
142
+ const classes = el.className && typeof el.className === 'string' ? `.${el.className.split(' ').filter(c => c).join('.')}` : '';
143
+ let descriptor = `<${tagName}`;
144
+ if (testId)
145
+ descriptor += ` data-testid="${testId}"`;
146
+ else if (id)
147
+ descriptor += id;
148
+ else if (classes)
149
+ descriptor += classes;
150
+ descriptor += '>';
151
+ return descriptor;
152
+ });
153
+ // Build compact text format
154
+ const viewportPercent = Math.round(visibilityData.viewportRatio * 100);
155
+ let output = multipleMatchWarning + `Visibility: ${tagInfo}\n\n`;
156
+ // Status line
157
+ const visibleSymbol = isVisible ? '✓' : '✗';
158
+ const viewportSymbol = visibilityData.isInViewport ? '✓' : '✗';
159
+ const viewportText = visibilityData.isInViewport
160
+ ? `in viewport${viewportPercent < 100 ? ` (${viewportPercent}% visible)` : ''}`
161
+ : `not in viewport${viewportPercent > 0 ? ` (${viewportPercent}% visible)` : ''}`;
162
+ output += `${visibleSymbol} ${isVisible ? 'visible' : 'hidden'}, ${viewportSymbol} ${viewportText}\n`;
163
+ // CSS properties
164
+ output += `opacity: ${visibilityData.opacity}, display: ${visibilityData.display}, visibility: ${visibilityData.visibility}\n`;
165
+ // Interactability section
166
+ const interactabilityIssues = [];
167
+ if (visibilityData.isDisabled) {
168
+ interactabilityIssues.push('disabled');
169
+ }
170
+ if (visibilityData.isReadonly) {
171
+ interactabilityIssues.push('readonly');
172
+ }
173
+ if (visibilityData.ariaDisabled) {
174
+ interactabilityIssues.push('aria-disabled');
175
+ }
176
+ if (visibilityData.pointerEvents === 'none') {
177
+ interactabilityIssues.push('pointer-events: none');
178
+ }
179
+ if (interactabilityIssues.length > 0) {
180
+ output += `⚠ interactability: ${interactabilityIssues.join(', ')}\n`;
181
+ }
182
+ // Issues section
183
+ const issues = [];
184
+ if (visibilityData.isClipped) {
185
+ issues.push(' ✗ clipped by parent overflow:hidden');
186
+ }
187
+ if (visibilityData.isCovered) {
188
+ const coverageInfo = visibilityData.coveragePercent > 0
189
+ ? ` (~${visibilityData.coveragePercent}% covered)`
190
+ : '';
191
+ issues.push(` ✗ covered by another element${coverageInfo}`);
192
+ if (visibilityData.coveringElementInfo) {
193
+ issues.push(` Covering: ${visibilityData.coveringElementInfo}`);
194
+ }
195
+ }
196
+ if (needsScroll) {
197
+ issues.push(' ⚠ needs scroll to bring into view');
198
+ }
199
+ if (issues.length > 0) {
200
+ output += '\nIssues:\n';
201
+ output += issues.join('\n') + '\n';
202
+ }
203
+ // Suggestions
204
+ const suggestions = [];
205
+ if (needsScroll) {
206
+ suggestions.push('→ Call scroll_to_element before clicking');
207
+ }
208
+ if (visibilityData.isCovered) {
209
+ suggestions.push('→ Element may be behind modal, overlay, or fixed header');
210
+ }
211
+ if (interactabilityIssues.length > 0) {
212
+ suggestions.push('→ Element cannot be interacted with in current state');
213
+ }
214
+ if (suggestions.length > 0) {
215
+ output += '\n' + suggestions.join('\n');
216
+ }
217
+ return createSuccessResponse(output.trim());
218
+ }
219
+ catch (error) {
220
+ return createErrorResponse(`Failed to check visibility: ${error.message}`);
221
+ }
222
+ });
223
+ }
224
+ }
@@ -0,0 +1,13 @@
1
+ import { ToolHandler } from '../common/types.js';
2
+ import { BrowserToolBase } from './base.js';
3
+ import type { ToolContext, ToolResponse } from '../common/types.js';
4
+ export interface FindByTextArgs {
5
+ text: string;
6
+ exact?: boolean;
7
+ caseSensitive?: boolean;
8
+ regex?: boolean;
9
+ limit?: number;
10
+ }
11
+ export declare class FindByTextTool extends BrowserToolBase implements ToolHandler {
12
+ execute(args: FindByTextArgs, context: ToolContext): Promise<ToolResponse>;
13
+ }
@@ -0,0 +1,207 @@
1
+ import { BrowserToolBase } from './base.js';
2
+ export class FindByTextTool extends BrowserToolBase {
3
+ async execute(args, context) {
4
+ return this.safeExecute(context, async (page) => {
5
+ const { text, exact = false, caseSensitive = false, regex = false, limit = 10 } = args;
6
+ // Build the text selector based on exact, caseSensitive, and regex options
7
+ let selector;
8
+ if (regex) {
9
+ // Validate and use user-provided regex pattern
10
+ try {
11
+ // Extract pattern and flags from /pattern/flags format
12
+ const regexMatch = text.match(/^\/(.+?)\/([gimuy]*)$/);
13
+ if (regexMatch) {
14
+ const [, pattern, flags] = regexMatch;
15
+ // Validate the regex pattern
16
+ new RegExp(pattern, flags);
17
+ selector = `text=/${pattern}/${flags}`;
18
+ }
19
+ else {
20
+ // If not in /pattern/flags format, treat as raw pattern
21
+ new RegExp(text);
22
+ selector = `text=/${text}/`;
23
+ }
24
+ }
25
+ catch (error) {
26
+ return {
27
+ content: [
28
+ {
29
+ type: 'text',
30
+ text: `✗ Invalid regex pattern: ${error.message}`
31
+ }
32
+ ],
33
+ isError: true
34
+ };
35
+ }
36
+ }
37
+ else if (exact) {
38
+ selector = `text="${text}"`;
39
+ }
40
+ else {
41
+ // Use regex for partial match with case sensitivity
42
+ const flags = caseSensitive ? '' : 'i';
43
+ const escapedText = text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
44
+ selector = `text=/${escapedText}/${flags}`;
45
+ }
46
+ // Find all matching elements
47
+ const locator = page.locator(selector);
48
+ const count = await locator.count();
49
+ if (count === 0) {
50
+ // Build "not found" message based on search type
51
+ let notFoundMsg;
52
+ if (regex) {
53
+ notFoundMsg = `✗ No elements found matching regex ${text}`;
54
+ }
55
+ else if (exact) {
56
+ notFoundMsg = `✗ No elements found with exact text "${text}"`;
57
+ }
58
+ else {
59
+ notFoundMsg = `✗ No elements found containing "${text}"`;
60
+ }
61
+ return {
62
+ content: [
63
+ {
64
+ type: 'text',
65
+ text: notFoundMsg
66
+ }
67
+ ],
68
+ isError: false
69
+ };
70
+ }
71
+ // Limit results
72
+ const elementsToShow = Math.min(count, limit);
73
+ const elements = [];
74
+ for (let i = 0; i < elementsToShow; i++) {
75
+ const element = locator.nth(i);
76
+ // Get element info
77
+ const [tagName, boundingBox, textContent, isVisible, isEnabled] = await Promise.all([
78
+ element.evaluate((el) => el.tagName.toLowerCase()),
79
+ element.boundingBox().catch(() => null),
80
+ element.textContent().catch(() => ''),
81
+ element.isVisible().catch(() => false),
82
+ element.isEnabled().catch(() => true)
83
+ ]);
84
+ // Get selector attributes for better identification
85
+ const selectorInfo = await element.evaluate((el) => {
86
+ const attrs = {};
87
+ if (el.id)
88
+ attrs.id = el.id;
89
+ if (el.className && typeof el.className === 'string') {
90
+ attrs.class = el.className.split(' ').slice(0, 2).join(' ');
91
+ }
92
+ const testId = el.getAttribute('data-testid') || el.getAttribute('data-test') || el.getAttribute('data-cy');
93
+ if (testId)
94
+ attrs.testid = testId;
95
+ if (el.getAttribute('href'))
96
+ attrs.href = el.getAttribute('href') || '';
97
+ if (el.getAttribute('name'))
98
+ attrs.name = el.getAttribute('name') || '';
99
+ if (el.getAttribute('type'))
100
+ attrs.type = el.getAttribute('type') || '';
101
+ if (el.getAttribute('aria-label'))
102
+ attrs['aria-label'] = el.getAttribute('aria-label') || '';
103
+ return attrs;
104
+ });
105
+ // Build selector string
106
+ let selectorStr = `<${tagName}`;
107
+ if (selectorInfo.id)
108
+ selectorStr += `#${selectorInfo.id}`;
109
+ if (selectorInfo.class)
110
+ selectorStr += ` class="${selectorInfo.class}"`;
111
+ if (selectorInfo.testid)
112
+ selectorStr += ` data-testid="${selectorInfo.testid}"`;
113
+ if (selectorInfo.name)
114
+ selectorStr += ` name="${selectorInfo.name}"`;
115
+ if (selectorInfo.type)
116
+ selectorStr += ` type="${selectorInfo.type}"`;
117
+ if (selectorInfo.href)
118
+ selectorStr += ` href="${selectorInfo.href.slice(0, 30)}${selectorInfo.href.length > 30 ? '...' : ''}"`;
119
+ if (selectorInfo['aria-label'])
120
+ selectorStr += ` aria-label="${selectorInfo['aria-label']}"`;
121
+ selectorStr += '>';
122
+ // Format position
123
+ let positionStr = '';
124
+ if (boundingBox) {
125
+ const x = Math.round(boundingBox.x);
126
+ const y = Math.round(boundingBox.y);
127
+ const w = Math.round(boundingBox.width);
128
+ const h = Math.round(boundingBox.height);
129
+ positionStr = ` @ (${x},${y}) ${w}x${h}px`;
130
+ }
131
+ else {
132
+ positionStr = ` @ (no bounding box)`;
133
+ }
134
+ // Format text content (truncate if too long)
135
+ const truncatedText = textContent && textContent.length > 100
136
+ ? textContent.slice(0, 100) + '...'
137
+ : textContent;
138
+ const textStr = ` "${truncatedText}"`;
139
+ // Format state
140
+ let stateStr = ' ';
141
+ if (isVisible) {
142
+ stateStr += '✓ visible';
143
+ }
144
+ else {
145
+ stateStr += '✗ hidden';
146
+ // Try to detect why it's hidden
147
+ const hiddenReason = await element.evaluate((el) => {
148
+ const style = window.getComputedStyle(el);
149
+ if (style.display === 'none')
150
+ return '(display: none)';
151
+ if (style.visibility === 'hidden')
152
+ return '(visibility: hidden)';
153
+ if (style.opacity === '0')
154
+ return '(opacity: 0)';
155
+ const rect = el.getBoundingClientRect();
156
+ if (rect.width === 0 || rect.height === 0)
157
+ return '(zero size)';
158
+ return '';
159
+ }).catch(() => '');
160
+ if (hiddenReason)
161
+ stateStr += ` ${hiddenReason}`;
162
+ }
163
+ // Check if interactive
164
+ const isInteractive = await element.evaluate((el) => {
165
+ const tag = el.tagName.toLowerCase();
166
+ if (['a', 'button', 'input', 'select', 'textarea'].includes(tag))
167
+ return true;
168
+ if (el.getAttribute('onclick'))
169
+ return true;
170
+ if (el.getAttribute('role') === 'button')
171
+ return true;
172
+ return false;
173
+ }).catch(() => false);
174
+ if (isVisible && isInteractive && isEnabled) {
175
+ stateStr += ', ⚡ interactive';
176
+ }
177
+ else if (isVisible && isInteractive && !isEnabled) {
178
+ stateStr += ', ✗ disabled';
179
+ }
180
+ elements.push(`[${i}] ${selectorStr}\n${positionStr}\n${textStr}\n${stateStr}`);
181
+ }
182
+ // Build header message based on search type
183
+ let searchDesc;
184
+ if (regex) {
185
+ searchDesc = `matching regex ${text}`;
186
+ }
187
+ else if (exact) {
188
+ searchDesc = `with exact text "${text}"`;
189
+ }
190
+ else {
191
+ searchDesc = `containing "${text}"`;
192
+ }
193
+ const header = count > limit
194
+ ? `Found ${count} elements ${searchDesc} (showing first ${limit}):\n`
195
+ : `Found ${count} element${count > 1 ? 's' : ''} ${searchDesc}:\n`;
196
+ return {
197
+ content: [
198
+ {
199
+ type: 'text',
200
+ text: header + '\n' + elements.join('\n\n')
201
+ }
202
+ ],
203
+ isError: false
204
+ };
205
+ });
206
+ }
207
+ }
@@ -0,0 +1,9 @@
1
+ import type { ToolContext, ToolResponse } from '../common/types.js';
2
+ import { BrowserToolBase } from './base.js';
3
+ interface GetRequestDetailsArgs {
4
+ index: number;
5
+ }
6
+ export declare class GetRequestDetailsTool extends BrowserToolBase {
7
+ execute(args: GetRequestDetailsArgs, context: ToolContext): Promise<ToolResponse>;
8
+ }
9
+ export {};