magector 2.9.0 → 2.10.0

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 +156 -6
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "magector",
3
- "version": "2.9.0",
3
+ "version": "2.10.0",
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.9.0",
37
- "@magector/cli-linux-x64": "2.9.0",
38
- "@magector/cli-linux-arm64": "2.9.0",
39
- "@magector/cli-win32-x64": "2.9.0"
36
+ "@magector/cli-darwin-arm64": "2.10.0",
37
+ "@magector/cli-linux-x64": "2.10.0",
38
+ "@magector/cli-linux-arm64": "2.10.0",
39
+ "@magector/cli-win32-x64": "2.10.0"
40
40
  },
41
41
  "keywords": [
42
42
  "magento",
package/src/mcp-server.js CHANGED
@@ -1855,6 +1855,83 @@ const ERROR_PATTERNS = [
1855
1855
  type: 'invalid_plugin_method',
1856
1856
  extract: (m) => ({ class: m[1], type: m[2], method: m[3] }),
1857
1857
  suggestion: (ctx) => `Plugin method ${ctx.type}${ctx.method} in ${ctx.class} doesn't match any public method on the target class. Check method name spelling.`
1858
+ },
1859
+ // ── Business logic / configuration patterns ──
1860
+ {
1861
+ pattern: /rule\s+(?:did\s+)?not\s+(?:match|apply|work)|(?:sales|cart|price)\s*rule.*(?:not\s+applied|didn'?t\s+match|failed)/i,
1862
+ type: 'rule_not_matching',
1863
+ extract: () => ({}),
1864
+ suggestion: () => [
1865
+ 'Sales/cart price rule not matching. Check BOTH code AND configuration:',
1866
+ '',
1867
+ '**Configuration checks (most common cause):**',
1868
+ '- Rule Actions tab: are ALL required shipping methods selected?',
1869
+ '- Rule Conditions: does the cart actually meet ALL conditions (customer group, subtotal threshold, date range)?',
1870
+ '- Rule status: is it Active? Check date range (from/to).',
1871
+ '- Rule priority/stop processing: is a higher-priority rule stopping this one?',
1872
+ '- Coupon: if coupon-based, was the correct coupon applied?',
1873
+ '- Website scope: is the rule assigned to the correct website?',
1874
+ '',
1875
+ '**Code checks (less common):**',
1876
+ '- Custom condition types: check classes extending AbstractCondition in vendor/',
1877
+ '- Plugins on Magento\\SalesRule\\Model\\Utility::canProcessRule() that may skip rules',
1878
+ '- Plugins on Magento\\SalesRule\\Model\\Rule\\Condition\\Address::validate()',
1879
+ '- DI preference overrides on Condition\\Address (e.g., marketplace container conditions)',
1880
+ '- The condition_type stored in conditions_serialized — does the PHP class exist and validate correctly?',
1881
+ '',
1882
+ 'Use magento_grep to find: custom conditions, plugins on validate/canProcessRule, and DI preferences on Address.',
1883
+ '',
1884
+ '**Ask the user for DB data:**',
1885
+ 'SELECT rule_id, name, conditions_serialized, actions_serialized, is_active, from_date, to_date',
1886
+ 'FROM salesrule WHERE rule_id = <ID>;',
1887
+ 'This lets you verify the exact condition_type classes and action configuration without guessing.'
1888
+ ].join('\n')
1889
+ },
1890
+ {
1891
+ pattern: /free\s*shipping.*(?:not|didn'?t|did\s+not)\s+(?:apply|work|match)|(?:not|didn'?t)\s+(?:get|receive)\s+free\s*shipping/i,
1892
+ type: 'free_shipping_not_applied',
1893
+ extract: () => ({}),
1894
+ suggestion: () => [
1895
+ 'Free shipping not applied. Most common causes:',
1896
+ '',
1897
+ '**1. Rule Actions — shipping method not selected (MOST COMMON)**',
1898
+ ' The free shipping rule must have the specific shipping method checked in Actions.',
1899
+ ' If "home delivery" is not selected but the customer chose home delivery → rule won\'t apply.',
1900
+ '',
1901
+ '**2. Condition threshold mismatch**',
1902
+ ' Check which subtotal attribute the condition uses: base_subtotal, subtotal_incl_tax,',
1903
+ ' drmax_free_shipping_price (custom). Each calculates differently (with/without tax, discounts).',
1904
+ '',
1905
+ '**3. Custom condition type**',
1906
+ ' Container attributes (SubtotalWithDiscountInclTax) aggregate per shop type.',
1907
+ ' If condition uses :1p suffix instead of :whole, only 1P items count.',
1908
+ '',
1909
+ '**4. Plugin interference**',
1910
+ ' Check plugins on Utility::canProcessRule() and Carrier::collectRates().',
1911
+ '',
1912
+ 'Start with: check the rule\'s Actions tab in admin for shipping method selection.',
1913
+ '',
1914
+ '**Ask the user for DB data:**',
1915
+ 'SELECT rule_id, name, conditions_serialized, actions_serialized, simple_free_shipping',
1916
+ 'FROM salesrule WHERE rule_id = <ID>;',
1917
+ 'The actions_serialized will show which shipping methods are enabled for free shipping.'
1918
+ ].join('\n')
1919
+ },
1920
+ {
1921
+ pattern: /(?:condition|rule|promotion|discount).*(?:custom|type).*(?:not|fail|wrong|weird|unexpected)/i,
1922
+ type: 'custom_condition_issue',
1923
+ extract: () => ({}),
1924
+ suggestion: () => [
1925
+ 'Possible custom condition type issue. Investigate:',
1926
+ '',
1927
+ '**1. Check the rule configuration FIRST** — most "condition not working" bugs are misconfiguration.',
1928
+ '**2. Find custom condition classes:** grep for "extends AbstractCondition" in vendor/',
1929
+ '**3. Check DI preference on Condition\\Address** — marketplace modules often override validate().',
1930
+ '**4. Check if new attributes are registered in the Address override\'s switch/mapping.**',
1931
+ '**5. Verify condition_type in DB:** the serialized condition must reference an existing PHP class.',
1932
+ '',
1933
+ 'If code analysis finds no bugs, the root cause is likely rule configuration in admin panel.'
1934
+ ].join('\n')
1858
1935
  }
1859
1936
  ];
1860
1937
 
@@ -4066,13 +4143,35 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
4066
4143
  },
4067
4144
  context: {
4068
4145
  type: 'number',
4069
- description: 'Lines of context around each match (default: 0). Like grep -C.',
4070
- default: 0
4146
+ description: 'Lines of context around each match (default: 2). Like grep -C.',
4147
+ default: 2
4071
4148
  }
4072
4149
  },
4073
4150
  required: ['pattern']
4074
4151
  }
4075
4152
  },
4153
+ {
4154
+ name: 'magento_read',
4155
+ description: 'Read a file from the Magento codebase. Use in magento_batch to read multiple files in a single MCP call (e.g., grep finds 5 files → read all 5 in one batch). Supports line ranges for large files.',
4156
+ inputSchema: {
4157
+ type: 'object',
4158
+ properties: {
4159
+ path: {
4160
+ type: 'string',
4161
+ description: 'File path relative to MAGENTO_ROOT. Example: "vendor/acme/module-sales/Model/OrderService.php"'
4162
+ },
4163
+ startLine: {
4164
+ type: 'number',
4165
+ description: 'Start reading from this line number (1-based). Default: 1 (beginning of file).'
4166
+ },
4167
+ endLine: {
4168
+ type: 'number',
4169
+ description: 'Stop reading at this line number (inclusive). Default: end of file. Use with startLine for large files.'
4170
+ }
4171
+ },
4172
+ required: ['path']
4173
+ }
4174
+ },
4076
4175
  ]
4077
4176
  }));
4078
4177
 
@@ -4091,7 +4190,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
4091
4190
  // These tools have filesystem/di.xml fallbacks — work without serve process
4092
4191
  'magento_find_class', 'magento_find_method', 'magento_find_plugin',
4093
4192
  'magento_find_observer', 'magento_find_di_wiring', 'magento_module_structure',
4094
- 'magento_batch', 'magento_find_config', 'magento_find_callers', 'magento_grep'];
4193
+ 'magento_batch', 'magento_find_config', 'magento_find_callers', 'magento_grep', 'magento_read'];
4095
4194
  if (warmupInProgress && !indexFreeTools.includes(name)) {
4096
4195
  logToFile('REQ', `${name} → blocked (warmup: loading index)`);
4097
4196
  return {
@@ -4892,7 +4991,22 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
4892
4991
  structure: structureOutput.categories
4893
4992
  });
4894
4993
 
4895
- return { content: [{ type: 'text', text: jsonOutput }] };
4994
+ // Include README.md if it exists in the module directory
4995
+ let readmeText = '';
4996
+ if (results.length > 0) {
4997
+ // Find module root from first result path
4998
+ const firstPath = results[0].path || '';
4999
+ const moduleRoot = firstPath.split('/').slice(0, 3).join('/');
5000
+ if (moduleRoot) {
5001
+ const readmePath = path.join(config.magentoRoot, moduleRoot, 'README.md');
5002
+ try {
5003
+ const readme = readFileSync(readmePath, 'utf-8');
5004
+ readmeText = '\n\n## README.md\n\n' + readme.slice(0, 2000) + (readme.length > 2000 ? '\n...(truncated)' : '');
5005
+ } catch { /* no README */ }
5006
+ }
5007
+ }
5008
+
5009
+ return { content: [{ type: 'text', text: jsonOutput + readmeText }] };
4896
5010
  }
4897
5011
 
4898
5012
  case 'magento_analyze_diff': {
@@ -5906,13 +6020,28 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
5906
6020
  }
5907
6021
  break;
5908
6022
  }
6023
+ case 'magento_read': {
6024
+ const filePath = path.join(config.magentoRoot, a.path);
6025
+ let fileContent;
6026
+ try { fileContent = readFileSync(filePath, 'utf-8'); } catch {
6027
+ text = `File not found: ${a.path}`;
6028
+ break;
6029
+ }
6030
+ const allLines = fileContent.split('\n');
6031
+ const s = Math.max((a.startLine || 1) - 1, 0);
6032
+ const e = a.endLine ? Math.min(a.endLine, allLines.length) : allLines.length;
6033
+ const sl = allLines.slice(s, e);
6034
+ text = sl.map((line, i) => `${s + i + 1}\t${line}`).join('\n');
6035
+ break;
6036
+ }
5909
6037
  case 'magento_grep': {
5910
6038
  const searchPath = a.path || '.';
5911
6039
  const include = a.include || '*.php';
5912
6040
  const maxRes = Math.min(a.maxResults || 30, 100);
6041
+ const batchCtx = a.context !== undefined ? a.context : 2;
5913
6042
  const gArgs = ['-rn'];
5914
6043
  if (a.ignoreCase) gArgs.push('-i');
5915
- if (a.context) gArgs.push('-C', String(a.context));
6044
+ if (batchCtx > 0) gArgs.push('-C', String(batchCtx));
5916
6045
  for (const pat of include.split(',').map(p => p.trim())) gArgs.push('--include=' + pat);
5917
6046
  gArgs.push('--', a.pattern, searchPath);
5918
6047
  let out;
@@ -5946,9 +6075,10 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
5946
6075
  const searchPath = args.path || '.';
5947
6076
  const include = args.include || '*.php';
5948
6077
  const maxResults = Math.min(args.maxResults || 50, 200);
6078
+ const ctxLines = args.context !== undefined ? args.context : 2;
5949
6079
  const grepArgs = ['-rn'];
5950
6080
  if (args.ignoreCase) grepArgs.push('-i');
5951
- if (args.context) grepArgs.push('-C', String(args.context));
6081
+ if (ctxLines > 0) grepArgs.push('-C', String(ctxLines));
5952
6082
  // Support multiple include patterns (e.g., "*.{php,xml}")
5953
6083
  for (const pat of include.split(',').map(p => p.trim())) {
5954
6084
  grepArgs.push('--include=' + pat);
@@ -5976,6 +6106,26 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
5976
6106
  return { content: [{ type: 'text', text }] };
5977
6107
  }
5978
6108
 
6109
+ case 'magento_read': {
6110
+ const root = config.magentoRoot;
6111
+ if (!root) return { content: [{ type: 'text', text: 'MAGENTO_ROOT not set.' }], isError: true };
6112
+ const filePath = path.join(root, args.path);
6113
+ let content;
6114
+ try { content = readFileSync(filePath, 'utf-8'); } catch (err) {
6115
+ return { content: [{ type: 'text', text: `File not found: ${args.path}` }], isError: true };
6116
+ }
6117
+ const allLines = content.split('\n');
6118
+ const start = Math.max((args.startLine || 1) - 1, 0);
6119
+ const end = args.endLine ? Math.min(args.endLine, allLines.length) : allLines.length;
6120
+ const sliced = allLines.slice(start, end);
6121
+ // Format with line numbers
6122
+ const numbered = sliced.map((line, i) => `${start + i + 1}\t${line}`).join('\n');
6123
+ let text = `## ${args.path}`;
6124
+ if (args.startLine || args.endLine) text += ` (lines ${start + 1}-${end})`;
6125
+ text += `\n\n${numbered}`;
6126
+ return { content: [{ type: 'text', text }] };
6127
+ }
6128
+
5979
6129
  default:
5980
6130
  return {
5981
6131
  content: [{