@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,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;
|