fmea-api-mcp-server 1.0.7 → 1.1.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/README.md +5 -3
- package/dist/index.js +78 -14
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -98,10 +98,12 @@ When the package is published to NPM:
|
|
|
98
98
|
|
|
99
99
|
## Features
|
|
100
100
|
- **Resources**: Can read JSON files in the `endpoints` folder.
|
|
101
|
-
- **Tools**:
|
|
102
101
|
- **Tools**:
|
|
103
102
|
- `search_apis`:
|
|
104
|
-
- Smart search
|
|
103
|
+
- **Smart Search**: Supports multi-keyword matching (e.g., "project search"). Results are ranked by relevance (Summary > OperationID > Description > Path).
|
|
104
|
+
- **Deprecation Warnings**: Automatically detects if a V1 endpoint has a V2 equivalent and includes a warning in the results.
|
|
105
105
|
- Supports filters: `query` (use `*` for all), `method`, `version`, `page` (default 1).
|
|
106
106
|
- Results limited to 10 per page. Returns meta info (total, totalPages) and guidance.
|
|
107
|
-
- `get_api_details`:
|
|
107
|
+
- `get_api_details`:
|
|
108
|
+
- Get full details (schema, parameters) for a specific endpoint.
|
|
109
|
+
- Includes **Deprecation Warnings** if a newer version of the API exists.
|
package/dist/index.js
CHANGED
|
@@ -214,7 +214,9 @@ class ApiDocsServer {
|
|
|
214
214
|
async searchInFiles(query, filterMethod, filterVersion, page = 1) {
|
|
215
215
|
const files = await this.getAllFiles(ENDPOINTS_DIR);
|
|
216
216
|
let allMatches = [];
|
|
217
|
-
const isWildcard = query === "*" || query === "";
|
|
217
|
+
const isWildcard = query.trim() === "*" || query.trim() === "";
|
|
218
|
+
// Tokenize query: split by space, filter empty
|
|
219
|
+
const tokens = query.toLowerCase().split(/\s+/).filter(t => t.length > 0);
|
|
218
220
|
for (const filePath of files) {
|
|
219
221
|
try {
|
|
220
222
|
const content = await fs.readFile(filePath, "utf-8");
|
|
@@ -236,20 +238,34 @@ class ApiDocsServer {
|
|
|
236
238
|
// Scoring Logic
|
|
237
239
|
let score = 0;
|
|
238
240
|
if (isWildcard) {
|
|
239
|
-
score = 1;
|
|
241
|
+
score = 1;
|
|
240
242
|
}
|
|
241
243
|
else {
|
|
242
244
|
const summary = (endpoint.summary || "").toLowerCase();
|
|
243
245
|
const description = (endpoint.description || "").toLowerCase();
|
|
244
246
|
const apiPath = (endpoint.path || "").toLowerCase();
|
|
245
247
|
const operationId = (endpoint.operationId || "").toLowerCase();
|
|
246
|
-
//
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
248
|
+
// Calculate score for each token
|
|
249
|
+
for (const token of tokens) {
|
|
250
|
+
let tokenScore = 0;
|
|
251
|
+
// Summary: Highest weight (Exact > Partial)
|
|
252
|
+
if (summary === token)
|
|
253
|
+
tokenScore += 20;
|
|
254
|
+
else if (summary.includes(token))
|
|
255
|
+
tokenScore += 10;
|
|
256
|
+
// OperationID: High weight
|
|
257
|
+
if (operationId === token)
|
|
258
|
+
tokenScore += 15;
|
|
259
|
+
else if (operationId.includes(token))
|
|
260
|
+
tokenScore += 8;
|
|
261
|
+
// Description: Medium weight
|
|
262
|
+
if (description.includes(token))
|
|
263
|
+
tokenScore += 5;
|
|
264
|
+
// Path: Low weight
|
|
265
|
+
if (apiPath.includes(token))
|
|
266
|
+
tokenScore += 3;
|
|
267
|
+
score += tokenScore;
|
|
268
|
+
}
|
|
253
269
|
}
|
|
254
270
|
if (score > 0) {
|
|
255
271
|
allMatches.push({
|
|
@@ -258,7 +274,8 @@ class ApiDocsServer {
|
|
|
258
274
|
method: endpoint.method,
|
|
259
275
|
path: endpoint.path,
|
|
260
276
|
summary: endpoint.summary,
|
|
261
|
-
description: endpoint.description
|
|
277
|
+
description: endpoint.description,
|
|
278
|
+
operationId: endpoint.operationId
|
|
262
279
|
});
|
|
263
280
|
}
|
|
264
281
|
}
|
|
@@ -282,10 +299,25 @@ class ApiDocsServer {
|
|
|
282
299
|
// Pagination
|
|
283
300
|
const LIMIT = 10;
|
|
284
301
|
const totalPages = Math.ceil(totalFound / LIMIT);
|
|
285
|
-
const currentPage = Math.max(1, page);
|
|
302
|
+
const currentPage = Math.max(1, page);
|
|
286
303
|
const start = (currentPage - 1) * LIMIT;
|
|
287
304
|
const end = start + LIMIT;
|
|
288
|
-
|
|
305
|
+
// Get the page slice
|
|
306
|
+
const slice = allMatches.slice(start, end);
|
|
307
|
+
// Post-processing: Add warnings for V1 endpoints if V2 exists
|
|
308
|
+
const finalResults = await Promise.all(slice.map(async (item) => {
|
|
309
|
+
// Remove score before returning to user
|
|
310
|
+
const { score, ...rest } = item;
|
|
311
|
+
if (rest.path && rest.path.includes("/v1/")) {
|
|
312
|
+
const v2Path = rest.path.replace("/v1/", "/v2/");
|
|
313
|
+
// We check if this v2 path exists using our internal lookup logic
|
|
314
|
+
const v2Exists = await this.findEndpointInFiles(files, v2Path, rest.method);
|
|
315
|
+
if (v2Exists) {
|
|
316
|
+
rest.warning = "DEPRECATED: Version v1 is deprecated. Please use v2 endpoint: " + v2Path;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
return rest;
|
|
320
|
+
}));
|
|
289
321
|
let warning = undefined;
|
|
290
322
|
if (totalPages > 1) {
|
|
291
323
|
warning = `Found ${totalFound} results. Showing page ${currentPage} of ${totalPages}.`;
|
|
@@ -294,7 +326,7 @@ class ApiDocsServer {
|
|
|
294
326
|
}
|
|
295
327
|
}
|
|
296
328
|
return {
|
|
297
|
-
results:
|
|
329
|
+
results: finalResults,
|
|
298
330
|
meta: {
|
|
299
331
|
total: totalFound,
|
|
300
332
|
page: currentPage,
|
|
@@ -316,10 +348,20 @@ class ApiDocsServer {
|
|
|
316
348
|
if (method && endpoint.method.toUpperCase() !== method) {
|
|
317
349
|
continue;
|
|
318
350
|
}
|
|
319
|
-
|
|
351
|
+
const result = {
|
|
320
352
|
sourceFile: path.relative(ENDPOINTS_DIR, filePath),
|
|
321
353
|
...endpoint
|
|
322
354
|
};
|
|
355
|
+
// Check for V1 Deprecation
|
|
356
|
+
if (apiPath.includes("/v1/")) {
|
|
357
|
+
const v2Path = apiPath.replace("/v1/", "/v2/");
|
|
358
|
+
const v2Exists = await this.findEndpointInFiles(files, v2Path, method);
|
|
359
|
+
if (v2Exists) {
|
|
360
|
+
// Inject a top-level deprecation warning in the details
|
|
361
|
+
result.deprecation_warning = `NOTICE: This v1 endpoint is deprecated. A newer version (v2) exists at ${v2Path}`;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
return result;
|
|
323
365
|
}
|
|
324
366
|
}
|
|
325
367
|
}
|
|
@@ -330,6 +372,28 @@ class ApiDocsServer {
|
|
|
330
372
|
}
|
|
331
373
|
return null;
|
|
332
374
|
}
|
|
375
|
+
// Efficiently check if an endpoint exists without reading files if content is not needed
|
|
376
|
+
// Note: Since we don't cache file contents in memory for this simple server,
|
|
377
|
+
// we re-read files. For a production server with many files, we would cache the map.
|
|
378
|
+
async findEndpointInFiles(files, apiPath, method) {
|
|
379
|
+
for (const filePath of files) {
|
|
380
|
+
try {
|
|
381
|
+
const content = await fs.readFile(filePath, "utf-8");
|
|
382
|
+
const json = JSON.parse(content);
|
|
383
|
+
if (json.endpoints && Array.isArray(json.endpoints)) {
|
|
384
|
+
for (const ep of json.endpoints) {
|
|
385
|
+
if (ep.path === apiPath) {
|
|
386
|
+
if (method && ep.method.toUpperCase() !== method)
|
|
387
|
+
continue;
|
|
388
|
+
return true;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
catch (e) { }
|
|
394
|
+
}
|
|
395
|
+
return false;
|
|
396
|
+
}
|
|
333
397
|
async run() {
|
|
334
398
|
const transport = new StdioServerTransport();
|
|
335
399
|
await this.server.connect(transport);
|