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