@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,395 @@
1
+ const ToolBase = require('../base/ToolBase');
2
+
3
+ /**
4
+ * API Session Status Tool - Query API test session status and results
5
+ */
6
+ class ApiSessionStatusTool extends ToolBase {
7
+ static definition = {
8
+ name: "api_session_status",
9
+ description: "Query API test session status, logs, and results by sessionId. Provides detailed information about request history and validation results.",
10
+ input_schema: {
11
+ type: "object",
12
+ properties: {
13
+ sessionId: {
14
+ type: "string",
15
+ description: "The session ID to query"
16
+ },
17
+ includeDetails: {
18
+ type: "boolean",
19
+ default: true,
20
+ description: "Whether to include detailed request/response data"
21
+ },
22
+ filterByType: {
23
+ type: "string",
24
+ enum: ["single", "request", "chain", "all"],
25
+ default: "all",
26
+ description: "Filter logs by request type"
27
+ },
28
+ limit: {
29
+ type: "number",
30
+ default: 50,
31
+ description: "Maximum number of log entries to return"
32
+ }
33
+ },
34
+ required: ["sessionId"]
35
+ },
36
+ output_schema: {
37
+ type: "object",
38
+ properties: {
39
+ success: { type: "boolean", description: "Whether the session was found" },
40
+ found: { type: "boolean", description: "Whether the session exists" },
41
+ session: {
42
+ type: "object",
43
+ properties: {
44
+ sessionId: { type: "string" },
45
+ status: { type: "string" },
46
+ startTime: { type: "string" },
47
+ endTime: { type: "string" },
48
+ executionTime: { type: "number" },
49
+ error: { type: "string" }
50
+ },
51
+ description: "Session metadata"
52
+ },
53
+ summary: {
54
+ type: "object",
55
+ properties: {
56
+ totalRequests: { type: "number" },
57
+ successfulRequests: { type: "number" },
58
+ failedRequests: { type: "number" },
59
+ chainSteps: { type: "number" },
60
+ logEntries: { type: "number" }
61
+ },
62
+ description: "Session summary statistics"
63
+ },
64
+ logs: {
65
+ type: "array",
66
+ description: "Session log entries (filtered and limited)"
67
+ },
68
+ validationSummary: {
69
+ type: "object",
70
+ properties: {
71
+ passedValidations: { type: "number" },
72
+ failedValidations: { type: "number" },
73
+ validationRate: { type: "number" }
74
+ },
75
+ description: "Validation statistics"
76
+ }
77
+ },
78
+ required: ["success", "found"]
79
+ }
80
+ };
81
+
82
+ constructor() {
83
+ super();
84
+ // Access the global session store
85
+ if (!global.__API_SESSION_STORE__) {
86
+ global.__API_SESSION_STORE__ = new Map();
87
+ }
88
+ this.sessionStore = global.__API_SESSION_STORE__;
89
+ }
90
+
91
+ async execute(parameters) {
92
+ const {
93
+ sessionId,
94
+ includeDetails = true,
95
+ filterByType = "all",
96
+ limit = 50
97
+ } = parameters;
98
+
99
+ const session = this.sessionStore.get(sessionId);
100
+
101
+ if (!session) {
102
+ return {
103
+ success: false,
104
+ found: false,
105
+ message: `Session not found: ${sessionId}`,
106
+ availableSessions: Array.from(this.sessionStore.keys())
107
+ };
108
+ }
109
+
110
+ // Filter logs by type
111
+ let filteredLogs = session.logs || [];
112
+ if (filterByType !== "all") {
113
+ filteredLogs = filteredLogs.filter(log => log.type === filterByType);
114
+ }
115
+
116
+ // Apply limit
117
+ const logs = filteredLogs.slice(-limit);
118
+
119
+ // Generate summary statistics
120
+ const summary = this.generateSummary(session.logs || []);
121
+ const validationSummary = this.generateValidationSummary(session.logs || []);
122
+
123
+ // Prepare session metadata (without sensitive details if not requested)
124
+ const sessionMetadata = {
125
+ sessionId: session.sessionId,
126
+ status: session.status,
127
+ startTime: session.startTime,
128
+ endTime: session.endTime,
129
+ executionTime: session.executionTime,
130
+ error: session.error
131
+ };
132
+
133
+ // Optionally strip detailed request/response data
134
+ const processedLogs = includeDetails
135
+ ? logs
136
+ : logs.map(log => this.stripSensitiveData(log));
137
+
138
+ return {
139
+ success: true,
140
+ found: true,
141
+ session: sessionMetadata,
142
+ summary,
143
+ validationSummary,
144
+ logs: processedLogs,
145
+ logCount: logs.length,
146
+ totalLogCount: (session.logs || []).length
147
+ };
148
+ }
149
+
150
+ /**
151
+ * Generate summary statistics for the session
152
+ */
153
+ generateSummary(logs) {
154
+ const summary = {
155
+ totalRequests: 0,
156
+ successfulRequests: 0,
157
+ failedRequests: 0,
158
+ chainSteps: 0,
159
+ singleRequests: 0,
160
+ logEntries: logs.length
161
+ };
162
+
163
+ for (const log of logs) {
164
+ switch (log.type) {
165
+ case 'single':
166
+ summary.totalRequests++;
167
+ summary.singleRequests++;
168
+ if (this.isRequestSuccessful(log)) {
169
+ summary.successfulRequests++;
170
+ } else {
171
+ summary.failedRequests++;
172
+ }
173
+ break;
174
+
175
+ case 'request':
176
+ summary.totalRequests++;
177
+ summary.chainSteps++;
178
+ if (this.isRequestSuccessful(log)) {
179
+ summary.successfulRequests++;
180
+ } else {
181
+ summary.failedRequests++;
182
+ }
183
+ break;
184
+
185
+ case 'chain':
186
+ // Chain logs contain summary of multiple steps
187
+ if (log.steps) {
188
+ summary.chainSteps += log.steps.length;
189
+ }
190
+ break;
191
+ }
192
+ }
193
+
194
+ return summary;
195
+ }
196
+
197
+ /**
198
+ * Generate validation summary statistics
199
+ */
200
+ generateValidationSummary(logs) {
201
+ let passedValidations = 0;
202
+ let failedValidations = 0;
203
+ let totalValidations = 0;
204
+
205
+ for (const log of logs) {
206
+ if (log.validation && log.bodyValidation) {
207
+ totalValidations++;
208
+
209
+ const isValid = log.validation.status &&
210
+ log.validation.contentType &&
211
+ log.bodyValidation.matched;
212
+
213
+ if (isValid) {
214
+ passedValidations++;
215
+ } else {
216
+ failedValidations++;
217
+ }
218
+ }
219
+
220
+ // Also check chain steps
221
+ if (log.type === 'chain' && log.steps) {
222
+ for (const step of log.steps) {
223
+ if (step.validation && step.bodyValidation) {
224
+ totalValidations++;
225
+
226
+ const isValid = step.validation.status &&
227
+ step.validation.contentType &&
228
+ step.bodyValidation.matched;
229
+
230
+ if (isValid) {
231
+ passedValidations++;
232
+ } else {
233
+ failedValidations++;
234
+ }
235
+ }
236
+ }
237
+ }
238
+ }
239
+
240
+ return {
241
+ passedValidations,
242
+ failedValidations,
243
+ totalValidations,
244
+ validationRate: totalValidations > 0
245
+ ? Math.round((passedValidations / totalValidations) * 100) / 100
246
+ : 0
247
+ };
248
+ }
249
+
250
+ /**
251
+ * Check if a request was successful based on validation
252
+ */
253
+ isRequestSuccessful(log) {
254
+ return log.validation &&
255
+ log.bodyValidation &&
256
+ log.validation.status &&
257
+ log.validation.contentType &&
258
+ log.bodyValidation.matched;
259
+ }
260
+
261
+ /**
262
+ * Remove sensitive data from logs when details are not requested
263
+ */
264
+ stripSensitiveData(log) {
265
+ const stripped = {
266
+ type: log.type,
267
+ timestamp: log.timestamp
268
+ };
269
+
270
+ if (log.request) {
271
+ stripped.request = {
272
+ method: log.request.method,
273
+ url: log.request.url,
274
+ hasHeaders: !!(log.request.headers && Object.keys(log.request.headers).length > 0),
275
+ hasData: !!log.request.data
276
+ };
277
+ }
278
+
279
+ if (log.response) {
280
+ stripped.response = {
281
+ status: log.response.status,
282
+ contentType: log.response.contentType,
283
+ hasBody: !!log.response.body
284
+ };
285
+ }
286
+
287
+ if (log.validation) {
288
+ stripped.validation = log.validation;
289
+ }
290
+
291
+ if (log.bodyValidation) {
292
+ stripped.bodyValidation = {
293
+ matched: log.bodyValidation.matched,
294
+ reason: log.bodyValidation.reason
295
+ };
296
+ }
297
+
298
+ // Handle chain steps
299
+ if (log.steps) {
300
+ stripped.steps = log.steps.map(step => this.stripSensitiveData({
301
+ type: 'request',
302
+ request: step.request || {
303
+ method: step.method,
304
+ url: step.url,
305
+ headers: step.headers,
306
+ data: step.data
307
+ },
308
+ response: {
309
+ status: step.status,
310
+ contentType: step.contentType,
311
+ body: step.body
312
+ },
313
+ validation: step.validation,
314
+ bodyValidation: step.bodyValidation
315
+ }));
316
+ }
317
+
318
+ return stripped;
319
+ }
320
+
321
+ /**
322
+ * Get detailed analysis of a specific request by index
323
+ */
324
+ getRequestDetails(sessionId, requestIndex) {
325
+ const session = this.sessionStore.get(sessionId);
326
+ if (!session || !session.logs) {
327
+ return null;
328
+ }
329
+
330
+ const requestLogs = session.logs.filter(log =>
331
+ log.type === 'single' || log.type === 'request'
332
+ );
333
+
334
+ if (requestIndex >= requestLogs.length) {
335
+ return null;
336
+ }
337
+
338
+ return requestLogs[requestIndex];
339
+ }
340
+
341
+ /**
342
+ * Get session timing analysis
343
+ */
344
+ getTimingAnalysis(sessionId) {
345
+ const session = this.sessionStore.get(sessionId);
346
+ if (!session || !session.logs) {
347
+ return null;
348
+ }
349
+
350
+ const requests = session.logs.filter(log =>
351
+ log.type === 'single' || log.type === 'request'
352
+ );
353
+
354
+ if (requests.length === 0) {
355
+ return { message: 'No requests found for timing analysis' };
356
+ }
357
+
358
+ // Calculate request intervals
359
+ const timings = [];
360
+ for (let i = 0; i < requests.length; i++) {
361
+ const current = new Date(requests[i].timestamp);
362
+ const previous = i > 0 ? new Date(requests[i - 1].timestamp) : new Date(session.startTime);
363
+
364
+ timings.push({
365
+ requestIndex: i,
366
+ timestamp: requests[i].timestamp,
367
+ intervalMs: current.getTime() - previous.getTime()
368
+ });
369
+ }
370
+
371
+ return {
372
+ totalRequests: requests.length,
373
+ sessionDuration: session.executionTime || 0,
374
+ averageInterval: timings.reduce((sum, t) => sum + t.intervalMs, 0) / timings.length,
375
+ timings
376
+ };
377
+ }
378
+
379
+ /**
380
+ * List all available sessions
381
+ */
382
+ listAllSessions() {
383
+ return Array.from(this.sessionStore.entries()).map(([id, session]) => ({
384
+ sessionId: id,
385
+ status: session.status,
386
+ startTime: session.startTime,
387
+ requestCount: (session.logs || []).filter(log =>
388
+ log.type === 'single' || log.type === 'request'
389
+ ).length,
390
+ logCount: (session.logs || []).length
391
+ }));
392
+ }
393
+ }
394
+
395
+ module.exports = ApiSessionStatusTool;
@@ -0,0 +1,230 @@
1
+ /**
2
+ * Base class for all MCP tools
3
+ * Provides common functionality and enforces a consistent interface
4
+ */
5
+ class ToolBase {
6
+ constructor() {
7
+ if (this.constructor === ToolBase) {
8
+ throw new Error("ToolBase is an abstract class and cannot be instantiated directly");
9
+ }
10
+
11
+ // Validate that required static properties are defined
12
+ if (!this.constructor.definition) {
13
+ throw new Error(`Tool ${this.constructor.name} must define a static 'definition' property`);
14
+ }
15
+
16
+ this.validateDefinition(this.constructor.definition);
17
+
18
+ // Initialize configuration
19
+ this.config = require('../../config');
20
+ this.toolConfig = this._getToolConfig();
21
+ }
22
+
23
+ /**
24
+ * Get tool-specific configuration
25
+ * @returns {object} - Tool configuration
26
+ */
27
+ _getToolConfig() {
28
+ const toolName = this.constructor.definition.name;
29
+ const category = this._getToolCategory(toolName);
30
+
31
+ // Get configuration in order of precedence:
32
+ // 1. Tool-specific config
33
+ // 2. Category config
34
+ // 3. Default config
35
+ return {
36
+ ...this.config.getToolConfig('default', {}),
37
+ ...this.config.getToolConfig(category, {}),
38
+ ...this.config.getToolConfig(category, toolName)
39
+ };
40
+ }
41
+
42
+ /**
43
+ * Get tool category from tool name
44
+ * @param {string} toolName - The tool name
45
+ * @returns {string} - The tool category
46
+ */
47
+ _getToolCategory(toolName) {
48
+ if (toolName.startsWith('browser_')) return 'browser';
49
+ if (toolName.startsWith('file_')) return 'file';
50
+ if (toolName.startsWith('network_')) return 'network';
51
+ return 'other';
52
+ }
53
+
54
+ /**
55
+ * Get a configuration value for this tool
56
+ * @param {string} key - Configuration key
57
+ * @param {any} defaultValue - Default value
58
+ * @returns {any} - Configuration value
59
+ */
60
+ getConfig(key, defaultValue = undefined) {
61
+ return this.toolConfig[key] !== undefined ? this.toolConfig[key] : defaultValue;
62
+ }
63
+
64
+ /**
65
+ * Validates the tool definition schema
66
+ * @param {object} definition - The tool definition object
67
+ */
68
+ validateDefinition(definition) {
69
+ const required = ['name', 'description', 'input_schema'];
70
+ for (const field of required) {
71
+ if (!definition[field]) {
72
+ throw new Error(`Tool definition missing required field: ${field}`);
73
+ }
74
+ }
75
+
76
+ if (!definition.input_schema.type || definition.input_schema.type !== 'object') {
77
+ throw new Error("Tool input_schema must be of type 'object'");
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Validates input parameters against the tool's schema
83
+ * @param {object} parameters - The input parameters to validate
84
+ */
85
+ validateParameters(parameters) {
86
+ const schema = this.constructor.definition.input_schema;
87
+
88
+ // Check required parameters
89
+ if (schema.required) {
90
+ for (const required of schema.required) {
91
+ if (parameters[required] === undefined || parameters[required] === null) {
92
+ throw new Error(`Missing required parameter: ${required}`);
93
+ }
94
+ }
95
+ }
96
+
97
+ // Basic type checking for defined properties
98
+ if (schema.properties) {
99
+ for (const [key, value] of Object.entries(parameters)) {
100
+ if (schema.properties[key]) {
101
+ const expectedType = schema.properties[key].type;
102
+ const actualType = typeof value;
103
+
104
+ if (expectedType === 'string' && actualType !== 'string') {
105
+ throw new Error(`Parameter '${key}' must be a string, got ${actualType}`);
106
+ }
107
+ if (expectedType === 'number' && actualType !== 'number') {
108
+ throw new Error(`Parameter '${key}' must be a number, got ${actualType}`);
109
+ }
110
+ if (expectedType === 'boolean' && actualType !== 'boolean') {
111
+ throw new Error(`Parameter '${key}' must be a boolean, got ${actualType}`);
112
+ }
113
+ if (expectedType === 'object' && (actualType !== 'object' || Array.isArray(value))) {
114
+ throw new Error(`Parameter '${key}' must be an object, got ${actualType}`);
115
+ }
116
+ }
117
+ }
118
+ }
119
+ }
120
+
121
+ /**
122
+ * Executes the tool with the given parameters
123
+ * This method must be implemented by subclasses
124
+ * @param {object} parameters - The input parameters
125
+ * @returns {Promise<any>} - The tool execution result
126
+ */
127
+ async execute(parameters) {
128
+ throw new Error("execute() method must be implemented by subclasses");
129
+ }
130
+
131
+ /**
132
+ * Wrapper method that handles validation, execution, and error handling
133
+ * @param {object} parameters - The input parameters
134
+ * @returns {Promise<object>} - Formatted MCP response
135
+ */
136
+ async run(parameters = {}) {
137
+ const toolName = this.constructor.definition.name;
138
+ const enableDebug = this.config.isFeatureEnabled('enableDebugMode');
139
+
140
+ try {
141
+ if (enableDebug) {
142
+ console.error(`[Tool:${toolName}] Executing with parameters:`, JSON.stringify(parameters));
143
+ }
144
+
145
+ // Validate input parameters if validation is enabled
146
+ if (this.getConfig('enableInputValidation', true)) {
147
+ this.validateParameters(parameters);
148
+ }
149
+
150
+ // Execute the tool with timeout if configured
151
+ const timeout = this.getConfig('timeout', 30000);
152
+ const result = await this._executeWithTimeout(parameters, timeout);
153
+
154
+ if (enableDebug) {
155
+ console.error(`[Tool:${toolName}] Execution successful`);
156
+ }
157
+
158
+ // Return formatted MCP response
159
+ return {
160
+ content: [{
161
+ type: "text",
162
+ text: JSON.stringify(result)
163
+ }]
164
+ };
165
+
166
+ } catch (error) {
167
+ const enableDetailedErrors = this.getConfig('enableDetailedErrors', true);
168
+
169
+ if (enableDetailedErrors || enableDebug) {
170
+ console.error(`[Tool:${toolName}] Error during execution:`, error.message);
171
+ }
172
+
173
+ // Throw properly formatted MCP error
174
+ throw {
175
+ code: -32000,
176
+ message: `Tool '${toolName}' execution failed: ${error.message}`,
177
+ data: enableDetailedErrors ? {
178
+ tool_name: toolName,
179
+ parameters: parameters,
180
+ original_error: error.message,
181
+ config: this.toolConfig
182
+ } : {
183
+ tool_name: toolName,
184
+ original_error: error.message
185
+ }
186
+ };
187
+ }
188
+ }
189
+
190
+ /**
191
+ * Execute the tool with a timeout
192
+ * @param {object} parameters - The input parameters
193
+ * @param {number} timeout - Timeout in milliseconds
194
+ * @returns {Promise<any>} - Execution result
195
+ */
196
+ async _executeWithTimeout(parameters, timeout) {
197
+ return new Promise(async (resolve, reject) => {
198
+ const timeoutId = setTimeout(() => {
199
+ reject(new Error(`Tool execution timed out after ${timeout}ms`));
200
+ }, timeout);
201
+
202
+ try {
203
+ const result = await this.execute(parameters);
204
+ clearTimeout(timeoutId);
205
+ resolve(result);
206
+ } catch (error) {
207
+ clearTimeout(timeoutId);
208
+ reject(error);
209
+ }
210
+ });
211
+ }
212
+
213
+ /**
214
+ * Gets the tool definition
215
+ * @returns {object} - The tool definition
216
+ */
217
+ static getDefinition() {
218
+ return this.definition;
219
+ }
220
+
221
+ /**
222
+ * Gets the tool name from the definition
223
+ * @returns {string} - The tool name
224
+ */
225
+ static getName() {
226
+ return this.definition.name;
227
+ }
228
+ }
229
+
230
+ module.exports = ToolBase;