@democratize-quality/mcp-server 1.2.0 → 1.2.1

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 (56) hide show
  1. package/cli.js +248 -0
  2. package/package.json +7 -5
  3. package/src/chatmodes//360/237/214/220 api-generator.chatmode.md" +409 -0
  4. package/src/chatmodes//360/237/214/220 api-healer.chatmode.md" +494 -0
  5. package/src/chatmodes//360/237/214/220 api-planner.chatmode.md" +954 -0
  6. package/src/config/environments/api-only.js +72 -0
  7. package/src/config/environments/development.js +73 -0
  8. package/src/config/environments/production.js +88 -0
  9. package/src/config/index.js +360 -0
  10. package/src/config/server.js +60 -0
  11. package/src/config/tools/api.js +86 -0
  12. package/src/config/tools/browser.js +109 -0
  13. package/src/config/tools/default.js +51 -0
  14. package/src/docs/Agent_README.md +310 -0
  15. package/src/docs/QUICK_REFERENCE.md +111 -0
  16. package/src/server.ts +234 -0
  17. package/src/services/browserService.js +344 -0
  18. package/src/skills/api-planning/SKILL.md +224 -0
  19. package/src/skills/test-execution/SKILL.md +777 -0
  20. package/src/skills/test-generation/SKILL.md +309 -0
  21. package/src/skills/test-healing/SKILL.md +405 -0
  22. package/src/tools/api/api-generator.js +1884 -0
  23. package/src/tools/api/api-healer.js +636 -0
  24. package/src/tools/api/api-planner.js +2617 -0
  25. package/src/tools/api/api-project-setup.js +332 -0
  26. package/src/tools/api/api-request.js +660 -0
  27. package/src/tools/api/api-session-report.js +1297 -0
  28. package/src/tools/api/api-session-status.js +414 -0
  29. package/src/tools/api/prompts/README.md +293 -0
  30. package/src/tools/api/prompts/generation-prompts.js +722 -0
  31. package/src/tools/api/prompts/healing-prompts.js +214 -0
  32. package/src/tools/api/prompts/index.js +44 -0
  33. package/src/tools/api/prompts/orchestrator.js +353 -0
  34. package/src/tools/api/prompts/validation-rules.js +358 -0
  35. package/src/tools/base/ToolBase.js +249 -0
  36. package/src/tools/base/ToolRegistry.js +288 -0
  37. package/src/tools/browser/advanced/browser-console.js +403 -0
  38. package/src/tools/browser/advanced/browser-dialog.js +338 -0
  39. package/src/tools/browser/advanced/browser-evaluate.js +356 -0
  40. package/src/tools/browser/advanced/browser-file.js +499 -0
  41. package/src/tools/browser/advanced/browser-keyboard.js +362 -0
  42. package/src/tools/browser/advanced/browser-mouse.js +351 -0
  43. package/src/tools/browser/advanced/browser-network.js +440 -0
  44. package/src/tools/browser/advanced/browser-pdf.js +426 -0
  45. package/src/tools/browser/advanced/browser-tabs.js +516 -0
  46. package/src/tools/browser/advanced/browser-wait.js +397 -0
  47. package/src/tools/browser/click.js +187 -0
  48. package/src/tools/browser/close.js +79 -0
  49. package/src/tools/browser/dom.js +89 -0
  50. package/src/tools/browser/launch.js +86 -0
  51. package/src/tools/browser/navigate.js +289 -0
  52. package/src/tools/browser/screenshot.js +370 -0
  53. package/src/tools/browser/type.js +193 -0
  54. package/src/tools/index.js +114 -0
  55. package/src/utils/agentInstaller.js +437 -0
  56. package/src/utils/browserHelpers.js +102 -0
@@ -0,0 +1,499 @@
1
+ /**
2
+ * Copyright (C) 2025 Democratize Quality
3
+ *
4
+ * This file is part of Democratize Quality MCP Server.
5
+ *
6
+ * Democratize Quality MCP Server is free software: you can redistribute it and/or modify
7
+ * it under the terms of the GNU Affero General Public License as published by
8
+ * the Free Software Foundation, either version 3 of the License, or
9
+ * (at your option) any later version.
10
+ *
11
+ * Democratize Quality MCP Server is distributed in the hope that it will be useful,
12
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ * GNU Affero General Public License for more details.
15
+ *
16
+ * You should have received a copy of the GNU Affero General Public License
17
+ * along with Democratize Quality MCP Server. If not, see <https://www.gnu.org/licenses/>.
18
+ */
19
+
20
+ const ToolBase = require('../../base/ToolBase');
21
+ const browserService = require('../../../services/browserService');
22
+ const fs = require('fs').promises;
23
+ const path = require('path');
24
+
25
+ /**
26
+ * Enhanced File Tool - Handle file uploads, downloads, and file system interactions
27
+ * Inspired by Playwright MCP file handling capabilities
28
+ */
29
+ class BrowserFileTool extends ToolBase {
30
+ static definition = {
31
+ name: "browser_file",
32
+ description: "Handle file operations including uploads, downloads, and file input interactions. Supports various file formats and validation.",
33
+ input_schema: {
34
+ type: "object",
35
+ properties: {
36
+ browserId: {
37
+ type: "string",
38
+ description: "The ID of the browser instance"
39
+ },
40
+ action: {
41
+ type: "string",
42
+ enum: ["upload", "download", "setDownloadPath", "getDownloads", "clearDownloads"],
43
+ description: "The file operation to perform"
44
+ },
45
+ selector: {
46
+ type: "string",
47
+ description: "CSS selector for file input element (for upload action)"
48
+ },
49
+ filePath: {
50
+ type: "string",
51
+ description: "Path to the file to upload or download location"
52
+ },
53
+ files: {
54
+ type: "array",
55
+ items: { type: "string" },
56
+ description: "Array of file paths for multiple file upload"
57
+ },
58
+ downloadPath: {
59
+ type: "string",
60
+ description: "Directory path for downloads (for setDownloadPath action)"
61
+ },
62
+ url: {
63
+ type: "string",
64
+ description: "Direct download URL (for download action without clicking)"
65
+ },
66
+ fileName: {
67
+ type: "string",
68
+ description: "Specific filename for download"
69
+ },
70
+ timeout: {
71
+ type: "number",
72
+ default: 30000,
73
+ description: "Timeout in milliseconds for file operations"
74
+ },
75
+ waitForDownload: {
76
+ type: "boolean",
77
+ default: true,
78
+ description: "Whether to wait for download completion"
79
+ },
80
+ overwrite: {
81
+ type: "boolean",
82
+ default: false,
83
+ description: "Whether to overwrite existing files"
84
+ }
85
+ },
86
+ required: ["browserId", "action"]
87
+ },
88
+ output_schema: {
89
+ type: "object",
90
+ properties: {
91
+ success: { type: "boolean", description: "Whether the operation was successful" },
92
+ action: { type: "string", description: "The action that was performed" },
93
+ filePath: { type: "string", description: "Path of the uploaded/downloaded file" },
94
+ files: {
95
+ type: "array",
96
+ items: {
97
+ type: "object",
98
+ properties: {
99
+ name: { type: "string" },
100
+ path: { type: "string" },
101
+ size: { type: "number" },
102
+ type: { type: "string" },
103
+ url: { type: "string" }
104
+ }
105
+ },
106
+ description: "List of files"
107
+ },
108
+ downloadInfo: {
109
+ type: "object",
110
+ properties: {
111
+ url: { type: "string" },
112
+ filename: { type: "string" },
113
+ state: { type: "string" },
114
+ totalBytes: { type: "number" },
115
+ receivedBytes: { type: "number" }
116
+ },
117
+ description: "Download progress information"
118
+ },
119
+ message: { type: "string", description: "Operation result message" },
120
+ browserId: { type: "string", description: "Browser instance ID" }
121
+ },
122
+ required: ["success", "action", "browserId"]
123
+ }
124
+ };
125
+
126
+ constructor() {
127
+ super();
128
+ this.downloadCallbacks = new Map(); // browserId -> callback functions
129
+ this.downloadStates = new Map(); // browserId -> download states
130
+ }
131
+
132
+ async execute(parameters) {
133
+ const {
134
+ browserId,
135
+ action,
136
+ selector,
137
+ filePath,
138
+ files = [],
139
+ downloadPath,
140
+ url,
141
+ fileName,
142
+ timeout = 30000,
143
+ waitForDownload = true,
144
+ overwrite = false
145
+ } = parameters;
146
+
147
+ const browser = browserService.getBrowserInstance(browserId);
148
+ if (!browser) {
149
+ throw new Error(`Browser instance '${browserId}' not found`);
150
+ }
151
+
152
+ const client = browser.client;
153
+
154
+ let result = {
155
+ success: false,
156
+ action: action,
157
+ browserId: browserId
158
+ };
159
+
160
+ switch (action) {
161
+ case 'upload':
162
+ if (!selector) {
163
+ throw new Error('Selector is required for upload action');
164
+ }
165
+ const uploadPaths = filePath ? [filePath] : files;
166
+ if (uploadPaths.length === 0) {
167
+ throw new Error('Either filePath or files array is required for upload');
168
+ }
169
+ await this.uploadFiles(client, selector, uploadPaths);
170
+ result.success = true;
171
+ result.files = await this.getFileInfo(uploadPaths);
172
+ result.message = `Uploaded ${uploadPaths.length} file(s)`;
173
+ break;
174
+
175
+ case 'download':
176
+ if (!url && !selector) {
177
+ throw new Error('Either URL or selector is required for download action');
178
+ }
179
+ const downloadResult = await this.downloadFile(client, url, selector, downloadPath, fileName, timeout, waitForDownload);
180
+ result.success = true;
181
+ result.downloadInfo = downloadResult;
182
+ result.filePath = downloadResult.path;
183
+ result.message = 'Download completed';
184
+ break;
185
+
186
+ case 'setDownloadPath':
187
+ if (!downloadPath) {
188
+ throw new Error('Download path is required for setDownloadPath action');
189
+ }
190
+ await this.setDownloadPath(client, downloadPath);
191
+ result.success = true;
192
+ result.message = `Download path set to: ${downloadPath}`;
193
+ break;
194
+
195
+ case 'getDownloads':
196
+ const downloads = this.getDownloadHistory(browserId);
197
+ result.success = true;
198
+ result.files = downloads;
199
+ result.message = `Found ${downloads.length} download(s)`;
200
+ break;
201
+
202
+ case 'clearDownloads':
203
+ this.clearDownloadHistory(browserId);
204
+ result.success = true;
205
+ result.message = 'Download history cleared';
206
+ break;
207
+
208
+ default:
209
+ throw new Error(`Unsupported file action: ${action}`);
210
+ }
211
+
212
+ return result;
213
+ }
214
+
215
+ /**
216
+ * Upload files to a file input element
217
+ */
218
+ async uploadFiles(client, selector, filePaths) {
219
+ // Validate files exist
220
+ for (const filePath of filePaths) {
221
+ try {
222
+ await fs.access(filePath);
223
+ } catch (error) {
224
+ throw new Error(`File not found: ${filePath}`);
225
+ }
226
+ }
227
+
228
+ // Enable DOM and Runtime domains
229
+ await client.DOM.enable();
230
+ await client.Runtime.enable();
231
+
232
+ // Find the file input element
233
+ const document = await client.DOM.getDocument();
234
+ const element = await client.DOM.querySelector({
235
+ nodeId: document.root.nodeId,
236
+ selector: selector
237
+ });
238
+
239
+ if (!element.nodeId) {
240
+ throw new Error(`File input element not found with selector: ${selector}`);
241
+ }
242
+
243
+ // Get element attributes to verify it's a file input
244
+ const attributes = await client.DOM.getAttributes({
245
+ nodeId: element.nodeId
246
+ });
247
+
248
+ const attrMap = {};
249
+ for (let i = 0; i < attributes.attributes.length; i += 2) {
250
+ attrMap[attributes.attributes[i]] = attributes.attributes[i + 1];
251
+ }
252
+
253
+ if (attrMap.type !== 'file') {
254
+ throw new Error(`Element is not a file input: ${JSON.stringify(attrMap)}`);
255
+ }
256
+
257
+ // Set files on the input element
258
+ await client.DOM.setFileInputFiles({
259
+ files: filePaths,
260
+ nodeId: element.nodeId
261
+ });
262
+
263
+ // Trigger change event
264
+ await client.Runtime.evaluate({
265
+ expression: `
266
+ (function() {
267
+ const element = document.querySelector('${selector}');
268
+ if (element) {
269
+ const event = new Event('change', { bubbles: true });
270
+ element.dispatchEvent(event);
271
+ return true;
272
+ }
273
+ return false;
274
+ })()
275
+ `
276
+ });
277
+ }
278
+
279
+ /**
280
+ * Download a file either by URL or by clicking an element
281
+ */
282
+ async downloadFile(client, url, selector, downloadPath, fileName, timeout, waitForDownload) {
283
+ // Enable Page domain for download events
284
+ await client.Page.enable();
285
+
286
+ let downloadInfo = null;
287
+ const downloadPromise = new Promise((resolve, reject) => {
288
+ const timeoutId = setTimeout(() => {
289
+ reject(new Error(`Download timeout after ${timeout}ms`));
290
+ }, timeout);
291
+
292
+ // Listen for download events
293
+ client.Page.downloadWillBegin((params) => {
294
+ clearTimeout(timeoutId);
295
+ downloadInfo = {
296
+ url: params.url,
297
+ filename: params.suggestedFilename,
298
+ guid: params.guid
299
+ };
300
+ });
301
+
302
+ client.Page.downloadProgress((params) => {
303
+ if (downloadInfo && params.guid === downloadInfo.guid) {
304
+ downloadInfo.state = params.state;
305
+ downloadInfo.totalBytes = params.totalBytes;
306
+ downloadInfo.receivedBytes = params.receivedBytes;
307
+
308
+ if (params.state === 'completed') {
309
+ resolve(downloadInfo);
310
+ } else if (params.state === 'canceled' || params.state === 'interrupted') {
311
+ reject(new Error(`Download ${params.state}: ${downloadInfo.filename}`));
312
+ }
313
+ }
314
+ });
315
+ });
316
+
317
+ // Set download behavior if path is specified
318
+ if (downloadPath) {
319
+ await this.setDownloadPath(client, downloadPath);
320
+ }
321
+
322
+ // Initiate download
323
+ if (url) {
324
+ // Direct URL download
325
+ await client.Page.navigate({ url: url });
326
+ } else if (selector) {
327
+ // Click element to trigger download
328
+ await client.Runtime.evaluate({
329
+ expression: `
330
+ (function() {
331
+ const element = document.querySelector('${selector}');
332
+ if (element) {
333
+ element.click();
334
+ return true;
335
+ }
336
+ return false;
337
+ })()
338
+ `
339
+ });
340
+ }
341
+
342
+ // Wait for download if requested
343
+ if (waitForDownload) {
344
+ downloadInfo = await downloadPromise;
345
+
346
+ // Store download info
347
+ this.addDownloadToHistory(browserService.getBrowserInstance(client.browserId)?.id || 'unknown', downloadInfo);
348
+ }
349
+
350
+ return downloadInfo || { state: 'initiated' };
351
+ }
352
+
353
+ /**
354
+ * Set the download directory
355
+ */
356
+ async setDownloadPath(client, downloadPath) {
357
+ // Ensure directory exists
358
+ try {
359
+ await fs.mkdir(downloadPath, { recursive: true });
360
+ } catch (error) {
361
+ throw new Error(`Could not create download directory: ${error.message}`);
362
+ }
363
+
364
+ // Set download behavior
365
+ await client.Page.setDownloadBehavior({
366
+ behavior: 'allow',
367
+ downloadPath: downloadPath
368
+ });
369
+ }
370
+
371
+ /**
372
+ * Get file information for uploaded files
373
+ */
374
+ async getFileInfo(filePaths) {
375
+ const fileInfos = [];
376
+
377
+ for (const filePath of filePaths) {
378
+ try {
379
+ const stats = await fs.stat(filePath);
380
+ const info = {
381
+ name: path.basename(filePath),
382
+ path: filePath,
383
+ size: stats.size,
384
+ type: this.getFileType(filePath),
385
+ lastModified: stats.mtime
386
+ };
387
+ fileInfos.push(info);
388
+ } catch (error) {
389
+ fileInfos.push({
390
+ name: path.basename(filePath),
391
+ path: filePath,
392
+ error: error.message
393
+ });
394
+ }
395
+ }
396
+
397
+ return fileInfos;
398
+ }
399
+
400
+ /**
401
+ * Get file type based on extension
402
+ */
403
+ getFileType(filePath) {
404
+ const ext = path.extname(filePath).toLowerCase();
405
+ const mimeTypes = {
406
+ '.txt': 'text/plain',
407
+ '.pdf': 'application/pdf',
408
+ '.doc': 'application/msword',
409
+ '.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
410
+ '.xls': 'application/vnd.ms-excel',
411
+ '.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
412
+ '.png': 'image/png',
413
+ '.jpg': 'image/jpeg',
414
+ '.jpeg': 'image/jpeg',
415
+ '.gif': 'image/gif',
416
+ '.mp4': 'video/mp4',
417
+ '.zip': 'application/zip',
418
+ '.json': 'application/json',
419
+ '.csv': 'text/csv'
420
+ };
421
+
422
+ return mimeTypes[ext] || 'application/octet-stream';
423
+ }
424
+
425
+ /**
426
+ * Add download to history
427
+ */
428
+ addDownloadToHistory(browserId, downloadInfo) {
429
+ if (!this.downloadStates.has(browserId)) {
430
+ this.downloadStates.set(browserId, []);
431
+ }
432
+
433
+ const downloads = this.downloadStates.get(browserId);
434
+ downloads.push({
435
+ ...downloadInfo,
436
+ timestamp: new Date().toISOString()
437
+ });
438
+
439
+ // Keep only last 100 downloads
440
+ if (downloads.length > 100) {
441
+ downloads.splice(0, downloads.length - 100);
442
+ }
443
+ }
444
+
445
+ /**
446
+ * Get download history for a browser
447
+ */
448
+ getDownloadHistory(browserId) {
449
+ return this.downloadStates.get(browserId) || [];
450
+ }
451
+
452
+ /**
453
+ * Clear download history for a browser
454
+ */
455
+ clearDownloadHistory(browserId) {
456
+ this.downloadStates.delete(browserId);
457
+ }
458
+
459
+ /**
460
+ * Create a temporary file for testing
461
+ */
462
+ async createTempFile(content, extension = '.txt') {
463
+ const tempDir = process.env.TMPDIR || '/tmp';
464
+ const fileName = `temp-${Date.now()}${extension}`;
465
+ const filePath = path.join(tempDir, fileName);
466
+
467
+ await fs.writeFile(filePath, content);
468
+ return filePath;
469
+ }
470
+
471
+ /**
472
+ * Validate file upload constraints
473
+ */
474
+ validateFileUpload(filePath, constraints = {}) {
475
+ const stats = require('fs').statSync(filePath);
476
+ const ext = path.extname(filePath).toLowerCase();
477
+
478
+ const validation = {
479
+ valid: true,
480
+ errors: []
481
+ };
482
+
483
+ // Check file size
484
+ if (constraints.maxSize && stats.size > constraints.maxSize) {
485
+ validation.valid = false;
486
+ validation.errors.push(`File size ${stats.size} exceeds maximum ${constraints.maxSize}`);
487
+ }
488
+
489
+ // Check file type
490
+ if (constraints.allowedTypes && !constraints.allowedTypes.includes(ext)) {
491
+ validation.valid = false;
492
+ validation.errors.push(`File type ${ext} not allowed. Allowed: ${constraints.allowedTypes.join(', ')}`);
493
+ }
494
+
495
+ return validation;
496
+ }
497
+ }
498
+
499
+ module.exports = BrowserFileTool;