magector 2.3.1 → 2.4.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 +202 -15
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "magector",
3
- "version": "2.3.1",
3
+ "version": "2.4.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.3.1",
37
- "@magector/cli-linux-x64": "2.3.1",
38
- "@magector/cli-linux-arm64": "2.3.1",
39
- "@magector/cli-win32-x64": "2.3.1"
36
+ "@magector/cli-darwin-arm64": "2.4.1",
37
+ "@magector/cli-linux-x64": "2.4.1",
38
+ "@magector/cli-linux-arm64": "2.4.1",
39
+ "@magector/cli-win32-x64": "2.4.1"
40
40
  },
41
41
  "keywords": [
42
42
  "magento",
package/src/mcp-server.js CHANGED
@@ -32,6 +32,8 @@ import {
32
32
  } from 'ruvector/dist/analysis/complexity.js';
33
33
  import { resolveBinary } from './binary.js';
34
34
  import { resolveModels } from './model.js';
35
+ import { createRequire } from 'module';
36
+ const __pkg = createRequire(import.meta.url)('../package.json');
35
37
 
36
38
  const config = {
37
39
  dbPath: process.env.MAGECTOR_DB || './.magector/index.db',
@@ -2816,7 +2818,7 @@ async function traceCallChain(startClass, startMethod, maxDepth = 3) {
2816
2818
  const server = new Server(
2817
2819
  {
2818
2820
  name: 'magector',
2819
- version: '2.1.1'
2821
+ version: __pkg.version
2820
2822
  },
2821
2823
  {
2822
2824
  capabilities: {
@@ -3086,7 +3088,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
3086
3088
  },
3087
3089
  {
3088
3090
  name: 'magento_find_db_schema',
3089
- 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).',
3091
+ description: 'Find database table definitions, columns, indexes, and constraints declared in db_schema.xml (Magento declarative schema) AND legacy Setup scripts (InstallSchema, UpgradeSchema). Covers both modern declarative schema and legacy $setup->newTable() / addColumn() table definitions. See also: magento_find_trigger (DB triggers), magento_find_table_usage (cross-module table references).',
3090
3092
  inputSchema: {
3091
3093
  type: 'object',
3092
3094
  properties: {
@@ -3098,6 +3100,37 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
3098
3100
  required: ['tableName']
3099
3101
  }
3100
3102
  },
3103
+ {
3104
+ name: 'magento_find_trigger',
3105
+ description: 'Find MySQL database trigger definitions in Magento Setup scripts. Detects triggers created via TriggerFactory (setName, setTable, setEvent, setTime, addStatement, createTrigger). Returns trigger name, target table, event type (INSERT/UPDATE/DELETE), timing (BEFORE/AFTER), and SQL statements. Use when investigating DB-level automation, trigger chains, or performance issues caused by cascading triggers.',
3106
+ inputSchema: {
3107
+ type: 'object',
3108
+ properties: {
3109
+ triggerName: {
3110
+ type: 'string',
3111
+ description: 'Optional trigger name or pattern to search for. If omitted, finds all triggers in the codebase.'
3112
+ },
3113
+ tableName: {
3114
+ type: 'string',
3115
+ description: 'Optional table name to find triggers targeting this table.'
3116
+ }
3117
+ }
3118
+ }
3119
+ },
3120
+ {
3121
+ name: 'magento_find_table_usage',
3122
+ description: 'Find all code that references a database table — across db_schema.xml, Setup scripts (InstallSchema/UpgradeSchema), raw SQL (Zend_Db_Expr, $connection->query), getTable() calls, and resource model definitions. Builds a cross-module dependency map showing who reads/writes/creates a given table. Essential for impact analysis of schema changes.',
3123
+ inputSchema: {
3124
+ type: 'object',
3125
+ properties: {
3126
+ tableName: {
3127
+ type: 'string',
3128
+ description: 'Database table name to find all references for. Examples: "salesrule_ordered", "catalog_product_entity", "quote_item"'
3129
+ }
3130
+ },
3131
+ required: ['tableName']
3132
+ }
3133
+ },
3101
3134
  {
3102
3135
  name: 'magento_module_structure',
3103
3136
  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.',
@@ -3622,30 +3655,39 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
3622
3655
  if (args.targetClass) {
3623
3656
  const fpRoot = config.magentoRoot;
3624
3657
  const diFiles = await glob('**/etc/**/di.xml', { cwd: fpRoot, absolute: true, nodir: true });
3625
- const shortTarget = args.targetClass.split('\\').pop();
3658
+ // Normalize target class for matching (both \ and \\)
3659
+ const normalizedTarget = args.targetClass.replace(/\\\\/g, '\\');
3626
3660
  for (const diFile of diFiles) {
3627
3661
  let content;
3628
3662
  try { content = readFileSync(diFile, 'utf-8'); } catch { continue; }
3629
- if (!content.includes(shortTarget)) continue;
3663
+ if (!content.includes(normalizedTarget)) continue;
3630
3664
  const relPath = diFile.replace(fpRoot + '/', '');
3631
3665
  // Find plugin registrations for this target
3632
3666
  const typeBlockRegex = /<type\s+name="([^"]+)"[^>]*>([\s\S]*?)<\/type>/g;
3633
3667
  let tm;
3634
3668
  while ((tm = typeBlockRegex.exec(content)) !== null) {
3635
- const typeName = tm[1];
3636
- if (!typeName.includes(shortTarget)) continue;
3669
+ const typeName = tm[1].replace(/\\\\/g, '\\');
3670
+ if (typeName !== normalizedTarget) continue;
3637
3671
  const block = tm[2];
3638
- const pluginRegex = /<plugin\s+name="([^"]+)"[^>]*type="([^"]+)"[^>]*/g;
3672
+ const pluginRegex = /<plugin\s+([^/>]*)\/?>/g;
3639
3673
  let pm;
3640
3674
  while ((pm = pluginRegex.exec(block)) !== null) {
3675
+ const attrs = {};
3676
+ const localAttrRe = /(\w+)="([^"]*)"/g;
3677
+ let am;
3678
+ while ((am = localAttrRe.exec(pm[1])) !== null) {
3679
+ attrs[am[1]] = am[2];
3680
+ }
3641
3681
  let area = 'global';
3642
3682
  if (relPath.includes('/etc/adminhtml/')) area = 'adminhtml';
3643
3683
  else if (relPath.includes('/etc/frontend/')) area = 'frontend';
3644
3684
  else if (relPath.includes('/etc/graphql/')) area = 'graphql';
3645
3685
  diRegistrations.push({
3646
3686
  target: typeName,
3647
- pluginName: pm[1],
3648
- pluginClass: pm[2],
3687
+ pluginName: attrs.name || '',
3688
+ pluginClass: attrs.type || '',
3689
+ disabled: attrs.disabled === 'true',
3690
+ sortOrder: attrs.sortOrder || null,
3649
3691
  area,
3650
3692
  file: relPath
3651
3693
  });
@@ -3658,7 +3700,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
3658
3700
  if (diRegistrations.length > 0) {
3659
3701
  text += `\n\n### DI Plugin Registrations for ${args.targetClass} (${diRegistrations.length})\n`;
3660
3702
  for (const reg of diRegistrations) {
3661
- text += `- **${reg.pluginName}** \`${reg.pluginClass}\` [${reg.area}] (${reg.file})\n`;
3703
+ const disabledTag = reg.disabled ? ' **[DISABLED]**' : '';
3704
+ const sortTag = reg.sortOrder ? ` (sortOrder: ${reg.sortOrder})` : '';
3705
+ text += `- **${reg.pluginName}** → \`${reg.pluginClass}\` [${reg.area}]${sortTag}${disabledTag} (${reg.file})\n`;
3662
3706
  }
3663
3707
  }
3664
3708
 
@@ -3807,17 +3851,160 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
3807
3851
  }
3808
3852
 
3809
3853
  case 'magento_find_db_schema': {
3810
- const query = `db_schema.xml table ${args.tableName} column declarative schema`;
3811
- const raw = await rustSearchAsync(query, 40);
3812
- let results = raw.map(normalizeResult).filter(r =>
3854
+ // Search both declarative schema (db_schema.xml) and legacy Setup scripts
3855
+ const declQuery = `db_schema.xml table ${args.tableName} column declarative schema`;
3856
+ const legacyQuery = `create_table ${args.tableName} legacy_schema table_created ${args.tableName} setup install schema newTable addColumn`;
3857
+ const [declRaw, legacyRaw] = await Promise.all([
3858
+ rustSearchAsync(declQuery, 40),
3859
+ rustSearchAsync(legacyQuery, 30)
3860
+ ]);
3861
+
3862
+ let declResults = declRaw.map(normalizeResult).filter(r =>
3813
3863
  r.path?.includes('db_schema.xml')
3814
3864
  );
3815
- results = rerank(results, { fileType: 'xml', pathContains: ['db_schema.xml'] });
3865
+ declResults = rerank(declResults, { fileType: 'xml', pathContains: ['db_schema.xml'] });
3866
+
3867
+ let legacyResults = legacyRaw.map(normalizeResult).filter(r => {
3868
+ const p = r.path || '';
3869
+ return (p.includes('/Setup/') || p.includes('InstallSchema') ||
3870
+ p.includes('UpgradeSchema') || p.includes('/Patch/')) &&
3871
+ (r.snippet?.toLowerCase().includes(args.tableName.toLowerCase()) ||
3872
+ r.searchText?.toLowerCase().includes(args.tableName.toLowerCase()));
3873
+ });
3874
+
3875
+ // Deduplicate by path
3876
+ const seen = new Set(declResults.map(r => r.path));
3877
+ for (const r of legacyResults) {
3878
+ if (!seen.has(r.path)) {
3879
+ seen.add(r.path);
3880
+ declResults.push(r);
3881
+ }
3882
+ }
3883
+
3884
+ // Add section headers
3885
+ let output = '';
3886
+ const xmlResults = declResults.filter(r => r.path?.includes('db_schema.xml'));
3887
+ const setupResults = declResults.filter(r => !r.path?.includes('db_schema.xml'));
3888
+
3889
+ if (xmlResults.length > 0) {
3890
+ output += formatSearchResults(xmlResults.slice(0, 10));
3891
+ }
3892
+ if (setupResults.length > 0) {
3893
+ output += `\n\n### Legacy Setup Scripts (InstallSchema/UpgradeSchema)\n`;
3894
+ output += formatSearchResults(setupResults.slice(0, 10));
3895
+ }
3816
3896
 
3817
3897
  return {
3818
3898
  content: [{
3819
3899
  type: 'text',
3820
- text: formatSearchResults(results.slice(0, 15))
3900
+ text: output || formatSearchResults([])
3901
+ }]
3902
+ };
3903
+ }
3904
+
3905
+ case 'magento_find_trigger': {
3906
+ // Search for DB trigger definitions in Setup scripts
3907
+ const triggerTerm = args.triggerName || args.tableName || '';
3908
+ const query = `db_trigger create_trigger sql_trigger TriggerFactory createTrigger setName setEvent ${triggerTerm} setup database_trigger trigger`;
3909
+ const raw = await rustSearchAsync(query, 50);
3910
+ let results = raw.map(normalizeResult).filter(r => {
3911
+ const p = r.path || '';
3912
+ const s = (r.searchText || '') + ' ' + (r.snippet || '');
3913
+ // Must be a Setup file that contains trigger-related terms
3914
+ return (p.includes('/Setup/') || p.includes('InstallSchema') ||
3915
+ p.includes('UpgradeSchema') || p.includes('/Patch/')) &&
3916
+ (s.toLowerCase().includes('trigger') || s.includes('db_trigger') || s.includes('create_trigger'));
3917
+ });
3918
+
3919
+ // If searching for specific trigger/table, filter further
3920
+ if (triggerTerm) {
3921
+ const term = triggerTerm.toLowerCase();
3922
+ const filtered = results.filter(r => {
3923
+ const s = ((r.searchText || '') + ' ' + (r.snippet || '')).toLowerCase();
3924
+ return s.includes(term);
3925
+ });
3926
+ if (filtered.length > 0) results = filtered;
3927
+ }
3928
+
3929
+ results = rerank(results, { pathContains: ['/Setup/', 'UpgradeSchema', 'InstallSchema'] });
3930
+
3931
+ return {
3932
+ content: [{
3933
+ type: 'text',
3934
+ text: results.length > 0
3935
+ ? `### DB Trigger Definitions\n\n` + formatSearchResults(results.slice(0, 15)) +
3936
+ `\n\n**Tip:** Read the matched Setup files to see trigger names, target tables, events (INSERT/UPDATE/DELETE), timing (BEFORE/AFTER), and SQL statements.`
3937
+ : `No database triggers found${triggerTerm ? ` matching "${triggerTerm}"` : ''}. Triggers are defined in Setup scripts using TriggerFactory.`
3938
+ }]
3939
+ };
3940
+ }
3941
+
3942
+ case 'magento_find_table_usage': {
3943
+ const table = args.tableName;
3944
+ // Multi-query strategy: search declarative schema, setup scripts, and inline SQL references
3945
+ const queries = [
3946
+ `table ${table} db_schema.xml declarative schema column`,
3947
+ `sql_table ${table} table_reference ${table} Zend_Db_Expr getTable`,
3948
+ `create_table ${table} legacy_schema setup trigger ${table}`,
3949
+ `${table.replace(/_/g, ' ')} resource model collection`,
3950
+ ];
3951
+
3952
+ const rawResults = await Promise.all(queries.map(q => rustSearchAsync(q, 25)));
3953
+ const allResults = rawResults.flat().map(normalizeResult);
3954
+
3955
+ // Deduplicate by path
3956
+ const pathMap = new Map();
3957
+ for (const r of allResults) {
3958
+ if (!r.path) continue;
3959
+ const s = ((r.searchText || '') + ' ' + (r.snippet || '')).toLowerCase();
3960
+ if (s.includes(table.toLowerCase()) || r.path.includes('db_schema.xml')) {
3961
+ if (!pathMap.has(r.path) || (r.score || 0) > (pathMap.get(r.path).score || 0)) {
3962
+ pathMap.set(r.path, r);
3963
+ }
3964
+ }
3965
+ }
3966
+ const unique = Array.from(pathMap.values());
3967
+
3968
+ // Categorize results
3969
+ const categories = {
3970
+ 'Declarative Schema (db_schema.xml)': unique.filter(r => r.path?.includes('db_schema.xml')),
3971
+ 'Setup Scripts (InstallSchema/UpgradeSchema/Patch)': unique.filter(r => {
3972
+ const p = r.path || '';
3973
+ return !p.includes('db_schema.xml') &&
3974
+ (p.includes('/Setup/') || p.includes('InstallSchema') ||
3975
+ p.includes('UpgradeSchema') || p.includes('/Patch/'));
3976
+ }),
3977
+ 'PHP Code (raw SQL, getTable, Zend_Db_Expr)': unique.filter(r => {
3978
+ const p = r.path || '';
3979
+ return p.endsWith('.php') && !p.includes('db_schema.xml') &&
3980
+ !p.includes('/Setup/') && !p.includes('InstallSchema') &&
3981
+ !p.includes('UpgradeSchema') && !p.includes('/Patch/');
3982
+ }),
3983
+ 'XML Config': unique.filter(r => {
3984
+ const p = r.path || '';
3985
+ return p.endsWith('.xml') && !p.includes('db_schema.xml');
3986
+ }),
3987
+ };
3988
+
3989
+ let output = `### Table Usage: \`${table}\`\n\n`;
3990
+ output += `Found ${unique.length} file(s) referencing this table.\n\n`;
3991
+
3992
+ for (const [category, items] of Object.entries(categories)) {
3993
+ if (items.length > 0) {
3994
+ output += `#### ${category} (${items.length})\n`;
3995
+ output += formatSearchResults(items.slice(0, 8));
3996
+ output += '\n';
3997
+ }
3998
+ }
3999
+
4000
+ if (unique.length === 0) {
4001
+ output += `No references found for table "${table}". Try a broader search with magento_search.`;
4002
+ }
4003
+
4004
+ return {
4005
+ content: [{
4006
+ type: 'text',
4007
+ text: output
3821
4008
  }]
3822
4009
  };
3823
4010
  }