fmea-api-mcp-server 1.1.7 → 1.1.8
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 +2 -2
- package/dist/intents.js +34 -0
- package/dist/services/search/FileSearchService.js +15 -7
- package/dist/services/search/IntentDetector.js +44 -0
- package/dist/services/search/OramaSearchService.js +9 -58
- package/dist/utils/fs-helper.js +1 -1
- package/dist/utils/stemming.js +0 -1
- package/package.json +8 -2
package/dist/index.js
CHANGED
|
@@ -28,7 +28,7 @@ class ApiDocsServer {
|
|
|
28
28
|
constructor() {
|
|
29
29
|
this.server = new Server({
|
|
30
30
|
name: "api-docs-mcp",
|
|
31
|
-
version: "1.1.
|
|
31
|
+
version: "1.1.8",
|
|
32
32
|
}, {
|
|
33
33
|
capabilities: {
|
|
34
34
|
resources: {},
|
|
@@ -124,7 +124,7 @@ class ApiDocsServer {
|
|
|
124
124
|
],
|
|
125
125
|
};
|
|
126
126
|
}
|
|
127
|
-
catch (
|
|
127
|
+
catch (_error) {
|
|
128
128
|
throw new McpError(ErrorCode.InvalidRequest, `File not found: ${relativePath}`);
|
|
129
129
|
}
|
|
130
130
|
});
|
package/dist/intents.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Action Intent Detection Keywords
|
|
3
|
+
* Maps intent types to user query patterns for detecting user actions.
|
|
4
|
+
*/
|
|
5
|
+
export const ACTION_KEYWORDS = {
|
|
6
|
+
// Auth Actions
|
|
7
|
+
'logout': ['logout', 'sign out', 'log out', 'signoff', 'logoff'],
|
|
8
|
+
'login': ['login', 'signin', 'log in', 'sign in', 'authenticate', 'auth'],
|
|
9
|
+
// Data Transfer
|
|
10
|
+
'import': ['import', 'upload', 'restore', 'ingest'],
|
|
11
|
+
'export': ['export', 'download', 'backup', 'dump'],
|
|
12
|
+
// Monitoring
|
|
13
|
+
'MONITOR': ['monitor', 'track', 'audit', 'inspect', 'history', 'logs', 'usage', 'metrics', 'stats'],
|
|
14
|
+
// CRUD
|
|
15
|
+
'create': ['create', 'add', 'new', 'make', 'generate', 'register'],
|
|
16
|
+
'update': ['update', 'edit', 'change', 'modify', 'save', 'set', 'reset', 'password', 'refresh'],
|
|
17
|
+
'delete': ['delete', 'remove', 'drop', 'erase'],
|
|
18
|
+
'list': ['list', 'get', 'show', 'fetch', 'all', 'view', 'files', 'status', 'check'],
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Order matters: More specific actions should be checked first.
|
|
22
|
+
* e.g., 'logout' should be checked before 'delete' to avoid misclassification.
|
|
23
|
+
*/
|
|
24
|
+
export const ACTION_PRIORITY = [
|
|
25
|
+
'logout',
|
|
26
|
+
'import',
|
|
27
|
+
'export',
|
|
28
|
+
'MONITOR',
|
|
29
|
+
'login',
|
|
30
|
+
'create',
|
|
31
|
+
'update',
|
|
32
|
+
'delete',
|
|
33
|
+
'list',
|
|
34
|
+
];
|
|
@@ -11,7 +11,7 @@ export class FileSearchService {
|
|
|
11
11
|
this.endpointsDir = endpointsDir;
|
|
12
12
|
this.debugMode = debugMode;
|
|
13
13
|
}
|
|
14
|
-
async search(query, filterMethod, filterVersion, page = 1,
|
|
14
|
+
async search(query, filterMethod, filterVersion, page = 1, _explicitPath) {
|
|
15
15
|
const files = await getAllFiles(this.endpointsDir);
|
|
16
16
|
let documents = [];
|
|
17
17
|
// 1. Prepare Documents (Corpus)
|
|
@@ -54,8 +54,9 @@ export class FileSearchService {
|
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
|
-
catch (
|
|
57
|
+
catch (_e) {
|
|
58
58
|
// Ignore parse errors
|
|
59
|
+
console.error("FileSearchService.search: Error parsing endpoint file:", _e);
|
|
59
60
|
}
|
|
60
61
|
}
|
|
61
62
|
const totalFound = documents.length;
|
|
@@ -70,7 +71,7 @@ export class FileSearchService {
|
|
|
70
71
|
const rawQueryTokens = query.toLowerCase().split(/\s+/).filter(t => t.length > 0);
|
|
71
72
|
// Check for Wildcard
|
|
72
73
|
if (rawQueryTokens.length === 0 || (rawQueryTokens.length === 1 && rawQueryTokens[0] === "*")) {
|
|
73
|
-
const finalResults = documents.map(({ tokens, docLength, ...rest }) => rest);
|
|
74
|
+
const finalResults = documents.map(({ tokens: _tokens, docLength: _docLength, ...rest }) => rest);
|
|
74
75
|
const { results, meta } = paginateResults(finalResults, page, DEFAULT_PAGE_SIZE);
|
|
75
76
|
return {
|
|
76
77
|
results,
|
|
@@ -153,7 +154,7 @@ export class FileSearchService {
|
|
|
153
154
|
const { results: slice, meta } = paginateResults(scoredDocs, page, DEFAULT_PAGE_SIZE);
|
|
154
155
|
// Post-processing: Add warnings for V1 endpoints AND strip heavy fields
|
|
155
156
|
const finalResults = await Promise.all(slice.map(async (item) => {
|
|
156
|
-
const { score, tokens, docLength, parameters, requestBody, responses, tags, file, ...lightweightItem } = item;
|
|
157
|
+
const { score, tokens: _tokens, docLength: _docLength, parameters: _parameters, requestBody: _requestBody, responses: _responses, tags: _tags, file: _file, ...lightweightItem } = item;
|
|
157
158
|
if (lightweightItem.path && lightweightItem.path.includes("/v1/")) {
|
|
158
159
|
lightweightItem.warning = await this.generateDeprecationWarning(lightweightItem.path, lightweightItem.method);
|
|
159
160
|
}
|
|
@@ -197,8 +198,9 @@ export class FileSearchService {
|
|
|
197
198
|
}
|
|
198
199
|
}
|
|
199
200
|
}
|
|
200
|
-
catch (
|
|
201
|
+
catch (_e) {
|
|
201
202
|
// Ignore parse errors
|
|
203
|
+
console.error("FileSearchService.getDetails: Error parsing endpoint file:", _e);
|
|
202
204
|
}
|
|
203
205
|
}
|
|
204
206
|
return null;
|
|
@@ -250,7 +252,10 @@ export class FileSearchService {
|
|
|
250
252
|
}
|
|
251
253
|
}
|
|
252
254
|
}
|
|
253
|
-
catch (
|
|
255
|
+
catch (_e) {
|
|
256
|
+
// Ignore parse errors
|
|
257
|
+
console.error("FileSearchService.findEndpointInFiles: Error parsing endpoint file:", _e);
|
|
258
|
+
}
|
|
254
259
|
}
|
|
255
260
|
return false;
|
|
256
261
|
}
|
|
@@ -282,7 +287,10 @@ export class FileSearchService {
|
|
|
282
287
|
}
|
|
283
288
|
}
|
|
284
289
|
}
|
|
285
|
-
catch (
|
|
290
|
+
catch (_e) {
|
|
291
|
+
// Ignore parse errors
|
|
292
|
+
console.error("FileSearchService.generateDeprecationWarning: Error parsing endpoint file:", _e);
|
|
293
|
+
}
|
|
286
294
|
}
|
|
287
295
|
if (domainExists) {
|
|
288
296
|
return `LEGACY: Direct v2 replacement not found, but newer '${domain}' related features exist in v2. Please search for '${domain}' in v2.`;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { ACTION_KEYWORDS, ACTION_PRIORITY } from '../../intents.js';
|
|
2
|
+
import { RESOURCE_ALIASES } from '../../synonyms.js';
|
|
3
|
+
export class IntentDetector {
|
|
4
|
+
detect(query) {
|
|
5
|
+
const lower = query.toLowerCase();
|
|
6
|
+
// Action Detection
|
|
7
|
+
let action = undefined;
|
|
8
|
+
for (const actionType of ACTION_PRIORITY) {
|
|
9
|
+
const keywords = ACTION_KEYWORDS[actionType];
|
|
10
|
+
if (keywords.some(k => lower.includes(k))) {
|
|
11
|
+
action = actionType;
|
|
12
|
+
break;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
// Resource Detection
|
|
16
|
+
let resource = undefined;
|
|
17
|
+
const potentialResources = [];
|
|
18
|
+
for (const [resType, aliases] of Object.entries(RESOURCE_ALIASES)) {
|
|
19
|
+
const typeName = resType.replace(/-/g, ' ');
|
|
20
|
+
if (lower.includes(typeName)) {
|
|
21
|
+
potentialResources.push({ type: resType, length: typeName.length });
|
|
22
|
+
}
|
|
23
|
+
for (const alias of aliases) {
|
|
24
|
+
if (lower.includes(alias)) {
|
|
25
|
+
potentialResources.push({ type: resType, length: alias.length });
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
potentialResources.sort((a, b) => b.length - a.length);
|
|
30
|
+
if (potentialResources.length > 0) {
|
|
31
|
+
resource = potentialResources[0].type;
|
|
32
|
+
}
|
|
33
|
+
if (!action && resource) {
|
|
34
|
+
action = 'list';
|
|
35
|
+
}
|
|
36
|
+
if (!resource && lower.includes('save work')) {
|
|
37
|
+
resource = 'project';
|
|
38
|
+
}
|
|
39
|
+
if (['login', 'logout'].includes(action || '') && resource !== 'oauth') {
|
|
40
|
+
resource = 'auth';
|
|
41
|
+
}
|
|
42
|
+
return { action, resource };
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -4,6 +4,7 @@ import fs from 'fs/promises';
|
|
|
4
4
|
import { paginateResults, DEFAULT_PAGE_SIZE } from '../../utils/search-helper.js';
|
|
5
5
|
import { endpointSchema } from './schema.js';
|
|
6
6
|
import { RESOURCE_ALIASES } from '../../synonyms.js';
|
|
7
|
+
import { ACTION_KEYWORDS, ACTION_PRIORITY } from '../../intents.js';
|
|
7
8
|
import { ScoreCalculator } from '../../utils/ScoreCalculator.js';
|
|
8
9
|
export class OramaSearchService {
|
|
9
10
|
indexPath;
|
|
@@ -139,12 +140,9 @@ export class OramaSearchService {
|
|
|
139
140
|
});
|
|
140
141
|
// (Debug removed)
|
|
141
142
|
// 5. Threshold Filtering & Mapping with Structural Analysis
|
|
142
|
-
const THRESHOLD = 0.25;
|
|
143
143
|
const results = [];
|
|
144
144
|
// Intent detection moved up.
|
|
145
|
-
let hitIdx = 0;
|
|
146
145
|
for (const hit of searchResult.hits) {
|
|
147
|
-
hitIdx++;
|
|
148
146
|
let score = hit.score;
|
|
149
147
|
// (Debug removed)
|
|
150
148
|
// Structural Metadata from Index
|
|
@@ -334,28 +332,15 @@ export class OramaSearchService {
|
|
|
334
332
|
}
|
|
335
333
|
detectIntent(query) {
|
|
336
334
|
const lower = query.toLowerCase();
|
|
335
|
+
// Action Detection - Use ACTION_PRIORITY for correct order
|
|
337
336
|
let action = undefined;
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
action = 'export';
|
|
346
|
-
else if (['monitor', 'track', 'audit', 'inspect', 'history', 'logs', 'usage', 'metrics', 'stats'].some(k => lower.includes(k)))
|
|
347
|
-
action = 'MONITOR'; // [R15]
|
|
348
|
-
else if (['login', 'signin', 'log in', 'sign in', 'authenticate', 'auth'].some(k => lower.includes(k)))
|
|
349
|
-
action = 'login'; // [R16] Specific Login Action
|
|
350
|
-
// Core CRUD
|
|
351
|
-
else if (['create', 'add', 'new', 'make', 'generate', 'register'].some(k => lower.includes(k)))
|
|
352
|
-
action = 'create';
|
|
353
|
-
else if (['update', 'edit', 'change', 'modify', 'save', 'set', 'reset', 'password', 'refresh'].some(k => lower.includes(k)))
|
|
354
|
-
action = 'update'; // 'change', 'reset', 'refresh' -> update
|
|
355
|
-
else if (['delete', 'remove', 'drop', 'erase'].some(k => lower.includes(k)))
|
|
356
|
-
action = 'delete';
|
|
357
|
-
else if (['list', 'get', 'show', 'fetch', 'all', 'view', 'files', 'status', 'check'].some(k => lower.includes(k)))
|
|
358
|
-
action = 'list';
|
|
337
|
+
for (const actionType of ACTION_PRIORITY) {
|
|
338
|
+
const keywords = ACTION_KEYWORDS[actionType];
|
|
339
|
+
if (keywords.some(k => lower.includes(k))) {
|
|
340
|
+
action = actionType;
|
|
341
|
+
break;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
359
344
|
// Resource Detection
|
|
360
345
|
// Simple lookup against known headers or extracting main noun
|
|
361
346
|
let resource = undefined; // [Fix] Explicit declaration for scope visibility
|
|
@@ -403,37 +388,3 @@ export class OramaSearchService {
|
|
|
403
388
|
return { action, resource };
|
|
404
389
|
}
|
|
405
390
|
}
|
|
406
|
-
/**
|
|
407
|
-
* Basic Levenshtein Distance implementation
|
|
408
|
-
* Source: https://en.wikibooks.org/wiki/Algorithm_Implementation/Strings/Levenshtein_distance#JavaScript
|
|
409
|
-
*/
|
|
410
|
-
function levenshtein(a, b) {
|
|
411
|
-
if (a.length === 0)
|
|
412
|
-
return b.length;
|
|
413
|
-
if (b.length === 0)
|
|
414
|
-
return a.length;
|
|
415
|
-
const matrix = [];
|
|
416
|
-
// increment along the first column of each row
|
|
417
|
-
for (let i = 0; i <= b.length; i++) {
|
|
418
|
-
matrix[i] = [i];
|
|
419
|
-
}
|
|
420
|
-
// increment each column in the first row
|
|
421
|
-
for (let j = 0; j <= a.length; j++) {
|
|
422
|
-
matrix[0][j] = j;
|
|
423
|
-
}
|
|
424
|
-
// Fill in the rest of the matrix
|
|
425
|
-
for (let i = 1; i <= b.length; i++) {
|
|
426
|
-
for (let j = 1; j <= a.length; j++) {
|
|
427
|
-
if (b.charAt(i - 1) == a.charAt(j - 1)) {
|
|
428
|
-
matrix[i][j] = matrix[i - 1][j - 1];
|
|
429
|
-
}
|
|
430
|
-
else {
|
|
431
|
-
matrix[i][j] = Math.min(matrix[i - 1][j - 1] + 1, // substitution
|
|
432
|
-
Math.min(matrix[i][j - 1] + 1, // insertion
|
|
433
|
-
matrix[i - 1][j] + 1 // deletion
|
|
434
|
-
));
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
return matrix[b.length][a.length];
|
|
439
|
-
}
|
package/dist/utils/fs-helper.js
CHANGED
package/dist/utils/stemming.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fmea-api-mcp-server",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.8",
|
|
4
4
|
"description": "MCP server for serving API documentation from endpoints directory",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -10,12 +10,15 @@
|
|
|
10
10
|
"scripts": {
|
|
11
11
|
"prebuild": "rm -rf endpoints && cp -r ../endpoints ./endpoints",
|
|
12
12
|
"build": "tsc",
|
|
13
|
+
"lint": "eslint .",
|
|
14
|
+
"lint:fix": "eslint . --fix",
|
|
13
15
|
"start": "node dist/index.js",
|
|
14
16
|
"watch": "tsc --watch",
|
|
15
17
|
"test": "node tests/verify-mcp.cjs",
|
|
16
18
|
"test:synonyms": "node tests/test-synonyms.mjs",
|
|
17
19
|
"test:plural": "node tests/verify-plural-fix.mjs",
|
|
18
|
-
"test:search": "node tests/
|
|
20
|
+
"test:search": "node tests/search-json.mjs",
|
|
21
|
+
"test:search:verbose": "SEARCH_MODE=orama DEBUG_MODE=true node tests/verify-search-enhancement.mjs",
|
|
19
22
|
"build:search-index": "npx tsx scripts/build-index.ts",
|
|
20
23
|
"build-index": "npx tsx scripts/build-index.ts",
|
|
21
24
|
"analyze-keywords": "npx tsx scripts/analyze-keywords.ts",
|
|
@@ -31,6 +34,9 @@
|
|
|
31
34
|
},
|
|
32
35
|
"devDependencies": {
|
|
33
36
|
"@types/node": "^20.10.0",
|
|
37
|
+
"@typescript-eslint/eslint-plugin": "^8.53.0",
|
|
38
|
+
"@typescript-eslint/parser": "^8.53.0",
|
|
39
|
+
"eslint": "^9.39.2",
|
|
34
40
|
"typescript": "^5.3.0"
|
|
35
41
|
},
|
|
36
42
|
"files": [
|