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.
Files changed (3) hide show
  1. package/README.md +7 -5
  2. package/dist/index.js +123 -28
  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 (Summary > Description > Path).
105
- - Supports filters: `query`, `method` (GET/POST), `version` (v1/v2).
106
- - Results limited to top 10 by default to prevent context pollution.
107
- - `get_api_details`: Get full details (schema, parameters) for a specific endpoint.
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 results = await this.searchInFiles(query, method, version);
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 limits
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 (File level or content level check)
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]; // e.g. "v1" from "v1/projects/..."
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
- const summary = (endpoint.summary || "").toLowerCase();
234
- const description = (endpoint.description || "").toLowerCase();
235
- const apiPath = (endpoint.path || "").toLowerCase();
236
- const operationId = (endpoint.operationId || "").toLowerCase();
237
- // 1. Exact/High relevance matches
238
- if (summary.includes(query) || operationId.includes(query))
239
- score += 10;
240
- if (description.includes(query))
241
- score += 5;
242
- if (apiPath.includes(query))
243
- score += 3;
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
- const limitedResults = allMatches.slice(0, LIMIT).map(({ score, ...rest }) => rest); // Remove score from output
267
- if (totalFound > LIMIT) {
289
+ if (totalFound === 0) {
268
290
  return {
269
- results: limitedResults,
270
- warning: `Found ${totalFound} results. Showing top ${LIMIT}. Please refine your search query or use filters.`
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
- return limitedResults;
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
- return {
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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fmea-api-mcp-server",
3
- "version": "1.0.6",
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",