@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,641 @@
1
+ const ToolBase = require('../base/ToolBase');
2
+ const https = require('https');
3
+ const http = require('http');
4
+ const { URL } = require('url');
5
+
6
+ let z;
7
+ try {
8
+ // import Zod
9
+ const zod = require('zod');
10
+ z = zod.z || zod.default?.z || zod;
11
+ if (!z || typeof z.object !== 'function') {
12
+ throw new Error('Zod not properly loaded');
13
+ }
14
+ } catch (error) {
15
+ console.error('Failed to load Zod:', error.message);
16
+ // Fallback: create a simple validation function
17
+ z = {
18
+ object: (schema) => ({ parse: (data) => data }),
19
+ string: () => ({ optional: () => ({}) }),
20
+ enum: () => ({ optional: () => ({}) }),
21
+ record: () => ({ optional: () => ({}) }),
22
+ any: () => ({ optional: () => ({}) }),
23
+ number: () => ({ optional: () => ({}) }),
24
+ array: () => ({ optional: () => ({}) })
25
+ };
26
+ }
27
+
28
+ // Input schema for the API request tool
29
+ const chainStepSchema = z.object({
30
+ name: z.string(),
31
+ method: z.enum(['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS']).optional(),
32
+ url: z.string(),
33
+ headers: z.record(z.string()).optional(),
34
+ data: z.any().optional(),
35
+ expect: z.object({
36
+ status: z.number().optional(),
37
+ contentType: z.string().optional(),
38
+ body: z.any().optional(),
39
+ bodyRegex: z.string().optional()
40
+ }).optional(),
41
+ extract: z.record(z.string()).optional() // { varName: 'field' }
42
+ });
43
+
44
+ const apiRequestInputSchema = z.object({
45
+ sessionId: z.string().optional(), // New: session management
46
+ // Single-request legacy mode
47
+ method: z.enum(['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS']).optional(),
48
+ url: z.string().optional(),
49
+ headers: z.record(z.string()).optional(),
50
+ data: z.any().optional(),
51
+ expect: z.object({
52
+ status: z.number().optional(),
53
+ contentType: z.string().optional(),
54
+ body: z.any().optional(),
55
+ bodyRegex: z.string().optional()
56
+ }).optional(),
57
+ // Chaining mode
58
+ chain: z.array(chainStepSchema).optional()
59
+ });
60
+
61
+ // --- In-memory session store ---
62
+ const sessionStore = global.__API_SESSION_STORE__ || new Map();
63
+ global.__API_SESSION_STORE__ = sessionStore;
64
+
65
+ /**
66
+ * API Request Tool - Perform HTTP API requests with validation and session management
67
+ */
68
+ class ApiRequestTool extends ToolBase {
69
+ static definition = {
70
+ name: "api_request",
71
+ description: "Perform HTTP API requests with validation, session management, and request chaining capabilities for comprehensive API testing.",
72
+ input_schema: {
73
+ type: "object",
74
+ properties: {
75
+ sessionId: {
76
+ type: "string",
77
+ description: "Optional session ID for managing related requests"
78
+ },
79
+ method: {
80
+ type: "string",
81
+ enum: ["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"],
82
+ description: "HTTP method for the request"
83
+ },
84
+ url: {
85
+ type: "string",
86
+ description: "Target URL for the HTTP request"
87
+ },
88
+ headers: {
89
+ type: "object",
90
+ additionalProperties: { type: "string" },
91
+ description: "HTTP headers as key-value pairs"
92
+ },
93
+ data: {
94
+ description: "Request body data (JSON object, string, etc.)"
95
+ },
96
+ expect: {
97
+ type: "object",
98
+ properties: {
99
+ status: {
100
+ type: "number",
101
+ description: "Expected HTTP status code"
102
+ },
103
+ contentType: {
104
+ type: "string",
105
+ description: "Expected content type"
106
+ },
107
+ body: {
108
+ description: "Expected response body content"
109
+ },
110
+ bodyRegex: {
111
+ type: "string",
112
+ description: "Regex pattern to match against response body"
113
+ }
114
+ },
115
+ description: "Validation expectations for the response"
116
+ },
117
+ chain: {
118
+ type: "array",
119
+ items: {
120
+ type: "object",
121
+ properties: {
122
+ name: {
123
+ type: "string",
124
+ description: "Step name for reference in chaining"
125
+ },
126
+ method: {
127
+ type: "string",
128
+ enum: ["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"],
129
+ description: "HTTP method for this step"
130
+ },
131
+ url: {
132
+ type: "string",
133
+ description: "URL for this step (supports templating)"
134
+ },
135
+ headers: {
136
+ type: "object",
137
+ additionalProperties: { type: "string" },
138
+ description: "Headers for this step"
139
+ },
140
+ data: {
141
+ description: "Data for this step"
142
+ },
143
+ expect: {
144
+ type: "object",
145
+ properties: {
146
+ status: { type: "number" },
147
+ contentType: { type: "string" },
148
+ body: {},
149
+ bodyRegex: { type: "string" }
150
+ },
151
+ description: "Validation expectations"
152
+ },
153
+ extract: {
154
+ type: "object",
155
+ additionalProperties: { type: "string" },
156
+ description: "Fields to extract for use in subsequent steps"
157
+ }
158
+ },
159
+ required: ["name", "url"]
160
+ },
161
+ description: "Chain of requests to execute sequentially"
162
+ }
163
+ },
164
+ additionalProperties: false
165
+ }
166
+ };
167
+
168
+ constructor() {
169
+ super();
170
+ this.sessionStore = sessionStore;
171
+ }
172
+
173
+ /**
174
+ * Simple validation function as fallback if Zod fails
175
+ */
176
+ validateInput(input) {
177
+ // Basic validation
178
+ if (typeof input !== 'object' || input === null) {
179
+ throw new Error('Input must be an object');
180
+ }
181
+
182
+ // If chain is provided, validate chain structure
183
+ if (input.chain && Array.isArray(input.chain)) {
184
+ for (let i = 0; i < input.chain.length; i++) {
185
+ const step = input.chain[i];
186
+ if (!step.name || !step.url) {
187
+ throw new Error(`Chain step ${i}: missing required fields 'name' or 'url'`);
188
+ }
189
+ if (step.method && !['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS'].includes(step.method)) {
190
+ throw new Error(`Chain step ${i}: invalid method '${step.method}'`);
191
+ }
192
+ }
193
+ }
194
+
195
+ // For single request mode, url is required if no chain
196
+ if (!input.chain && !input.url) {
197
+ throw new Error('URL is required for single request mode');
198
+ }
199
+
200
+ if (input.method && !['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS'].includes(input.method)) {
201
+ throw new Error(`Invalid method '${input.method}'`);
202
+ }
203
+
204
+ return input;
205
+ }
206
+
207
+ async execute(parameters) {
208
+ try {
209
+ // Validate input - try Zod first, fallback to simple validation
210
+ let input;
211
+ try {
212
+ if (typeof apiRequestInputSchema.parse === 'function') {
213
+ input = apiRequestInputSchema.parse(parameters);
214
+ } else {
215
+ input = this.validateInput(parameters);
216
+ }
217
+ } catch (zodError) {
218
+ console.warn('Zod validation failed, using fallback validation:', zodError.message);
219
+ input = this.validateInput(parameters);
220
+ }
221
+
222
+ // --- Session Management ---
223
+ const uuid = () => {
224
+ if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function')
225
+ return crypto.randomUUID();
226
+ // Simple pseudo-unique fallback: not cryptographically secure, but fine for session IDs
227
+ return 'session-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 10);
228
+ };
229
+
230
+ const sessionId = input.sessionId || uuid();
231
+ if (!sessionStore.has(sessionId)) {
232
+ sessionStore.set(sessionId, {
233
+ sessionId,
234
+ startTime: new Date().toISOString(),
235
+ logs: [],
236
+ status: 'running'
237
+ });
238
+ }
239
+ const session = sessionStore.get(sessionId);
240
+
241
+ // --- API CHAINING SUPPORT ---
242
+ function renderTemplate(str, vars) {
243
+ if (typeof str !== 'string') return str;
244
+ return str.replace(/\{\{\s*([\w.]+)\s*\}\}/g, (_, path) => {
245
+ const [step, ...rest] = path.split('.');
246
+ let val = vars[step];
247
+ for (const p of rest)
248
+ val = val?.[p];
249
+ return val !== undefined ? String(val) : '';
250
+ });
251
+ }
252
+
253
+ function extractFields(obj, extract) {
254
+ const result = {};
255
+ if (!extract || typeof extract !== 'object') return result;
256
+
257
+ for (const [k, path] of Object.entries(extract)) {
258
+ if (typeof path !== 'string') continue;
259
+ const parts = path.split('.');
260
+ let val = obj;
261
+ for (const p of parts) {
262
+ val = val?.[p];
263
+ if (val === undefined) break;
264
+ }
265
+ result[k] = val;
266
+ }
267
+ return result;
268
+ }
269
+
270
+ // If 'chain' is present, execute steps sequentially
271
+ if (Array.isArray(input.chain)) {
272
+ const stepVars = {};
273
+ const results = [];
274
+
275
+ for (const step of input.chain) {
276
+ // Validate step has required fields
277
+ if (!step.name || !step.url) {
278
+ throw new Error(`Invalid chain step: missing name or url`);
279
+ }
280
+
281
+ // Render templates in url, headers, data
282
+ const url = renderTemplate(step.url, stepVars);
283
+ const headers = {};
284
+ for (const k in (step.headers || {})) {
285
+ headers[k] = renderTemplate(step.headers[k], stepVars);
286
+ }
287
+
288
+ let data = step.data;
289
+ if (typeof data === 'string') {
290
+ data = renderTemplate(data, stepVars);
291
+ } else if (typeof data === 'object' && data !== null) {
292
+ // Handle object data with template rendering
293
+ try {
294
+ data = JSON.parse(renderTemplate(JSON.stringify(data), stepVars));
295
+ } catch (e) {
296
+ // If JSON parsing fails, keep original data
297
+ console.warn('Failed to parse templated data, using original:', e.message);
298
+ }
299
+ }
300
+
301
+ // Execute request using Node.js built-in modules
302
+ const response = await this.makeHttpRequest(
303
+ step.method || 'GET',
304
+ url,
305
+ headers,
306
+ data
307
+ );
308
+
309
+ const status = response.statusCode;
310
+ const statusText = response.statusMessage || '';
311
+ const contentType = response.headers['content-type'] || '';
312
+ let responseBody = response.body;
313
+
314
+ if (contentType.includes('application/json')) {
315
+ try {
316
+ responseBody = JSON.parse(response.body);
317
+ } catch (e) {
318
+ // Keep as string if JSON parsing fails
319
+ }
320
+ }
321
+
322
+ // Validation
323
+ const expect = step.expect || {};
324
+ const validation = {
325
+ status: expect.status ? status === expect.status : true,
326
+ contentType: expect.contentType ? contentType.includes(expect.contentType) : true
327
+ };
328
+
329
+ let bodyValidation = { matched: true, reason: 'No body expectation set.' };
330
+ if (expect.body !== undefined) {
331
+ if (typeof responseBody === 'object' && responseBody !== null && typeof expect.body === 'object') {
332
+ bodyValidation.matched = Object.entries(expect.body).every(
333
+ ([k, v]) => JSON.stringify(responseBody[k]) === JSON.stringify(v)
334
+ );
335
+ bodyValidation.reason = bodyValidation.matched
336
+ ? 'Partial/exact body match succeeded.'
337
+ : 'Partial/exact body match failed.';
338
+ } else if (typeof expect.body === 'string') {
339
+ bodyValidation.matched = JSON.stringify(responseBody) === expect.body || responseBody === expect.body;
340
+ bodyValidation.reason = bodyValidation.matched
341
+ ? 'Exact string match succeeded.'
342
+ : 'Exact string match failed.';
343
+ } else {
344
+ bodyValidation.matched = false;
345
+ bodyValidation.reason = 'Body type mismatch.';
346
+ }
347
+ }
348
+ if (expect.bodyRegex) {
349
+ try {
350
+ const pattern = new RegExp(expect.bodyRegex);
351
+ const target = typeof responseBody === 'string' ? responseBody : JSON.stringify(responseBody);
352
+ const regexMatch = pattern.test(target);
353
+ bodyValidation = {
354
+ matched: regexMatch,
355
+ reason: regexMatch ? 'Regex match succeeded.' : 'Regex match failed.'
356
+ };
357
+ } catch (regexError) {
358
+ bodyValidation = {
359
+ matched: false,
360
+ reason: `Invalid regex pattern: ${regexError.message}`
361
+ };
362
+ }
363
+ }
364
+
365
+ // Extract variables
366
+ const extracted = step.extract ? extractFields(responseBody, step.extract) : {};
367
+ // Add extracted variables directly to stepVars for template rendering
368
+ Object.assign(stepVars, extracted);
369
+ // Also store step results for reference (including all response data)
370
+ stepVars[step.name] = {
371
+ ...extracted,
372
+ body: responseBody,
373
+ status,
374
+ contentType,
375
+ // Add common response fields for easier access
376
+ id: responseBody?.id,
377
+ userId: responseBody?.userId,
378
+ title: responseBody?.title
379
+ };
380
+
381
+ // Record step result
382
+ results.push({
383
+ name: step.name,
384
+ status,
385
+ statusText,
386
+ contentType,
387
+ body: responseBody,
388
+ expectations: step.expect || {},
389
+ validation,
390
+ bodyValidation,
391
+ extracted
392
+ });
393
+
394
+ // Log to session
395
+ session.logs.push({
396
+ type: 'request',
397
+ request: {
398
+ method: step.method || 'GET',
399
+ url,
400
+ headers,
401
+ data
402
+ },
403
+ response: {
404
+ status,
405
+ statusText,
406
+ contentType,
407
+ body: responseBody
408
+ },
409
+ expectations: step.expect || {},
410
+ validation,
411
+ bodyValidation,
412
+ timestamp: new Date().toISOString()
413
+ });
414
+ }
415
+
416
+ // Log to session
417
+ session.logs.push({
418
+ type: 'chain',
419
+ steps: results,
420
+ timestamp: new Date().toISOString()
421
+ });
422
+ session.status = 'completed';
423
+
424
+ return {
425
+ content: [{
426
+ type: 'text',
427
+ text: JSON.stringify({ sessionId, results }, null, 2)
428
+ }]
429
+ };
430
+ }
431
+
432
+ // --- SINGLE REQUEST MODE (legacy) ---
433
+ const { method, url, headers, data, expect } = input;
434
+
435
+ // Validate required parameters for single request mode
436
+ if (!url)
437
+ throw new Error('URL is required for single request mode');
438
+
439
+ const response = await this.makeHttpRequest(
440
+ method || 'GET',
441
+ url,
442
+ headers,
443
+ data
444
+ );
445
+
446
+ const status = response.statusCode;
447
+ const statusText = response.statusMessage || '';
448
+ const contentType = response.headers['content-type'] || '';
449
+ let responseBody = response.body;
450
+
451
+ if (contentType.includes('application/json')) {
452
+ try {
453
+ responseBody = JSON.parse(response.body);
454
+ } catch (e) {
455
+ // Keep as string if JSON parsing fails
456
+ }
457
+ }
458
+
459
+ // Basic validation
460
+ const validation = {
461
+ status: expect?.status ? status === expect.status : true,
462
+ contentType: expect?.contentType ? contentType.includes(expect?.contentType) : true
463
+ };
464
+
465
+ // --- Enhanced Response Body Validation ---
466
+ let bodyValidation = { matched: true, reason: 'No body expectation set.' };
467
+ if (expect?.body !== undefined) {
468
+ if (typeof responseBody === 'object' && responseBody !== null && typeof expect.body === 'object') {
469
+ // Partial match: all keys/values in expect.body must be present in responseBody
470
+ bodyValidation.matched = Object.entries(expect.body).every(
471
+ ([k, v]) => JSON.stringify(responseBody[k]) === JSON.stringify(v)
472
+ );
473
+ bodyValidation.reason = bodyValidation.matched
474
+ ? 'Partial/exact body match succeeded.'
475
+ : 'Partial/exact body match failed.';
476
+ } else if (typeof expect.body === 'string') {
477
+ bodyValidation.matched = JSON.stringify(responseBody) === expect.body || responseBody === expect.body;
478
+ bodyValidation.reason = bodyValidation.matched
479
+ ? 'Exact string match succeeded.'
480
+ : 'Exact string match failed.';
481
+ } else {
482
+ bodyValidation.matched = false;
483
+ bodyValidation.reason = 'Body type mismatch.';
484
+ }
485
+ }
486
+ if (expect?.bodyRegex) {
487
+ try {
488
+ const pattern = new RegExp(expect.bodyRegex);
489
+ const target = typeof responseBody === 'string' ? responseBody : JSON.stringify(responseBody);
490
+ const regexMatch = pattern.test(target);
491
+ bodyValidation = {
492
+ matched: regexMatch,
493
+ reason: regexMatch ? 'Regex match succeeded.' : 'Regex match failed.'
494
+ };
495
+ } catch (regexError) {
496
+ bodyValidation = {
497
+ matched: false,
498
+ reason: `Invalid regex pattern: ${regexError.message}`
499
+ };
500
+ }
501
+ }
502
+ // --- End Enhanced Validation ---
503
+
504
+ // Log to session
505
+ if (session && session.logs) {
506
+ session.logs.push({
507
+ type: 'single',
508
+ request: {
509
+ method: method || 'GET',
510
+ url,
511
+ headers,
512
+ data
513
+ },
514
+ response: {
515
+ status,
516
+ statusText,
517
+ contentType,
518
+ body: responseBody
519
+ },
520
+ expectations: expect || {},
521
+ validation,
522
+ bodyValidation,
523
+ timestamp: new Date().toISOString()
524
+ });
525
+ }
526
+
527
+ return {
528
+ content: [{
529
+ type: 'text',
530
+ text: JSON.stringify({
531
+ ok: validation.status && validation.contentType && bodyValidation.matched,
532
+ status,
533
+ statusText,
534
+ contentType,
535
+ body: responseBody,
536
+ validation,
537
+ bodyValidation
538
+ }, null, 2)
539
+ }]
540
+ };
541
+
542
+ } catch (error) {
543
+ // Error handling for API request execution
544
+ const errorMessage = error.message || 'Unknown error occurred';
545
+ console.error('ApiRequestTool execution error:', errorMessage);
546
+
547
+ return {
548
+ content: [{
549
+ type: 'text',
550
+ text: JSON.stringify({
551
+ error: true,
552
+ message: errorMessage,
553
+ stack: error.stack
554
+ }, null, 2)
555
+ }]
556
+ };
557
+ }
558
+ }
559
+
560
+ /**
561
+ * Make HTTP request using Node.js built-in modules
562
+ */
563
+ async makeHttpRequest(method, urlString, headers = {}, data, timeout = 30000) {
564
+ return new Promise((resolve, reject) => {
565
+ let url;
566
+ try {
567
+ url = new URL(urlString);
568
+ } catch (urlError) {
569
+ reject(new Error(`Invalid URL: ${urlString}`));
570
+ return;
571
+ }
572
+
573
+ const isHttps = url.protocol === 'https:';
574
+ const lib = isHttps ? https : http;
575
+
576
+ const options = {
577
+ hostname: url.hostname,
578
+ port: url.port || (isHttps ? 443 : 80),
579
+ path: url.pathname + url.search,
580
+ method: method.toUpperCase(),
581
+ headers: {
582
+ 'User-Agent': 'Democratize-Quality-MCP/1.0',
583
+ ...headers
584
+ },
585
+ timeout
586
+ };
587
+
588
+ // Adding Content-Length for requests with body
589
+ if (data) {
590
+ const bodyString = typeof data === 'string' ? data : JSON.stringify(data);
591
+ options.headers['Content-Length'] = Buffer.byteLength(bodyString);
592
+
593
+ if (!options.headers['Content-Type']) {
594
+ options.headers['Content-Type'] = typeof data === 'object'
595
+ ? 'application/json'
596
+ : 'text/plain';
597
+ }
598
+ }
599
+
600
+ const req = lib.request(options, (res) => {
601
+ let body = '';
602
+
603
+ res.on('data', (chunk) => {
604
+ body += chunk;
605
+ });
606
+
607
+ res.on('end', () => {
608
+ resolve({
609
+ statusCode: res.statusCode,
610
+ headers: res.headers,
611
+ body: body
612
+ });
613
+ });
614
+ });
615
+
616
+ req.on('error', (error) => {
617
+ reject(new Error(`Request failed: ${error.message}`));
618
+ });
619
+
620
+ req.on('timeout', () => {
621
+ req.destroy();
622
+ reject(new Error(`Request timeout after ${timeout}ms`));
623
+ });
624
+
625
+ // Write back request body if present
626
+ if (data) {
627
+ try {
628
+ const bodyString = typeof data === 'string' ? data : JSON.stringify(data);
629
+ req.write(bodyString);
630
+ } catch (writeError) {
631
+ reject(new Error(`Failed to write request body: ${writeError.message}`));
632
+ return;
633
+ }
634
+ }
635
+
636
+ req.end();
637
+ });
638
+ }
639
+ }
640
+
641
+ module.exports = ApiRequestTool;