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 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.7",
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 (error) {
127
+ catch (_error) {
128
128
  throw new McpError(ErrorCode.InvalidRequest, `File not found: ${relativePath}`);
129
129
  }
130
130
  });
@@ -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, explicitPath) {
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 (e) {
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 (e) {
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 (e) { }
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 (e) { }
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
- // Action Detection
339
- // [Round 14 Improvement] Specific Actions First
340
- if (['logout', 'sign out', 'log out', 'signoff', 'logoff'].some(k => lower.includes(k)))
341
- action = 'logout';
342
- else if (['import', 'upload', 'restore', 'ingest'].some(k => lower.includes(k)))
343
- action = 'import';
344
- else if (['export', 'download', 'backup', 'dump'].some(k => lower.includes(k)))
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
- }
@@ -18,7 +18,7 @@ export async function getAllFiles(dir) {
18
18
  }
19
19
  }
20
20
  }
21
- catch (err) {
21
+ catch (_err) {
22
22
  // Directory might not exist or be empty
23
23
  console.error(`Warning: Could not read directory ${dir}`);
24
24
  }
@@ -1,4 +1,3 @@
1
- // @ts-ignore
2
1
  import { stemmer } from "stemmer";
3
2
  // Domain terms that should NOT be stemmed
4
3
  export const DOMAIN_TERMS = new Set([
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fmea-api-mcp-server",
3
- "version": "1.1.7",
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/verify-search-enhancement.mjs",
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": [