magector 1.2.10 → 1.2.12

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 (2) hide show
  1. package/package.json +8 -6
  2. package/src/mcp-server.js +340 -122
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "magector",
3
- "version": "1.2.10",
3
+ "version": "1.2.12",
4
4
  "description": "Semantic code search for Magento 2 — index, search, MCP server",
5
5
  "type": "module",
6
6
  "main": "src/mcp-server.js",
@@ -21,7 +21,9 @@
21
21
  "validate:keep": "node src/cli.js validate --verbose --keep",
22
22
  "benchmark": "node src/cli.js benchmark",
23
23
  "test": "node tests/mcp-server.test.js",
24
- "test:no-index": "node tests/mcp-server.test.js --no-index"
24
+ "test:no-index": "node tests/mcp-server.test.js --no-index",
25
+ "test:accuracy": "node tests/mcp-accuracy.test.js",
26
+ "test:accuracy:verbose": "node tests/mcp-accuracy.test.js --verbose"
25
27
  },
26
28
  "dependencies": {
27
29
  "@modelcontextprotocol/sdk": "^1.0.0",
@@ -31,10 +33,10 @@
31
33
  "ruvector": "^0.1.96"
32
34
  },
33
35
  "optionalDependencies": {
34
- "@magector/cli-darwin-arm64": "1.2.10",
35
- "@magector/cli-linux-x64": "1.2.10",
36
- "@magector/cli-linux-arm64": "1.2.10",
37
- "@magector/cli-win32-x64": "1.2.10"
36
+ "@magector/cli-darwin-arm64": "1.2.12",
37
+ "@magector/cli-linux-x64": "1.2.12",
38
+ "@magector/cli-linux-arm64": "1.2.12",
39
+ "@magector/cli-win32-x64": "1.2.12"
38
40
  },
39
41
  "keywords": [
40
42
  "magento",
package/src/mcp-server.js CHANGED
@@ -14,7 +14,8 @@ import {
14
14
  ListResourcesRequestSchema,
15
15
  ReadResourceRequestSchema
16
16
  } from '@modelcontextprotocol/sdk/types.js';
17
- import { execFileSync } from 'child_process';
17
+ import { execFileSync, spawn } from 'child_process';
18
+ import { createInterface } from 'readline';
18
19
  import { existsSync } from 'fs';
19
20
  import { stat } from 'fs/promises';
20
21
  import { glob } from 'glob';
@@ -47,7 +48,112 @@ const rustEnv = {
47
48
  RUST_LOG: 'error',
48
49
  };
49
50
 
50
- function rustSearch(query, limit = 10) {
51
+ /**
52
+ * Query cache: avoid re-embedding identical queries.
53
+ * Keyed by "query|limit", capped at 200 entries (LRU eviction).
54
+ */
55
+ const searchCache = new Map();
56
+ const CACHE_MAX = 200;
57
+
58
+ function cacheSet(key, value) {
59
+ if (searchCache.size >= CACHE_MAX) {
60
+ const oldest = searchCache.keys().next().value;
61
+ searchCache.delete(oldest);
62
+ }
63
+ searchCache.set(key, value);
64
+ }
65
+
66
+ // ─── Persistent Rust Serve Process ──────────────────────────────
67
+ // Keeps ONNX model + HNSW index loaded; eliminates ~2.6s cold start per query.
68
+ // Falls back to execFileSync if serve mode unavailable.
69
+
70
+ let serveProcess = null;
71
+ let serveReady = false;
72
+ let servePending = new Map();
73
+ let serveNextId = 1;
74
+ let serveReadline = null;
75
+
76
+ function startServeProcess() {
77
+ try {
78
+ const proc = spawn(config.rustBinary, [
79
+ 'serve',
80
+ '-d', config.dbPath,
81
+ '-c', config.modelCache
82
+ ], { stdio: ['pipe', 'pipe', 'pipe'], env: rustEnv });
83
+
84
+ proc.on('error', () => { serveProcess = null; serveReady = false; });
85
+ proc.on('exit', () => { serveProcess = null; serveReady = false; });
86
+ proc.stderr.on('data', () => {}); // drain stderr
87
+
88
+ serveReadline = createInterface({ input: proc.stdout });
89
+ serveReadline.on('line', (line) => {
90
+ let parsed;
91
+ try { parsed = JSON.parse(line); } catch { return; }
92
+
93
+ // First line is ready signal
94
+ if (parsed.ready) {
95
+ serveReady = true;
96
+ return;
97
+ }
98
+
99
+ // Route response to pending request by order (FIFO)
100
+ if (servePending.size > 0) {
101
+ const [id, resolver] = servePending.entries().next().value;
102
+ servePending.delete(id);
103
+ resolver.resolve(parsed);
104
+ }
105
+ });
106
+
107
+ serveProcess = proc;
108
+ } catch {
109
+ serveProcess = null;
110
+ serveReady = false;
111
+ }
112
+ }
113
+
114
+ function serveQuery(command, params = {}, timeoutMs = 30000) {
115
+ return new Promise((resolve, reject) => {
116
+ const id = serveNextId++;
117
+ const timer = setTimeout(() => {
118
+ servePending.delete(id);
119
+ reject(new Error('Serve query timeout'));
120
+ }, timeoutMs);
121
+ servePending.set(id, {
122
+ resolve: (v) => { clearTimeout(timer); resolve(v); }
123
+ });
124
+ const msg = JSON.stringify({ command, ...params });
125
+ serveProcess.stdin.write(msg + '\n');
126
+ });
127
+ }
128
+
129
+ async function rustSearchAsync(query, limit = 10) {
130
+ const cacheKey = `${query}|${limit}`;
131
+ if (searchCache.has(cacheKey)) {
132
+ return searchCache.get(cacheKey);
133
+ }
134
+
135
+ // Try persistent serve process first
136
+ if (serveProcess && serveReady) {
137
+ try {
138
+ const resp = await serveQuery('search', { query, limit });
139
+ if (resp.ok && resp.data) {
140
+ cacheSet(cacheKey, resp.data);
141
+ return resp.data;
142
+ }
143
+ } catch {
144
+ // Fall through to execFileSync
145
+ }
146
+ }
147
+
148
+ // Fallback: cold-start execFileSync
149
+ return rustSearchSync(query, limit);
150
+ }
151
+
152
+ function rustSearchSync(query, limit = 10) {
153
+ const cacheKey = `${query}|${limit}`;
154
+ if (searchCache.has(cacheKey)) {
155
+ return searchCache.get(cacheKey);
156
+ }
51
157
  const result = execFileSync(config.rustBinary, [
52
158
  'search', query,
53
159
  '-d', config.dbPath,
@@ -55,10 +161,18 @@ function rustSearch(query, limit = 10) {
55
161
  '-l', String(limit),
56
162
  '-f', 'json'
57
163
  ], { encoding: 'utf-8', timeout: 30000, stdio: ['pipe', 'pipe', 'pipe'], env: rustEnv });
58
- return JSON.parse(result);
164
+ const parsed = JSON.parse(result);
165
+ cacheSet(cacheKey, parsed);
166
+ return parsed;
167
+ }
168
+
169
+ // Keep backward compat: synchronous wrapper (used by tools)
170
+ function rustSearch(query, limit = 10) {
171
+ return rustSearchSync(query, limit);
59
172
  }
60
173
 
61
174
  function rustIndex(magentoRoot) {
175
+ searchCache.clear(); // invalidate cache on reindex
62
176
  const result = execFileSync(config.rustBinary, [
63
177
  'index',
64
178
  '-m', magentoRoot,
@@ -72,7 +186,7 @@ function rustStats() {
72
186
  const result = execFileSync(config.rustBinary, [
73
187
  'stats',
74
188
  '-d', config.dbPath
75
- ], { encoding: 'utf-8', timeout: 10000, stdio: ['pipe', 'pipe', 'pipe'], env: rustEnv });
189
+ ], { encoding: 'utf-8', timeout: 30000, stdio: ['pipe', 'pipe', 'pipe'], env: rustEnv });
76
190
  // Parse text output: "Total vectors: N" and "Embedding dim: N"
77
191
  const vectors = result.match(/Total vectors:\s*(\d+)/)?.[1] || '0';
78
192
  const dim = result.match(/Embedding dim:\s*(\d+)/)?.[1] || '384';
@@ -127,7 +241,9 @@ function normalizeResult(r) {
127
241
  magentoType: meta.magento_type || meta.magentoType,
128
242
  className: meta.class_name || meta.className,
129
243
  methodName: meta.method_name || meta.methodName,
244
+ methods: meta.methods || [],
130
245
  namespace: meta.namespace,
246
+ searchText: meta.search_text || meta.searchText || '',
131
247
  isPlugin: meta.is_plugin || meta.isPlugin,
132
248
  isController: meta.is_controller || meta.isController,
133
249
  isObserver: meta.is_observer || meta.isObserver,
@@ -140,34 +256,78 @@ function normalizeResult(r) {
140
256
  };
141
257
  }
142
258
 
259
+ /**
260
+ * Re-rank results by boosting scores for metadata matches.
261
+ * @param {Array} results - normalized results
262
+ * @param {Object} boosts - e.g. { fileType: 'xml', pathContains: 'di.xml', isPlugin: true }
263
+ * @param {number} weight - boost multiplier (default 0.3 = 30% score bump per match)
264
+ */
265
+ function rerank(results, boosts = {}, weight = 0.3) {
266
+ if (!boosts || Object.keys(boosts).length === 0) return results;
267
+
268
+ return results.map(r => {
269
+ let bonus = 0;
270
+ if (boosts.fileType && r.type === boosts.fileType) bonus += weight;
271
+ if (boosts.pathContains) {
272
+ const patterns = Array.isArray(boosts.pathContains) ? boosts.pathContains : [boosts.pathContains];
273
+ for (const p of patterns) {
274
+ if (r.path?.toLowerCase().includes(p.toLowerCase())) bonus += weight;
275
+ }
276
+ }
277
+ if (boosts.isPlugin && r.isPlugin) bonus += weight;
278
+ if (boosts.isController && r.isController) bonus += weight;
279
+ if (boosts.isObserver && r.isObserver) bonus += weight;
280
+ if (boosts.isRepository && r.isRepository) bonus += weight;
281
+ if (boosts.isResolver && r.isResolver) bonus += weight;
282
+ if (boosts.isModel && r.isModel) bonus += weight;
283
+ if (boosts.isBlock && r.isBlock) bonus += weight;
284
+ if (boosts.magentoType && r.magentoType === boosts.magentoType) bonus += weight;
285
+ return { ...r, score: (r.score || 0) + bonus };
286
+ }).sort((a, b) => b.score - a.score);
287
+ }
288
+
143
289
  function formatSearchResults(results) {
144
290
  if (!results || results.length === 0) {
145
- return 'No results found.';
291
+ return JSON.stringify({ results: [], count: 0 });
146
292
  }
147
293
 
148
- return results.map((r, i) => {
149
- const header = `## Result ${i + 1} (score: ${r.score?.toFixed(3) || 'N/A'})`;
150
-
151
- const meta = [
152
- `**Path:** ${r.path || 'unknown'}`,
153
- r.module ? `**Module:** ${r.module}` : null,
154
- r.magentoType ? `**Magento Type:** ${r.magentoType}` : null,
155
- r.area && r.area !== 'global' ? `**Area:** ${r.area}` : null,
156
- r.className ? `**Class:** ${r.className}` : null,
157
- r.namespace ? `**Namespace:** ${r.namespace}` : null,
158
- r.methodName ? `**Method:** ${r.methodName}` : null,
159
- r.type ? `**File Type:** ${r.type}` : null,
160
- ].filter(Boolean).join('\n');
161
-
162
- let badges = '';
163
- if (r.isPlugin) badges += ' `plugin`';
164
- if (r.isController) badges += ' `controller`';
165
- if (r.isObserver) badges += ' `observer`';
166
- if (r.isRepository) badges += ' `repository`';
167
- if (r.isResolver) badges += ' `graphql-resolver`';
168
-
169
- return `${header}\n${meta}${badges}`;
170
- }).join('\n\n---\n\n');
294
+ const formatted = results.map((r, i) => {
295
+ const entry = {
296
+ rank: i + 1,
297
+ score: r.score ? parseFloat(r.score.toFixed(3)) : null,
298
+ path: r.path || 'unknown',
299
+ };
300
+ if (r.module) entry.module = r.module;
301
+ if (r.className) entry.className = r.className;
302
+ if (r.namespace) entry.namespace = r.namespace;
303
+ if (r.methodName) entry.methodName = r.methodName;
304
+ if (r.methods && r.methods.length > 0) entry.methods = r.methods;
305
+ if (r.magentoType) entry.magentoType = r.magentoType;
306
+ if (r.type) entry.fileType = r.type;
307
+ if (r.area && r.area !== 'global') entry.area = r.area;
308
+
309
+ // Badges concise role indicators
310
+ const badges = [];
311
+ if (r.isPlugin) badges.push('plugin');
312
+ if (r.isController) badges.push('controller');
313
+ if (r.isObserver) badges.push('observer');
314
+ if (r.isRepository) badges.push('repository');
315
+ if (r.isResolver) badges.push('graphql-resolver');
316
+ if (r.isModel) badges.push('model');
317
+ if (r.isBlock) badges.push('block');
318
+ if (badges.length > 0) entry.badges = badges;
319
+
320
+ // Snippet — first 300 chars of indexed content for quick assessment
321
+ if (r.searchText) {
322
+ entry.snippet = r.searchText.length > 300
323
+ ? r.searchText.slice(0, 300) + '...'
324
+ : r.searchText;
325
+ }
326
+
327
+ return entry;
328
+ });
329
+
330
+ return JSON.stringify({ results: formatted, count: formatted.length });
171
331
  }
172
332
 
173
333
  // ─── MCP Server ─────────────────────────────────────────────────
@@ -189,17 +349,17 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
189
349
  tools: [
190
350
  {
191
351
  name: 'magento_search',
192
- description: 'Search Magento codebase semantically. Find classes, methods, configurations, templates by describing what you need.',
352
+ description: 'Search Magento codebase semantically find any PHP class, method, XML config, PHTML template, JS file, or GraphQL schema by describing what you need in natural language. Use this as a general-purpose search when no specialized tool fits. See also: magento_find_class, magento_find_method, magento_find_config for targeted searches.',
193
353
  inputSchema: {
194
354
  type: 'object',
195
355
  properties: {
196
356
  query: {
197
357
  type: 'string',
198
- description: 'Natural language search query (e.g., "product price calculation", "checkout controller", "customer authentication")'
358
+ description: 'Natural language search query describing what you want to find. Examples: "product price calculation logic", "checkout controller", "customer authentication", "add to cart", "order placement flow"'
199
359
  },
200
360
  limit: {
201
361
  type: 'number',
202
- description: 'Maximum results to return (default: 10)',
362
+ description: 'Maximum results to return (default: 10, max: 100)',
203
363
  default: 10
204
364
  }
205
365
  },
@@ -208,17 +368,17 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
208
368
  },
209
369
  {
210
370
  name: 'magento_find_class',
211
- description: 'Find a specific PHP class, interface, or trait in Magento',
371
+ description: 'Find a PHP class, interface, abstract class, or trait by name in Magento. Locates repositories, models, resource models, blocks, helpers, controllers, API interfaces, and data objects. See also: magento_find_plugin (interceptors for this class), magento_find_preference (DI overrides), magento_find_method (methods in the class).',
212
372
  inputSchema: {
213
373
  type: 'object',
214
374
  properties: {
215
375
  className: {
216
376
  type: 'string',
217
- description: 'Class name to find (e.g., "ProductRepository", "AbstractModel")'
377
+ description: 'Full or partial PHP class name. Examples: "ProductRepository", "AbstractModel", "CartManagementInterface", "CustomerData", "StockItemRepository"'
218
378
  },
219
379
  namespace: {
220
380
  type: 'string',
221
- description: 'Optional namespace filter'
381
+ description: 'Optional PHP namespace filter to narrow results. Example: "Magento\\Catalog\\Model"'
222
382
  }
223
383
  },
224
384
  required: ['className']
@@ -226,17 +386,17 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
226
386
  },
227
387
  {
228
388
  name: 'magento_find_method',
229
- description: 'Find implementations of a specific method across Magento',
389
+ description: 'Find implementations of a PHP method or function across the Magento codebase. Searches method names, function definitions, and class method lists. See also: magento_find_class (parent class), magento_find_plugin (interceptors around this method).',
230
390
  inputSchema: {
231
391
  type: 'object',
232
392
  properties: {
233
393
  methodName: {
234
394
  type: 'string',
235
- description: 'Method name to find (e.g., "execute", "getPrice", "save")'
395
+ description: 'PHP method or function name to find. Examples: "execute", "getPrice", "save", "getById", "getList", "beforeSave", "afterDelete", "toHtml", "dispatch"'
236
396
  },
237
397
  className: {
238
398
  type: 'string',
239
- description: 'Optional class name filter'
399
+ description: 'Optional class name to narrow method search. Example: "ProductRepository"'
240
400
  }
241
401
  },
242
402
  required: ['methodName']
@@ -244,18 +404,18 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
244
404
  },
245
405
  {
246
406
  name: 'magento_find_config',
247
- description: 'Find XML configuration files and nodes in Magento',
407
+ description: 'Find XML configuration files and nodes in Magento — di.xml (dependency injection), events.xml (observers), routes.xml (routing), system.xml (admin config), webapi.xml (REST/SOAP), module.xml (module declarations), layout XML. See also: magento_find_observer (events.xml), magento_find_preference (di.xml), magento_find_api (webapi.xml).',
248
408
  inputSchema: {
249
409
  type: 'object',
250
410
  properties: {
251
411
  query: {
252
412
  type: 'string',
253
- description: 'Configuration to find (e.g., "di.xml preference", "routes.xml", "system.xml field")'
413
+ description: 'What configuration to find. Examples: "di.xml preference for ProductRepository", "routes.xml catalog", "system.xml payment field", "events.xml checkout", "layout xml catalog_product_view"'
254
414
  },
255
415
  configType: {
256
416
  type: 'string',
257
417
  enum: ['di', 'routes', 'system', 'events', 'webapi', 'module', 'layout', 'other'],
258
- description: 'Type of configuration'
418
+ description: 'Type of XML configuration: di (dependency injection/preferences/virtualTypes), routes (URL routing), system (admin config fields/sections), events (event observers/listeners), webapi (REST/SOAP endpoint definitions), module (module.xml declarations/setup_version), layout (page layout XML/blocks/containers)'
259
419
  }
260
420
  },
261
421
  required: ['query']
@@ -263,18 +423,18 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
263
423
  },
264
424
  {
265
425
  name: 'magento_find_template',
266
- description: 'Find PHTML templates in Magento',
426
+ description: 'Find PHTML template files in Magento for frontend or admin rendering. Locates view templates for product pages, checkout, customer account, cart, CMS, catalog listing, and more. See also: magento_find_block (Block class rendering the template).',
267
427
  inputSchema: {
268
428
  type: 'object',
269
429
  properties: {
270
430
  query: {
271
431
  type: 'string',
272
- description: 'Template description (e.g., "product listing", "checkout form", "customer account")'
432
+ description: 'Template description or filename pattern. Examples: "product listing", "checkout form", "customer account dashboard", "minicart", "breadcrumbs", "category view", "order summary"'
273
433
  },
274
434
  area: {
275
435
  type: 'string',
276
436
  enum: ['frontend', 'adminhtml', 'base'],
277
- description: 'Magento area'
437
+ description: 'Magento area: frontend (customer-facing storefront), adminhtml (admin panel), base (shared/fallback)'
278
438
  }
279
439
  },
280
440
  required: ['query']
@@ -282,20 +442,20 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
282
442
  },
283
443
  {
284
444
  name: 'magento_index',
285
- description: 'Index or re-index Magento codebase for semantic search (uses Rust core)',
445
+ description: 'Index or re-index the Magento codebase for semantic search. Run this after code changes to update the search index. Indexes PHP, XML, JS, PHTML, and GraphQL files.',
286
446
  inputSchema: {
287
447
  type: 'object',
288
448
  properties: {
289
449
  path: {
290
450
  type: 'string',
291
- description: 'Path to Magento root (uses configured path if not specified)'
451
+ description: 'Absolute path to Magento 2 root directory. Uses configured MAGENTO_ROOT if not specified.'
292
452
  },
293
453
  }
294
454
  }
295
455
  },
296
456
  {
297
457
  name: 'magento_stats',
298
- description: 'Get index statistics from Rust core',
458
+ description: 'Get index statistics total indexed vectors, embedding dimensions, and database path. Use this to verify the index is loaded and check its size.',
299
459
  inputSchema: {
300
460
  type: 'object',
301
461
  properties: {}
@@ -303,30 +463,30 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
303
463
  },
304
464
  {
305
465
  name: 'magento_find_plugin',
306
- description: 'Find plugins (interceptors) for a class or method - before/after/around methods',
466
+ description: 'Find Magento plugins (interceptors) that modify class behavior via before/after/around methods. Locates Plugin classes and di.xml interceptor declarations. See also: magento_find_class (target class details), magento_find_method (intercepted method), magento_find_config with configType=di.',
307
467
  inputSchema: {
308
468
  type: 'object',
309
469
  properties: {
310
470
  targetClass: {
311
471
  type: 'string',
312
- description: 'Class being intercepted (e.g., "ProductRepository", "CartManagement")'
472
+ description: 'Class being intercepted by plugins. Examples: "ProductRepository", "CartManagement", "CustomerRepository", "OrderRepository", "Topmenu"'
313
473
  },
314
474
  targetMethod: {
315
475
  type: 'string',
316
- description: 'Method being intercepted (e.g., "save", "getList")'
476
+ description: 'Specific method being intercepted. Examples: "save", "getList", "getById", "getHtml", "dispatch"'
317
477
  }
318
478
  }
319
479
  }
320
480
  },
321
481
  {
322
482
  name: 'magento_find_observer',
323
- description: 'Find observers for a specific event',
483
+ description: 'Find event observers (listeners) for a Magento event. Locates Observer classes and events.xml declarations. See also: magento_find_config with configType=events for raw XML.',
324
484
  inputSchema: {
325
485
  type: 'object',
326
486
  properties: {
327
487
  eventName: {
328
488
  type: 'string',
329
- description: 'Event name (e.g., "checkout_cart_add_product_complete", "sales_order_place_after")'
489
+ description: 'Magento event name. Examples: "checkout_cart_add_product_complete", "sales_order_place_after", "catalog_product_save_after", "customer_login", "controller_action_predispatch"'
330
490
  }
331
491
  },
332
492
  required: ['eventName']
@@ -334,13 +494,13 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
334
494
  },
335
495
  {
336
496
  name: 'magento_find_preference',
337
- description: 'Find DI preference overrides for an interface or class',
497
+ description: 'Find DI preference overrides which concrete class implements an interface or replaces another class via di.xml. See also: magento_find_class (implementation details), magento_find_config with configType=di.',
338
498
  inputSchema: {
339
499
  type: 'object',
340
500
  properties: {
341
501
  interfaceName: {
342
502
  type: 'string',
343
- description: 'Interface or class name to find preferences for (e.g., "ProductRepositoryInterface")'
503
+ description: 'Interface or class name to find preference/implementation for. Examples: "ProductRepositoryInterface", "StoreManagerInterface", "LoggerInterface", "OrderRepositoryInterface", "CustomerRepositoryInterface"'
344
504
  }
345
505
  },
346
506
  required: ['interfaceName']
@@ -348,18 +508,18 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
348
508
  },
349
509
  {
350
510
  name: 'magento_find_api',
351
- description: 'Find REST/SOAP API endpoints and their implementations',
511
+ description: 'Find REST and SOAP API endpoint definitions in webapi.xml and their service class implementations. See also: magento_find_config with configType=webapi, magento_find_class (service class).',
352
512
  inputSchema: {
353
513
  type: 'object',
354
514
  properties: {
355
515
  query: {
356
516
  type: 'string',
357
- description: 'API endpoint URL pattern or service method (e.g., "/V1/products", "getList")'
517
+ description: 'API endpoint URL pattern or service method name. Examples: "/V1/products", "/V1/orders", "/V1/carts", "/V1/customers", "/V1/categories", "getList", "save"'
358
518
  },
359
519
  method: {
360
520
  type: 'string',
361
521
  enum: ['GET', 'POST', 'PUT', 'DELETE'],
362
- description: 'HTTP method filter'
522
+ description: 'Filter by HTTP method: GET (read), POST (create), PUT (update), DELETE (remove)'
363
523
  }
364
524
  },
365
525
  required: ['query']
@@ -367,18 +527,18 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
367
527
  },
368
528
  {
369
529
  name: 'magento_find_controller',
370
- description: 'Find controllers by route or action',
530
+ description: 'Find MVC controllers by frontend or admin route path. Maps URL routes to Controller action classes with execute() method. See also: magento_find_config with configType=routes.',
371
531
  inputSchema: {
372
532
  type: 'object',
373
533
  properties: {
374
534
  route: {
375
535
  type: 'string',
376
- description: 'Route path (e.g., "catalog/product/view", "checkout/cart/add")'
536
+ description: 'URL route path in frontName/controller/action format. Examples: "catalog/product/view", "checkout/cart/add", "customer/account/login", "sales/order/view", "cms/page/view", "wishlist/index/add"'
377
537
  },
378
538
  area: {
379
539
  type: 'string',
380
540
  enum: ['frontend', 'adminhtml'],
381
- description: 'Magento area'
541
+ description: 'Magento area: frontend (storefront routes) or adminhtml (admin panel routes)'
382
542
  }
383
543
  },
384
544
  required: ['route']
@@ -386,13 +546,13 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
386
546
  },
387
547
  {
388
548
  name: 'magento_find_block',
389
- description: 'Find Block classes by name or template',
549
+ description: 'Find Magento Block classes used for view rendering and template logic. Blocks bridge controllers and templates. See also: magento_find_template (PHTML template rendered by the block), magento_find_config with configType=layout.',
390
550
  inputSchema: {
391
551
  type: 'object',
392
552
  properties: {
393
553
  query: {
394
554
  type: 'string',
395
- description: 'Block class name or functionality (e.g., "Product\\View", "cart totals")'
555
+ description: 'Block class name or functionality description. Examples: "Product\\View", "cart totals", "category listing", "customer account navigation", "order view", "Topmenu"'
396
556
  }
397
557
  },
398
558
  required: ['query']
@@ -400,13 +560,13 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
400
560
  },
401
561
  {
402
562
  name: 'magento_find_cron',
403
- description: 'Find cron jobs by name or schedule',
563
+ description: 'Find scheduled cron jobs defined in crontab.xml and their handler classes in Cron/ directories. See also: magento_find_config for crontab.xml raw XML.',
404
564
  inputSchema: {
405
565
  type: 'object',
406
566
  properties: {
407
567
  jobName: {
408
568
  type: 'string',
409
- description: 'Cron job name or pattern (e.g., "catalog_product", "indexer")'
569
+ description: 'Cron job name or keyword. Examples: "catalog_product", "indexer", "sitemap", "currency", "newsletter", "reindex", "aggregate", "clean"'
410
570
  }
411
571
  },
412
572
  required: ['jobName']
@@ -414,18 +574,18 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
414
574
  },
415
575
  {
416
576
  name: 'magento_find_graphql',
417
- description: 'Find GraphQL types, queries, mutations, or resolvers',
577
+ description: 'Find GraphQL schema definitions (.graphqls), types, queries, mutations, and resolver PHP classes. See also: magento_find_class (resolver implementation), magento_find_method (resolver execute method).',
418
578
  inputSchema: {
419
579
  type: 'object',
420
580
  properties: {
421
581
  query: {
422
582
  type: 'string',
423
- description: 'GraphQL type, query, or mutation name (e.g., "products", "createCustomer", "CartItemInterface")'
583
+ description: 'GraphQL type, query, mutation, or interface name. Examples: "products", "createCustomer", "CartItemInterface", "cart", "categoryList", "placeOrder", "createEmptyCart"'
424
584
  },
425
585
  schemaType: {
426
586
  type: 'string',
427
587
  enum: ['type', 'query', 'mutation', 'interface', 'resolver'],
428
- description: 'Type of GraphQL schema element'
588
+ description: 'Filter by GraphQL schema element: type (object types), query (read operations), mutation (write operations), interface (shared contracts), resolver (PHP resolver classes)'
429
589
  }
430
590
  },
431
591
  required: ['query']
@@ -433,13 +593,13 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
433
593
  },
434
594
  {
435
595
  name: 'magento_find_db_schema',
436
- description: 'Find database table definitions and columns',
596
+ description: 'Find database table definitions, columns, indexes, and constraints declared in db_schema.xml (Magento declarative schema). See also: magento_find_class (model/resource model for the table).',
437
597
  inputSchema: {
438
598
  type: 'object',
439
599
  properties: {
440
600
  tableName: {
441
601
  type: 'string',
442
- description: 'Table name pattern (e.g., "catalog_product", "sales_order")'
602
+ description: 'Database table name or pattern. Examples: "catalog_product_entity", "sales_order", "customer_entity", "quote", "cms_page", "inventory_source"'
443
603
  }
444
604
  },
445
605
  required: ['tableName']
@@ -447,13 +607,13 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
447
607
  },
448
608
  {
449
609
  name: 'magento_module_structure',
450
- description: 'Get complete structure of a Magento module - all its classes, configs, templates',
610
+ description: 'Get the complete structure of a Magento module lists all controllers, models, blocks, plugins, observers, API classes, XML configs, and templates. Provides an overview of module architecture.',
451
611
  inputSchema: {
452
612
  type: 'object',
453
613
  properties: {
454
614
  moduleName: {
455
615
  type: 'string',
456
- description: 'Full module name (e.g., "Magento_Catalog", "Vendor_CustomModule")'
616
+ description: 'Full Magento module name in Vendor_Module format. Examples: "Magento_Catalog", "Magento_Sales", "Magento_Customer", "Magento_Checkout", "Vendor_CustomModule"'
457
617
  }
458
618
  },
459
619
  required: ['moduleName']
@@ -461,17 +621,17 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
461
621
  },
462
622
  {
463
623
  name: 'magento_analyze_diff',
464
- description: 'Analyze git diffs for risk scoring, change classification, and per-file analysis. Works on commits or staged changes.',
624
+ description: 'Analyze git diffs for risk scoring, change classification, and per-file impact analysis. Works on specific commits or staged changes. Useful for code review.',
465
625
  inputSchema: {
466
626
  type: 'object',
467
627
  properties: {
468
628
  commitHash: {
469
629
  type: 'string',
470
- description: 'Git commit hash to analyze. If omitted, analyzes staged changes.'
630
+ description: 'Git commit hash to analyze. If omitted, analyzes currently staged (git add) changes instead.'
471
631
  },
472
632
  staged: {
473
633
  type: 'boolean',
474
- description: 'Analyze staged (git add) changes instead of a commit',
634
+ description: 'Set true to analyze staged changes, false to require commitHash. Default: true.',
475
635
  default: true
476
636
  }
477
637
  }
@@ -479,21 +639,21 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
479
639
  },
480
640
  {
481
641
  name: 'magento_complexity',
482
- description: 'Analyze code complexity (cyclomatic complexity, function count, lines) for Magento files. Finds complex hotspots.',
642
+ description: 'Analyze code complexity cyclomatic complexity, function count, and line count for PHP files. Identifies complex hotspots and rates each file. Use for refactoring prioritization.',
483
643
  inputSchema: {
484
644
  type: 'object',
485
645
  properties: {
486
646
  module: {
487
647
  type: 'string',
488
- description: 'Magento module to analyze (e.g., "Magento_Catalog"). Finds all PHP files in the module.'
648
+ description: 'Magento module to analyze. Finds all PHP files in the module. Examples: "Magento_Catalog", "Magento_Checkout", "Magento_Sales"'
489
649
  },
490
650
  path: {
491
651
  type: 'string',
492
- description: 'Specific file or directory path to analyze'
652
+ description: 'Specific file or directory path to analyze instead of a module name'
493
653
  },
494
654
  threshold: {
495
655
  type: 'number',
496
- description: 'Minimum cyclomatic complexity to report (default: 0, show all)',
656
+ description: 'Minimum cyclomatic complexity to report. Set higher (e.g., 10) to only see complex files. Default: 0 (show all)',
497
657
  default: 0
498
658
  }
499
659
  }
@@ -508,7 +668,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
508
668
  try {
509
669
  switch (name) {
510
670
  case 'magento_search': {
511
- const raw = rustSearch(args.query, args.limit || 10);
671
+ const raw = await rustSearchAsync(args.query, args.limit || 10);
512
672
  const results = raw.map(normalizeResult);
513
673
  return {
514
674
  content: [{
@@ -519,10 +679,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
519
679
  }
520
680
 
521
681
  case 'magento_find_class': {
522
- const query = `class ${args.className} ${args.namespace || ''}`.trim();
523
- const raw = rustSearch(query, 10);
682
+ const ns = args.namespace || '';
683
+ const query = `${args.className} ${ns}`.trim();
684
+ const raw = await rustSearchAsync(query, 30);
685
+ const classLower = args.className.toLowerCase();
524
686
  const results = raw.map(normalizeResult).filter(r =>
525
- r.className?.toLowerCase().includes(args.className.toLowerCase())
687
+ r.className?.toLowerCase().includes(classLower) ||
688
+ r.path?.toLowerCase().includes(classLower.replace(/\\/g, '/'))
526
689
  );
527
690
  return {
528
691
  content: [{
@@ -533,12 +696,21 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
533
696
  }
534
697
 
535
698
  case 'magento_find_method': {
536
- const query = `function ${args.methodName} ${args.className || ''}`.trim();
537
- const raw = rustSearch(query, 20);
538
- const results = raw.map(normalizeResult).filter(r =>
539
- r.methodName?.toLowerCase() === args.methodName.toLowerCase() ||
540
- r.path?.toLowerCase().includes(args.methodName.toLowerCase())
699
+ const query = `method ${args.methodName} function ${args.className || ''}`.trim();
700
+ const raw = await rustSearchAsync(query, 30);
701
+ const methodLower = args.methodName.toLowerCase();
702
+ let results = raw.map(normalizeResult).filter(r =>
703
+ r.methodName?.toLowerCase() === methodLower ||
704
+ r.methodName?.toLowerCase().includes(methodLower) ||
705
+ r.methods?.some(m => m.toLowerCase() === methodLower || m.toLowerCase().includes(methodLower)) ||
706
+ r.path?.toLowerCase().includes(methodLower)
541
707
  );
708
+ // Boost exact method matches to top
709
+ results = results.map(r => {
710
+ const exact = r.methodName?.toLowerCase() === methodLower ||
711
+ r.methods?.some(m => m.toLowerCase() === methodLower);
712
+ return { ...r, score: (r.score || 0) + (exact ? 0.5 : 0) };
713
+ }).sort((a, b) => b.score - a.score);
542
714
  return {
543
715
  content: [{
544
716
  type: 'text',
@@ -552,12 +724,21 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
552
724
  if (args.configType && args.configType !== 'other') {
553
725
  query = `${args.configType}.xml ${args.query}`;
554
726
  }
555
- const raw = rustSearch(query, 10);
556
- const results = raw.map(normalizeResult);
727
+ const raw = await rustSearchAsync(query, 30);
728
+ const pathBoost = args.configType ? [`${args.configType}.xml`] : ['.xml'];
729
+ let normalized = raw.map(normalizeResult);
730
+ // Prefer XML results when configType is specified, but don't hard-exclude
731
+ if (args.configType) {
732
+ const xmlOnly = normalized.filter(r =>
733
+ r.type === 'xml' || r.path?.endsWith('.xml') || r.path?.includes('.xml')
734
+ );
735
+ if (xmlOnly.length > 0) normalized = xmlOnly;
736
+ }
737
+ const results = rerank(normalized, { fileType: 'xml', pathContains: pathBoost });
557
738
  return {
558
739
  content: [{
559
740
  type: 'text',
560
- text: formatSearchResults(results)
741
+ text: formatSearchResults(results.slice(0, 10))
561
742
  }]
562
743
  };
563
744
  }
@@ -566,12 +747,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
566
747
  let query = args.query;
567
748
  if (args.area) query = `${args.area} ${query}`;
568
749
  query += ' template phtml';
569
- const raw = rustSearch(query, 10);
570
- const results = raw.map(normalizeResult);
750
+ const raw = await rustSearchAsync(query, 15);
751
+ const results = rerank(raw.map(normalizeResult), { pathContains: ['.phtml'] });
571
752
  return {
572
753
  content: [{
573
754
  type: 'text',
574
- text: formatSearchResults(results)
755
+ text: formatSearchResults(results.slice(0, 10))
575
756
  }]
576
757
  };
577
758
  }
@@ -602,45 +783,48 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
602
783
  if (args.targetClass) query += ` ${args.targetClass}`;
603
784
  if (args.targetMethod) query += ` ${args.targetMethod} before after around`;
604
785
 
605
- const raw = rustSearch(query, 15);
606
- const results = raw.map(normalizeResult).filter(r =>
786
+ const raw = await rustSearchAsync(query, 30);
787
+ let results = raw.map(normalizeResult).filter(r =>
607
788
  r.isPlugin || r.path?.includes('/Plugin/') || r.path?.includes('di.xml')
608
789
  );
790
+ results = rerank(results, { isPlugin: true, pathContains: ['/Plugin/', 'di.xml'] });
609
791
 
610
792
  return {
611
793
  content: [{
612
794
  type: 'text',
613
- text: formatSearchResults(results)
795
+ text: formatSearchResults(results.slice(0, 15))
614
796
  }]
615
797
  };
616
798
  }
617
799
 
618
800
  case 'magento_find_observer': {
619
801
  const query = `event ${args.eventName} observer`;
620
- const raw = rustSearch(query, 15);
621
- const results = raw.map(normalizeResult).filter(r =>
802
+ const raw = await rustSearchAsync(query, 30);
803
+ let results = raw.map(normalizeResult).filter(r =>
622
804
  r.isObserver || r.path?.includes('/Observer/') || r.path?.includes('events.xml')
623
805
  );
806
+ results = rerank(results, { isObserver: true, pathContains: ['events.xml', '/Observer/'] });
624
807
 
625
808
  return {
626
809
  content: [{
627
810
  type: 'text',
628
- text: `## Observers for event: ${args.eventName}\n\n` + formatSearchResults(results)
811
+ text: formatSearchResults(results.slice(0, 15))
629
812
  }]
630
813
  };
631
814
  }
632
815
 
633
816
  case 'magento_find_preference': {
634
- const query = `preference ${args.interfaceName}`;
635
- const raw = rustSearch(query, 15);
636
- const results = raw.map(normalizeResult).filter(r =>
817
+ const query = `preference ${args.interfaceName} di.xml type`;
818
+ const raw = await rustSearchAsync(query, 30);
819
+ let results = raw.map(normalizeResult).filter(r =>
637
820
  r.path?.includes('di.xml')
638
821
  );
822
+ results = rerank(results, { fileType: 'xml', pathContains: ['di.xml'] });
639
823
 
640
824
  return {
641
825
  content: [{
642
826
  type: 'text',
643
- text: `## Preferences for: ${args.interfaceName}\n\n` + formatSearchResults(results)
827
+ text: formatSearchResults(results.slice(0, 15))
644
828
  }]
645
829
  };
646
830
  }
@@ -649,26 +833,40 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
649
833
  let query = `webapi route ${args.query}`;
650
834
  if (args.method) query += ` method="${args.method}"`;
651
835
 
652
- const raw = rustSearch(query, 15);
653
- const results = raw.map(normalizeResult);
836
+ const raw = await rustSearchAsync(query, 30);
837
+ let results = rerank(raw.map(normalizeResult), { pathContains: ['webapi.xml'] });
654
838
 
655
839
  return {
656
840
  content: [{
657
841
  type: 'text',
658
- text: `## API Endpoints matching: ${args.query}\n\n` + formatSearchResults(results)
842
+ text: formatSearchResults(results.slice(0, 15))
659
843
  }]
660
844
  };
661
845
  }
662
846
 
663
847
  case 'magento_find_controller': {
664
848
  const parts = args.route.split('/');
665
- const query = `controller ${parts.join(' ')} execute action`;
849
+ // Map route to Magento namespace: catalog/product/view → Catalog Controller Product View
850
+ const namespaceParts = parts.map(p => p.charAt(0).toUpperCase() + p.slice(1));
851
+ const query = `${namespaceParts.join(' ')} controller execute action`;
666
852
 
667
- const raw = rustSearch(query, 15);
853
+ const raw = await rustSearchAsync(query, 30);
668
854
  let results = raw.map(normalizeResult).filter(r =>
669
855
  r.isController || r.path?.includes('/Controller/')
670
856
  );
671
857
 
858
+ // Boost results whose path matches the route segments
859
+ if (parts.length >= 2) {
860
+ const pathPattern = parts.map(p => p.charAt(0).toUpperCase() + p.slice(1));
861
+ results.sort((a, b) => {
862
+ const aPath = a.path || '';
863
+ const bPath = b.path || '';
864
+ const aMatches = pathPattern.filter(p => aPath.includes(p)).length;
865
+ const bMatches = pathPattern.filter(p => bPath.includes(p)).length;
866
+ return bMatches - aMatches;
867
+ });
868
+ }
869
+
672
870
  if (args.area) {
673
871
  results = results.filter(r => r.area === args.area || r.path?.includes(`/${args.area}/`));
674
872
  }
@@ -676,37 +874,39 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
676
874
  return {
677
875
  content: [{
678
876
  type: 'text',
679
- text: `## Controllers for route: ${args.route}\n\n` + formatSearchResults(results)
877
+ text: formatSearchResults(results)
680
878
  }]
681
879
  };
682
880
  }
683
881
 
684
882
  case 'magento_find_block': {
685
883
  const query = `block ${args.query}`;
686
- const raw = rustSearch(query, 15);
687
- const results = raw.map(normalizeResult).filter(r =>
884
+ const raw = await rustSearchAsync(query, 30);
885
+ let results = raw.map(normalizeResult).filter(r =>
688
886
  r.isBlock || r.path?.includes('/Block/')
689
887
  );
888
+ results = rerank(results, { isBlock: true, pathContains: ['/Block/'] });
690
889
 
691
890
  return {
692
891
  content: [{
693
892
  type: 'text',
694
- text: formatSearchResults(results)
893
+ text: formatSearchResults(results.slice(0, 15))
695
894
  }]
696
895
  };
697
896
  }
698
897
 
699
898
  case 'magento_find_cron': {
700
899
  const query = `cron job ${args.jobName}`;
701
- const raw = rustSearch(query, 15);
702
- const results = raw.map(normalizeResult).filter(r =>
900
+ const raw = await rustSearchAsync(query, 30);
901
+ let results = raw.map(normalizeResult).filter(r =>
703
902
  r.path?.includes('crontab.xml') || r.path?.includes('/Cron/')
704
903
  );
904
+ results = rerank(results, { pathContains: ['crontab.xml', '/Cron/'] });
705
905
 
706
906
  return {
707
907
  content: [{
708
908
  type: 'text',
709
- text: `## Cron jobs matching: ${args.jobName}\n\n` + formatSearchResults(results)
909
+ text: formatSearchResults(results.slice(0, 15))
710
910
  }]
711
911
  };
712
912
  }
@@ -715,37 +915,39 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
715
915
  let query = `graphql ${args.query}`;
716
916
  if (args.schemaType) query += ` ${args.schemaType}`;
717
917
 
718
- const raw = rustSearch(query, 15);
719
- const results = raw.map(normalizeResult).filter(r =>
918
+ const raw = await rustSearchAsync(query, 40);
919
+ let results = raw.map(normalizeResult).filter(r =>
720
920
  r.isResolver || r.path?.includes('/Resolver/') ||
721
921
  r.path?.includes('.graphqls') || r.type === 'graphql'
722
922
  );
923
+ results = rerank(results, { isResolver: true, pathContains: ['.graphqls', '/Resolver/'] });
723
924
 
724
925
  return {
725
926
  content: [{
726
927
  type: 'text',
727
- text: `## GraphQL matching: ${args.query}\n\n` + formatSearchResults(results)
928
+ text: formatSearchResults(results.slice(0, 15))
728
929
  }]
729
930
  };
730
931
  }
731
932
 
732
933
  case 'magento_find_db_schema': {
733
- const query = `table ${args.tableName} column db_schema`;
734
- const raw = rustSearch(query, 15);
735
- const results = raw.map(normalizeResult).filter(r =>
934
+ const query = `db_schema.xml table ${args.tableName} column declarative schema`;
935
+ const raw = await rustSearchAsync(query, 40);
936
+ let results = raw.map(normalizeResult).filter(r =>
736
937
  r.path?.includes('db_schema.xml')
737
938
  );
939
+ results = rerank(results, { fileType: 'xml', pathContains: ['db_schema.xml'] });
738
940
 
739
941
  return {
740
942
  content: [{
741
943
  type: 'text',
742
- text: `## Database schema for: ${args.tableName}\n\n` + formatSearchResults(results)
944
+ text: formatSearchResults(results.slice(0, 15))
743
945
  }]
744
946
  };
745
947
  }
746
948
 
747
949
  case 'magento_module_structure': {
748
- const raw = rustSearch(args.moduleName, 100);
950
+ const raw = await rustSearchAsync(args.moduleName, 100);
749
951
  const moduleName = args.moduleName.replace('_', '/');
750
952
  const results = raw.map(normalizeResult).filter(r =>
751
953
  r.path?.includes(moduleName) || r.module?.includes(args.moduleName)
@@ -914,9 +1116,25 @@ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
914
1116
  });
915
1117
 
916
1118
  async function main() {
1119
+ // Try to start persistent Rust serve process for fast queries
1120
+ try {
1121
+ startServeProcess();
1122
+ // Give it a moment to load model+index
1123
+ await new Promise(r => setTimeout(r, 100));
1124
+ } catch {
1125
+ // Non-fatal: falls back to execFileSync per query
1126
+ }
1127
+
917
1128
  const transport = new StdioServerTransport();
918
1129
  await server.connect(transport);
919
1130
  console.error('Magector MCP server started (Rust core backend)');
920
1131
  }
921
1132
 
1133
+ // Cleanup on exit
1134
+ process.on('exit', () => {
1135
+ if (serveProcess) {
1136
+ serveProcess.kill();
1137
+ }
1138
+ });
1139
+
922
1140
  main().catch(console.error);