magector 2.5.1 → 2.5.2
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.
- package/package.json +5 -5
- package/src/mcp-server.js +105 -8
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "magector",
|
|
3
|
-
"version": "2.5.
|
|
3
|
+
"version": "2.5.2",
|
|
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.
|
|
37
|
-
"@magector/cli-linux-x64": "2.5.
|
|
38
|
-
"@magector/cli-linux-arm64": "2.5.
|
|
39
|
-
"@magector/cli-win32-x64": "2.5.
|
|
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"
|
|
40
40
|
},
|
|
41
41
|
"keywords": [
|
|
42
42
|
"magento",
|
package/src/mcp-server.js
CHANGED
|
@@ -1004,6 +1004,41 @@ function readMethodSnippet(filePath, methodName, maxLines = 15) {
|
|
|
1004
1004
|
return null;
|
|
1005
1005
|
}
|
|
1006
1006
|
|
|
1007
|
+
/**
|
|
1008
|
+
* Read the FULL method body using brace-counting to find the closing brace.
|
|
1009
|
+
* Unlike readMethodSnippet (fixed N lines), this extracts the complete method
|
|
1010
|
+
* regardless of length, up to a safety limit.
|
|
1011
|
+
* @param {string} filePath - absolute or relative PHP file path
|
|
1012
|
+
* @param {string} methodName - method name to find
|
|
1013
|
+
* @param {number} maxLines - safety limit to prevent reading huge methods (default: 60)
|
|
1014
|
+
* @returns {string|null} complete method source or null if not found
|
|
1015
|
+
*/
|
|
1016
|
+
function readFullMethodBody(filePath, methodName, maxLines = 60) {
|
|
1017
|
+
const absPath = filePath.startsWith('/') ? filePath : path.join(config.magentoRoot, filePath);
|
|
1018
|
+
let content;
|
|
1019
|
+
try { content = readFileSync(absPath, 'utf-8'); } catch { return null; }
|
|
1020
|
+
const lines = content.split('\n');
|
|
1021
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1022
|
+
if (lines[i].includes(`function ${methodName}(`)) {
|
|
1023
|
+
// Use brace counting to find the complete method body
|
|
1024
|
+
let braceCount = 0;
|
|
1025
|
+
let started = false;
|
|
1026
|
+
for (let j = i; j < lines.length && j < i + maxLines; j++) {
|
|
1027
|
+
for (const ch of lines[j]) {
|
|
1028
|
+
if (ch === '{') { braceCount++; started = true; }
|
|
1029
|
+
if (ch === '}') braceCount--;
|
|
1030
|
+
}
|
|
1031
|
+
if (started && braceCount <= 0) {
|
|
1032
|
+
return lines.slice(i, j + 1).join('\n');
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
// Safety: if closing brace not found within maxLines, return what we have
|
|
1036
|
+
return lines.slice(i, Math.min(i + maxLines, lines.length)).join('\n');
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
return null;
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1007
1042
|
/**
|
|
1008
1043
|
* Parse all fieldset.xml files in the Magento root.
|
|
1009
1044
|
* Returns array of { file, scope, fieldset, fields: [{ field, aspect }] }.
|
|
@@ -1568,8 +1603,12 @@ function formatSearchResults(results) {
|
|
|
1568
1603
|
: r.searchText;
|
|
1569
1604
|
}
|
|
1570
1605
|
|
|
1606
|
+
// Full method body — attached by magento_find_method for complete understanding
|
|
1607
|
+
if (r.fullMethodBody) {
|
|
1608
|
+
entry.codePreview = r.fullMethodBody;
|
|
1609
|
+
}
|
|
1571
1610
|
// Code preview — read actual source lines for PHP files with known class/method
|
|
1572
|
-
if (r.path && (r.path.endsWith('.php') || r.path.endsWith('.phtml'))) {
|
|
1611
|
+
else if (r.path && (r.path.endsWith('.php') || r.path.endsWith('.phtml'))) {
|
|
1573
1612
|
if (r.methodName) {
|
|
1574
1613
|
const preview = readMethodSnippet(r.path, r.methodName, 10);
|
|
1575
1614
|
if (preview) entry.codePreview = preview;
|
|
@@ -2553,6 +2592,21 @@ async function findDiWiring(className) {
|
|
|
2553
2592
|
const root = config.magentoRoot;
|
|
2554
2593
|
const shortName = className.split('\\').pop();
|
|
2555
2594
|
const shortLower = shortName.toLowerCase();
|
|
2595
|
+
// When a FQCN is provided (contains \), use it for precise matching
|
|
2596
|
+
const hasFqcn = className.includes('\\');
|
|
2597
|
+
// Normalize FQCN for XML matching: di.xml uses backslash-escaped names
|
|
2598
|
+
const fqcnNormalized = hasFqcn ? className.replace(/\\\\/g, '\\') : null;
|
|
2599
|
+
const fqcnLower = fqcnNormalized ? fqcnNormalized.toLowerCase() : null;
|
|
2600
|
+
|
|
2601
|
+
// Helper: check if a di.xml class name matches the requested class.
|
|
2602
|
+
// When FQCN is available, require full namespace match; otherwise fall back to short name.
|
|
2603
|
+
function matchesClass(xmlClassName) {
|
|
2604
|
+
const xmlLower = xmlClassName.toLowerCase().replace(/\\\\/g, '\\');
|
|
2605
|
+
if (fqcnLower) {
|
|
2606
|
+
return xmlLower === fqcnLower || xmlLower.endsWith('\\' + fqcnLower);
|
|
2607
|
+
}
|
|
2608
|
+
return xmlLower.includes(shortLower);
|
|
2609
|
+
}
|
|
2556
2610
|
|
|
2557
2611
|
const result = {
|
|
2558
2612
|
className,
|
|
@@ -2573,15 +2627,14 @@ async function findDiWiring(className) {
|
|
|
2573
2627
|
const relativePath = diFile.replace(root + '/', '');
|
|
2574
2628
|
const contentLower = content.toLowerCase();
|
|
2575
2629
|
|
|
2630
|
+
// Quick pre-filter: skip files that don't contain the short name at all
|
|
2576
2631
|
if (!contentLower.includes(shortLower)) continue;
|
|
2577
2632
|
|
|
2578
2633
|
// 1. Preferences where this class is the "for" or "type"
|
|
2579
2634
|
const prefRegex = /<preference\s+for="([^"]+)"\s+type="([^"]+)"\s*\/?>/g;
|
|
2580
2635
|
let m;
|
|
2581
2636
|
while ((m = prefRegex.exec(content)) !== null) {
|
|
2582
|
-
|
|
2583
|
-
const typeLower = m[2].toLowerCase();
|
|
2584
|
-
if (forLower.includes(shortLower) || typeLower.includes(shortLower)) {
|
|
2637
|
+
if (matchesClass(m[1]) || matchesClass(m[2])) {
|
|
2585
2638
|
result.preferences.push({ for: m[1], type: m[2], file: relativePath });
|
|
2586
2639
|
}
|
|
2587
2640
|
}
|
|
@@ -2591,10 +2644,9 @@ async function findDiWiring(className) {
|
|
|
2591
2644
|
let typeMatch;
|
|
2592
2645
|
while ((typeMatch = typeBlockRegex.exec(content)) !== null) {
|
|
2593
2646
|
const typeName = typeMatch[1];
|
|
2594
|
-
const typeNameLower = typeName.toLowerCase();
|
|
2595
2647
|
const typeBlock = typeMatch[2];
|
|
2596
2648
|
|
|
2597
|
-
if (!
|
|
2649
|
+
if (!matchesClass(typeName)) continue;
|
|
2598
2650
|
|
|
2599
2651
|
// Plugins
|
|
2600
2652
|
const pluginRegex = /<plugin\s+name="([^"]+)"[^>]*type="([^"]+)"[^>]*/g;
|
|
@@ -2641,7 +2693,7 @@ async function findDiWiring(className) {
|
|
|
2641
2693
|
const vtRegex = /<virtualType\s+name="([^"]+)"[^>]*type="([^"]+)"[^>]*(?:\/>|>([\s\S]*?)<\/virtualType>)/g;
|
|
2642
2694
|
while ((m = vtRegex.exec(content)) !== null) {
|
|
2643
2695
|
const vtType = m[2];
|
|
2644
|
-
if (!vtType
|
|
2696
|
+
if (!matchesClass(vtType)) continue;
|
|
2645
2697
|
const vtEntry = { name: m[1], type: vtType, file: relativePath };
|
|
2646
2698
|
if (m[3]) {
|
|
2647
2699
|
const argRegex = /<argument\s+name="([^"]+)"[^>]*xsi:type="([^"]+)"[^>]*>([^<]*)<\/argument>/g;
|
|
@@ -2657,6 +2709,7 @@ async function findDiWiring(className) {
|
|
|
2657
2709
|
}
|
|
2658
2710
|
|
|
2659
2711
|
// 4. Constructor arguments from PHP class
|
|
2712
|
+
// When FQCN is provided, verify namespace matches to avoid class name collisions
|
|
2660
2713
|
const phpFiles = await glob(`**/${shortName}.php`, { cwd: root, absolute: true, nodir: true });
|
|
2661
2714
|
for (const phpFile of phpFiles) {
|
|
2662
2715
|
let content;
|
|
@@ -2664,6 +2717,17 @@ async function findDiWiring(className) {
|
|
|
2664
2717
|
const classMatch = content.match(/(?:class|abstract\s+class)\s+(\w+)/);
|
|
2665
2718
|
if (!classMatch || classMatch[1] !== shortName) continue;
|
|
2666
2719
|
|
|
2720
|
+
// FQCN namespace verification: when a full class name was provided,
|
|
2721
|
+
// check that the PHP file's namespace matches to avoid collisions
|
|
2722
|
+
// (e.g., two different ViewPlugin classes in different modules)
|
|
2723
|
+
if (fqcnNormalized) {
|
|
2724
|
+
const nsMatch = content.match(/namespace\s+([\w\\]+)\s*;/);
|
|
2725
|
+
if (nsMatch) {
|
|
2726
|
+
const fileFqcn = (nsMatch[1] + '\\' + shortName).toLowerCase();
|
|
2727
|
+
if (fileFqcn !== fqcnLower) continue; // Wrong class, skip
|
|
2728
|
+
}
|
|
2729
|
+
}
|
|
2730
|
+
|
|
2667
2731
|
const ctorMatch = content.match(/function\s+__construct\s*\(([\s\S]*?)\)\s*[{:]/);
|
|
2668
2732
|
if (ctorMatch) {
|
|
2669
2733
|
const paramRegex = /(?:([\w\\]+)\s+)?(\$\w+)/g;
|
|
@@ -2672,6 +2736,7 @@ async function findDiWiring(className) {
|
|
|
2672
2736
|
result.constructorArguments.push({ typeHint: pm[1] || null, variable: pm[2] });
|
|
2673
2737
|
}
|
|
2674
2738
|
}
|
|
2739
|
+
result.constructorSourceFile = phpFile.replace(root + '/', '');
|
|
2675
2740
|
break;
|
|
2676
2741
|
}
|
|
2677
2742
|
|
|
@@ -3992,10 +4057,18 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
3992
4057
|
if (r.methodName?.toLowerCase() === methodLower) bonus += 0.3;
|
|
3993
4058
|
return { ...r, score: (r.score || 0) + bonus };
|
|
3994
4059
|
}).sort((a, b) => b.score - a.score);
|
|
4060
|
+
// Attach full method body to each result for complete understanding
|
|
4061
|
+
const sliced = results.slice(0, 10);
|
|
4062
|
+
for (const r of sliced) {
|
|
4063
|
+
if (r.path && r.path.endsWith('.php')) {
|
|
4064
|
+
const body = readFullMethodBody(r.path, args.methodName);
|
|
4065
|
+
if (body) r.fullMethodBody = body;
|
|
4066
|
+
}
|
|
4067
|
+
}
|
|
3995
4068
|
return {
|
|
3996
4069
|
content: [{
|
|
3997
4070
|
type: 'text',
|
|
3998
|
-
text: formatSearchResults(
|
|
4071
|
+
text: formatSearchResults(sliced)
|
|
3999
4072
|
}]
|
|
4000
4073
|
};
|
|
4001
4074
|
}
|
|
@@ -5394,6 +5467,30 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
5394
5467
|
for (const o of flow.observers.slice(0, 10)) text += `- ${o.name}: ${o.instance} (${o.file})\n`;
|
|
5395
5468
|
break;
|
|
5396
5469
|
}
|
|
5470
|
+
case 'magento_find_di_wiring': {
|
|
5471
|
+
const wiring = await findDiWiring(a.className);
|
|
5472
|
+
text = `Prefs: ${wiring.preferences.length}, Plugins: ${wiring.plugins.length}, VTs: ${wiring.virtualTypes.length}, Ctor: ${wiring.constructorArguments.length}\n`;
|
|
5473
|
+
for (const p of wiring.plugins.slice(0, 5)) text += `- plugin: ${p.pluginName} → ${p.pluginClass}\n`;
|
|
5474
|
+
for (const c of wiring.constructorArguments.slice(0, 10)) text += `- ctor: ${c.typeHint || '?'} ${c.variable}\n`;
|
|
5475
|
+
if (wiring.constructorSourceFile) text += `Source: ${wiring.constructorSourceFile}\n`;
|
|
5476
|
+
break;
|
|
5477
|
+
}
|
|
5478
|
+
case 'magento_find_method': {
|
|
5479
|
+
const qr = `method ${a.methodName} function ${a.className || ''}`.trim();
|
|
5480
|
+
const raw = await rustSearchAsync(qr, 50);
|
|
5481
|
+
const ml = a.methodName.toLowerCase();
|
|
5482
|
+
let res = raw.map(normalizeResult).filter(r =>
|
|
5483
|
+
r.methodName?.toLowerCase() === ml || r.methods?.some(m => m.toLowerCase() === ml)
|
|
5484
|
+
);
|
|
5485
|
+
for (const r of res.slice(0, 5)) {
|
|
5486
|
+
if (r.path?.endsWith('.php')) {
|
|
5487
|
+
const body = readFullMethodBody(r.path, a.methodName);
|
|
5488
|
+
if (body) r.fullMethodBody = body;
|
|
5489
|
+
}
|
|
5490
|
+
}
|
|
5491
|
+
text = formatSearchResults(res.slice(0, 5));
|
|
5492
|
+
break;
|
|
5493
|
+
}
|
|
5397
5494
|
default:
|
|
5398
5495
|
text = `Unsupported batch tool: ${q.tool}`;
|
|
5399
5496
|
}
|