fmea-api-mcp-server 1.1.1 → 1.1.3
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/dist/index.js +170 -35
- package/dist/synonyms.js +6 -4
- package/endpoints/README.md +19 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -9,6 +9,54 @@ import { fileURLToPath } from "url";
|
|
|
9
9
|
import { getSynonyms } from "./synonyms.js";
|
|
10
10
|
const __filename = fileURLToPath(import.meta.url);
|
|
11
11
|
const __dirname = path.dirname(__filename);
|
|
12
|
+
/**
|
|
13
|
+
* Normalize token to singular form for better matching.
|
|
14
|
+
* Handles common English plural patterns.
|
|
15
|
+
*/
|
|
16
|
+
function normalizeToken(token) {
|
|
17
|
+
const normalized = [token];
|
|
18
|
+
// Simple pluralization rules (token -> singular or singular -> plural)
|
|
19
|
+
const rules = [
|
|
20
|
+
// Remove 's' for common plurals
|
|
21
|
+
[/([a-z]+)s$/, '$1'],
|
|
22
|
+
// Remove 'es' for words ending with s, x, z, ch, sh
|
|
23
|
+
[/([a-z]+)es$/, '$1'],
|
|
24
|
+
// Remove 'ies' and replace with 'y'
|
|
25
|
+
[/([a-z]+)ies$/, '$1y'],
|
|
26
|
+
// Remove 'ves' and replace with 'f' (leaf -> leaves)
|
|
27
|
+
[/([a-z]+)ves$/, '$1f'],
|
|
28
|
+
// Remove 'men' and replace with 'man' (woman -> women)
|
|
29
|
+
[/([a-z]+)men$/, '$1man'],
|
|
30
|
+
];
|
|
31
|
+
// Generate singular variants
|
|
32
|
+
for (const [pattern, replacement] of rules) {
|
|
33
|
+
if (pattern.test(token)) {
|
|
34
|
+
const singular = token.replace(pattern, replacement);
|
|
35
|
+
if (singular !== token && singular.length > 2) {
|
|
36
|
+
normalized.push(singular);
|
|
37
|
+
}
|
|
38
|
+
break;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
// Add plural variant (append 's' if not ending with 's')
|
|
42
|
+
if (!token.endsWith('s') && token.length > 2) {
|
|
43
|
+
normalized.push(token + 's');
|
|
44
|
+
}
|
|
45
|
+
return normalized;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Expand tokens to include singular/plural variants
|
|
49
|
+
*/
|
|
50
|
+
function expandTokenVariants(tokens) {
|
|
51
|
+
const variants = new Set(tokens);
|
|
52
|
+
for (const token of tokens) {
|
|
53
|
+
const normalized = normalizeToken(token);
|
|
54
|
+
for (const variant of normalized) {
|
|
55
|
+
variants.add(variant);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return variants;
|
|
59
|
+
}
|
|
12
60
|
// Directory where endpoint definitions are stored.
|
|
13
61
|
// Priority:
|
|
14
62
|
// 1. Environment variable ENDPOINTS_DIR
|
|
@@ -120,7 +168,7 @@ class ApiDocsServer {
|
|
|
120
168
|
description: "Page number for pagination (default: 1).",
|
|
121
169
|
},
|
|
122
170
|
},
|
|
123
|
-
required: [
|
|
171
|
+
required: [],
|
|
124
172
|
},
|
|
125
173
|
},
|
|
126
174
|
{
|
|
@@ -146,10 +194,21 @@ class ApiDocsServer {
|
|
|
146
194
|
});
|
|
147
195
|
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
148
196
|
if (request.params.name === "search_apis") {
|
|
149
|
-
const query = String(request.params.arguments?.query).toLowerCase();
|
|
197
|
+
const query = request.params.arguments?.query ? String(request.params.arguments?.query).toLowerCase() : "";
|
|
150
198
|
const method = request.params.arguments?.method ? String(request.params.arguments?.method).toUpperCase() : undefined;
|
|
151
199
|
const version = request.params.arguments?.version ? String(request.params.arguments?.version).toLowerCase() : undefined;
|
|
152
200
|
const page = request.params.arguments?.page ? Number(request.params.arguments?.page) : 1;
|
|
201
|
+
if (!query) {
|
|
202
|
+
const categories = await this.listApiCategories();
|
|
203
|
+
return {
|
|
204
|
+
content: [
|
|
205
|
+
{
|
|
206
|
+
type: "text",
|
|
207
|
+
text: JSON.stringify(categories, null, 2),
|
|
208
|
+
},
|
|
209
|
+
],
|
|
210
|
+
};
|
|
211
|
+
}
|
|
153
212
|
const results = await this.searchInFiles(query, method, version, page);
|
|
154
213
|
return {
|
|
155
214
|
content: [
|
|
@@ -211,6 +270,38 @@ class ApiDocsServer {
|
|
|
211
270
|
}
|
|
212
271
|
return results;
|
|
213
272
|
}
|
|
273
|
+
// Helper to list API categories (directories) for exploration
|
|
274
|
+
async listApiCategories() {
|
|
275
|
+
const categories = [];
|
|
276
|
+
try {
|
|
277
|
+
const topLevel = await fs.readdir(ENDPOINTS_DIR);
|
|
278
|
+
for (const versionDir of topLevel) {
|
|
279
|
+
const versionPath = path.join(ENDPOINTS_DIR, versionDir);
|
|
280
|
+
const stat = await fs.stat(versionPath);
|
|
281
|
+
if (stat.isDirectory() && !versionDir.startsWith('.')) {
|
|
282
|
+
// Look one level deeper for domains (e.g. v1/projects)
|
|
283
|
+
const domains = await fs.readdir(versionPath);
|
|
284
|
+
for (const domain of domains) {
|
|
285
|
+
const domainPath = path.join(versionPath, domain);
|
|
286
|
+
const domainStat = await fs.stat(domainPath);
|
|
287
|
+
if (domainStat.isDirectory()) {
|
|
288
|
+
categories.push({
|
|
289
|
+
category: `${versionDir}/${domain}`,
|
|
290
|
+
description: `API endpoints for ${domain} (${versionDir})`
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
catch (e) {
|
|
298
|
+
console.error("Error listing categories:", e);
|
|
299
|
+
}
|
|
300
|
+
return {
|
|
301
|
+
categories: categories,
|
|
302
|
+
message: "Use 'search_apis' with a query to find specific endpoints, or 'get_api_details' to see full schemas."
|
|
303
|
+
};
|
|
304
|
+
}
|
|
214
305
|
// Smart search helper with BM25 scoring, Synonyms, and AND logic
|
|
215
306
|
async searchInFiles(query, filterMethod, filterVersion, page = 1) {
|
|
216
307
|
const files = await this.getAllFiles(ENDPOINTS_DIR);
|
|
@@ -243,11 +334,13 @@ class ApiDocsServer {
|
|
|
243
334
|
(endpoint.path || "").toLowerCase()
|
|
244
335
|
].join(" ");
|
|
245
336
|
const tokens = searchableText.split(/\s+/).filter(t => t.length > 0);
|
|
337
|
+
// Expand document tokens with singular/plural variants
|
|
338
|
+
const expandedTokens = Array.from(expandTokenVariants(tokens));
|
|
246
339
|
documents.push({
|
|
247
340
|
file: fileName,
|
|
248
341
|
...endpoint,
|
|
249
|
-
tokens, //
|
|
250
|
-
docLength:
|
|
342
|
+
tokens: expandedTokens, // Expanded tokens for BM25 calculation
|
|
343
|
+
docLength: expandedTokens.length
|
|
251
344
|
});
|
|
252
345
|
}
|
|
253
346
|
}
|
|
@@ -279,12 +372,15 @@ class ApiDocsServer {
|
|
|
279
372
|
meta: { total: totalFound, page: currentPage, totalPages: totalPages }
|
|
280
373
|
};
|
|
281
374
|
}
|
|
282
|
-
// Filter Documents: AND Logic with Synonym Expansion
|
|
283
|
-
// Every query token (or one of its synonyms) MUST be present in the document
|
|
375
|
+
// Filter Documents: AND Logic with Synonym Expansion + Plural/Singular Normalization
|
|
376
|
+
// Every query token (or one of its synonyms/plurals) MUST be present in the document
|
|
284
377
|
const filteredDocs = documents.filter(doc => {
|
|
285
378
|
return rawQueryTokens.every(qToken => {
|
|
379
|
+
// Get synonyms first
|
|
286
380
|
const synonyms = getSynonyms(qToken);
|
|
287
|
-
|
|
381
|
+
// Expand with plural/singular variants
|
|
382
|
+
const expandedQuery = Array.from(expandTokenVariants(synonyms));
|
|
383
|
+
return expandedQuery.some((variant) => doc.tokens.includes(variant));
|
|
288
384
|
});
|
|
289
385
|
});
|
|
290
386
|
if (filteredDocs.length === 0) {
|
|
@@ -299,13 +395,18 @@ class ApiDocsServer {
|
|
|
299
395
|
const k1 = 1.2;
|
|
300
396
|
const b = 0.75;
|
|
301
397
|
const avgdl = documents.reduce((acc, doc) => acc + doc.docLength, 0) / totalFound;
|
|
302
|
-
// Calculate IDF (using full corpus) for *expanded* tokens
|
|
303
|
-
//
|
|
304
|
-
// If multiple synonyms match, take the max score or sum? Sum is risky (double count).
|
|
305
|
-
// We will iterate query tokens, find the *best matching synonym* in the doc, and score that.
|
|
306
|
-
// Pre-calculate IDF for all potential terms in query (raw + synonyms)
|
|
398
|
+
// Calculate IDF (using full corpus) for *expanded* tokens (synonyms + plurals)
|
|
399
|
+
// We expand query terms with both synonyms and plural/singular variants
|
|
307
400
|
const allQueryTerms = new Set();
|
|
308
|
-
rawQueryTokens.forEach(t =>
|
|
401
|
+
rawQueryTokens.forEach(t => {
|
|
402
|
+
const synonyms = getSynonyms(t);
|
|
403
|
+
synonyms.forEach((s) => {
|
|
404
|
+
allQueryTerms.add(s);
|
|
405
|
+
// Also add plural/singular variants
|
|
406
|
+
const variants = normalizeToken(s);
|
|
407
|
+
variants.forEach(v => allQueryTerms.add(v));
|
|
408
|
+
});
|
|
409
|
+
});
|
|
309
410
|
const idf = {};
|
|
310
411
|
for (const term of allQueryTerms) {
|
|
311
412
|
let n_q = 0;
|
|
@@ -319,13 +420,12 @@ class ApiDocsServer {
|
|
|
319
420
|
let scoredDocs = filteredDocs.map(doc => {
|
|
320
421
|
let score = 0;
|
|
321
422
|
for (const qToken of rawQueryTokens) {
|
|
322
|
-
// Find which synonyms of qToken are present in this doc
|
|
423
|
+
// Find which synonyms + plural variants of qToken are present in this doc
|
|
323
424
|
const synonyms = getSynonyms(qToken);
|
|
324
|
-
const
|
|
325
|
-
|
|
326
|
-
//
|
|
327
|
-
|
|
328
|
-
for (const term of presentSynonyms) {
|
|
425
|
+
const expandedQuery = Array.from(expandTokenVariants(synonyms));
|
|
426
|
+
const presentTerms = expandedQuery.filter((term) => doc.tokens.includes(term));
|
|
427
|
+
// Sum up scores for all matching terms (synonyms + plurals)
|
|
428
|
+
for (const term of presentTerms) {
|
|
329
429
|
const f_q = doc.tokens.filter((t) => t === term).length;
|
|
330
430
|
const numerator = idf[term] * f_q * (k1 + 1);
|
|
331
431
|
const denominator = f_q + k1 * (1 - b + b * (doc.docLength / avgdl));
|
|
@@ -344,17 +444,17 @@ class ApiDocsServer {
|
|
|
344
444
|
const start = (currentPage - 1) * LIMIT;
|
|
345
445
|
// Slice
|
|
346
446
|
const slice = scoredDocs.slice(start, start + LIMIT);
|
|
347
|
-
// Post-processing: Add warnings for V1 endpoints
|
|
447
|
+
// Post-processing: Add warnings for V1 endpoints AND strip heavy fields
|
|
348
448
|
const finalResults = await Promise.all(slice.map(async (item) => {
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
449
|
+
// Deconstruct to remove heavy fields (parameters, requestBody, responses, tags) and internal scoring props
|
|
450
|
+
const { score, tokens, docLength, parameters, requestBody, responses, tags, file, ...lightweightItem } = item;
|
|
451
|
+
if (lightweightItem.path && lightweightItem.path.includes("/v1/")) {
|
|
452
|
+
// Check for V1 Deprecation
|
|
453
|
+
// Always generate a warning for v1 endpoints using the 3-step logic
|
|
454
|
+
// We do this check after determining it is a v1 endpoint
|
|
455
|
+
lightweightItem.warning = await this.generateDeprecationWarning(lightweightItem.path, lightweightItem.method);
|
|
356
456
|
}
|
|
357
|
-
return
|
|
457
|
+
return lightweightItem;
|
|
358
458
|
}));
|
|
359
459
|
let warning = undefined;
|
|
360
460
|
if (totalPages > 1) {
|
|
@@ -384,17 +484,15 @@ class ApiDocsServer {
|
|
|
384
484
|
continue;
|
|
385
485
|
}
|
|
386
486
|
const result = {
|
|
387
|
-
sourceFile: path.relative(ENDPOINTS_DIR, filePath),
|
|
388
487
|
...endpoint
|
|
389
488
|
};
|
|
390
489
|
// Check for V1 Deprecation
|
|
391
490
|
if (apiPath.includes("/v1/")) {
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
}
|
|
491
|
+
// Check for V1 Deprecation
|
|
492
|
+
// Always generate a warning for v1 endpoints using the 3-step logic
|
|
493
|
+
// We do this check regardless of whether a direct v2 exists or not,
|
|
494
|
+
// because generateDeprecationWarning handles all cases.
|
|
495
|
+
result.warning = await this.generateDeprecationWarning(apiPath, method);
|
|
398
496
|
}
|
|
399
497
|
return result;
|
|
400
498
|
}
|
|
@@ -429,6 +527,43 @@ class ApiDocsServer {
|
|
|
429
527
|
}
|
|
430
528
|
return false;
|
|
431
529
|
}
|
|
530
|
+
// 3-Step Intelligent Warning Logic
|
|
531
|
+
async generateDeprecationWarning(v1Path, method) {
|
|
532
|
+
const files = await this.getAllFiles(ENDPOINTS_DIR);
|
|
533
|
+
// Step 1: Direct Match (v1 -> v2)
|
|
534
|
+
const v2Path = v1Path.replace("/v1/", "/v2/");
|
|
535
|
+
const v2Exists = await this.findEndpointInFiles(files, v2Path, method);
|
|
536
|
+
if (v2Exists) {
|
|
537
|
+
return `DEPRECATED: Version v1 is deprecated. Please use v2 endpoint: ${v2Path}`;
|
|
538
|
+
}
|
|
539
|
+
// Step 2: Domain Hint (e.g. /api/v1/auth/login -> Check /api/v2/auth/)
|
|
540
|
+
// Extract domain: /api/v1/projects/... -> projects, /api/v1/auth/... -> auth
|
|
541
|
+
const match = v1Path.match(/\/api\/v1\/([^/]+)/);
|
|
542
|
+
if (match && match[1]) {
|
|
543
|
+
const domain = match[1];
|
|
544
|
+
const v2DomainPathStart = `/api/v2/${domain}`;
|
|
545
|
+
// Check if any v2 endpoint exists in this domain
|
|
546
|
+
let domainExists = false;
|
|
547
|
+
for (const filePath of files) {
|
|
548
|
+
try {
|
|
549
|
+
const content = await fs.readFile(filePath, "utf-8");
|
|
550
|
+
const json = JSON.parse(content);
|
|
551
|
+
if (json.endpoints && Array.isArray(json.endpoints)) {
|
|
552
|
+
if (json.endpoints.some((ep) => ep.path && ep.path.startsWith(v2DomainPathStart))) {
|
|
553
|
+
domainExists = true;
|
|
554
|
+
break;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
catch (e) { }
|
|
559
|
+
}
|
|
560
|
+
if (domainExists) {
|
|
561
|
+
return `LEGACY: Direct v2 replacement not found, but newer '${domain}' related features exist in v2. Please search for '${domain}' in v2.`;
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
// Step 3: General Legacy Warning
|
|
565
|
+
return "LEGACY: This v1 endpoint is deprecated and may be removed in the future.";
|
|
566
|
+
}
|
|
432
567
|
async run() {
|
|
433
568
|
const transport = new StdioServerTransport();
|
|
434
569
|
await this.server.connect(transport);
|
package/dist/synonyms.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
export const SYNONYM_GROUPS = {
|
|
2
2
|
// Read / Retrieve
|
|
3
|
-
"get": ["fetch", "retrieve", "read", "load", "find", "search", "query", "list"],
|
|
4
|
-
"find": ["get", "search", "retrieve", "lookup"],
|
|
5
|
-
"search": ["find", "get", "query", "lookup"],
|
|
6
|
-
"
|
|
3
|
+
"get": ["fetch", "retrieve", "read", "load", "find", "search", "query", "list", "show"],
|
|
4
|
+
"find": ["get", "search", "retrieve", "lookup", "show"],
|
|
5
|
+
"search": ["find", "get", "query", "lookup", "show"],
|
|
6
|
+
"show": ["get", "display", "view", "fetch", "find"],
|
|
7
|
+
"list": ["get", "all", "collection", "show", "summary"],
|
|
8
|
+
"summary": ["list", "all", "overview", "collection"],
|
|
7
9
|
// Create
|
|
8
10
|
"create": ["add", "insert", "make", "new", "post", "generate"],
|
|
9
11
|
"add": ["create", "insert", "append", "attach"],
|
package/endpoints/README.md
CHANGED
|
@@ -18,6 +18,16 @@ endpoints/
|
|
|
18
18
|
└── ...
|
|
19
19
|
```
|
|
20
20
|
|
|
21
|
+
### Classification Rules
|
|
22
|
+
|
|
23
|
+
To prevent ambiguity, follow these classification principles:
|
|
24
|
+
|
|
25
|
+
- **Domain Specific**: If an endpoint operates *within the context* of a specific resource (e.g., "Export THIS resource's sub-data"), place it under that resource's directory.
|
|
26
|
+
- Example: `/api/v2/main-resources/{id}/sub-feature` -> `endpoints/v2/main-resources/sub-feature.json`
|
|
27
|
+
|
|
28
|
+
- **Global / Utility**: If an endpoint is a generic utility or operates independently of other resources (e.g., "Process ANY file"), create a dedicated top-level resource directory.
|
|
29
|
+
- Example: `/api/v2/standalone-feature` -> `endpoints/v2/standalone-feature/core.json`
|
|
30
|
+
|
|
21
31
|
### Rules
|
|
22
32
|
|
|
23
33
|
1. **Directory Required**: Every resource (e.g., `projects`, `users`) MUST be a directory, even if it only contains one file.
|
|
@@ -27,7 +37,15 @@ endpoints/
|
|
|
27
37
|
- `category`: String (Resource category name)
|
|
28
38
|
- `version`: String ("v1" or "v2")
|
|
29
39
|
- `description`: String (Resource description)
|
|
30
|
-
- `endpoints`: Array of endpoint objects.
|
|
40
|
+
- `endpoints`: Array of endpoint objects. Each object MUST contain:
|
|
41
|
+
- `path`: String (URI path)
|
|
42
|
+
- `method`: String (HTTP method)
|
|
43
|
+
- `summary`: String (Short summary)
|
|
44
|
+
- `description`: String (Detailed description)
|
|
45
|
+
- `tags`: Array of Strings
|
|
46
|
+
- `operationId`: String (Unique identifier)
|
|
47
|
+
- `parameters`: Array (Optional parameters)
|
|
48
|
+
- `responses`: Object (Response definitions)
|
|
31
49
|
|
|
32
50
|
### Example
|
|
33
51
|
|