@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.
Files changed (48) hide show
  1. package/LICENSE +15 -0
  2. package/README.md +423 -0
  3. package/browserControl.js +113 -0
  4. package/cli.js +187 -0
  5. package/docs/api/tool-reference.md +317 -0
  6. package/docs/api_tools_usage.md +477 -0
  7. package/docs/development/adding-tools.md +274 -0
  8. package/docs/development/configuration.md +332 -0
  9. package/docs/examples/authentication.md +124 -0
  10. package/docs/examples/basic-automation.md +105 -0
  11. package/docs/getting-started.md +214 -0
  12. package/docs/index.md +61 -0
  13. package/mcpServer.js +280 -0
  14. package/package.json +83 -0
  15. package/run-server.js +140 -0
  16. package/src/config/environments/api-only.js +53 -0
  17. package/src/config/environments/development.js +54 -0
  18. package/src/config/environments/production.js +69 -0
  19. package/src/config/index.js +341 -0
  20. package/src/config/server.js +41 -0
  21. package/src/config/tools/api.js +67 -0
  22. package/src/config/tools/browser.js +90 -0
  23. package/src/config/tools/default.js +32 -0
  24. package/src/services/browserService.js +325 -0
  25. package/src/tools/api/api-request.js +641 -0
  26. package/src/tools/api/api-session-report.js +1262 -0
  27. package/src/tools/api/api-session-status.js +395 -0
  28. package/src/tools/base/ToolBase.js +230 -0
  29. package/src/tools/base/ToolRegistry.js +269 -0
  30. package/src/tools/browser/advanced/browser-console.js +384 -0
  31. package/src/tools/browser/advanced/browser-dialog.js +319 -0
  32. package/src/tools/browser/advanced/browser-evaluate.js +337 -0
  33. package/src/tools/browser/advanced/browser-file.js +480 -0
  34. package/src/tools/browser/advanced/browser-keyboard.js +343 -0
  35. package/src/tools/browser/advanced/browser-mouse.js +332 -0
  36. package/src/tools/browser/advanced/browser-network.js +421 -0
  37. package/src/tools/browser/advanced/browser-pdf.js +407 -0
  38. package/src/tools/browser/advanced/browser-tabs.js +497 -0
  39. package/src/tools/browser/advanced/browser-wait.js +378 -0
  40. package/src/tools/browser/click.js +168 -0
  41. package/src/tools/browser/close.js +60 -0
  42. package/src/tools/browser/dom.js +70 -0
  43. package/src/tools/browser/launch.js +67 -0
  44. package/src/tools/browser/navigate.js +270 -0
  45. package/src/tools/browser/screenshot.js +351 -0
  46. package/src/tools/browser/type.js +174 -0
  47. package/src/tools/index.js +95 -0
  48. 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;