@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.
- package/LICENSE +15 -0
- package/README.md +423 -0
- package/browserControl.js +113 -0
- package/cli.js +187 -0
- package/docs/api/tool-reference.md +317 -0
- package/docs/api_tools_usage.md +477 -0
- package/docs/development/adding-tools.md +274 -0
- package/docs/development/configuration.md +332 -0
- package/docs/examples/authentication.md +124 -0
- package/docs/examples/basic-automation.md +105 -0
- package/docs/getting-started.md +214 -0
- package/docs/index.md +61 -0
- package/mcpServer.js +280 -0
- package/package.json +83 -0
- package/run-server.js +140 -0
- package/src/config/environments/api-only.js +53 -0
- package/src/config/environments/development.js +54 -0
- package/src/config/environments/production.js +69 -0
- package/src/config/index.js +341 -0
- package/src/config/server.js +41 -0
- package/src/config/tools/api.js +67 -0
- package/src/config/tools/browser.js +90 -0
- package/src/config/tools/default.js +32 -0
- package/src/services/browserService.js +325 -0
- package/src/tools/api/api-request.js +641 -0
- package/src/tools/api/api-session-report.js +1262 -0
- package/src/tools/api/api-session-status.js +395 -0
- package/src/tools/base/ToolBase.js +230 -0
- package/src/tools/base/ToolRegistry.js +269 -0
- package/src/tools/browser/advanced/browser-console.js +384 -0
- package/src/tools/browser/advanced/browser-dialog.js +319 -0
- package/src/tools/browser/advanced/browser-evaluate.js +337 -0
- package/src/tools/browser/advanced/browser-file.js +480 -0
- package/src/tools/browser/advanced/browser-keyboard.js +343 -0
- package/src/tools/browser/advanced/browser-mouse.js +332 -0
- package/src/tools/browser/advanced/browser-network.js +421 -0
- package/src/tools/browser/advanced/browser-pdf.js +407 -0
- package/src/tools/browser/advanced/browser-tabs.js +497 -0
- package/src/tools/browser/advanced/browser-wait.js +378 -0
- package/src/tools/browser/click.js +168 -0
- package/src/tools/browser/close.js +60 -0
- package/src/tools/browser/dom.js +70 -0
- package/src/tools/browser/launch.js +67 -0
- package/src/tools/browser/navigate.js +270 -0
- package/src/tools/browser/screenshot.js +351 -0
- package/src/tools/browser/type.js +174 -0
- package/src/tools/index.js +95 -0
- 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;
|