fmea-api-mcp-server 1.0.6 → 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 +7 -5
- package/dist/index.js +123 -28
- 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
|
|
105
|
-
-
|
|
106
|
-
-
|
|
107
|
-
|
|
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
|
+
- Supports filters: `query` (use `*` for all), `method`, `version`, `page` (default 1).
|
|
106
|
+
- Results limited to 10 per page. Returns meta info (total, totalPages) and guidance.
|
|
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
|
@@ -114,6 +114,10 @@ class ApiDocsServer {
|
|
|
114
114
|
type: "string",
|
|
115
115
|
description: "Filter by API version (e.g. 'v1', 'v2'). Optional.",
|
|
116
116
|
},
|
|
117
|
+
page: {
|
|
118
|
+
type: "integer",
|
|
119
|
+
description: "Page number for pagination (default: 1).",
|
|
120
|
+
},
|
|
117
121
|
},
|
|
118
122
|
required: ["query"],
|
|
119
123
|
},
|
|
@@ -144,7 +148,8 @@ class ApiDocsServer {
|
|
|
144
148
|
const query = String(request.params.arguments?.query).toLowerCase();
|
|
145
149
|
const method = request.params.arguments?.method ? String(request.params.arguments?.method).toUpperCase() : undefined;
|
|
146
150
|
const version = request.params.arguments?.version ? String(request.params.arguments?.version).toLowerCase() : undefined;
|
|
147
|
-
const
|
|
151
|
+
const page = request.params.arguments?.page ? Number(request.params.arguments?.page) : 1;
|
|
152
|
+
const results = await this.searchInFiles(query, method, version, page);
|
|
148
153
|
return {
|
|
149
154
|
content: [
|
|
150
155
|
{
|
|
@@ -205,19 +210,21 @@ class ApiDocsServer {
|
|
|
205
210
|
}
|
|
206
211
|
return results;
|
|
207
212
|
}
|
|
208
|
-
// Smart search helper with scoring, filtering, and
|
|
209
|
-
async searchInFiles(query, filterMethod, filterVersion) {
|
|
213
|
+
// Smart search helper with scoring, filtering, limits, and pagination
|
|
214
|
+
async searchInFiles(query, filterMethod, filterVersion, page = 1) {
|
|
210
215
|
const files = await this.getAllFiles(ENDPOINTS_DIR);
|
|
211
216
|
let allMatches = [];
|
|
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);
|
|
212
220
|
for (const filePath of files) {
|
|
213
221
|
try {
|
|
214
222
|
const content = await fs.readFile(filePath, "utf-8");
|
|
215
223
|
const json = JSON.parse(content);
|
|
216
224
|
const fileName = path.relative(ENDPOINTS_DIR, filePath);
|
|
217
|
-
// Version Filtering
|
|
218
|
-
// Check if file path contains version (e.g. "v1/...") or json has version field
|
|
225
|
+
// Version Filtering
|
|
219
226
|
if (filterVersion) {
|
|
220
|
-
const fileVersion = fileName.split(path.sep)[0];
|
|
227
|
+
const fileVersion = fileName.split(path.sep)[0];
|
|
221
228
|
if (fileVersion !== filterVersion && json.version !== filterVersion) {
|
|
222
229
|
continue;
|
|
223
230
|
}
|
|
@@ -230,17 +237,36 @@ class ApiDocsServer {
|
|
|
230
237
|
}
|
|
231
238
|
// Scoring Logic
|
|
232
239
|
let score = 0;
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
score
|
|
242
|
-
|
|
243
|
-
|
|
240
|
+
if (isWildcard) {
|
|
241
|
+
score = 1;
|
|
242
|
+
}
|
|
243
|
+
else {
|
|
244
|
+
const summary = (endpoint.summary || "").toLowerCase();
|
|
245
|
+
const description = (endpoint.description || "").toLowerCase();
|
|
246
|
+
const apiPath = (endpoint.path || "").toLowerCase();
|
|
247
|
+
const operationId = (endpoint.operationId || "").toLowerCase();
|
|
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
|
+
}
|
|
269
|
+
}
|
|
244
270
|
if (score > 0) {
|
|
245
271
|
allMatches.push({
|
|
246
272
|
score,
|
|
@@ -248,7 +274,8 @@ class ApiDocsServer {
|
|
|
248
274
|
method: endpoint.method,
|
|
249
275
|
path: endpoint.path,
|
|
250
276
|
summary: endpoint.summary,
|
|
251
|
-
description: endpoint.description
|
|
277
|
+
description: endpoint.description,
|
|
278
|
+
operationId: endpoint.operationId
|
|
252
279
|
});
|
|
253
280
|
}
|
|
254
281
|
}
|
|
@@ -258,19 +285,55 @@ class ApiDocsServer {
|
|
|
258
285
|
// Ignore parse errors
|
|
259
286
|
}
|
|
260
287
|
}
|
|
261
|
-
// Sort by score descending
|
|
262
|
-
allMatches.sort((a, b) => b.score - a.score);
|
|
263
|
-
// Limit results
|
|
264
|
-
const LIMIT = 10;
|
|
265
288
|
const totalFound = allMatches.length;
|
|
266
|
-
|
|
267
|
-
if (totalFound > LIMIT) {
|
|
289
|
+
if (totalFound === 0) {
|
|
268
290
|
return {
|
|
269
|
-
results:
|
|
270
|
-
|
|
291
|
+
results: [],
|
|
292
|
+
message: `No results found for '${query}'. Try using '*' to list all endpoints, or check your version/method filters.`
|
|
271
293
|
};
|
|
272
294
|
}
|
|
273
|
-
|
|
295
|
+
// Sort by score descending (only meaningful if not wildcard)
|
|
296
|
+
if (!isWildcard) {
|
|
297
|
+
allMatches.sort((a, b) => b.score - a.score);
|
|
298
|
+
}
|
|
299
|
+
// Pagination
|
|
300
|
+
const LIMIT = 10;
|
|
301
|
+
const totalPages = Math.ceil(totalFound / LIMIT);
|
|
302
|
+
const currentPage = Math.max(1, page);
|
|
303
|
+
const start = (currentPage - 1) * LIMIT;
|
|
304
|
+
const end = start + LIMIT;
|
|
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
|
+
}));
|
|
321
|
+
let warning = undefined;
|
|
322
|
+
if (totalPages > 1) {
|
|
323
|
+
warning = `Found ${totalFound} results. Showing page ${currentPage} of ${totalPages}.`;
|
|
324
|
+
if (currentPage < totalPages) {
|
|
325
|
+
warning += ` Use 'page: ${currentPage + 1}' to see next results.`;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
return {
|
|
329
|
+
results: finalResults,
|
|
330
|
+
meta: {
|
|
331
|
+
total: totalFound,
|
|
332
|
+
page: currentPage,
|
|
333
|
+
totalPages: totalPages
|
|
334
|
+
},
|
|
335
|
+
warning
|
|
336
|
+
};
|
|
274
337
|
}
|
|
275
338
|
// Helper to get full details of an API
|
|
276
339
|
async getApiDetails(apiPath, method) {
|
|
@@ -285,10 +348,20 @@ class ApiDocsServer {
|
|
|
285
348
|
if (method && endpoint.method.toUpperCase() !== method) {
|
|
286
349
|
continue;
|
|
287
350
|
}
|
|
288
|
-
|
|
351
|
+
const result = {
|
|
289
352
|
sourceFile: path.relative(ENDPOINTS_DIR, filePath),
|
|
290
353
|
...endpoint
|
|
291
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;
|
|
292
365
|
}
|
|
293
366
|
}
|
|
294
367
|
}
|
|
@@ -299,6 +372,28 @@ class ApiDocsServer {
|
|
|
299
372
|
}
|
|
300
373
|
return null;
|
|
301
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
|
+
}
|
|
302
397
|
async run() {
|
|
303
398
|
const transport = new StdioServerTransport();
|
|
304
399
|
await this.server.connect(transport);
|