@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,497 @@
1
+ const ToolBase = require('../../base/ToolBase');
2
+ const browserService = require('../../../services/browserService');
3
+
4
+ /**
5
+ * Enhanced Tabs Tool - Comprehensive tab management
6
+ * Inspired by Playwright MCP tabs capabilities
7
+ */
8
+ class BrowserTabsTool extends ToolBase {
9
+ static definition = {
10
+ name: "browser_tabs",
11
+ description: "Manage browser tabs - create, close, switch, list, and organize tabs with advanced features.",
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
+ enum: ["list", "create", "close", "switch", "duplicate", "pin", "unpin", "mute", "unmute", "refresh", "goBack", "goForward"],
22
+ description: "The tab action to perform"
23
+ },
24
+ tabId: {
25
+ type: "string",
26
+ description: "The ID of the tab (for close, switch, duplicate, pin, mute, refresh, etc.)"
27
+ },
28
+ url: {
29
+ type: "string",
30
+ description: "URL to open in new tab (for create action)"
31
+ },
32
+ title: {
33
+ type: "string",
34
+ description: "Title filter for finding tabs (supports partial matching)"
35
+ },
36
+ active: {
37
+ type: "boolean",
38
+ description: "Whether to make the tab active (for create and switch actions)"
39
+ },
40
+ windowId: {
41
+ type: "string",
42
+ description: "Window ID for tab operations"
43
+ },
44
+ pattern: {
45
+ type: "string",
46
+ description: "URL pattern to match tabs (regex supported)"
47
+ },
48
+ includeDetails: {
49
+ type: "boolean",
50
+ default: false,
51
+ description: "Include detailed tab information (for list action)"
52
+ }
53
+ },
54
+ required: ["browserId", "action"]
55
+ },
56
+ output_schema: {
57
+ type: "object",
58
+ properties: {
59
+ success: { type: "boolean", description: "Whether the operation was successful" },
60
+ action: { type: "string", description: "The action that was performed" },
61
+ tabs: {
62
+ type: "array",
63
+ items: {
64
+ type: "object",
65
+ properties: {
66
+ id: { type: "string" },
67
+ url: { type: "string" },
68
+ title: { type: "string" },
69
+ active: { type: "boolean" },
70
+ pinned: { type: "boolean" },
71
+ muted: { type: "boolean" },
72
+ windowId: { type: "string" },
73
+ status: { type: "string" }
74
+ }
75
+ },
76
+ description: "List of tabs"
77
+ },
78
+ tabId: { type: "string", description: "ID of the affected tab" },
79
+ message: { type: "string", description: "Operation result message" },
80
+ browserId: { type: "string", description: "Browser instance ID" }
81
+ },
82
+ required: ["success", "action", "browserId"]
83
+ }
84
+ };
85
+
86
+ async execute(parameters) {
87
+ const {
88
+ browserId,
89
+ action,
90
+ tabId,
91
+ url,
92
+ title,
93
+ active = true,
94
+ windowId,
95
+ pattern,
96
+ includeDetails = false
97
+ } = parameters;
98
+
99
+ const browser = browserService.getBrowserInstance(browserId);
100
+ if (!browser) {
101
+ throw new Error(`Browser instance '${browserId}' not found`);
102
+ }
103
+
104
+ const client = browser.client;
105
+
106
+ let result = {
107
+ success: false,
108
+ action: action,
109
+ browserId: browserId
110
+ };
111
+
112
+ switch (action) {
113
+ case 'list':
114
+ const tabs = await this.listTabs(client, title, pattern, includeDetails);
115
+ result.success = true;
116
+ result.tabs = tabs;
117
+ break;
118
+
119
+ case 'create':
120
+ if (!url) {
121
+ throw new Error('URL is required for create action');
122
+ }
123
+ const newTab = await this.createTab(client, url, active);
124
+ result.success = true;
125
+ result.tabId = newTab.targetId;
126
+ result.message = `Tab created with ID: ${newTab.targetId}`;
127
+ result.tabs = [newTab];
128
+ break;
129
+
130
+ case 'close':
131
+ if (!tabId) {
132
+ throw new Error('Tab ID is required for close action');
133
+ }
134
+ await this.closeTab(client, tabId);
135
+ result.success = true;
136
+ result.tabId = tabId;
137
+ result.message = `Tab ${tabId} closed`;
138
+ break;
139
+
140
+ case 'switch':
141
+ if (!tabId) {
142
+ throw new Error('Tab ID is required for switch action');
143
+ }
144
+ await this.switchToTab(client, tabId);
145
+ result.success = true;
146
+ result.tabId = tabId;
147
+ result.message = `Switched to tab ${tabId}`;
148
+ break;
149
+
150
+ case 'duplicate':
151
+ if (!tabId) {
152
+ throw new Error('Tab ID is required for duplicate action');
153
+ }
154
+ const duplicatedTab = await this.duplicateTab(client, tabId);
155
+ result.success = true;
156
+ result.tabId = duplicatedTab.targetId;
157
+ result.message = `Tab duplicated with ID: ${duplicatedTab.targetId}`;
158
+ result.tabs = [duplicatedTab];
159
+ break;
160
+
161
+ case 'refresh':
162
+ if (!tabId) {
163
+ throw new Error('Tab ID is required for refresh action');
164
+ }
165
+ await this.refreshTab(client, tabId);
166
+ result.success = true;
167
+ result.tabId = tabId;
168
+ result.message = `Tab ${tabId} refreshed`;
169
+ break;
170
+
171
+ case 'goBack':
172
+ if (!tabId) {
173
+ throw new Error('Tab ID is required for goBack action');
174
+ }
175
+ await this.goBack(client, tabId);
176
+ result.success = true;
177
+ result.tabId = tabId;
178
+ result.message = `Navigated back in tab ${tabId}`;
179
+ break;
180
+
181
+ case 'goForward':
182
+ if (!tabId) {
183
+ throw new Error('Tab ID is required for goForward action');
184
+ }
185
+ await this.goForward(client, tabId);
186
+ result.success = true;
187
+ result.tabId = tabId;
188
+ result.message = `Navigated forward in tab ${tabId}`;
189
+ break;
190
+
191
+ case 'pin':
192
+ case 'unpin':
193
+ case 'mute':
194
+ case 'unmute':
195
+ // These actions would require additional Chrome extension APIs
196
+ // For now, we'll return a message indicating the limitation
197
+ result.success = false;
198
+ result.message = `${action} action requires browser extension capabilities not available via DevTools Protocol`;
199
+ break;
200
+
201
+ default:
202
+ throw new Error(`Unsupported tabs action: ${action}`);
203
+ }
204
+
205
+ return result;
206
+ }
207
+
208
+ /**
209
+ * List all tabs with optional filtering
210
+ */
211
+ async listTabs(client, titleFilter, urlPattern, includeDetails) {
212
+ const targets = await client.Target.getTargets();
213
+ const tabs = targets.targetInfos
214
+ .filter(target => target.type === 'page')
215
+ .map(target => this.formatTabInfo(target, includeDetails));
216
+
217
+ let filteredTabs = tabs;
218
+
219
+ // Apply title filter
220
+ if (titleFilter) {
221
+ filteredTabs = filteredTabs.filter(tab =>
222
+ tab.title && tab.title.toLowerCase().includes(titleFilter.toLowerCase())
223
+ );
224
+ }
225
+
226
+ // Apply URL pattern filter
227
+ if (urlPattern) {
228
+ const regex = new RegExp(urlPattern, 'i');
229
+ filteredTabs = filteredTabs.filter(tab =>
230
+ tab.url && regex.test(tab.url)
231
+ );
232
+ }
233
+
234
+ // Get additional details if requested
235
+ if (includeDetails) {
236
+ for (const tab of filteredTabs) {
237
+ try {
238
+ const runtime = await client.Runtime.evaluate({
239
+ expression: `({
240
+ readyState: document.readyState,
241
+ visibilityState: document.visibilityState,
242
+ hasFocus: document.hasFocus(),
243
+ scrollPosition: { x: window.scrollX, y: window.scrollY },
244
+ windowSize: { width: window.innerWidth, height: window.innerHeight }
245
+ })`,
246
+ contextId: undefined,
247
+ targetId: tab.id
248
+ });
249
+
250
+ if (runtime.result && runtime.result.value) {
251
+ tab.details = runtime.result.value;
252
+ }
253
+ } catch (error) {
254
+ // Ignore errors for inactive tabs
255
+ tab.details = { error: 'Could not retrieve details' };
256
+ }
257
+ }
258
+ }
259
+
260
+ return filteredTabs;
261
+ }
262
+
263
+ /**
264
+ * Create a new tab
265
+ */
266
+ async createTab(client, url, makeActive = true) {
267
+ const target = await client.Target.createTarget({
268
+ url: url,
269
+ newWindow: false
270
+ });
271
+
272
+ if (makeActive) {
273
+ await this.switchToTab(client, target.targetId);
274
+ }
275
+
276
+ // Get updated tab info
277
+ const targets = await client.Target.getTargets();
278
+ const tabInfo = targets.targetInfos.find(t => t.targetId === target.targetId);
279
+
280
+ return this.formatTabInfo(tabInfo);
281
+ }
282
+
283
+ /**
284
+ * Close a tab
285
+ */
286
+ async closeTab(client, tabId) {
287
+ await client.Target.closeTarget({
288
+ targetId: tabId
289
+ });
290
+ }
291
+
292
+ /**
293
+ * Switch to a specific tab
294
+ */
295
+ async switchToTab(client, tabId) {
296
+ await client.Target.activateTarget({
297
+ targetId: tabId
298
+ });
299
+ }
300
+
301
+ /**
302
+ * Duplicate a tab
303
+ */
304
+ async duplicateTab(client, tabId) {
305
+ // Get the URL of the tab to duplicate
306
+ const targets = await client.Target.getTargets();
307
+ const sourceTab = targets.targetInfos.find(t => t.targetId === tabId);
308
+
309
+ if (!sourceTab) {
310
+ throw new Error(`Tab with ID ${tabId} not found`);
311
+ }
312
+
313
+ // Create new tab with same URL
314
+ return this.createTab(client, sourceTab.url, false);
315
+ }
316
+
317
+ /**
318
+ * Refresh a tab
319
+ */
320
+ async refreshTab(client, tabId) {
321
+ // First, attach to the target
322
+ const session = await client.Target.attachToTarget({
323
+ targetId: tabId,
324
+ flatten: true
325
+ });
326
+
327
+ // Use the session to reload the page
328
+ await client.Page.reload({
329
+ sessionId: session.sessionId
330
+ });
331
+
332
+ // Detach from the target
333
+ await client.Target.detachFromTarget({
334
+ sessionId: session.sessionId
335
+ });
336
+ }
337
+
338
+ /**
339
+ * Navigate back in tab history
340
+ */
341
+ async goBack(client, tabId) {
342
+ const session = await client.Target.attachToTarget({
343
+ targetId: tabId,
344
+ flatten: true
345
+ });
346
+
347
+ const history = await client.Page.getNavigationHistory({
348
+ sessionId: session.sessionId
349
+ });
350
+
351
+ if (history.currentIndex > 0) {
352
+ await client.Page.navigateToHistoryEntry({
353
+ entryId: history.entries[history.currentIndex - 1].id,
354
+ sessionId: session.sessionId
355
+ });
356
+ }
357
+
358
+ await client.Target.detachFromTarget({
359
+ sessionId: session.sessionId
360
+ });
361
+ }
362
+
363
+ /**
364
+ * Navigate forward in tab history
365
+ */
366
+ async goForward(client, tabId) {
367
+ const session = await client.Target.attachToTarget({
368
+ targetId: tabId,
369
+ flatten: true
370
+ });
371
+
372
+ const history = await client.Page.getNavigationHistory({
373
+ sessionId: session.sessionId
374
+ });
375
+
376
+ if (history.currentIndex < history.entries.length - 1) {
377
+ await client.Page.navigateToHistoryEntry({
378
+ entryId: history.entries[history.currentIndex + 1].id,
379
+ sessionId: session.sessionId
380
+ });
381
+ }
382
+
383
+ await client.Target.detachFromTarget({
384
+ sessionId: session.sessionId
385
+ });
386
+ }
387
+
388
+ /**
389
+ * Format tab information for output
390
+ */
391
+ formatTabInfo(target, includeDetails = false) {
392
+ const formatted = {
393
+ id: target.targetId,
394
+ url: target.url,
395
+ title: target.title,
396
+ type: target.type,
397
+ attached: target.attached || false
398
+ };
399
+
400
+ // Add browser context info if available
401
+ if (target.browserContextId) {
402
+ formatted.browserContextId = target.browserContextId;
403
+ }
404
+
405
+ // Add opener info if available
406
+ if (target.openerId) {
407
+ formatted.openerId = target.openerId;
408
+ }
409
+
410
+ return formatted;
411
+ }
412
+
413
+ /**
414
+ * Find tabs by various criteria
415
+ */
416
+ async findTabs(client, criteria = {}) {
417
+ const tabs = await this.listTabs(client, criteria.title, criteria.url, false);
418
+
419
+ return tabs.filter(tab => {
420
+ let matches = true;
421
+
422
+ if (criteria.active !== undefined) {
423
+ // Note: We'd need additional logic to determine which tab is currently active
424
+ // This would require tracking focus or using additional CDP calls
425
+ }
426
+
427
+ if (criteria.url && !tab.url.includes(criteria.url)) {
428
+ matches = false;
429
+ }
430
+
431
+ if (criteria.title && !tab.title.toLowerCase().includes(criteria.title.toLowerCase())) {
432
+ matches = false;
433
+ }
434
+
435
+ return matches;
436
+ });
437
+ }
438
+
439
+ /**
440
+ * Get detailed information about a specific tab
441
+ */
442
+ async getTabDetails(client, tabId) {
443
+ const targets = await client.Target.getTargets();
444
+ const tab = targets.targetInfos.find(t => t.targetId === tabId);
445
+
446
+ if (!tab) {
447
+ throw new Error(`Tab with ID ${tabId} not found`);
448
+ }
449
+
450
+ const formatted = this.formatTabInfo(tab, true);
451
+
452
+ try {
453
+ // Try to get runtime information
454
+ const session = await client.Target.attachToTarget({
455
+ targetId: tabId,
456
+ flatten: true
457
+ });
458
+
459
+ const runtime = await client.Runtime.evaluate({
460
+ expression: `({
461
+ readyState: document.readyState,
462
+ visibilityState: document.visibilityState,
463
+ hasFocus: document.hasFocus(),
464
+ location: {
465
+ href: location.href,
466
+ protocol: location.protocol,
467
+ hostname: location.hostname,
468
+ pathname: location.pathname,
469
+ search: location.search,
470
+ hash: location.hash
471
+ },
472
+ viewport: {
473
+ width: window.innerWidth,
474
+ height: window.innerHeight,
475
+ scrollX: window.scrollX,
476
+ scrollY: window.scrollY
477
+ }
478
+ })`,
479
+ sessionId: session.sessionId
480
+ });
481
+
482
+ if (runtime.result && runtime.result.value) {
483
+ formatted.runtime = runtime.result.value;
484
+ }
485
+
486
+ await client.Target.detachFromTarget({
487
+ sessionId: session.sessionId
488
+ });
489
+ } catch (error) {
490
+ formatted.error = 'Could not retrieve runtime details: ' + error.message;
491
+ }
492
+
493
+ return formatted;
494
+ }
495
+ }
496
+
497
+ module.exports = BrowserTabsTool;