@democratize-quality/mcp-server 1.0.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 +15 -0
- package/README.md +423 -0
- package/browserControl.js +113 -0
- package/cli.js +187 -0
- package/docs/api/tool-reference.md +317 -0
- package/docs/api_tools_usage.md +477 -0
- package/docs/development/adding-tools.md +274 -0
- package/docs/development/configuration.md +332 -0
- package/docs/examples/authentication.md +124 -0
- package/docs/examples/basic-automation.md +105 -0
- package/docs/getting-started.md +214 -0
- package/docs/index.md +61 -0
- package/mcpServer.js +280 -0
- package/package.json +83 -0
- package/run-server.js +140 -0
- package/src/config/environments/api-only.js +53 -0
- package/src/config/environments/development.js +54 -0
- package/src/config/environments/production.js +69 -0
- package/src/config/index.js +341 -0
- package/src/config/server.js +41 -0
- package/src/config/tools/api.js +67 -0
- package/src/config/tools/browser.js +90 -0
- package/src/config/tools/default.js +32 -0
- package/src/services/browserService.js +325 -0
- package/src/tools/api/api-request.js +641 -0
- package/src/tools/api/api-session-report.js +1262 -0
- package/src/tools/api/api-session-status.js +395 -0
- package/src/tools/base/ToolBase.js +230 -0
- package/src/tools/base/ToolRegistry.js +269 -0
- package/src/tools/browser/advanced/browser-console.js +384 -0
- package/src/tools/browser/advanced/browser-dialog.js +319 -0
- package/src/tools/browser/advanced/browser-evaluate.js +337 -0
- package/src/tools/browser/advanced/browser-file.js +480 -0
- package/src/tools/browser/advanced/browser-keyboard.js +343 -0
- package/src/tools/browser/advanced/browser-mouse.js +332 -0
- package/src/tools/browser/advanced/browser-network.js +421 -0
- package/src/tools/browser/advanced/browser-pdf.js +407 -0
- package/src/tools/browser/advanced/browser-tabs.js +497 -0
- package/src/tools/browser/advanced/browser-wait.js +378 -0
- package/src/tools/browser/click.js +168 -0
- package/src/tools/browser/close.js +60 -0
- package/src/tools/browser/dom.js +70 -0
- package/src/tools/browser/launch.js +67 -0
- package/src/tools/browser/navigate.js +270 -0
- package/src/tools/browser/screenshot.js +351 -0
- package/src/tools/browser/type.js +174 -0
- package/src/tools/index.js +95 -0
- package/src/utils/browserHelpers.js +83 -0
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
const ToolBase = require('../../base/ToolBase');
|
|
2
|
+
const browserService = require('../../../services/browserService');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Enhanced Wait Tool - Provides sophisticated waiting strategies
|
|
6
|
+
* Inspired by Playwright MCP wait capabilities
|
|
7
|
+
*/
|
|
8
|
+
class BrowserWaitTool extends ToolBase {
|
|
9
|
+
static definition = {
|
|
10
|
+
name: "browser_wait",
|
|
11
|
+
description: "Wait for various conditions including elements, text, network requests, or time delays. Supports complex waiting scenarios.",
|
|
12
|
+
input_schema: {
|
|
13
|
+
type: "object",
|
|
14
|
+
properties: {
|
|
15
|
+
browserId: {
|
|
16
|
+
type: "string",
|
|
17
|
+
description: "The ID of the browser instance"
|
|
18
|
+
},
|
|
19
|
+
condition: {
|
|
20
|
+
type: "string",
|
|
21
|
+
enum: ["time", "element", "text", "textGone", "url", "networkIdle", "domContentLoaded", "load"],
|
|
22
|
+
description: "The condition to wait for"
|
|
23
|
+
},
|
|
24
|
+
value: {
|
|
25
|
+
type: "string",
|
|
26
|
+
description: "Value for the condition (selector for element, text content, URL pattern, etc.)"
|
|
27
|
+
},
|
|
28
|
+
timeout: {
|
|
29
|
+
type: "number",
|
|
30
|
+
default: 30000,
|
|
31
|
+
description: "Maximum wait time in milliseconds"
|
|
32
|
+
},
|
|
33
|
+
time: {
|
|
34
|
+
type: "number",
|
|
35
|
+
description: "Time to wait in seconds (for 'time' condition)"
|
|
36
|
+
},
|
|
37
|
+
state: {
|
|
38
|
+
type: "string",
|
|
39
|
+
enum: ["visible", "hidden", "attached", "detached"],
|
|
40
|
+
default: "visible",
|
|
41
|
+
description: "Element state to wait for (for 'element' condition)"
|
|
42
|
+
},
|
|
43
|
+
networkIdleTime: {
|
|
44
|
+
type: "number",
|
|
45
|
+
default: 500,
|
|
46
|
+
description: "Time in ms to consider network idle (no requests for this duration)"
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
required: ["browserId", "condition"]
|
|
50
|
+
},
|
|
51
|
+
output_schema: {
|
|
52
|
+
type: "object",
|
|
53
|
+
properties: {
|
|
54
|
+
success: { type: "boolean", description: "Whether the wait condition was met" },
|
|
55
|
+
condition: { type: "string", description: "The condition that was waited for" },
|
|
56
|
+
value: { type: "string", description: "The value/selector that was waited for" },
|
|
57
|
+
actualValue: { type: "string", description: "The actual value found (for text conditions)" },
|
|
58
|
+
waitTime: { type: "number", description: "Actual time waited in milliseconds" },
|
|
59
|
+
timedOut: { type: "boolean", description: "Whether the wait timed out" },
|
|
60
|
+
browserId: { type: "string", description: "Browser instance ID" }
|
|
61
|
+
},
|
|
62
|
+
required: ["success", "condition", "browserId"]
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
async execute(parameters) {
|
|
67
|
+
const {
|
|
68
|
+
browserId,
|
|
69
|
+
condition,
|
|
70
|
+
value,
|
|
71
|
+
timeout = 30000,
|
|
72
|
+
time,
|
|
73
|
+
state = 'visible',
|
|
74
|
+
networkIdleTime = 500
|
|
75
|
+
} = parameters;
|
|
76
|
+
|
|
77
|
+
const browser = browserService.getBrowserInstance(browserId);
|
|
78
|
+
if (!browser) {
|
|
79
|
+
throw new Error(`Browser instance '${browserId}' not found`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const client = browser.client;
|
|
83
|
+
const startTime = Date.now();
|
|
84
|
+
|
|
85
|
+
let result = {
|
|
86
|
+
success: false,
|
|
87
|
+
condition: condition,
|
|
88
|
+
browserId: browserId,
|
|
89
|
+
waitTime: 0,
|
|
90
|
+
timedOut: false
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
switch (condition) {
|
|
95
|
+
case 'time':
|
|
96
|
+
if (!time) {
|
|
97
|
+
throw new Error('Time value is required for time condition');
|
|
98
|
+
}
|
|
99
|
+
await this.waitForTime(time * 1000);
|
|
100
|
+
result.success = true;
|
|
101
|
+
result.value = `${time} seconds`;
|
|
102
|
+
break;
|
|
103
|
+
|
|
104
|
+
case 'element':
|
|
105
|
+
if (!value) {
|
|
106
|
+
throw new Error('Selector is required for element condition');
|
|
107
|
+
}
|
|
108
|
+
const elementResult = await this.waitForElement(client, value, state, timeout);
|
|
109
|
+
result.success = elementResult.found;
|
|
110
|
+
result.value = value;
|
|
111
|
+
result.state = state;
|
|
112
|
+
break;
|
|
113
|
+
|
|
114
|
+
case 'text':
|
|
115
|
+
if (!value) {
|
|
116
|
+
throw new Error('Text is required for text condition');
|
|
117
|
+
}
|
|
118
|
+
const textResult = await this.waitForText(client, value, true, timeout);
|
|
119
|
+
result.success = textResult.found;
|
|
120
|
+
result.value = value;
|
|
121
|
+
result.actualValue = textResult.actualText;
|
|
122
|
+
break;
|
|
123
|
+
|
|
124
|
+
case 'textGone':
|
|
125
|
+
if (!value) {
|
|
126
|
+
throw new Error('Text is required for textGone condition');
|
|
127
|
+
}
|
|
128
|
+
const textGoneResult = await this.waitForText(client, value, false, timeout);
|
|
129
|
+
result.success = textGoneResult.found;
|
|
130
|
+
result.value = value;
|
|
131
|
+
break;
|
|
132
|
+
|
|
133
|
+
case 'url':
|
|
134
|
+
if (!value) {
|
|
135
|
+
throw new Error('URL pattern is required for url condition');
|
|
136
|
+
}
|
|
137
|
+
const urlResult = await this.waitForUrl(client, value, timeout);
|
|
138
|
+
result.success = urlResult.found;
|
|
139
|
+
result.value = value;
|
|
140
|
+
result.actualValue = urlResult.actualUrl;
|
|
141
|
+
break;
|
|
142
|
+
|
|
143
|
+
case 'networkIdle':
|
|
144
|
+
const networkResult = await this.waitForNetworkIdle(client, networkIdleTime, timeout);
|
|
145
|
+
result.success = networkResult.idle;
|
|
146
|
+
result.value = `${networkIdleTime}ms idle`;
|
|
147
|
+
break;
|
|
148
|
+
|
|
149
|
+
case 'domContentLoaded':
|
|
150
|
+
const domResult = await this.waitForDOMContentLoaded(client, timeout);
|
|
151
|
+
result.success = domResult.loaded;
|
|
152
|
+
break;
|
|
153
|
+
|
|
154
|
+
case 'load':
|
|
155
|
+
const loadResult = await this.waitForLoad(client, timeout);
|
|
156
|
+
result.success = loadResult.loaded;
|
|
157
|
+
break;
|
|
158
|
+
|
|
159
|
+
default:
|
|
160
|
+
throw new Error(`Unsupported wait condition: ${condition}`);
|
|
161
|
+
}
|
|
162
|
+
} catch (error) {
|
|
163
|
+
if (error.message.includes('timeout')) {
|
|
164
|
+
result.timedOut = true;
|
|
165
|
+
} else {
|
|
166
|
+
throw error;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
result.waitTime = Date.now() - startTime;
|
|
171
|
+
return result;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Wait for a specific amount of time
|
|
176
|
+
*/
|
|
177
|
+
async waitForTime(milliseconds) {
|
|
178
|
+
return new Promise(resolve => setTimeout(resolve, milliseconds));
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Wait for element to be in specified state
|
|
183
|
+
*/
|
|
184
|
+
async waitForElement(client, selector, state, timeout) {
|
|
185
|
+
const startTime = Date.now();
|
|
186
|
+
|
|
187
|
+
while (Date.now() - startTime < timeout) {
|
|
188
|
+
const result = await client.Runtime.evaluate({
|
|
189
|
+
expression: `
|
|
190
|
+
(() => {
|
|
191
|
+
const element = document.querySelector('${selector}');
|
|
192
|
+
if (!element) return { exists: false };
|
|
193
|
+
|
|
194
|
+
const rect = element.getBoundingClientRect();
|
|
195
|
+
const isVisible = rect.width > 0 && rect.height > 0 &&
|
|
196
|
+
window.getComputedStyle(element).visibility !== 'hidden' &&
|
|
197
|
+
window.getComputedStyle(element).display !== 'none';
|
|
198
|
+
|
|
199
|
+
return {
|
|
200
|
+
exists: true,
|
|
201
|
+
visible: isVisible,
|
|
202
|
+
attached: element.isConnected
|
|
203
|
+
};
|
|
204
|
+
})()
|
|
205
|
+
`
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
const elementData = result.result.value;
|
|
209
|
+
|
|
210
|
+
if (!elementData) {
|
|
211
|
+
if (state === 'detached') return { found: true };
|
|
212
|
+
await this.waitForTime(100);
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
switch (state) {
|
|
217
|
+
case 'visible':
|
|
218
|
+
if (elementData.exists && elementData.visible) return { found: true };
|
|
219
|
+
break;
|
|
220
|
+
case 'hidden':
|
|
221
|
+
if (elementData.exists && !elementData.visible) return { found: true };
|
|
222
|
+
break;
|
|
223
|
+
case 'attached':
|
|
224
|
+
if (elementData.exists && elementData.attached) return { found: true };
|
|
225
|
+
break;
|
|
226
|
+
case 'detached':
|
|
227
|
+
if (!elementData.exists || !elementData.attached) return { found: true };
|
|
228
|
+
break;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
await this.waitForTime(100);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
throw new Error(`Timeout waiting for element '${selector}' to be ${state}`);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Wait for text to appear or disappear
|
|
239
|
+
*/
|
|
240
|
+
async waitForText(client, text, shouldAppear, timeout) {
|
|
241
|
+
const startTime = Date.now();
|
|
242
|
+
|
|
243
|
+
while (Date.now() - startTime < timeout) {
|
|
244
|
+
const result = await client.Runtime.evaluate({
|
|
245
|
+
expression: `
|
|
246
|
+
(() => {
|
|
247
|
+
const bodyText = document.body.innerText || document.body.textContent || '';
|
|
248
|
+
const found = bodyText.includes('${text}');
|
|
249
|
+
return { found: found, bodyText: bodyText.substring(0, 500) };
|
|
250
|
+
})()
|
|
251
|
+
`
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
const textData = result.result.value;
|
|
255
|
+
|
|
256
|
+
if (shouldAppear && textData.found) {
|
|
257
|
+
return { found: true, actualText: text };
|
|
258
|
+
} else if (!shouldAppear && !textData.found) {
|
|
259
|
+
return { found: true };
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
await this.waitForTime(100);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const condition = shouldAppear ? 'appear' : 'disappear';
|
|
266
|
+
throw new Error(`Timeout waiting for text '${text}' to ${condition}`);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Wait for URL to match pattern
|
|
271
|
+
*/
|
|
272
|
+
async waitForUrl(client, urlPattern, timeout) {
|
|
273
|
+
const startTime = Date.now();
|
|
274
|
+
|
|
275
|
+
while (Date.now() - startTime < timeout) {
|
|
276
|
+
const result = await client.Runtime.evaluate({
|
|
277
|
+
expression: 'window.location.href'
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
const currentUrl = result.result.value;
|
|
281
|
+
const regex = new RegExp(urlPattern);
|
|
282
|
+
|
|
283
|
+
if (regex.test(currentUrl)) {
|
|
284
|
+
return { found: true, actualUrl: currentUrl };
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
await this.waitForTime(100);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
throw new Error(`Timeout waiting for URL to match pattern '${urlPattern}'`);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Wait for network to be idle
|
|
295
|
+
*/
|
|
296
|
+
async waitForNetworkIdle(client, idleTime, timeout) {
|
|
297
|
+
// Enable network monitoring
|
|
298
|
+
await client.Network.enable();
|
|
299
|
+
|
|
300
|
+
let requestCount = 0;
|
|
301
|
+
let lastRequestTime = Date.now();
|
|
302
|
+
|
|
303
|
+
const requestStarted = () => {
|
|
304
|
+
requestCount++;
|
|
305
|
+
lastRequestTime = Date.now();
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
const requestFinished = () => {
|
|
309
|
+
requestCount--;
|
|
310
|
+
lastRequestTime = Date.now();
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
client.Network.requestWillBeSent(requestStarted);
|
|
314
|
+
client.Network.responseReceived(requestFinished);
|
|
315
|
+
|
|
316
|
+
const startTime = Date.now();
|
|
317
|
+
|
|
318
|
+
while (Date.now() - startTime < timeout) {
|
|
319
|
+
const timeSinceLastRequest = Date.now() - lastRequestTime;
|
|
320
|
+
|
|
321
|
+
if (requestCount === 0 && timeSinceLastRequest >= idleTime) {
|
|
322
|
+
return { idle: true };
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
await this.waitForTime(100);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
throw new Error(`Timeout waiting for network idle (${idleTime}ms)`);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Wait for DOM content loaded
|
|
333
|
+
*/
|
|
334
|
+
async waitForDOMContentLoaded(client, timeout) {
|
|
335
|
+
const startTime = Date.now();
|
|
336
|
+
|
|
337
|
+
while (Date.now() - startTime < timeout) {
|
|
338
|
+
const result = await client.Runtime.evaluate({
|
|
339
|
+
expression: 'document.readyState'
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
const readyState = result.result.value;
|
|
343
|
+
|
|
344
|
+
if (readyState === 'interactive' || readyState === 'complete') {
|
|
345
|
+
return { loaded: true };
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
await this.waitForTime(100);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
throw new Error('Timeout waiting for DOM content loaded');
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Wait for page load complete
|
|
356
|
+
*/
|
|
357
|
+
async waitForLoad(client, timeout) {
|
|
358
|
+
const startTime = Date.now();
|
|
359
|
+
|
|
360
|
+
while (Date.now() - startTime < timeout) {
|
|
361
|
+
const result = await client.Runtime.evaluate({
|
|
362
|
+
expression: 'document.readyState'
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
const readyState = result.result.value;
|
|
366
|
+
|
|
367
|
+
if (readyState === 'complete') {
|
|
368
|
+
return { loaded: true };
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
await this.waitForTime(100);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
throw new Error('Timeout waiting for page load complete');
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
module.exports = BrowserWaitTool;
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
const ToolBase = require('../base/ToolBase');
|
|
2
|
+
const browserService = require('../../services/browserService');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Browser Click Tool
|
|
6
|
+
* Simulates a click on a specific element in the browser with Playwright-style locators
|
|
7
|
+
*/
|
|
8
|
+
class BrowserClickTool extends ToolBase {
|
|
9
|
+
static definition = {
|
|
10
|
+
name: "browser_click",
|
|
11
|
+
description: "Simulates a click on a specific element in the browser.",
|
|
12
|
+
input_schema: {
|
|
13
|
+
type: "object",
|
|
14
|
+
properties: {
|
|
15
|
+
browserId: {
|
|
16
|
+
type: "string",
|
|
17
|
+
description: "The ID of the browser instance."
|
|
18
|
+
},
|
|
19
|
+
locatorType: {
|
|
20
|
+
type: "string",
|
|
21
|
+
enum: ["css", "xpath", "text", "role", "label", "placeholder", "testId", "altText"],
|
|
22
|
+
default: "css",
|
|
23
|
+
description: "The type of locator to use"
|
|
24
|
+
},
|
|
25
|
+
locatorValue: {
|
|
26
|
+
type: "string",
|
|
27
|
+
description: "The locator value (CSS selector, XPath, text content, etc.)"
|
|
28
|
+
},
|
|
29
|
+
// Keep backward compatibility
|
|
30
|
+
selector: {
|
|
31
|
+
type: "string",
|
|
32
|
+
description: "The CSS selector of the element to click (deprecated, use locatorType and locatorValue instead)."
|
|
33
|
+
},
|
|
34
|
+
options: {
|
|
35
|
+
type: "object",
|
|
36
|
+
properties: {
|
|
37
|
+
timeout: {
|
|
38
|
+
type: "number",
|
|
39
|
+
default: 30000,
|
|
40
|
+
description: "Timeout in milliseconds"
|
|
41
|
+
},
|
|
42
|
+
force: {
|
|
43
|
+
type: "boolean",
|
|
44
|
+
default: false,
|
|
45
|
+
description: "Force click even if element is not actionable"
|
|
46
|
+
},
|
|
47
|
+
noWaitAfter: {
|
|
48
|
+
type: "boolean",
|
|
49
|
+
default: false,
|
|
50
|
+
description: "Do not wait for navigation after click"
|
|
51
|
+
},
|
|
52
|
+
button: {
|
|
53
|
+
type: "string",
|
|
54
|
+
enum: ["left", "right", "middle"],
|
|
55
|
+
default: "left",
|
|
56
|
+
description: "Mouse button to click"
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
description: "Additional click options"
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
anyOf: [
|
|
63
|
+
{ required: ["browserId", "locatorType", "locatorValue"] },
|
|
64
|
+
{ required: ["browserId", "selector"] }
|
|
65
|
+
]
|
|
66
|
+
},
|
|
67
|
+
output_schema: {
|
|
68
|
+
type: "object",
|
|
69
|
+
properties: {
|
|
70
|
+
success: { type: "boolean", description: "Indicates if the click was successful." },
|
|
71
|
+
locatorType: { type: "string", description: "The locator type that was used." },
|
|
72
|
+
locatorValue: { type: "string", description: "The locator value that was used." },
|
|
73
|
+
browserId: { type: "string", description: "The browser instance ID that was used." },
|
|
74
|
+
elementFound: { type: "boolean", description: "Whether the element was found." },
|
|
75
|
+
message: { type: "string", description: "Success or error message." }
|
|
76
|
+
},
|
|
77
|
+
required: ["success", "browserId"]
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
async execute(parameters) {
|
|
82
|
+
const { browserId, locatorType, locatorValue, selector, options = {} } = parameters;
|
|
83
|
+
|
|
84
|
+
// Handle backward compatibility
|
|
85
|
+
let finalLocatorType, finalLocatorValue;
|
|
86
|
+
if (selector) {
|
|
87
|
+
finalLocatorType = "css";
|
|
88
|
+
finalLocatorValue = selector;
|
|
89
|
+
} else {
|
|
90
|
+
finalLocatorType = locatorType || "css";
|
|
91
|
+
finalLocatorValue = locatorValue;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
console.error(`[BrowserClickTool] Simulating click in browser ${browserId} with locator: ${finalLocatorType}="${finalLocatorValue}"`);
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
// Convert Playwright-style locator to appropriate format for browser service
|
|
98
|
+
const elementSelector = this.convertLocatorToSelector(finalLocatorType, finalLocatorValue);
|
|
99
|
+
|
|
100
|
+
await browserService.clickElement(browserId, elementSelector, options);
|
|
101
|
+
|
|
102
|
+
console.error(`[BrowserClickTool] Successfully clicked element in browser: ${browserId}`);
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
success: true,
|
|
106
|
+
browserId: browserId,
|
|
107
|
+
locatorType: finalLocatorType,
|
|
108
|
+
locatorValue: finalLocatorValue,
|
|
109
|
+
elementFound: true,
|
|
110
|
+
message: `Successfully clicked element with ${finalLocatorType} locator: ${finalLocatorValue}`
|
|
111
|
+
};
|
|
112
|
+
} catch (error) {
|
|
113
|
+
console.error(`[BrowserClickTool] Failed to click element:`, error.message);
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
success: false,
|
|
117
|
+
browserId: browserId,
|
|
118
|
+
locatorType: finalLocatorType,
|
|
119
|
+
locatorValue: finalLocatorValue,
|
|
120
|
+
elementFound: false,
|
|
121
|
+
message: `Failed to click element: ${error.message}`
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Convert Playwright-style locator to CSS selector or XPath
|
|
128
|
+
*/
|
|
129
|
+
convertLocatorToSelector(locatorType, locatorValue) {
|
|
130
|
+
switch (locatorType) {
|
|
131
|
+
case "css":
|
|
132
|
+
return locatorValue;
|
|
133
|
+
|
|
134
|
+
case "xpath":
|
|
135
|
+
return locatorValue;
|
|
136
|
+
|
|
137
|
+
case "text":
|
|
138
|
+
// Convert text locator to XPath
|
|
139
|
+
return `//*[contains(text(), "${locatorValue}")]`;
|
|
140
|
+
|
|
141
|
+
case "role":
|
|
142
|
+
// Convert role locator to CSS attribute selector
|
|
143
|
+
return `[role="${locatorValue}"]`;
|
|
144
|
+
|
|
145
|
+
case "label":
|
|
146
|
+
// Convert label locator to XPath for label association
|
|
147
|
+
return `//input[@aria-label="${locatorValue}"] | //input[@id=//label[contains(text(), "${locatorValue}")]/@for]`;
|
|
148
|
+
|
|
149
|
+
case "placeholder":
|
|
150
|
+
// Convert placeholder locator to CSS attribute selector
|
|
151
|
+
return `[placeholder="${locatorValue}"]`;
|
|
152
|
+
|
|
153
|
+
case "testId":
|
|
154
|
+
// Convert test ID to CSS attribute selector (assuming data-testid)
|
|
155
|
+
return `[data-testid="${locatorValue}"]`;
|
|
156
|
+
|
|
157
|
+
case "altText":
|
|
158
|
+
// Convert alt text to CSS attribute selector
|
|
159
|
+
return `[alt="${locatorValue}"]`;
|
|
160
|
+
|
|
161
|
+
default:
|
|
162
|
+
// Default to CSS selector
|
|
163
|
+
return locatorValue;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
module.exports = BrowserClickTool;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
const ToolBase = require('../base/ToolBase');
|
|
2
|
+
const browserService = require('../../services/browserService');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Browser Close Tool
|
|
6
|
+
* Closes a specific browser instance and cleans up its resources
|
|
7
|
+
*/
|
|
8
|
+
class BrowserCloseTool extends ToolBase {
|
|
9
|
+
static definition = {
|
|
10
|
+
name: "browser_close",
|
|
11
|
+
description: "Closes a specific browser instance and cleans up its resources. Always call this when done with a browser.",
|
|
12
|
+
input_schema: {
|
|
13
|
+
type: "object",
|
|
14
|
+
properties: {
|
|
15
|
+
browserId: {
|
|
16
|
+
type: "string",
|
|
17
|
+
description: "The ID of the browser instance to close."
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
required: ["browserId"]
|
|
21
|
+
},
|
|
22
|
+
output_schema: {
|
|
23
|
+
type: "object",
|
|
24
|
+
properties: {
|
|
25
|
+
message: { type: "string", description: "Confirmation message of successful closure." },
|
|
26
|
+
browserId: { type: "string", description: "The browser instance ID that was closed." }
|
|
27
|
+
},
|
|
28
|
+
required: ["message", "browserId"]
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
async execute(parameters) {
|
|
33
|
+
const { browserId } = parameters;
|
|
34
|
+
|
|
35
|
+
console.error(`[BrowserCloseTool] Closing browser: ${browserId}`);
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
await browserService.closeBrowser(browserId);
|
|
39
|
+
|
|
40
|
+
console.error(`[BrowserCloseTool] Successfully closed browser: ${browserId}`);
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
message: `Browser ${browserId} closed successfully`,
|
|
44
|
+
browserId: browserId
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
} catch (error) {
|
|
48
|
+
console.error(`[BrowserCloseTool] Failed to close browser:`, error.message);
|
|
49
|
+
|
|
50
|
+
// Provide more specific error messages
|
|
51
|
+
if (error.message.includes('not found')) {
|
|
52
|
+
throw new Error(`Browser instance '${browserId}' not found or already closed.`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
throw new Error(`Failed to close browser ${browserId}: ${error.message}`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
module.exports = BrowserCloseTool;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
const ToolBase = require('../base/ToolBase');
|
|
2
|
+
const browserService = require('../../services/browserService');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Browser DOM Tool
|
|
6
|
+
* Interacts with the DOM of the current browser page.
|
|
7
|
+
*/
|
|
8
|
+
class BrowserDOMTool extends ToolBase {
|
|
9
|
+
static definition = {
|
|
10
|
+
name: "browser_dom",
|
|
11
|
+
description: "Interacts with the DOM of the current browser page.",
|
|
12
|
+
input_schema: {
|
|
13
|
+
type: "object",
|
|
14
|
+
properties: {
|
|
15
|
+
browserId: {
|
|
16
|
+
type: "string",
|
|
17
|
+
description: "The ID of the browser instance."
|
|
18
|
+
},
|
|
19
|
+
action: {
|
|
20
|
+
type: "string",
|
|
21
|
+
description: "The DOM action to perform (e.g., 'click', 'type')."
|
|
22
|
+
},
|
|
23
|
+
selector: {
|
|
24
|
+
type: "string",
|
|
25
|
+
description: "The CSS selector of the element to interact with."
|
|
26
|
+
},
|
|
27
|
+
text: {
|
|
28
|
+
type: "string",
|
|
29
|
+
description: "The text to type into the element (if applicable)."
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
required: ["browserId", "action", "selector"]
|
|
33
|
+
},
|
|
34
|
+
output_schema: {
|
|
35
|
+
type: "object",
|
|
36
|
+
properties: {
|
|
37
|
+
success: { type: "boolean", description: "Indicates if the DOM action was successful." },
|
|
38
|
+
browserId: { type: "string", description: "The browser instance ID that was used." }
|
|
39
|
+
},
|
|
40
|
+
required: ["success", "browserId"]
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
async execute(parameters) {
|
|
45
|
+
const { browserId, action, selector, text } = parameters;
|
|
46
|
+
|
|
47
|
+
console.error(`[BrowserDOMTool] Performing ${action} in browser ${browserId} on element: ${selector}`);
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
switch (action) {
|
|
51
|
+
case 'click':
|
|
52
|
+
await browserService.clickElement(browserId, selector);
|
|
53
|
+
break;
|
|
54
|
+
case 'type':
|
|
55
|
+
await browserService.typeText(browserId, selector, text);
|
|
56
|
+
break;
|
|
57
|
+
default:
|
|
58
|
+
throw new Error(`Unknown action: ${action}`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
console.error(`[BrowserDOMTool] Successfully performed ${action} in browser: ${browserId}`);
|
|
62
|
+
return { success: true, browserId: browserId };
|
|
63
|
+
} catch (error) {
|
|
64
|
+
console.error(`[BrowserDOMTool] Failed to perform ${action}:`, error.message);
|
|
65
|
+
throw new Error(`Failed to perform ${action} in browser ${browserId}: ${error.message}`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
module.exports = BrowserDOMTool;
|