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.
Files changed (3) hide show
  1. package/README.md +5 -3
  2. package/dist/index.js +78 -14
  3. 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 with relevance scoring and pagination.
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`: Get full details (schema, parameters) for a specific endpoint.
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; // All match in wildcard mode
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
- // Exact/High relevance matches
247
- if (summary.includes(query) || operationId.includes(query))
248
- score += 10;
249
- if (description.includes(query))
250
- score += 5;
251
- if (apiPath.includes(query))
252
- score += 3;
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); // Ensure page is at least 1
302
+ const currentPage = Math.max(1, page);
286
303
  const start = (currentPage - 1) * LIMIT;
287
304
  const end = start + LIMIT;
288
- const slicedResults = allMatches.slice(start, end).map(({ score, ...rest }) => rest);
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: slicedResults,
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
- return {
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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fmea-api-mcp-server",
3
- "version": "1.0.7",
3
+ "version": "1.1.0",
4
4
  "description": "MCP server for serving API documentation from endpoints directory",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",