magector 2.5.2 → 2.6.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 +71 -29
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "magector",
3
- "version": "2.5.2",
3
+ "version": "2.6.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.5.2",
37
- "@magector/cli-linux-x64": "2.5.2",
38
- "@magector/cli-linux-arm64": "2.5.2",
39
- "@magector/cli-win32-x64": "2.5.2"
36
+ "@magector/cli-darwin-arm64": "2.6.0",
37
+ "@magector/cli-linux-x64": "2.6.0",
38
+ "@magector/cli-linux-arm64": "2.6.0",
39
+ "@magector/cli-win32-x64": "2.6.0"
40
40
  },
41
41
  "keywords": [
42
42
  "magento",
package/src/mcp-server.js CHANGED
@@ -1596,8 +1596,12 @@ function formatSearchResults(results) {
1596
1596
  if (r.isBlock) badges.push('block');
1597
1597
  if (badges.length > 0) entry.badges = badges;
1598
1598
 
1599
+ // Only include verbose content (snippet, codePreview) for top-ranked results
1600
+ // to reduce token consumption — lower-ranked results just show metadata
1601
+ const isTopRanked = i < 3;
1602
+
1599
1603
  // Snippet — first 300 chars of indexed content for quick assessment
1600
- if (r.searchText) {
1604
+ if (isTopRanked && r.searchText) {
1601
1605
  entry.snippet = r.searchText.length > 300
1602
1606
  ? r.searchText.slice(0, 300) + '...'
1603
1607
  : r.searchText;
@@ -1608,7 +1612,7 @@ function formatSearchResults(results) {
1608
1612
  entry.codePreview = r.fullMethodBody;
1609
1613
  }
1610
1614
  // Code preview — read actual source lines for PHP files with known class/method
1611
- else if (r.path && (r.path.endsWith('.php') || r.path.endsWith('.phtml'))) {
1615
+ else if (isTopRanked && r.path && (r.path.endsWith('.php') || r.path.endsWith('.phtml'))) {
1612
1616
  if (r.methodName) {
1613
1617
  const preview = readMethodSnippet(r.path, r.methodName, 10);
1614
1618
  if (preview) entry.codePreview = preview;
@@ -1639,6 +1643,47 @@ function formatSearchResults(results) {
1639
1643
  return JSON.stringify({ results: formatted, count: formatted.length });
1640
1644
  }
1641
1645
 
1646
+ // ─── DI XML Session Cache ─────────────────────────────────────
1647
+
1648
+ /**
1649
+ * Session-level cache for parsed di.xml file contents.
1650
+ * Avoids re-reading and re-globbing di.xml files across multiple tool calls
1651
+ * (findDiWiring, traceDependency, magento_find_plugin all scan di.xml).
1652
+ */
1653
+ const diXmlCache = {
1654
+ /** @type {Map<string, string>} path → file content */
1655
+ files: new Map(),
1656
+ /** @type {string[]|null} cached list of all di.xml absolute paths */
1657
+ paths: null,
1658
+ /** @type {string|null} root used for caching (invalidate if root changes) */
1659
+ root: null
1660
+ };
1661
+
1662
+ /**
1663
+ * Get all di.xml file paths and their contents, using session cache.
1664
+ * @param {string} root - Magento root path
1665
+ * @returns {Promise<Array<{absPath: string, relPath: string, content: string}>>}
1666
+ */
1667
+ async function getDiXmlFiles(root) {
1668
+ if (diXmlCache.root !== root || !diXmlCache.paths) {
1669
+ diXmlCache.root = root;
1670
+ diXmlCache.paths = await glob('**/etc/**/di.xml', { cwd: root, absolute: true, nodir: true });
1671
+ diXmlCache.files.clear();
1672
+ }
1673
+ const results = [];
1674
+ for (const absPath of diXmlCache.paths) {
1675
+ let content = diXmlCache.files.get(absPath);
1676
+ if (content === undefined) {
1677
+ try { content = readFileSync(absPath, 'utf-8'); } catch { content = null; }
1678
+ diXmlCache.files.set(absPath, content);
1679
+ }
1680
+ if (content !== null) {
1681
+ results.push({ absPath, relPath: absPath.replace(root + '/', ''), content });
1682
+ }
1683
+ }
1684
+ return results;
1685
+ }
1686
+
1642
1687
  // ─── DI Dependency Tracing ─────────────────────────────────────
1643
1688
 
1644
1689
  /**
@@ -1647,7 +1692,7 @@ function formatSearchResults(results) {
1647
1692
  */
1648
1693
  async function traceDependency(className, direction = 'both') {
1649
1694
  const root = config.magentoRoot;
1650
- const diFiles = await glob('**/etc/**/di.xml', { cwd: root, absolute: true, nodir: true });
1695
+ const diFiles = await getDiXmlFiles(root);
1651
1696
  const classLower = className.toLowerCase();
1652
1697
  const classShort = className.split('\\').pop().toLowerCase();
1653
1698
 
@@ -1660,13 +1705,7 @@ async function traceDependency(className, direction = 'both') {
1660
1705
  totalDiFiles: diFiles.length
1661
1706
  };
1662
1707
 
1663
- for (const diFile of diFiles) {
1664
- let content;
1665
- try {
1666
- content = readFileSync(diFile, 'utf-8');
1667
- } catch { continue; }
1668
-
1669
- const relativePath = diFile.replace(root + '/', '');
1708
+ for (const { content, relPath: relativePath } of diFiles) {
1670
1709
 
1671
1710
  if (direction === 'resolve' || direction === 'both') {
1672
1711
  // Find preferences: <preference for="ClassName" type="Implementation"/>
@@ -2618,13 +2657,10 @@ async function findDiWiring(className) {
2618
2657
  totalDiFiles: 0
2619
2658
  };
2620
2659
 
2621
- const diFiles = await glob('**/etc/**/di.xml', { cwd: root, absolute: true, nodir: true });
2660
+ const diFiles = await getDiXmlFiles(root);
2622
2661
  result.totalDiFiles = diFiles.length;
2623
2662
 
2624
- for (const diFile of diFiles) {
2625
- let content;
2626
- try { content = readFileSync(diFile, 'utf-8'); } catch { continue; }
2627
- const relativePath = diFile.replace(root + '/', '');
2663
+ for (const { content, relPath: relativePath } of diFiles) {
2628
2664
  const contentLower = content.toLowerCase();
2629
2665
 
2630
2666
  // Quick pre-filter: skip files that don't contain the short name at all
@@ -4011,7 +4047,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
4011
4047
  return {
4012
4048
  content: [{
4013
4049
  type: 'text',
4014
- text: formatSearchResults(results.slice(0, args.limit || 10))
4050
+ text: formatSearchResults(results.slice(0, args.limit || 5))
4015
4051
  }]
4016
4052
  };
4017
4053
  }
@@ -4028,7 +4064,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
4028
4064
  return {
4029
4065
  content: [{
4030
4066
  type: 'text',
4031
- text: formatSearchResults(results.slice(0, 5))
4067
+ text: formatSearchResults(results.slice(0, 3))
4032
4068
  }]
4033
4069
  };
4034
4070
  }
@@ -4058,7 +4094,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
4058
4094
  return { ...r, score: (r.score || 0) + bonus };
4059
4095
  }).sort((a, b) => b.score - a.score);
4060
4096
  // Attach full method body to each result for complete understanding
4061
- const sliced = results.slice(0, 10);
4097
+ const sliced = results.slice(0, 5);
4062
4098
  for (const r of sliced) {
4063
4099
  if (r.path && r.path.endsWith('.php')) {
4064
4100
  const body = readFullMethodBody(r.path, args.methodName);
@@ -4166,18 +4202,15 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
4166
4202
  return { ...r, diArea };
4167
4203
  });
4168
4204
 
4169
- // If targetClass provided, also scan di.xml for explicit registrations
4205
+ // If targetClass provided, also scan di.xml for explicit registrations (using session cache)
4170
4206
  let diRegistrations = [];
4171
4207
  if (args.targetClass) {
4172
4208
  const fpRoot = config.magentoRoot;
4173
- const diFiles = await glob('**/etc/**/di.xml', { cwd: fpRoot, absolute: true, nodir: true });
4209
+ const diFiles = await getDiXmlFiles(fpRoot);
4174
4210
  // Normalize target class for matching (both \ and \\)
4175
4211
  const normalizedTarget = args.targetClass.replace(/\\\\/g, '\\');
4176
- for (const diFile of diFiles) {
4177
- let content;
4178
- try { content = readFileSync(diFile, 'utf-8'); } catch { continue; }
4212
+ for (const { content, relPath } of diFiles) {
4179
4213
  if (!content.includes(normalizedTarget)) continue;
4180
- const relPath = diFile.replace(fpRoot + '/', '');
4181
4214
  // Find plugin registrations for this target
4182
4215
  const typeBlockRegex = /<type\s+name="([^"]+)"[^>]*>([\s\S]*?)<\/type>/g;
4183
4216
  let tm;
@@ -4212,14 +4245,19 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
4212
4245
  }
4213
4246
  }
4214
4247
 
4215
- // Resolve plugin methods for DI registrations
4216
- const fpRoot = args.targetClass ? config.magentoRoot : null;
4248
+ // Resolve plugin methods + method bodies for DI registrations
4249
+ const fpRoot2 = args.targetClass ? config.magentoRoot : null;
4217
4250
  for (const reg of diRegistrations) {
4218
- if (reg.pluginClass && fpRoot) {
4219
- const pluginFile = findClassFile(fpRoot, reg.pluginClass);
4251
+ if (reg.pluginClass && fpRoot2) {
4252
+ const pluginFile = findClassFile(fpRoot2, reg.pluginClass);
4220
4253
  if (pluginFile) {
4221
4254
  reg.methods = extractPluginMethods(pluginFile);
4222
- reg.resolvedFile = pluginFile.replace(fpRoot + '/', '');
4255
+ reg.resolvedFile = pluginFile.replace(fpRoot2 + '/', '');
4256
+ // Read full method bodies so the agent sees actual code without follow-up calls
4257
+ for (const m of reg.methods) {
4258
+ const body = readFullMethodBody(pluginFile, m.name);
4259
+ if (body) m.body = body;
4260
+ }
4223
4261
  }
4224
4262
  }
4225
4263
  }
@@ -4237,6 +4275,10 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
4237
4275
  if (reg.methods?.length > 0) {
4238
4276
  for (const m of reg.methods) {
4239
4277
  text += ` - \`${m.type}\` **${m.targetMethod}** → \`${m.name}()\`\n`;
4278
+ if (m.body) {
4279
+ const indentedBody = m.body.split('\n').join('\n ');
4280
+ text += ' ' + '```php\n ' + indentedBody + '\n ' + '```\n';
4281
+ }
4240
4282
  }
4241
4283
  }
4242
4284
  }