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,725 @@
|
|
|
1
|
+
import { chromium, firefox, webkit, devices } from 'playwright';
|
|
2
|
+
import { BROWSER_TOOLS } from './tools.js';
|
|
3
|
+
import { ScreenshotTool, NavigationTool, CloseBrowserTool, ConsoleLogsTool } from './tools/browser/index.js';
|
|
4
|
+
import { ClickTool, FillTool, SelectTool, HoverTool, EvaluateTool, UploadFileTool } from './tools/browser/interaction.js';
|
|
5
|
+
import { VisibleTextTool, VisibleHtmlTool } from './tools/browser/visiblePage.js';
|
|
6
|
+
import { ElementVisibilityTool } from './tools/browser/elementVisibility.js';
|
|
7
|
+
import { ElementPositionTool } from './tools/browser/elementPosition.js';
|
|
8
|
+
import { InspectDomTool } from './tools/browser/inspectDom.js';
|
|
9
|
+
import { GetTestIdsTool } from './tools/browser/getTestIds.js';
|
|
10
|
+
import { QuerySelectorAllTool } from './tools/browser/querySelectorAll.js';
|
|
11
|
+
import { FindByTextTool } from './tools/browser/findByText.js';
|
|
12
|
+
import { GetComputedStylesTool } from './tools/browser/computedStyles.js';
|
|
13
|
+
import { MeasureElementTool } from './tools/browser/measureElement.js';
|
|
14
|
+
import { ElementExistsTool } from './tools/browser/elementExists.js';
|
|
15
|
+
import { ComparePositionsTool } from './tools/browser/comparePositions.js';
|
|
16
|
+
import { GoBackTool, GoForwardTool } from './tools/browser/navigation.js';
|
|
17
|
+
import { DragTool, PressKeyTool } from './tools/browser/interaction.js';
|
|
18
|
+
import { WaitForElementTool } from './tools/browser/waitForElement.js';
|
|
19
|
+
import { WaitForNetworkIdleTool } from './tools/browser/waitForNetworkIdle.js';
|
|
20
|
+
import { ListNetworkRequestsTool } from './tools/browser/listNetworkRequests.js';
|
|
21
|
+
import { GetRequestDetailsTool } from './tools/browser/getRequestDetails.js';
|
|
22
|
+
// Global state
|
|
23
|
+
let browser;
|
|
24
|
+
let page;
|
|
25
|
+
let currentBrowserType = 'chromium';
|
|
26
|
+
let networkLog = [];
|
|
27
|
+
let sessionConfig = {
|
|
28
|
+
saveSession: false,
|
|
29
|
+
userDataDir: './.mcp-web-inspector/user-data',
|
|
30
|
+
screenshotsDir: './.mcp-web-inspector/screenshots',
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* Sets the session configuration
|
|
34
|
+
*/
|
|
35
|
+
export function setSessionConfig(config) {
|
|
36
|
+
sessionConfig = { ...sessionConfig, ...config };
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Gets the screenshots directory
|
|
40
|
+
*/
|
|
41
|
+
export function getScreenshotsDir() {
|
|
42
|
+
return sessionConfig.screenshotsDir;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Resets browser and page variables
|
|
46
|
+
* Used when browser is closed
|
|
47
|
+
*/
|
|
48
|
+
export function resetBrowserState() {
|
|
49
|
+
browser = undefined;
|
|
50
|
+
page = undefined;
|
|
51
|
+
currentBrowserType = 'chromium';
|
|
52
|
+
networkLog = [];
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Gets the network log
|
|
56
|
+
*/
|
|
57
|
+
export function getNetworkLog() {
|
|
58
|
+
return networkLog;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Clears the network log
|
|
62
|
+
*/
|
|
63
|
+
export function clearNetworkLog() {
|
|
64
|
+
networkLog = [];
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Sets the provided page to the global page variable
|
|
68
|
+
* @param newPage The Page object to set as the global page
|
|
69
|
+
*/
|
|
70
|
+
export function setGlobalPage(newPage) {
|
|
71
|
+
page = newPage;
|
|
72
|
+
page.bringToFront(); // Bring the new tab to the front
|
|
73
|
+
console.log("Global page has been updated.");
|
|
74
|
+
}
|
|
75
|
+
// Tool instances
|
|
76
|
+
let screenshotTool;
|
|
77
|
+
let navigationTool;
|
|
78
|
+
let closeBrowserTool;
|
|
79
|
+
let consoleLogsTool;
|
|
80
|
+
let clickTool;
|
|
81
|
+
let fillTool;
|
|
82
|
+
let selectTool;
|
|
83
|
+
let hoverTool;
|
|
84
|
+
let uploadFileTool;
|
|
85
|
+
let evaluateTool;
|
|
86
|
+
let visibleTextTool;
|
|
87
|
+
let visibleHtmlTool;
|
|
88
|
+
let goBackTool;
|
|
89
|
+
let goForwardTool;
|
|
90
|
+
let dragTool;
|
|
91
|
+
let pressKeyTool;
|
|
92
|
+
let elementVisibilityTool;
|
|
93
|
+
let elementPositionTool;
|
|
94
|
+
let inspectDomTool;
|
|
95
|
+
let getTestIdsTool;
|
|
96
|
+
let querySelectorAllTool;
|
|
97
|
+
let findByTextTool;
|
|
98
|
+
let getComputedStylesTool;
|
|
99
|
+
let measureElementTool;
|
|
100
|
+
let elementExistsTool;
|
|
101
|
+
let comparePositionsTool;
|
|
102
|
+
let waitForElementTool;
|
|
103
|
+
let waitForNetworkIdleTool;
|
|
104
|
+
let listNetworkRequestsTool;
|
|
105
|
+
let getRequestDetailsTool;
|
|
106
|
+
/**
|
|
107
|
+
* Device preset mapping to Playwright device descriptors
|
|
108
|
+
*/
|
|
109
|
+
const DEVICE_PRESETS = {
|
|
110
|
+
'iphone-se': 'iPhone SE',
|
|
111
|
+
'iphone-14': 'iPhone 14',
|
|
112
|
+
'iphone-14-pro': 'iPhone 14 Pro',
|
|
113
|
+
'pixel-5': 'Pixel 5',
|
|
114
|
+
'ipad': 'iPad (gen 7)',
|
|
115
|
+
'samsung-s21': 'Galaxy S21'
|
|
116
|
+
};
|
|
117
|
+
/**
|
|
118
|
+
* Register network event listeners
|
|
119
|
+
*/
|
|
120
|
+
async function registerNetworkListeners(page) {
|
|
121
|
+
page.on('request', (request) => {
|
|
122
|
+
networkLog.push({
|
|
123
|
+
index: networkLog.length,
|
|
124
|
+
method: request.method(),
|
|
125
|
+
url: request.url(),
|
|
126
|
+
resourceType: request.resourceType(),
|
|
127
|
+
timestamp: Date.now(),
|
|
128
|
+
requestData: {
|
|
129
|
+
headers: request.headers(),
|
|
130
|
+
postData: request.postData() || null
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
page.on('response', async (response) => {
|
|
135
|
+
// Find the matching request by URL and method (most recent match)
|
|
136
|
+
const url = response.url();
|
|
137
|
+
const method = response.request().method();
|
|
138
|
+
for (let i = networkLog.length - 1; i >= 0; i--) {
|
|
139
|
+
if (networkLog[i].url === url &&
|
|
140
|
+
networkLog[i].method === method &&
|
|
141
|
+
!networkLog[i].status) {
|
|
142
|
+
networkLog[i].status = response.status();
|
|
143
|
+
networkLog[i].statusText = response.statusText();
|
|
144
|
+
networkLog[i].timing = Date.now() - networkLog[i].timestamp;
|
|
145
|
+
// Try to capture response body (may fail for some resource types)
|
|
146
|
+
let responseBody = null;
|
|
147
|
+
try {
|
|
148
|
+
responseBody = await response.text();
|
|
149
|
+
}
|
|
150
|
+
catch (e) {
|
|
151
|
+
// Ignore errors (e.g., image/binary responses)
|
|
152
|
+
responseBody = null;
|
|
153
|
+
}
|
|
154
|
+
networkLog[i].responseData = {
|
|
155
|
+
headers: response.headers(),
|
|
156
|
+
body: responseBody
|
|
157
|
+
};
|
|
158
|
+
break;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
async function registerConsoleMessage(page) {
|
|
164
|
+
page.on("console", (msg) => {
|
|
165
|
+
if (consoleLogsTool) {
|
|
166
|
+
const type = msg.type();
|
|
167
|
+
const text = msg.text();
|
|
168
|
+
// "Unhandled Rejection In Promise" we injected
|
|
169
|
+
if (text.startsWith("[Playwright]")) {
|
|
170
|
+
const payload = text.replace("[Playwright]", "");
|
|
171
|
+
consoleLogsTool.registerConsoleMessage("exception", payload);
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
consoleLogsTool.registerConsoleMessage(type, text);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
// Uncaught exception
|
|
179
|
+
page.on("pageerror", (error) => {
|
|
180
|
+
if (consoleLogsTool) {
|
|
181
|
+
const message = error.message;
|
|
182
|
+
const stack = error.stack || "";
|
|
183
|
+
const truncatedStack = stack
|
|
184
|
+
? '\n ' + stack.split('\n').slice(0, 3).join('\n ') + '\n ...[truncated]'
|
|
185
|
+
: '';
|
|
186
|
+
consoleLogsTool.registerConsoleMessage("exception", `${message}${truncatedStack}`);
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
// Unhandled rejection in promise
|
|
190
|
+
await page.addInitScript(() => {
|
|
191
|
+
window.addEventListener("unhandledrejection", (event) => {
|
|
192
|
+
const reason = event.reason;
|
|
193
|
+
const message = typeof reason === "object" && reason !== null
|
|
194
|
+
? reason.message || JSON.stringify(reason)
|
|
195
|
+
: String(reason);
|
|
196
|
+
const stack = reason?.stack || "";
|
|
197
|
+
const truncatedStack = stack
|
|
198
|
+
? '\n ' + stack.split('\n').slice(0, 3).join('\n ') + '\n ...[truncated]'
|
|
199
|
+
: '';
|
|
200
|
+
// Use console.error get "Unhandled Rejection In Promise"
|
|
201
|
+
console.error(`[Playwright][Unhandled Rejection In Promise] ${message}${truncatedStack}`);
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Ensures a browser is launched and returns the page
|
|
207
|
+
*/
|
|
208
|
+
export async function ensureBrowser(browserSettings) {
|
|
209
|
+
try {
|
|
210
|
+
// Check if browser exists but is disconnected
|
|
211
|
+
if (browser && !browser.isConnected()) {
|
|
212
|
+
console.error("Browser exists but is disconnected. Cleaning up...");
|
|
213
|
+
try {
|
|
214
|
+
await browser.close().catch(err => console.error("Error closing disconnected browser:", err));
|
|
215
|
+
}
|
|
216
|
+
catch (e) {
|
|
217
|
+
// Ignore errors when closing disconnected browser
|
|
218
|
+
}
|
|
219
|
+
// Reset browser and page references
|
|
220
|
+
resetBrowserState();
|
|
221
|
+
}
|
|
222
|
+
// Launch new browser if needed
|
|
223
|
+
if (!browser) {
|
|
224
|
+
const { viewport, userAgent, headless = false, browserType = 'chromium', device } = browserSettings ?? {};
|
|
225
|
+
// If browser type is changing, force a new browser instance
|
|
226
|
+
if (browser && currentBrowserType !== browserType) {
|
|
227
|
+
try {
|
|
228
|
+
await browser.close().catch(err => console.error("Error closing browser on type change:", err));
|
|
229
|
+
}
|
|
230
|
+
catch (e) {
|
|
231
|
+
// Ignore errors
|
|
232
|
+
}
|
|
233
|
+
resetBrowserState();
|
|
234
|
+
}
|
|
235
|
+
// Get device configuration if device preset is specified
|
|
236
|
+
let deviceConfig = null;
|
|
237
|
+
if (device && DEVICE_PRESETS[device]) {
|
|
238
|
+
const playwrightDeviceName = DEVICE_PRESETS[device];
|
|
239
|
+
deviceConfig = devices[playwrightDeviceName];
|
|
240
|
+
if (deviceConfig) {
|
|
241
|
+
console.error(`Using device preset: ${device} (${playwrightDeviceName})`);
|
|
242
|
+
}
|
|
243
|
+
else {
|
|
244
|
+
console.error(`Warning: Device preset ${playwrightDeviceName} not found in Playwright devices`);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
console.error(`Launching new ${browserType} browser instance...`);
|
|
248
|
+
// Use the appropriate browser engine
|
|
249
|
+
let browserInstance;
|
|
250
|
+
switch (browserType) {
|
|
251
|
+
case 'firefox':
|
|
252
|
+
browserInstance = firefox;
|
|
253
|
+
break;
|
|
254
|
+
case 'webkit':
|
|
255
|
+
browserInstance = webkit;
|
|
256
|
+
break;
|
|
257
|
+
case 'chromium':
|
|
258
|
+
default:
|
|
259
|
+
browserInstance = chromium;
|
|
260
|
+
break;
|
|
261
|
+
}
|
|
262
|
+
const executablePath = process.env.CHROME_EXECUTABLE_PATH;
|
|
263
|
+
// Prepare context options
|
|
264
|
+
const contextOptions = {
|
|
265
|
+
headless,
|
|
266
|
+
executablePath: executablePath,
|
|
267
|
+
};
|
|
268
|
+
// If device config exists, use it; otherwise use manual viewport/userAgent
|
|
269
|
+
if (deviceConfig) {
|
|
270
|
+
Object.assign(contextOptions, deviceConfig);
|
|
271
|
+
}
|
|
272
|
+
else {
|
|
273
|
+
if (userAgent) {
|
|
274
|
+
contextOptions.userAgent = userAgent;
|
|
275
|
+
}
|
|
276
|
+
contextOptions.viewport = {
|
|
277
|
+
width: viewport?.width ?? 1280,
|
|
278
|
+
height: viewport?.height ?? 720,
|
|
279
|
+
};
|
|
280
|
+
contextOptions.deviceScaleFactor = 1;
|
|
281
|
+
}
|
|
282
|
+
// Use persistent context if session saving is enabled
|
|
283
|
+
if (sessionConfig.saveSession) {
|
|
284
|
+
console.error(`Launching ${browserType} with persistent context at ${sessionConfig.userDataDir}...`);
|
|
285
|
+
const context = await browserInstance.launchPersistentContext(sessionConfig.userDataDir, contextOptions);
|
|
286
|
+
// Get the browser instance from the context
|
|
287
|
+
browser = context.browser();
|
|
288
|
+
currentBrowserType = browserType;
|
|
289
|
+
// Add cleanup logic when browser is disconnected
|
|
290
|
+
browser.on('disconnected', () => {
|
|
291
|
+
console.error("Browser disconnected event triggered");
|
|
292
|
+
browser = undefined;
|
|
293
|
+
page = undefined;
|
|
294
|
+
});
|
|
295
|
+
// Get or create the first page
|
|
296
|
+
const pages = context.pages();
|
|
297
|
+
page = pages.length > 0 ? pages[0] : await context.newPage();
|
|
298
|
+
}
|
|
299
|
+
else {
|
|
300
|
+
browser = await browserInstance.launch({
|
|
301
|
+
headless,
|
|
302
|
+
executablePath: executablePath
|
|
303
|
+
});
|
|
304
|
+
currentBrowserType = browserType;
|
|
305
|
+
// Add cleanup logic when browser is disconnected
|
|
306
|
+
browser.on('disconnected', () => {
|
|
307
|
+
console.error("Browser disconnected event triggered");
|
|
308
|
+
browser = undefined;
|
|
309
|
+
page = undefined;
|
|
310
|
+
});
|
|
311
|
+
// Prepare new context options (without headless and executablePath which are for launch)
|
|
312
|
+
const newContextOptions = {};
|
|
313
|
+
if (deviceConfig) {
|
|
314
|
+
Object.assign(newContextOptions, deviceConfig);
|
|
315
|
+
}
|
|
316
|
+
else {
|
|
317
|
+
if (userAgent) {
|
|
318
|
+
newContextOptions.userAgent = userAgent;
|
|
319
|
+
}
|
|
320
|
+
newContextOptions.viewport = {
|
|
321
|
+
width: viewport?.width ?? 1280,
|
|
322
|
+
height: viewport?.height ?? 720,
|
|
323
|
+
};
|
|
324
|
+
newContextOptions.deviceScaleFactor = 1;
|
|
325
|
+
}
|
|
326
|
+
const context = await browser.newContext(newContextOptions);
|
|
327
|
+
page = await context.newPage();
|
|
328
|
+
}
|
|
329
|
+
// Register console message handler and network listeners
|
|
330
|
+
await registerConsoleMessage(page);
|
|
331
|
+
await registerNetworkListeners(page);
|
|
332
|
+
}
|
|
333
|
+
// Verify page is still valid
|
|
334
|
+
if (!page || page.isClosed()) {
|
|
335
|
+
console.error("Page is closed or invalid. Creating new page...");
|
|
336
|
+
// Create a new page if the current one is invalid
|
|
337
|
+
const context = browser.contexts()[0] || await browser.newContext();
|
|
338
|
+
page = await context.newPage();
|
|
339
|
+
// Re-register console message handler and network listeners
|
|
340
|
+
await registerConsoleMessage(page);
|
|
341
|
+
await registerNetworkListeners(page);
|
|
342
|
+
}
|
|
343
|
+
return page;
|
|
344
|
+
}
|
|
345
|
+
catch (error) {
|
|
346
|
+
console.error("Error ensuring browser:", error);
|
|
347
|
+
// If something went wrong, clean up completely and retry once
|
|
348
|
+
try {
|
|
349
|
+
if (browser) {
|
|
350
|
+
await browser.close().catch(() => { });
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
catch (e) {
|
|
354
|
+
// Ignore errors during cleanup
|
|
355
|
+
}
|
|
356
|
+
resetBrowserState();
|
|
357
|
+
// Try one more time from scratch
|
|
358
|
+
const { viewport, userAgent, headless = false, browserType = 'chromium', device } = browserSettings ?? {};
|
|
359
|
+
// Get device configuration if device preset is specified
|
|
360
|
+
let deviceConfig = null;
|
|
361
|
+
if (device && DEVICE_PRESETS[device]) {
|
|
362
|
+
const playwrightDeviceName = DEVICE_PRESETS[device];
|
|
363
|
+
deviceConfig = devices[playwrightDeviceName];
|
|
364
|
+
}
|
|
365
|
+
// Use the appropriate browser engine
|
|
366
|
+
let browserInstance;
|
|
367
|
+
switch (browserType) {
|
|
368
|
+
case 'firefox':
|
|
369
|
+
browserInstance = firefox;
|
|
370
|
+
break;
|
|
371
|
+
case 'webkit':
|
|
372
|
+
browserInstance = webkit;
|
|
373
|
+
break;
|
|
374
|
+
case 'chromium':
|
|
375
|
+
default:
|
|
376
|
+
browserInstance = chromium;
|
|
377
|
+
break;
|
|
378
|
+
}
|
|
379
|
+
const executablePath = process.env.CHROME_EXECUTABLE_PATH;
|
|
380
|
+
// Prepare context options
|
|
381
|
+
const retryContextOptions = {
|
|
382
|
+
headless,
|
|
383
|
+
executablePath: executablePath,
|
|
384
|
+
};
|
|
385
|
+
// If device config exists, use it; otherwise use manual viewport/userAgent
|
|
386
|
+
if (deviceConfig) {
|
|
387
|
+
Object.assign(retryContextOptions, deviceConfig);
|
|
388
|
+
}
|
|
389
|
+
else {
|
|
390
|
+
if (userAgent) {
|
|
391
|
+
retryContextOptions.userAgent = userAgent;
|
|
392
|
+
}
|
|
393
|
+
retryContextOptions.viewport = {
|
|
394
|
+
width: viewport?.width ?? 1280,
|
|
395
|
+
height: viewport?.height ?? 720,
|
|
396
|
+
};
|
|
397
|
+
retryContextOptions.deviceScaleFactor = 1;
|
|
398
|
+
}
|
|
399
|
+
// Use persistent context if session saving is enabled
|
|
400
|
+
if (sessionConfig.saveSession) {
|
|
401
|
+
console.error(`Launching ${browserType} with persistent context at ${sessionConfig.userDataDir} (retry)...`);
|
|
402
|
+
const context = await browserInstance.launchPersistentContext(sessionConfig.userDataDir, retryContextOptions);
|
|
403
|
+
browser = context.browser();
|
|
404
|
+
currentBrowserType = browserType;
|
|
405
|
+
browser.on('disconnected', () => {
|
|
406
|
+
console.error("Browser disconnected event triggered (retry)");
|
|
407
|
+
browser = undefined;
|
|
408
|
+
page = undefined;
|
|
409
|
+
});
|
|
410
|
+
const pages = context.pages();
|
|
411
|
+
page = pages.length > 0 ? pages[0] : await context.newPage();
|
|
412
|
+
}
|
|
413
|
+
else {
|
|
414
|
+
browser = await browserInstance.launch({
|
|
415
|
+
headless,
|
|
416
|
+
executablePath: executablePath
|
|
417
|
+
});
|
|
418
|
+
currentBrowserType = browserType;
|
|
419
|
+
browser.on('disconnected', () => {
|
|
420
|
+
console.error("Browser disconnected event triggered (retry)");
|
|
421
|
+
browser = undefined;
|
|
422
|
+
page = undefined;
|
|
423
|
+
});
|
|
424
|
+
// Prepare new context options (without headless and executablePath which are for launch)
|
|
425
|
+
const retryNewContextOptions = {};
|
|
426
|
+
if (deviceConfig) {
|
|
427
|
+
Object.assign(retryNewContextOptions, deviceConfig);
|
|
428
|
+
}
|
|
429
|
+
else {
|
|
430
|
+
if (userAgent) {
|
|
431
|
+
retryNewContextOptions.userAgent = userAgent;
|
|
432
|
+
}
|
|
433
|
+
retryNewContextOptions.viewport = {
|
|
434
|
+
width: viewport?.width ?? 1280,
|
|
435
|
+
height: viewport?.height ?? 720,
|
|
436
|
+
};
|
|
437
|
+
retryNewContextOptions.deviceScaleFactor = 1;
|
|
438
|
+
}
|
|
439
|
+
const context = await browser.newContext(retryNewContextOptions);
|
|
440
|
+
page = await context.newPage();
|
|
441
|
+
}
|
|
442
|
+
await registerConsoleMessage(page);
|
|
443
|
+
await registerNetworkListeners(page);
|
|
444
|
+
return page;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
/**
|
|
448
|
+
* Initialize all tool instances
|
|
449
|
+
*/
|
|
450
|
+
function initializeTools(server) {
|
|
451
|
+
// Browser tools
|
|
452
|
+
if (!screenshotTool)
|
|
453
|
+
screenshotTool = new ScreenshotTool(server);
|
|
454
|
+
if (!navigationTool)
|
|
455
|
+
navigationTool = new NavigationTool(server);
|
|
456
|
+
if (!closeBrowserTool)
|
|
457
|
+
closeBrowserTool = new CloseBrowserTool(server);
|
|
458
|
+
if (!consoleLogsTool)
|
|
459
|
+
consoleLogsTool = new ConsoleLogsTool(server);
|
|
460
|
+
if (!clickTool)
|
|
461
|
+
clickTool = new ClickTool(server);
|
|
462
|
+
if (!fillTool)
|
|
463
|
+
fillTool = new FillTool(server);
|
|
464
|
+
if (!selectTool)
|
|
465
|
+
selectTool = new SelectTool(server);
|
|
466
|
+
if (!hoverTool)
|
|
467
|
+
hoverTool = new HoverTool(server);
|
|
468
|
+
if (!uploadFileTool)
|
|
469
|
+
uploadFileTool = new UploadFileTool(server);
|
|
470
|
+
if (!evaluateTool)
|
|
471
|
+
evaluateTool = new EvaluateTool(server);
|
|
472
|
+
if (!visibleTextTool)
|
|
473
|
+
visibleTextTool = new VisibleTextTool(server);
|
|
474
|
+
if (!visibleHtmlTool)
|
|
475
|
+
visibleHtmlTool = new VisibleHtmlTool(server);
|
|
476
|
+
if (!goBackTool)
|
|
477
|
+
goBackTool = new GoBackTool(server);
|
|
478
|
+
if (!goForwardTool)
|
|
479
|
+
goForwardTool = new GoForwardTool(server);
|
|
480
|
+
if (!dragTool)
|
|
481
|
+
dragTool = new DragTool(server);
|
|
482
|
+
if (!pressKeyTool)
|
|
483
|
+
pressKeyTool = new PressKeyTool(server);
|
|
484
|
+
if (!elementVisibilityTool)
|
|
485
|
+
elementVisibilityTool = new ElementVisibilityTool(server);
|
|
486
|
+
if (!elementPositionTool)
|
|
487
|
+
elementPositionTool = new ElementPositionTool(server);
|
|
488
|
+
if (!inspectDomTool)
|
|
489
|
+
inspectDomTool = new InspectDomTool(server);
|
|
490
|
+
if (!getTestIdsTool)
|
|
491
|
+
getTestIdsTool = new GetTestIdsTool(server);
|
|
492
|
+
if (!querySelectorAllTool)
|
|
493
|
+
querySelectorAllTool = new QuerySelectorAllTool(server);
|
|
494
|
+
if (!findByTextTool)
|
|
495
|
+
findByTextTool = new FindByTextTool(server);
|
|
496
|
+
if (!getComputedStylesTool)
|
|
497
|
+
getComputedStylesTool = new GetComputedStylesTool(server);
|
|
498
|
+
if (!measureElementTool)
|
|
499
|
+
measureElementTool = new MeasureElementTool(server);
|
|
500
|
+
if (!elementExistsTool)
|
|
501
|
+
elementExistsTool = new ElementExistsTool(server);
|
|
502
|
+
if (!comparePositionsTool)
|
|
503
|
+
comparePositionsTool = new ComparePositionsTool(server);
|
|
504
|
+
if (!waitForElementTool)
|
|
505
|
+
waitForElementTool = new WaitForElementTool(server);
|
|
506
|
+
if (!waitForNetworkIdleTool)
|
|
507
|
+
waitForNetworkIdleTool = new WaitForNetworkIdleTool(server);
|
|
508
|
+
if (!listNetworkRequestsTool)
|
|
509
|
+
listNetworkRequestsTool = new ListNetworkRequestsTool(server);
|
|
510
|
+
if (!getRequestDetailsTool)
|
|
511
|
+
getRequestDetailsTool = new GetRequestDetailsTool(server);
|
|
512
|
+
}
|
|
513
|
+
/**
|
|
514
|
+
* Main handler for tool calls
|
|
515
|
+
*/
|
|
516
|
+
export async function handleToolCall(name, args, server) {
|
|
517
|
+
// Initialize tools
|
|
518
|
+
initializeTools(server);
|
|
519
|
+
try {
|
|
520
|
+
// Special case for browser close to ensure it always works
|
|
521
|
+
if (name === "close") {
|
|
522
|
+
if (browser) {
|
|
523
|
+
try {
|
|
524
|
+
if (browser.isConnected()) {
|
|
525
|
+
await browser.close().catch(e => console.error("Error closing browser:", e));
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
catch (error) {
|
|
529
|
+
console.error("Error during browser close in handler:", error);
|
|
530
|
+
}
|
|
531
|
+
finally {
|
|
532
|
+
resetBrowserState();
|
|
533
|
+
}
|
|
534
|
+
return {
|
|
535
|
+
content: [{
|
|
536
|
+
type: "text",
|
|
537
|
+
text: "Browser closed successfully",
|
|
538
|
+
}],
|
|
539
|
+
isError: false,
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
return {
|
|
543
|
+
content: [{
|
|
544
|
+
type: "text",
|
|
545
|
+
text: "No browser instance to close",
|
|
546
|
+
}],
|
|
547
|
+
isError: false,
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
// Check if we have a disconnected browser that needs cleanup
|
|
551
|
+
if (browser && !browser.isConnected() && BROWSER_TOOLS.includes(name)) {
|
|
552
|
+
console.error("Detected disconnected browser before tool execution, cleaning up...");
|
|
553
|
+
try {
|
|
554
|
+
await browser.close().catch(() => { }); // Ignore errors
|
|
555
|
+
}
|
|
556
|
+
catch (e) {
|
|
557
|
+
// Ignore any errors during cleanup
|
|
558
|
+
}
|
|
559
|
+
resetBrowserState();
|
|
560
|
+
}
|
|
561
|
+
// Prepare context based on tool requirements
|
|
562
|
+
const context = {
|
|
563
|
+
server
|
|
564
|
+
};
|
|
565
|
+
// Set up browser if needed
|
|
566
|
+
if (BROWSER_TOOLS.includes(name)) {
|
|
567
|
+
const browserSettings = {
|
|
568
|
+
viewport: {
|
|
569
|
+
width: args.width,
|
|
570
|
+
height: args.height
|
|
571
|
+
},
|
|
572
|
+
userAgent: name === "set_user_agent" ? args.userAgent : undefined,
|
|
573
|
+
headless: args.headless,
|
|
574
|
+
browserType: args.browserType || 'chromium',
|
|
575
|
+
device: args.device
|
|
576
|
+
};
|
|
577
|
+
try {
|
|
578
|
+
context.page = await ensureBrowser(browserSettings);
|
|
579
|
+
context.browser = browser;
|
|
580
|
+
}
|
|
581
|
+
catch (error) {
|
|
582
|
+
console.error("Failed to ensure browser:", error);
|
|
583
|
+
return {
|
|
584
|
+
content: [{
|
|
585
|
+
type: "text",
|
|
586
|
+
text: `Failed to initialize browser: ${error.message}. Please try again.`,
|
|
587
|
+
}],
|
|
588
|
+
isError: true,
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
// Route to appropriate tool
|
|
593
|
+
switch (name) {
|
|
594
|
+
// Browser tools
|
|
595
|
+
case "navigate":
|
|
596
|
+
return await navigationTool.execute(args, context);
|
|
597
|
+
case "screenshot":
|
|
598
|
+
return await screenshotTool.execute(args, context);
|
|
599
|
+
case "close":
|
|
600
|
+
return await closeBrowserTool.execute(args, context);
|
|
601
|
+
case "get_console_logs":
|
|
602
|
+
return await consoleLogsTool.execute(args, context);
|
|
603
|
+
case "click":
|
|
604
|
+
return await clickTool.execute(args, context);
|
|
605
|
+
case "fill":
|
|
606
|
+
return await fillTool.execute(args, context);
|
|
607
|
+
case "select":
|
|
608
|
+
return await selectTool.execute(args, context);
|
|
609
|
+
case "hover":
|
|
610
|
+
return await hoverTool.execute(args, context);
|
|
611
|
+
case "upload_file":
|
|
612
|
+
return await uploadFileTool.execute(args, context);
|
|
613
|
+
case "evaluate":
|
|
614
|
+
return await evaluateTool.execute(args, context);
|
|
615
|
+
case "get_text":
|
|
616
|
+
return await visibleTextTool.execute(args, context);
|
|
617
|
+
case "get_html":
|
|
618
|
+
return await visibleHtmlTool.execute(args, context);
|
|
619
|
+
case "go_back":
|
|
620
|
+
return await goBackTool.execute(args, context);
|
|
621
|
+
case "go_forward":
|
|
622
|
+
return await goForwardTool.execute(args, context);
|
|
623
|
+
case "drag":
|
|
624
|
+
return await dragTool.execute(args, context);
|
|
625
|
+
case "press_key":
|
|
626
|
+
return await pressKeyTool.execute(args, context);
|
|
627
|
+
case "check_visibility":
|
|
628
|
+
return await elementVisibilityTool.execute(args, context);
|
|
629
|
+
case "get_position":
|
|
630
|
+
return await elementPositionTool.execute(args, context);
|
|
631
|
+
case "inspect_dom":
|
|
632
|
+
return await inspectDomTool.execute(args, context);
|
|
633
|
+
case "get_test_ids":
|
|
634
|
+
return await getTestIdsTool.execute(args, context);
|
|
635
|
+
case "query_selector":
|
|
636
|
+
return await querySelectorAllTool.execute(args, context);
|
|
637
|
+
case "find_by_text":
|
|
638
|
+
return await findByTextTool.execute(args, context);
|
|
639
|
+
case "get_computed_styles":
|
|
640
|
+
return await getComputedStylesTool.execute(args, context);
|
|
641
|
+
case "measure_element":
|
|
642
|
+
return await measureElementTool.execute(args, context);
|
|
643
|
+
case "element_exists":
|
|
644
|
+
return await elementExistsTool.execute(args, context);
|
|
645
|
+
case "compare_positions":
|
|
646
|
+
return await comparePositionsTool.execute(args, context);
|
|
647
|
+
case "wait_for_element":
|
|
648
|
+
return await waitForElementTool.execute(args, context);
|
|
649
|
+
case "wait_for_network_idle":
|
|
650
|
+
return await waitForNetworkIdleTool.execute(args, context);
|
|
651
|
+
case "list_network_requests":
|
|
652
|
+
return await listNetworkRequestsTool.execute(args, context);
|
|
653
|
+
case "get_request_details":
|
|
654
|
+
return await getRequestDetailsTool.execute(args, context);
|
|
655
|
+
default:
|
|
656
|
+
return {
|
|
657
|
+
content: [{
|
|
658
|
+
type: "text",
|
|
659
|
+
text: `Unknown tool: ${name}`,
|
|
660
|
+
}],
|
|
661
|
+
isError: true,
|
|
662
|
+
};
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
catch (error) {
|
|
666
|
+
console.error(`Error handling tool ${name}:`, error);
|
|
667
|
+
// Handle browser-specific errors at the top level
|
|
668
|
+
if (BROWSER_TOOLS.includes(name)) {
|
|
669
|
+
const errorMessage = error.message;
|
|
670
|
+
if (errorMessage.includes("Target page, context or browser has been closed") ||
|
|
671
|
+
errorMessage.includes("Browser has been disconnected") ||
|
|
672
|
+
errorMessage.includes("Target closed") ||
|
|
673
|
+
errorMessage.includes("Protocol error") ||
|
|
674
|
+
errorMessage.includes("Connection closed")) {
|
|
675
|
+
// Reset browser state if it's a connection issue
|
|
676
|
+
resetBrowserState();
|
|
677
|
+
return {
|
|
678
|
+
content: [{
|
|
679
|
+
type: "text",
|
|
680
|
+
text: `Browser connection error: ${errorMessage}. Browser state has been reset, please try again.`,
|
|
681
|
+
}],
|
|
682
|
+
isError: true,
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
return {
|
|
687
|
+
content: [{
|
|
688
|
+
type: "text",
|
|
689
|
+
text: error instanceof Error ? error.message : String(error),
|
|
690
|
+
}],
|
|
691
|
+
isError: true,
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
/**
|
|
696
|
+
* Get console logs
|
|
697
|
+
*/
|
|
698
|
+
export function getConsoleLogs() {
|
|
699
|
+
return consoleLogsTool?.getConsoleLogs() ?? [];
|
|
700
|
+
}
|
|
701
|
+
/**
|
|
702
|
+
* Get screenshots
|
|
703
|
+
*/
|
|
704
|
+
export function getScreenshots() {
|
|
705
|
+
return screenshotTool?.getScreenshots() ?? new Map();
|
|
706
|
+
}
|
|
707
|
+
/**
|
|
708
|
+
* Update last interaction timestamp
|
|
709
|
+
*/
|
|
710
|
+
export function updateLastInteractionTimestamp() {
|
|
711
|
+
consoleLogsTool?.updateLastInteractionTimestamp();
|
|
712
|
+
}
|
|
713
|
+
/**
|
|
714
|
+
* Update last navigation timestamp
|
|
715
|
+
*/
|
|
716
|
+
export function updateLastNavigationTimestamp() {
|
|
717
|
+
consoleLogsTool?.updateLastNavigationTimestamp();
|
|
718
|
+
}
|
|
719
|
+
/**
|
|
720
|
+
* Clear console logs
|
|
721
|
+
*/
|
|
722
|
+
export function clearConsoleLogs() {
|
|
723
|
+
consoleLogsTool?.clearConsoleLogs();
|
|
724
|
+
}
|
|
725
|
+
export { registerConsoleMessage };
|