magector 2.5.0 → 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 +359 -22
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;
|
|
@@ -2176,6 +2215,7 @@ async function analyzeImpact(className) {
|
|
|
2176
2215
|
diXmlReferences: [],
|
|
2177
2216
|
instantiations: [],
|
|
2178
2217
|
typeHints: [],
|
|
2218
|
+
runtimeCallers: [],
|
|
2179
2219
|
total: 0
|
|
2180
2220
|
};
|
|
2181
2221
|
|
|
@@ -2194,6 +2234,7 @@ async function analyzeImpact(className) {
|
|
|
2194
2234
|
];
|
|
2195
2235
|
|
|
2196
2236
|
// Check PHP files for direct references
|
|
2237
|
+
const escapedShort = shortName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
2197
2238
|
for (const r of relatedPaths.slice(0, 40)) {
|
|
2198
2239
|
const absPath = path.join(root, r.path);
|
|
2199
2240
|
if (!existsSync(absPath) || !r.path.endsWith('.php')) continue;
|
|
@@ -2208,13 +2249,38 @@ async function analyzeImpact(className) {
|
|
|
2208
2249
|
references.instantiations.push({ file: r.path, className: r.className });
|
|
2209
2250
|
}
|
|
2210
2251
|
if (content.includes(`@var ${shortName}`) || content.includes(`@param ${shortName}`) ||
|
|
2211
|
-
content.match(new RegExp(`:\\s*${
|
|
2252
|
+
content.match(new RegExp(`:\\s*${escapedShort}\\b`))) {
|
|
2212
2253
|
references.typeHints.push({ file: r.path, className: r.className });
|
|
2213
2254
|
}
|
|
2255
|
+
|
|
2256
|
+
// Runtime callers: find $this->property->method() where property is typed as this class
|
|
2257
|
+
if (hasUse) {
|
|
2258
|
+
const ctorMatch = content.match(/function\s+__construct\s*\(([\s\S]*?)\)\s*[{:]/);
|
|
2259
|
+
if (ctorMatch) {
|
|
2260
|
+
// Find constructor params typed as the target class
|
|
2261
|
+
const paramRegex = new RegExp(`(?:${escapedShort}|${className.replace(/\\/g, '\\\\')})\\s+\\$(\\w+)`, 'g');
|
|
2262
|
+
let pm;
|
|
2263
|
+
while ((pm = paramRegex.exec(ctorMatch[1])) !== null) {
|
|
2264
|
+
const propName = pm[1];
|
|
2265
|
+
// Find all calls to this property in the file
|
|
2266
|
+
const callRegex = new RegExp(`\\$this->${propName}->(\\w+)\\s*\\(`, 'g');
|
|
2267
|
+
let cm;
|
|
2268
|
+
while ((cm = callRegex.exec(content)) !== null) {
|
|
2269
|
+
references.runtimeCallers.push({
|
|
2270
|
+
file: r.path,
|
|
2271
|
+
callerClass: r.className,
|
|
2272
|
+
property: propName,
|
|
2273
|
+
calledMethod: cm[1]
|
|
2274
|
+
});
|
|
2275
|
+
}
|
|
2276
|
+
}
|
|
2277
|
+
}
|
|
2278
|
+
}
|
|
2214
2279
|
}
|
|
2215
2280
|
|
|
2216
2281
|
references.total = references.useStatements.length + references.diXmlReferences.length +
|
|
2217
|
-
references.instantiations.length + references.typeHints.length
|
|
2282
|
+
references.instantiations.length + references.typeHints.length +
|
|
2283
|
+
references.runtimeCallers.length;
|
|
2218
2284
|
|
|
2219
2285
|
return references;
|
|
2220
2286
|
}
|
|
@@ -2526,6 +2592,21 @@ async function findDiWiring(className) {
|
|
|
2526
2592
|
const root = config.magentoRoot;
|
|
2527
2593
|
const shortName = className.split('\\').pop();
|
|
2528
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
|
+
}
|
|
2529
2610
|
|
|
2530
2611
|
const result = {
|
|
2531
2612
|
className,
|
|
@@ -2546,15 +2627,14 @@ async function findDiWiring(className) {
|
|
|
2546
2627
|
const relativePath = diFile.replace(root + '/', '');
|
|
2547
2628
|
const contentLower = content.toLowerCase();
|
|
2548
2629
|
|
|
2630
|
+
// Quick pre-filter: skip files that don't contain the short name at all
|
|
2549
2631
|
if (!contentLower.includes(shortLower)) continue;
|
|
2550
2632
|
|
|
2551
2633
|
// 1. Preferences where this class is the "for" or "type"
|
|
2552
2634
|
const prefRegex = /<preference\s+for="([^"]+)"\s+type="([^"]+)"\s*\/?>/g;
|
|
2553
2635
|
let m;
|
|
2554
2636
|
while ((m = prefRegex.exec(content)) !== null) {
|
|
2555
|
-
|
|
2556
|
-
const typeLower = m[2].toLowerCase();
|
|
2557
|
-
if (forLower.includes(shortLower) || typeLower.includes(shortLower)) {
|
|
2637
|
+
if (matchesClass(m[1]) || matchesClass(m[2])) {
|
|
2558
2638
|
result.preferences.push({ for: m[1], type: m[2], file: relativePath });
|
|
2559
2639
|
}
|
|
2560
2640
|
}
|
|
@@ -2564,10 +2644,9 @@ async function findDiWiring(className) {
|
|
|
2564
2644
|
let typeMatch;
|
|
2565
2645
|
while ((typeMatch = typeBlockRegex.exec(content)) !== null) {
|
|
2566
2646
|
const typeName = typeMatch[1];
|
|
2567
|
-
const typeNameLower = typeName.toLowerCase();
|
|
2568
2647
|
const typeBlock = typeMatch[2];
|
|
2569
2648
|
|
|
2570
|
-
if (!
|
|
2649
|
+
if (!matchesClass(typeName)) continue;
|
|
2571
2650
|
|
|
2572
2651
|
// Plugins
|
|
2573
2652
|
const pluginRegex = /<plugin\s+name="([^"]+)"[^>]*type="([^"]+)"[^>]*/g;
|
|
@@ -2614,7 +2693,7 @@ async function findDiWiring(className) {
|
|
|
2614
2693
|
const vtRegex = /<virtualType\s+name="([^"]+)"[^>]*type="([^"]+)"[^>]*(?:\/>|>([\s\S]*?)<\/virtualType>)/g;
|
|
2615
2694
|
while ((m = vtRegex.exec(content)) !== null) {
|
|
2616
2695
|
const vtType = m[2];
|
|
2617
|
-
if (!vtType
|
|
2696
|
+
if (!matchesClass(vtType)) continue;
|
|
2618
2697
|
const vtEntry = { name: m[1], type: vtType, file: relativePath };
|
|
2619
2698
|
if (m[3]) {
|
|
2620
2699
|
const argRegex = /<argument\s+name="([^"]+)"[^>]*xsi:type="([^"]+)"[^>]*>([^<]*)<\/argument>/g;
|
|
@@ -2630,6 +2709,7 @@ async function findDiWiring(className) {
|
|
|
2630
2709
|
}
|
|
2631
2710
|
|
|
2632
2711
|
// 4. Constructor arguments from PHP class
|
|
2712
|
+
// When FQCN is provided, verify namespace matches to avoid class name collisions
|
|
2633
2713
|
const phpFiles = await glob(`**/${shortName}.php`, { cwd: root, absolute: true, nodir: true });
|
|
2634
2714
|
for (const phpFile of phpFiles) {
|
|
2635
2715
|
let content;
|
|
@@ -2637,6 +2717,17 @@ async function findDiWiring(className) {
|
|
|
2637
2717
|
const classMatch = content.match(/(?:class|abstract\s+class)\s+(\w+)/);
|
|
2638
2718
|
if (!classMatch || classMatch[1] !== shortName) continue;
|
|
2639
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
|
+
|
|
2640
2731
|
const ctorMatch = content.match(/function\s+__construct\s*\(([\s\S]*?)\)\s*[{:]/);
|
|
2641
2732
|
if (ctorMatch) {
|
|
2642
2733
|
const paramRegex = /(?:([\w\\]+)\s+)?(\$\w+)/g;
|
|
@@ -2645,6 +2736,7 @@ async function findDiWiring(className) {
|
|
|
2645
2736
|
result.constructorArguments.push({ typeHint: pm[1] || null, variable: pm[2] });
|
|
2646
2737
|
}
|
|
2647
2738
|
}
|
|
2739
|
+
result.constructorSourceFile = phpFile.replace(root + '/', '');
|
|
2648
2740
|
break;
|
|
2649
2741
|
}
|
|
2650
2742
|
|
|
@@ -2904,6 +2996,25 @@ async function traceCallChain(startClass, startMethod, maxDepth = 3) {
|
|
|
2904
2996
|
};
|
|
2905
2997
|
|
|
2906
2998
|
const classFileMap = new Map();
|
|
2999
|
+
const parentClassCache = new Map();
|
|
3000
|
+
|
|
3001
|
+
// Resolve parent class from extends declaration in PHP file content
|
|
3002
|
+
function resolveParentFromContent(content) {
|
|
3003
|
+
const extendsMatch = content.match(/class\s+\w+\s+extends\s+([\w\\]+)/);
|
|
3004
|
+
if (!extendsMatch) return null;
|
|
3005
|
+
const parent = extendsMatch[1];
|
|
3006
|
+
// If it's a short name, resolve using use statements
|
|
3007
|
+
if (!parent.includes('\\')) {
|
|
3008
|
+
const useMatch = content.match(new RegExp(`use\\s+([\\w\\\\]+\\\\${parent})\\s*;`));
|
|
3009
|
+
if (useMatch) return useMatch[1];
|
|
3010
|
+
// Check namespace-relative
|
|
3011
|
+
const nsMatch = content.match(/namespace\s+([\w\\]+)/);
|
|
3012
|
+
if (nsMatch) return `${nsMatch[1]}\\${parent}`;
|
|
3013
|
+
return parent;
|
|
3014
|
+
}
|
|
3015
|
+
// Leading backslash = fully qualified
|
|
3016
|
+
return parent.replace(/^\\/, '');
|
|
3017
|
+
}
|
|
2907
3018
|
|
|
2908
3019
|
async function resolveClassFile(className) {
|
|
2909
3020
|
const shortName = className.split('\\').pop();
|
|
@@ -3009,14 +3120,46 @@ async function traceCallChain(startClass, startMethod, maxDepth = 3) {
|
|
|
3009
3120
|
const relativePath = filePath.replace(root + '/', '');
|
|
3010
3121
|
|
|
3011
3122
|
// Extract method body (brace counting)
|
|
3123
|
+
const escapedMethod = methodName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
3012
3124
|
const methodRegex = new RegExp(
|
|
3013
|
-
`function\\s+${
|
|
3125
|
+
`function\\s+${escapedMethod}\\s*\\([^)]*\\)[^{]*\\{`
|
|
3014
3126
|
);
|
|
3015
|
-
|
|
3127
|
+
let methodStart = content.search(methodRegex);
|
|
3128
|
+
|
|
3129
|
+
// If method not found in this class, walk up the inheritance chain
|
|
3130
|
+
let resolvedClass = className;
|
|
3131
|
+
let resolvedContent = content;
|
|
3132
|
+
let resolvedFilePath = filePath;
|
|
3016
3133
|
if (methodStart === -1) {
|
|
3017
|
-
|
|
3018
|
-
|
|
3134
|
+
let currentContent = content;
|
|
3135
|
+
let found = false;
|
|
3136
|
+
const visited = new Set([className]);
|
|
3137
|
+
for (let i = 0; i < 10; i++) { // max 10 parent levels
|
|
3138
|
+
const parentFqcn = resolveParentFromContent(currentContent);
|
|
3139
|
+
if (!parentFqcn || visited.has(parentFqcn)) break;
|
|
3140
|
+
visited.add(parentFqcn);
|
|
3141
|
+
const parentFile = await resolveClassFile(parentFqcn);
|
|
3142
|
+
if (!parentFile) break;
|
|
3143
|
+
let parentContent;
|
|
3144
|
+
try { parentContent = readFileSync(parentFile, 'utf-8'); } catch { break; }
|
|
3145
|
+
const parentMethodStart = parentContent.search(methodRegex);
|
|
3146
|
+
if (parentMethodStart !== -1) {
|
|
3147
|
+
resolvedClass = parentFqcn;
|
|
3148
|
+
resolvedContent = parentContent;
|
|
3149
|
+
resolvedFilePath = parentFile;
|
|
3150
|
+
methodStart = parentMethodStart;
|
|
3151
|
+
found = true;
|
|
3152
|
+
break;
|
|
3153
|
+
}
|
|
3154
|
+
currentContent = parentContent;
|
|
3155
|
+
}
|
|
3156
|
+
if (!found) {
|
|
3157
|
+
result.chain.push({ depth, class: className, method: methodName, file: relativePath, status: 'method_not_found' });
|
|
3158
|
+
return;
|
|
3159
|
+
}
|
|
3019
3160
|
}
|
|
3161
|
+
content = resolvedContent;
|
|
3162
|
+
const resolvedRelPath = resolvedFilePath.replace(root + '/', '');
|
|
3020
3163
|
|
|
3021
3164
|
let braceCount = 0;
|
|
3022
3165
|
let bodyStart = content.indexOf('{', methodStart);
|
|
@@ -3028,8 +3171,11 @@ async function traceCallChain(startClass, startMethod, maxDepth = 3) {
|
|
|
3028
3171
|
}
|
|
3029
3172
|
const methodBody = content.slice(bodyStart, bodyEnd + 1);
|
|
3030
3173
|
|
|
3174
|
+
// Show where method was actually found if inherited
|
|
3175
|
+
const inheritedFrom = (resolvedClass !== className) ? resolvedClass : null;
|
|
3031
3176
|
const chainEntry = {
|
|
3032
3177
|
depth, class: className, method: methodName, file: relativePath,
|
|
3178
|
+
...(inheritedFrom ? { inheritedFrom, resolvedFile: resolvedRelPath } : {}),
|
|
3033
3179
|
calls: [], dispatches: []
|
|
3034
3180
|
};
|
|
3035
3181
|
|
|
@@ -3049,13 +3195,20 @@ async function traceCallChain(startClass, startMethod, maxDepth = 3) {
|
|
|
3049
3195
|
while ((dc = depCallRegex.exec(methodBody)) !== null) {
|
|
3050
3196
|
const property = dc[1];
|
|
3051
3197
|
const calledMethod = dc[2];
|
|
3052
|
-
// Resolve property type from constructor
|
|
3053
|
-
const ctorMatch = content.match(/function\s+__construct\s*\(([\s\S]*?)\)\s*[{:]/);
|
|
3198
|
+
// Resolve property type from constructor — check the original class first, then the resolved (parent) class
|
|
3054
3199
|
let resolvedType = null;
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3200
|
+
let originalContent = content;
|
|
3201
|
+
if (resolvedClass !== className) {
|
|
3202
|
+
try { originalContent = readFileSync(filePath, 'utf-8'); } catch { originalContent = content; }
|
|
3203
|
+
}
|
|
3204
|
+
const contentSources = (resolvedClass !== className) ? [originalContent, content] : [content];
|
|
3205
|
+
for (const src of contentSources) {
|
|
3206
|
+
const ctorMatch = src.match(/function\s+__construct\s*\(([\s\S]*?)\)\s*[{:]/);
|
|
3207
|
+
if (ctorMatch) {
|
|
3208
|
+
const paramRegex = new RegExp(`([\\w\\\\]+)\\s+\\$${property}\\b`);
|
|
3209
|
+
const pm = ctorMatch[1].match(paramRegex);
|
|
3210
|
+
if (pm) { resolvedType = pm[1]; break; }
|
|
3211
|
+
}
|
|
3059
3212
|
}
|
|
3060
3213
|
chainEntry.calls.push({ type: 'dependency', property, method: calledMethod, typeHint: resolvedType || null });
|
|
3061
3214
|
}
|
|
@@ -3144,6 +3297,11 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
3144
3297
|
description: 'Enable query expansion with Magento domain synonyms for better recall (default: true)',
|
|
3145
3298
|
default: true
|
|
3146
3299
|
},
|
|
3300
|
+
precise: {
|
|
3301
|
+
type: 'boolean',
|
|
3302
|
+
description: 'Precise mode for debugging: disables query expansion AND applies strict post-filtering — only returns results where the file content contains at least one query keyword. Use for specific debugging queries like "gift card subtotal infinite loop". Default: false.',
|
|
3303
|
+
default: false
|
|
3304
|
+
},
|
|
3147
3305
|
},
|
|
3148
3306
|
required: ['query']
|
|
3149
3307
|
}
|
|
@@ -3759,6 +3917,29 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
3759
3917
|
required: ['eventName']
|
|
3760
3918
|
}
|
|
3761
3919
|
},
|
|
3920
|
+
{
|
|
3921
|
+
name: 'magento_batch',
|
|
3922
|
+
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).',
|
|
3923
|
+
inputSchema: {
|
|
3924
|
+
type: 'object',
|
|
3925
|
+
properties: {
|
|
3926
|
+
queries: {
|
|
3927
|
+
type: 'array',
|
|
3928
|
+
description: 'Array of tool calls to execute. Each entry has a "tool" name and "args" object.',
|
|
3929
|
+
items: {
|
|
3930
|
+
type: 'object',
|
|
3931
|
+
properties: {
|
|
3932
|
+
tool: { type: 'string', description: 'Magector tool name (e.g., "magento_find_class", "magento_find_plugin")' },
|
|
3933
|
+
args: { type: 'object', description: 'Arguments for the tool call' }
|
|
3934
|
+
},
|
|
3935
|
+
required: ['tool', 'args']
|
|
3936
|
+
},
|
|
3937
|
+
maxItems: 10
|
|
3938
|
+
}
|
|
3939
|
+
},
|
|
3940
|
+
required: ['queries']
|
|
3941
|
+
}
|
|
3942
|
+
},
|
|
3762
3943
|
]
|
|
3763
3944
|
}));
|
|
3764
3945
|
|
|
@@ -3805,12 +3986,22 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
3805
3986
|
try {
|
|
3806
3987
|
switch (name) {
|
|
3807
3988
|
case 'magento_search': {
|
|
3808
|
-
const
|
|
3809
|
-
const
|
|
3989
|
+
const precise = args.precise === true;
|
|
3990
|
+
const searchQuery = (args.expand !== false && !precise) ? expandQuery(args.query) : args.query;
|
|
3991
|
+
const raw = await rustSearchAsync(searchQuery, Math.max(args.limit || 10, precise ? 60 : 30));
|
|
3810
3992
|
const arr = Array.isArray(raw) ? raw : [];
|
|
3811
3993
|
let results = arr.map(normalizeResult);
|
|
3812
3994
|
// Hybrid BM25 rerank for better exact-match handling
|
|
3813
3995
|
results = hybridRerank(results, args.query);
|
|
3996
|
+
// Precise mode: strict post-filter — result must contain at least one significant query keyword
|
|
3997
|
+
if (precise) {
|
|
3998
|
+
const queryTerms = args.query.toLowerCase().split(/\s+/).filter(t => t.length > 2);
|
|
3999
|
+
results = results.filter(r => {
|
|
4000
|
+
const haystack = [r.searchText, r.className, r.methodName, r.path, ...(r.methods || [])]
|
|
4001
|
+
.filter(Boolean).join(' ').toLowerCase();
|
|
4002
|
+
return queryTerms.some(term => haystack.includes(term));
|
|
4003
|
+
});
|
|
4004
|
+
}
|
|
3814
4005
|
// Apply module filter if specified
|
|
3815
4006
|
if (args.moduleFilter) {
|
|
3816
4007
|
results = filterByModule(results, args.moduleFilter);
|
|
@@ -3866,10 +4057,18 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
3866
4057
|
if (r.methodName?.toLowerCase() === methodLower) bonus += 0.3;
|
|
3867
4058
|
return { ...r, score: (r.score || 0) + bonus };
|
|
3868
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
|
+
}
|
|
3869
4068
|
return {
|
|
3870
4069
|
content: [{
|
|
3871
4070
|
type: 'text',
|
|
3872
|
-
text: formatSearchResults(
|
|
4071
|
+
text: formatSearchResults(sliced)
|
|
3873
4072
|
}]
|
|
3874
4073
|
};
|
|
3875
4074
|
}
|
|
@@ -4850,6 +5049,25 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
4850
5049
|
text += '\n';
|
|
4851
5050
|
}
|
|
4852
5051
|
|
|
5052
|
+
if (impact.runtimeCallers.length > 0) {
|
|
5053
|
+
text += `### Runtime Callers (${impact.runtimeCallers.length})\n`;
|
|
5054
|
+
text += '_Classes that inject this class and call its methods at runtime:_\n';
|
|
5055
|
+
// Group by caller class for readability
|
|
5056
|
+
const grouped = new Map();
|
|
5057
|
+
for (const c of impact.runtimeCallers) {
|
|
5058
|
+
const key = c.callerClass || c.file;
|
|
5059
|
+
if (!grouped.has(key)) grouped.set(key, { file: c.file, methods: new Set() });
|
|
5060
|
+
grouped.get(key).methods.add(`$this->${c.property}->${c.calledMethod}()`);
|
|
5061
|
+
}
|
|
5062
|
+
for (const [cls, info] of grouped) {
|
|
5063
|
+
text += `- **\`${cls}\`** (\`${info.file}\`)\n`;
|
|
5064
|
+
for (const m of info.methods) {
|
|
5065
|
+
text += ` - \`${m}\`\n`;
|
|
5066
|
+
}
|
|
5067
|
+
}
|
|
5068
|
+
text += '\n';
|
|
5069
|
+
}
|
|
5070
|
+
|
|
4853
5071
|
if (impact.total === 0) {
|
|
4854
5072
|
text += '_No references found. The class may be referenced under a different name or alias._\n';
|
|
4855
5073
|
}
|
|
@@ -5057,6 +5275,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
5057
5275
|
const status = entry.status ? ` [${entry.status}]` : '';
|
|
5058
5276
|
text += `${indent}- **${entry.class}::${entry.method}**${status}`;
|
|
5059
5277
|
if (entry.file) text += ` (${entry.file})`;
|
|
5278
|
+
if (entry.inheritedFrom) text += `\n${indent} _inherited from_ \`${entry.inheritedFrom}\` (${entry.resolvedFile})`;
|
|
5060
5279
|
text += '\n';
|
|
5061
5280
|
|
|
5062
5281
|
if (entry.calls) {
|
|
@@ -5170,6 +5389,124 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
5170
5389
|
return { content: [{ type: 'text', text }] };
|
|
5171
5390
|
}
|
|
5172
5391
|
|
|
5392
|
+
case 'magento_batch': {
|
|
5393
|
+
const queries = args.queries || [];
|
|
5394
|
+
if (queries.length === 0) {
|
|
5395
|
+
return { content: [{ type: 'text', text: 'No queries provided.' }] };
|
|
5396
|
+
}
|
|
5397
|
+
if (queries.length > 10) {
|
|
5398
|
+
return { content: [{ type: 'text', text: 'Maximum 10 queries per batch.' }], isError: true };
|
|
5399
|
+
}
|
|
5400
|
+
// Run batch queries in parallel using existing standalone functions
|
|
5401
|
+
const batchResults = await Promise.all(queries.map(async (q, idx) => {
|
|
5402
|
+
try {
|
|
5403
|
+
const a = q.args || {};
|
|
5404
|
+
let text = '';
|
|
5405
|
+
switch (q.tool) {
|
|
5406
|
+
case 'magento_find_class': {
|
|
5407
|
+
const ns = a.namespace || '';
|
|
5408
|
+
const qr = `${a.className} ${ns}`.trim();
|
|
5409
|
+
const raw = await rustSearchAsync(qr, 30);
|
|
5410
|
+
const cl = a.className.toLowerCase();
|
|
5411
|
+
const res = raw.map(normalizeResult).filter(r =>
|
|
5412
|
+
r.className?.toLowerCase().includes(cl) || r.path?.toLowerCase().includes(cl.replace(/\\/g, '/'))
|
|
5413
|
+
);
|
|
5414
|
+
text = formatSearchResults(res.slice(0, 5));
|
|
5415
|
+
break;
|
|
5416
|
+
}
|
|
5417
|
+
case 'magento_find_plugin': {
|
|
5418
|
+
const qr = `plugin interceptor ${a.targetClass || ''} ${a.targetMethod || ''}`.trim();
|
|
5419
|
+
const raw = await rustSearchAsync(qr, 30);
|
|
5420
|
+
const res = raw.map(normalizeResult).filter(r =>
|
|
5421
|
+
r.magentoType === 'plugin' || r.path?.toLowerCase().includes('plugin')
|
|
5422
|
+
);
|
|
5423
|
+
text = formatSearchResults(res.slice(0, 10));
|
|
5424
|
+
break;
|
|
5425
|
+
}
|
|
5426
|
+
case 'magento_find_observer': {
|
|
5427
|
+
const flow = await traceEventFlow(a.eventName);
|
|
5428
|
+
text = `Observers: ${flow.observers.length}\n`;
|
|
5429
|
+
for (const o of flow.observers.slice(0, 10)) {
|
|
5430
|
+
text += `- ${o.name}: ${o.instance}::${o.method} (${o.file})\n`;
|
|
5431
|
+
}
|
|
5432
|
+
break;
|
|
5433
|
+
}
|
|
5434
|
+
case 'magento_trace_dependency': {
|
|
5435
|
+
const dep = await traceDependency(a.className, a.direction || 'both');
|
|
5436
|
+
text = `Preferences: ${dep.preferences.length}, Plugins: ${dep.plugins.length}, VirtualTypes: ${dep.virtualTypes.length}, Args: ${dep.argumentOverrides.length}\n`;
|
|
5437
|
+
for (const p of dep.plugins.slice(0, 5)) text += `- plugin: ${p.pluginName} → ${p.pluginClass}\n`;
|
|
5438
|
+
for (const p of dep.preferences.slice(0, 5)) text += `- pref: ${p.for} → ${p.type}\n`;
|
|
5439
|
+
break;
|
|
5440
|
+
}
|
|
5441
|
+
case 'magento_impact_analysis': {
|
|
5442
|
+
const imp = await analyzeImpact(a.className);
|
|
5443
|
+
text = `Total: ${imp.total} (use: ${imp.useStatements.length}, di: ${imp.diXmlReferences.length}, new: ${imp.instantiations.length}, type: ${imp.typeHints.length}, runtime: ${imp.runtimeCallers.length})\n`;
|
|
5444
|
+
for (const c of imp.runtimeCallers.slice(0, 5)) text += `- ${c.callerClass}: $this->${c.property}->${c.calledMethod}()\n`;
|
|
5445
|
+
break;
|
|
5446
|
+
}
|
|
5447
|
+
case 'magento_search': {
|
|
5448
|
+
const precise = a.precise === true;
|
|
5449
|
+
const sq = (a.expand !== false && !precise) ? expandQuery(a.query) : a.query;
|
|
5450
|
+
const raw = await rustSearchAsync(sq, 30);
|
|
5451
|
+
let res = raw.map(normalizeResult);
|
|
5452
|
+
res = hybridRerank(res, a.query);
|
|
5453
|
+
text = formatSearchResults(res.slice(0, a.limit || 5));
|
|
5454
|
+
break;
|
|
5455
|
+
}
|
|
5456
|
+
case 'magento_find_callers': {
|
|
5457
|
+
const callers = await findCallers(a.methodName, a.className);
|
|
5458
|
+
text = `Callers: ${callers.callers?.length || 0}\n`;
|
|
5459
|
+
for (const c of (callers.callers || []).slice(0, 10)) {
|
|
5460
|
+
text += `- ${c.class}::${c.method} (${c.path}:${c.line})\n`;
|
|
5461
|
+
}
|
|
5462
|
+
break;
|
|
5463
|
+
}
|
|
5464
|
+
case 'magento_find_event_flow': {
|
|
5465
|
+
const flow = await traceEventFlow(a.eventName);
|
|
5466
|
+
text = `Dispatchers: ${flow.dispatchers.length}, Observers: ${flow.observers.length}\n`;
|
|
5467
|
+
for (const o of flow.observers.slice(0, 10)) text += `- ${o.name}: ${o.instance} (${o.file})\n`;
|
|
5468
|
+
break;
|
|
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
|
+
}
|
|
5494
|
+
default:
|
|
5495
|
+
text = `Unsupported batch tool: ${q.tool}`;
|
|
5496
|
+
}
|
|
5497
|
+
return { idx, tool: q.tool, text };
|
|
5498
|
+
} catch (err) {
|
|
5499
|
+
return { idx, tool: q.tool, text: `Error: ${err.message}` };
|
|
5500
|
+
}
|
|
5501
|
+
}));
|
|
5502
|
+
|
|
5503
|
+
let text = `## Batch Results (${batchResults.length} queries)\n\n`;
|
|
5504
|
+
for (const br of batchResults) {
|
|
5505
|
+
text += `---\n### [${br.idx + 1}] ${br.tool}\n\n${br.text}\n\n`;
|
|
5506
|
+
}
|
|
5507
|
+
return { content: [{ type: 'text', text }] };
|
|
5508
|
+
}
|
|
5509
|
+
|
|
5173
5510
|
default:
|
|
5174
5511
|
return {
|
|
5175
5512
|
content: [{
|