magector 2.16.0 → 2.16.1

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 +5 -5
  2. package/src/mcp-server.js +114 -58
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "magector",
3
- "version": "2.16.0",
3
+ "version": "2.16.1",
4
4
  "description": "Semantic code search for Magento 2 — index, search, MCP server",
5
5
  "type": "module",
6
6
  "main": "src/mcp-server.js",
@@ -33,10 +33,10 @@
33
33
  "ruvector": "^0.1.96"
34
34
  },
35
35
  "optionalDependencies": {
36
- "@magector/cli-darwin-arm64": "2.16.0",
37
- "@magector/cli-linux-x64": "2.16.0",
38
- "@magector/cli-linux-arm64": "2.16.0",
39
- "@magector/cli-win32-x64": "2.16.0"
36
+ "@magector/cli-darwin-arm64": "2.16.1",
37
+ "@magector/cli-linux-x64": "2.16.1",
38
+ "@magector/cli-linux-arm64": "2.16.1",
39
+ "@magector/cli-win32-x64": "2.16.1"
40
40
  },
41
41
  "keywords": [
42
42
  "magento",
package/src/mcp-server.js CHANGED
@@ -938,6 +938,10 @@ function tryConnectSocket() {
938
938
  let globalServeQuery = null;
939
939
 
940
940
  function serveQuery(command, params = {}, timeoutMs = 30000) {
941
+ if (!serveProcess || !serveReady) {
942
+ logToFile('WARN', `serveQuery(${command}): serve process not ready — returning error`);
943
+ return Promise.resolve({ ok: false, error: 'Serve process not ready' });
944
+ }
941
945
  return new Promise((resolve, reject) => {
942
946
  const id = serveNextId++;
943
947
  logToFile('QUERY', `[${id}] → ${command}(${JSON.stringify(params).slice(0, 200)})`);
@@ -3631,7 +3635,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
3631
3635
  tools: [
3632
3636
  {
3633
3637
  name: 'magento_search',
3634
- 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.',
3638
+ 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. Works best for Magento core and popular vendor modules. For small/custom project-specific modules (e.g. proprietary modules not widely known to the embedding model), use magento_grep instead — semantic search may return 0 results for these. See also: magento_find_class, magento_find_method, magento_find_config for targeted searches.',
3635
3639
  inputSchema: {
3636
3640
  type: 'object',
3637
3641
  properties: {
@@ -4278,7 +4282,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
4278
4282
  },
4279
4283
  {
4280
4284
  name: 'magento_batch',
4281
- description: 'Execute multiple Magector tool calls in a single request to reduce MCP round-trip overhead. Each query runs in parallel and returns combined results. Use this when you need 2+ independent lookups (e.g., find a class AND its plugins AND its observers in one call instead of three).',
4285
+ description: 'Execute multiple Magector tool calls in a single request to reduce MCP round-trip overhead. Each query runs in parallel and returns combined results. Use this when you need 2+ independent lookups (e.g., find a class AND its plugins AND its observers in one call instead of three). Supported tools: magento_search, magento_find_class, magento_find_method, magento_find_plugin, magento_find_observer, magento_find_config, magento_find_event_flow, magento_find_di_wiring, magento_find_callers, magento_find_preference, magento_find_fieldset, magento_module_structure, magento_trace_dependency, magento_impact_analysis, magento_grep, magento_read, magento_ast_search, magento_find_dataobject_issues, magento_find_null_risks.',
4282
4286
  inputSchema: {
4283
4287
  type: 'object',
4284
4288
  properties: {
@@ -4391,7 +4395,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
4391
4395
  },
4392
4396
  {
4393
4397
  name: 'magento_find_null_risks',
4394
- description: 'Find method chains without null guards using the pre-built enrichment index. Returns all ->firstMethod()->secondMethod() calls where no null check (=== null, !== null, ?->, ??, isset, is_null) was detected in surrounding code. Requires magento_enrich to have been run first. 100× faster than grep — O(1) SQLite query vs O(n) file scan. Use firstMethod to filter (e.g., "getPayment" finds all ->getPayment()->anything() without null guard). ⚡ For multi-query workflows use magento_batch.',
4398
+ description: 'Find method chains without null guards using the pre-built enrichment index. Returns all ->firstMethod()->secondMethod() calls where no null check (=== null, !== null, ?->, ??, isset, is_null) was detected in surrounding code. Requires magento_enrich to have been run first (magento_index triggers it automatically in the background). 100× faster than grep — O(1) SQLite query vs O(n) file scan. Use firstMethod to filter (e.g., "getPayment" finds all ->getPayment()->anything() without null guard). ⚡ For multi-query workflows use magento_batch.',
4395
4399
  inputSchema: {
4396
4400
  type: 'object',
4397
4401
  properties: {
@@ -5200,43 +5204,63 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
5200
5204
  }
5201
5205
 
5202
5206
  case 'magento_module_structure': {
5203
- const raw = await rustSearchAsync(args.moduleName, 200);
5204
- // Support both app/code (Magento/Catalog/) and vendor (module-catalog/) paths
5205
- const modulePath = args.moduleName.replace('_', '/') + '/';
5206
5207
  const parts = args.moduleName.split('_');
5208
+ // Support both app/code (Magento/Catalog/) and vendor (magento/module-catalog/) paths
5209
+ const modulePath = args.moduleName.replace('_', '/') + '/';
5207
5210
  // Hyphenate camelCase for vendor path: OrderSplit → order-split
5211
+ const vendorDir = parts.length === 2 ? parts[0].toLowerCase() : '';
5208
5212
  const vendorPath = parts.length === 2
5209
5213
  ? `module-${parts[1].replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase()}/`
5210
5214
  : '';
5211
- let results = raw.map(normalizeResult).filter(r => {
5212
- const p = r.path || '';
5213
- const mod = r.module || '';
5214
- // Exact module match or directory-level path match (trailing slash prevents Catalog matching CatalogRule)
5215
- return mod === args.moduleName ||
5216
- p.includes(modulePath) ||
5217
- (vendorPath && p.toLowerCase().includes(vendorPath));
5218
- });
5219
-
5220
- // Filesystem fallback: if vector search found nothing, glob the module directory
5221
- if (results.length === 0 && config.magentoRoot && vendorPath) {
5222
- logToFile('INFO', `module_structure: vector search returned 0 results for "${args.moduleName}" — using filesystem fallback (glob ${vendorPath})`);
5223
- try {
5224
- const vendorGlob = `**/${vendorPath}**/*.{php,xml,phtml}`;
5225
- const files = await glob(vendorGlob, { cwd: config.magentoRoot, absolute: false, nodir: true });
5226
- for (const f of files.slice(0, 100)) {
5227
- const entry = { path: f, score: 0.5 };
5228
- if (f.includes('/Controller/')) entry.isController = true;
5229
- if (f.includes('/Model/')) entry.isModel = true;
5230
- if (f.includes('/Block/')) entry.isBlock = true;
5231
- if (f.includes('/Plugin/')) entry.isPlugin = true;
5232
- if (f.includes('/Observer/')) entry.isObserver = true;
5233
- if (f.endsWith('.xml')) entry.type = 'xml';
5234
- // Extract class name from path
5235
- const phpMatch = f.match(/\/([A-Z]\w+)\.php$/);
5236
- if (phpMatch) entry.className = phpMatch[1];
5237
- results.push(entry);
5215
+ let results = [];
5216
+
5217
+ // Primary: filesystem-based (authoritative avoids mixing cross-references from vector search)
5218
+ if (config.magentoRoot) {
5219
+ const fsGlobs = [];
5220
+ if (parts.length === 2) {
5221
+ // app/code/{Vendor}/{Module}/
5222
+ fsGlobs.push(`app/code/${parts[0]}/${parts[1]}/**/*.{php,xml,phtml}`);
5223
+ // vendor/{vendor-lower}/{module-lower}/ — vendor-specific to avoid false positives
5224
+ if (vendorDir && vendorPath) {
5225
+ fsGlobs.push(`vendor/${vendorDir}/${vendorPath}**/*.{php,xml,phtml}`);
5238
5226
  }
5239
- } catch {}
5227
+ }
5228
+ for (const globPattern of fsGlobs) {
5229
+ try {
5230
+ const files = await glob(globPattern, { cwd: config.magentoRoot, absolute: false, nodir: true });
5231
+ if (files.length > 0) {
5232
+ logToFile('INFO', `module_structure: filesystem found ${files.length} files for "${args.moduleName}" (${globPattern})`);
5233
+ for (const f of files.slice(0, 100)) {
5234
+ const entry = { path: f, score: 1.0 };
5235
+ if (f.includes('/Controller/')) entry.isController = true;
5236
+ if (f.includes('/Model/')) entry.isModel = true;
5237
+ if (f.includes('/Block/')) entry.isBlock = true;
5238
+ if (f.includes('/Plugin/')) entry.isPlugin = true;
5239
+ if (f.includes('/Observer/')) entry.isObserver = true;
5240
+ if (f.endsWith('.xml')) entry.type = 'xml';
5241
+ const phpMatch = f.match(/\/([A-Z]\w+)\.php$/);
5242
+ if (phpMatch) entry.className = phpMatch[1];
5243
+ results.push(entry);
5244
+ }
5245
+ break; // Found in one location, stop
5246
+ }
5247
+ } catch {}
5248
+ }
5249
+ }
5250
+
5251
+ // Fallback: vector search with strict path/module filtering (only if filesystem found nothing)
5252
+ if (results.length === 0) {
5253
+ logToFile('INFO', `module_structure: filesystem found 0 files for "${args.moduleName}" — falling back to vector search`);
5254
+ const raw = await rustSearchAsync(args.moduleName, 200);
5255
+ results = raw.map(normalizeResult).filter(r => {
5256
+ const p = r.path || '';
5257
+ const mod = r.module || '';
5258
+ // Exact module match or directory-level path match (trailing slash prevents Catalog matching CatalogRule)
5259
+ return mod === args.moduleName ||
5260
+ p.includes(modulePath) ||
5261
+ // Vendor-specific path check to avoid matching other vendors' same-named modules
5262
+ (vendorDir && vendorPath && p.toLowerCase().includes(`${vendorDir}/${vendorPath}`));
5263
+ });
5240
5264
  }
5241
5265
 
5242
5266
  const structure = {
@@ -6182,6 +6206,21 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
6182
6206
  }
6183
6207
  break;
6184
6208
  }
6209
+ case 'magento_find_config': {
6210
+ let cfgQuery = a.query;
6211
+ if (a.configType && a.configType !== 'other') cfgQuery = `${a.configType}.xml xml config ${a.query}`;
6212
+ const cfgRaw = await rustSearchAsync(cfgQuery, 100);
6213
+ let cfgRes = cfgRaw.map(normalizeResult).filter(r =>
6214
+ r.type === 'xml' || r.path?.endsWith('.xml') || r.path?.includes('.xml')
6215
+ );
6216
+ if (a.configType && a.configType !== 'other') {
6217
+ const cfgTypeFile = `${a.configType}.xml`;
6218
+ const cfgTyped = cfgRes.filter(r => r.path?.includes(cfgTypeFile));
6219
+ if (cfgTyped.length >= 3) cfgRes = cfgTyped;
6220
+ }
6221
+ text = formatSearchResults(cfgRes.slice(0, 10));
6222
+ break;
6223
+ }
6185
6224
  case 'magento_trace_dependency': {
6186
6225
  const dep = await traceDependency(a.className, a.direction || 'both');
6187
6226
  text = `Preferences: ${dep.preferences.length}, Plugins: ${dep.plugins.length}, VirtualTypes: ${dep.virtualTypes.length}, Args: ${dep.argumentOverrides.length}\n`;
@@ -6271,35 +6310,52 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
6271
6310
  break;
6272
6311
  }
6273
6312
  case 'magento_module_structure': {
6274
- const raw = await rustSearchAsync(a.moduleName, 200);
6275
- const modulePath = a.moduleName.replace('_', '/') + '/';
6276
6313
  const mParts = a.moduleName.split('_');
6277
- // Hyphenate camelCase for vendor path: OrderSplit → order-split
6314
+ const modulePath = a.moduleName.replace('_', '/') + '/';
6315
+ const mVendorDir = mParts.length === 2 ? mParts[0].toLowerCase() : '';
6278
6316
  const vendorPath = mParts.length === 2
6279
6317
  ? `module-${mParts[1].replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase()}/`
6280
6318
  : '';
6281
- let res = raw.map(normalizeResult).filter(r => {
6282
- const p = r.path || '';
6283
- const mod = r.module || '';
6284
- return mod === a.moduleName || p.includes(modulePath) || (vendorPath && p.toLowerCase().includes(vendorPath));
6285
- });
6286
- // Filesystem fallback
6287
- if (res.length === 0 && config.magentoRoot && vendorPath) {
6288
- try {
6289
- const vendorGlob = `**/${vendorPath}**/*.{php,xml,phtml}`;
6290
- const files = await glob(vendorGlob, { cwd: config.magentoRoot, absolute: false, nodir: true });
6291
- for (const f of files.slice(0, 100)) {
6292
- const entry = { path: f, score: 0.5 };
6293
- if (f.includes('/Controller/')) entry.isController = true;
6294
- if (f.includes('/Model/')) entry.isModel = true;
6295
- if (f.includes('/Plugin/')) entry.isPlugin = true;
6296
- if (f.includes('/Observer/')) entry.isObserver = true;
6297
- if (f.endsWith('.xml')) entry.type = 'xml';
6298
- const phpMatch = f.match(/\/([A-Z]\w+)\.php$/);
6299
- if (phpMatch) entry.className = phpMatch[1];
6300
- res.push(entry);
6319
+ let res = [];
6320
+ // Primary: filesystem-based (avoids mixing cross-references)
6321
+ if (config.magentoRoot) {
6322
+ const msGlobs = [];
6323
+ if (mParts.length === 2) {
6324
+ msGlobs.push(`app/code/${mParts[0]}/${mParts[1]}/**/*.{php,xml,phtml}`);
6325
+ if (mVendorDir && vendorPath) {
6326
+ msGlobs.push(`vendor/${mVendorDir}/${vendorPath}**/*.{php,xml,phtml}`);
6301
6327
  }
6302
- } catch {}
6328
+ }
6329
+ for (const gp of msGlobs) {
6330
+ try {
6331
+ const files = await glob(gp, { cwd: config.magentoRoot, absolute: false, nodir: true });
6332
+ if (files.length > 0) {
6333
+ for (const f of files.slice(0, 100)) {
6334
+ const entry = { path: f, score: 1.0 };
6335
+ if (f.includes('/Controller/')) entry.isController = true;
6336
+ if (f.includes('/Model/')) entry.isModel = true;
6337
+ if (f.includes('/Plugin/')) entry.isPlugin = true;
6338
+ if (f.includes('/Observer/')) entry.isObserver = true;
6339
+ if (f.endsWith('.xml')) entry.type = 'xml';
6340
+ const phpMatch = f.match(/\/([A-Z]\w+)\.php$/);
6341
+ if (phpMatch) entry.className = phpMatch[1];
6342
+ res.push(entry);
6343
+ }
6344
+ break;
6345
+ }
6346
+ } catch {}
6347
+ }
6348
+ }
6349
+ // Fallback: vector search with vendor-specific path filtering
6350
+ if (res.length === 0) {
6351
+ const raw = await rustSearchAsync(a.moduleName, 200);
6352
+ res = raw.map(normalizeResult).filter(r => {
6353
+ const p = r.path || '';
6354
+ const mod = r.module || '';
6355
+ return mod === a.moduleName ||
6356
+ p.includes(modulePath) ||
6357
+ (mVendorDir && vendorPath && p.toLowerCase().includes(`${mVendorDir}/${vendorPath}`));
6358
+ });
6303
6359
  }
6304
6360
  text = `Module: ${a.moduleName} (${res.length} files)\n`;
6305
6361
  const cats = { controllers: '/Controller/', models: '/Model/', plugins: '/Plugin/', observers: '/Observer/', api: '/Api/' };