magector 2.4.1 → 2.5.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.
- package/README.md +4 -2
- package/package.json +5 -5
- package/src/mcp-server.js +680 -16
package/README.md
CHANGED
|
@@ -415,7 +415,8 @@ All search tools return structured JSON:
|
|
|
415
415
|
|------|-------------|
|
|
416
416
|
| `magento_find_config` | Find XML configuration (di.xml, events.xml, routes.xml, system.xml, webapi.xml, module.xml, layout) |
|
|
417
417
|
| `magento_find_template` | Find PHTML template files for frontend or admin rendering |
|
|
418
|
-
| `magento_find_plugin` | Find interceptor plugins (before/after/around methods) and di.xml declarations |
|
|
418
|
+
| `magento_find_plugin` | Find interceptor plugins (before/after/around methods) and di.xml declarations. Resolves plugin PHP files and extracts interceptor method signatures **(v2.5)** |
|
|
419
|
+
| `magento_find_fieldset` | Find fieldset.xml definitions controlling data copy between entities (order→quote, quote→order). Shows fields per aspect (to_order, to_edit) **(v2.5)** |
|
|
419
420
|
| `magento_find_observer` | Find event observers and events.xml declarations |
|
|
420
421
|
| `magento_find_preference` | Find DI preference overrides -- which class implements an interface |
|
|
421
422
|
| `magento_find_controller` | Find MVC controllers by frontend or admin route path |
|
|
@@ -429,7 +430,8 @@ All search tools return structured JSON:
|
|
|
429
430
|
|
|
430
431
|
| Tool | Description |
|
|
431
432
|
|------|-------------|
|
|
432
|
-
| `magento_trace_flow` | Trace execution flow from an entry point (route, API, GraphQL, event, cron) -- maps controller → plugins → observers → templates
|
|
433
|
+
| `magento_trace_flow` | Trace execution flow from an entry point (route, API, GraphQL, event, cron) -- maps controller → plugins → observers → templates with code snippets **(v2.5)** |
|
|
434
|
+
| `magento_trace_shipping_chain` | Trace the complete shipping rate chain: carriers → collectRates plugins → rate modifiers → totals collectors → fieldset mappings **(v2.5)** |
|
|
433
435
|
| `magento_trace_dependency` | Trace DI graph for a class/interface -- preferences, plugins, virtualTypes, argument overrides (parses all di.xml, no index needed) |
|
|
434
436
|
| `magento_find_event_flow` | Trace complete event chain: dispatchers → observers → handler PHP classes (parses events.xml + vector search) |
|
|
435
437
|
| `magento_find_event_dispatchers` | Find all PHP locations where a specific event is dispatched -- exact grep matching with method context and surrounding code **(v2.3)** |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "magector",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.5.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.
|
|
37
|
-
"@magector/cli-linux-x64": "2.
|
|
38
|
-
"@magector/cli-linux-arm64": "2.
|
|
39
|
-
"@magector/cli-win32-x64": "2.
|
|
36
|
+
"@magector/cli-darwin-arm64": "2.5.1",
|
|
37
|
+
"@magector/cli-linux-x64": "2.5.1",
|
|
38
|
+
"@magector/cli-linux-arm64": "2.5.1",
|
|
39
|
+
"@magector/cli-win32-x64": "2.5.1"
|
|
40
40
|
},
|
|
41
41
|
"keywords": [
|
|
42
42
|
"magento",
|
package/src/mcp-server.js
CHANGED
|
@@ -965,6 +965,228 @@ function normalizeResult(r) {
|
|
|
965
965
|
* @param {Object} boosts - e.g. { fileType: 'xml', pathContains: 'di.xml', isPlugin: true }
|
|
966
966
|
* @param {number} weight - boost multiplier (default 0.3 = 30% score bump per match)
|
|
967
967
|
*/
|
|
968
|
+
/**
|
|
969
|
+
* Extract before/after/around plugin method signatures from a PHP file.
|
|
970
|
+
* Returns array of { name, type, targetMethod, signature } objects.
|
|
971
|
+
*/
|
|
972
|
+
function extractPluginMethods(filePath) {
|
|
973
|
+
const absPath = filePath.startsWith('/') ? filePath : path.join(config.magentoRoot, filePath);
|
|
974
|
+
let content;
|
|
975
|
+
try { content = readFileSync(absPath, 'utf-8'); } catch { return []; }
|
|
976
|
+
const methods = [];
|
|
977
|
+
const methodRegex = /^\s*public\s+function\s+((?:before|after|around)([A-Z]\w*))\s*\(([^)]*)\)/gm;
|
|
978
|
+
let match;
|
|
979
|
+
while ((match = methodRegex.exec(content)) !== null) {
|
|
980
|
+
const name = match[1];
|
|
981
|
+
const targetMethod = match[2].charAt(0).toLowerCase() + match[2].slice(1);
|
|
982
|
+
let type = 'around';
|
|
983
|
+
if (name.startsWith('before')) type = 'before';
|
|
984
|
+
else if (name.startsWith('after')) type = 'after';
|
|
985
|
+
methods.push({ name, type, targetMethod, signature: match[0].trim() });
|
|
986
|
+
}
|
|
987
|
+
return methods;
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
/**
|
|
991
|
+
* Read a short code snippet around a method definition from a PHP file.
|
|
992
|
+
* Returns up to `maxLines` lines starting from the method signature.
|
|
993
|
+
*/
|
|
994
|
+
function readMethodSnippet(filePath, methodName, maxLines = 15) {
|
|
995
|
+
const absPath = filePath.startsWith('/') ? filePath : path.join(config.magentoRoot, filePath);
|
|
996
|
+
let content;
|
|
997
|
+
try { content = readFileSync(absPath, 'utf-8'); } catch { return null; }
|
|
998
|
+
const lines = content.split('\n');
|
|
999
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1000
|
+
if (lines[i].includes(`function ${methodName}(`)) {
|
|
1001
|
+
return lines.slice(i, i + maxLines).join('\n');
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
return null;
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
/**
|
|
1008
|
+
* Parse all fieldset.xml files in the Magento root.
|
|
1009
|
+
* Returns array of { file, scope, fieldset, fields: [{ field, aspect }] }.
|
|
1010
|
+
*/
|
|
1011
|
+
async function parseFieldsetXml(filterFieldset, filterAspect) {
|
|
1012
|
+
const root = config.magentoRoot;
|
|
1013
|
+
const fieldsetFiles = await glob('**/etc/fieldset.xml', { cwd: root, absolute: true, nodir: true });
|
|
1014
|
+
const results = [];
|
|
1015
|
+
for (const fsFile of fieldsetFiles) {
|
|
1016
|
+
let content;
|
|
1017
|
+
try { content = readFileSync(fsFile, 'utf-8'); } catch { continue; }
|
|
1018
|
+
const relPath = fsFile.replace(root + '/', '');
|
|
1019
|
+
// Parse <scope id="..."> blocks
|
|
1020
|
+
const scopeRegex = /<scope\s+id="([^"]+)">([\s\S]*?)<\/scope>/g;
|
|
1021
|
+
let scopeMatch;
|
|
1022
|
+
while ((scopeMatch = scopeRegex.exec(content)) !== null) {
|
|
1023
|
+
const scopeName = scopeMatch[1];
|
|
1024
|
+
const scopeBlock = scopeMatch[2];
|
|
1025
|
+
const innerFsRegex = /<fieldset\s+id="([^"]+)">([\s\S]*?)<\/fieldset>/g;
|
|
1026
|
+
let innerMatch;
|
|
1027
|
+
while ((innerMatch = innerFsRegex.exec(scopeBlock)) !== null) {
|
|
1028
|
+
const innerFieldsetId = innerMatch[1];
|
|
1029
|
+
if (filterFieldset && !innerFieldsetId.toLowerCase().includes(filterFieldset.toLowerCase()) && !scopeName.toLowerCase().includes(filterFieldset.toLowerCase())) continue;
|
|
1030
|
+
const innerBlock = innerMatch[2];
|
|
1031
|
+
const fieldRegex = /<field\s+name="([^"]+)">([\s\S]*?)<\/field>/g;
|
|
1032
|
+
let fieldMatch;
|
|
1033
|
+
const fields = [];
|
|
1034
|
+
while ((fieldMatch = fieldRegex.exec(innerBlock)) !== null) {
|
|
1035
|
+
const fieldName = fieldMatch[1];
|
|
1036
|
+
const fieldBlock = fieldMatch[2];
|
|
1037
|
+
const aspectRegex = /<aspect\s+name="([^"]+)"\s*(?:\/>|>[^<]*<\/aspect>)/g;
|
|
1038
|
+
let aspectMatch;
|
|
1039
|
+
while ((aspectMatch = aspectRegex.exec(fieldBlock)) !== null) {
|
|
1040
|
+
if (filterAspect && !aspectMatch[1].toLowerCase().includes(filterAspect.toLowerCase())) continue;
|
|
1041
|
+
fields.push({ field: fieldName, aspect: aspectMatch[1] });
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
if (fields.length > 0) {
|
|
1045
|
+
results.push({ file: relPath, scope: scopeName, fieldset: innerFieldsetId, fields });
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
// Also handle flat fieldset format: <fieldset id="..."> without scope wrapper
|
|
1050
|
+
const flatFsRegex = /<fieldset\s+id="([^"]+)">([\s\S]*?)<\/fieldset>/g;
|
|
1051
|
+
let flatMatch;
|
|
1052
|
+
while ((flatMatch = flatFsRegex.exec(content)) !== null) {
|
|
1053
|
+
const fieldsetId = flatMatch[1];
|
|
1054
|
+
if (filterFieldset && !fieldsetId.toLowerCase().includes(filterFieldset.toLowerCase())) continue;
|
|
1055
|
+
const block = flatMatch[2];
|
|
1056
|
+
if (block.includes('<fieldset')) continue; // skip nested (handled above)
|
|
1057
|
+
const fieldRegex = /<field\s+name="([^"]+)">([\s\S]*?)<\/field>/g;
|
|
1058
|
+
let fieldMatch;
|
|
1059
|
+
const fields = [];
|
|
1060
|
+
while ((fieldMatch = fieldRegex.exec(block)) !== null) {
|
|
1061
|
+
const fieldName = fieldMatch[1];
|
|
1062
|
+
const fieldBlock = fieldMatch[2];
|
|
1063
|
+
const aspectRegex = /<aspect\s+name="([^"]+)"\s*(?:\/>|>[^<]*<\/aspect>)/g;
|
|
1064
|
+
let aspectMatch;
|
|
1065
|
+
while ((aspectMatch = aspectRegex.exec(fieldBlock)) !== null) {
|
|
1066
|
+
if (filterAspect && !aspectMatch[1].toLowerCase().includes(filterAspect.toLowerCase())) continue;
|
|
1067
|
+
fields.push({ field: fieldName, aspect: aspectMatch[1] });
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
if (fields.length > 0) {
|
|
1071
|
+
results.push({ file: relPath, scope: null, fieldset: fieldsetId, fields });
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
return results;
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
/**
|
|
1079
|
+
* Resolve a PHP class name to a file path by converting namespace to path.
|
|
1080
|
+
*/
|
|
1081
|
+
function findClassFile(root, className) {
|
|
1082
|
+
if (!className) return '';
|
|
1083
|
+
const parts = className.replace(/\\\\/g, '\\').split('\\');
|
|
1084
|
+
if (parts.length < 3) return '';
|
|
1085
|
+
const vendor = parts[0].toLowerCase();
|
|
1086
|
+
const moduleParts = parts[1].replace(/([A-Z])/g, '-$1').toLowerCase().replace(/^-/, '').split('-');
|
|
1087
|
+
const candidates = [
|
|
1088
|
+
path.join(root, 'vendor', vendor, parts.slice(1).join('/').replace(/\\/g, '/') + '.php'),
|
|
1089
|
+
path.join(root, 'vendor', vendor, moduleParts.join('-'), parts.slice(2).join('/') + '.php'),
|
|
1090
|
+
path.join(root, 'app/code', parts.join('/') + '.php'),
|
|
1091
|
+
];
|
|
1092
|
+
for (const candidate of candidates) {
|
|
1093
|
+
if (existsSync(candidate)) return candidate;
|
|
1094
|
+
}
|
|
1095
|
+
// Glob fallback — search for the class filename
|
|
1096
|
+
const fileName = parts[parts.length - 1] + '.php';
|
|
1097
|
+
try {
|
|
1098
|
+
const matches = glob.sync(`**/${fileName}`, { cwd: root, absolute: true, nodir: true, ignore: ['**/Test/**', '**/test/**'] });
|
|
1099
|
+
for (const m of matches) {
|
|
1100
|
+
const content = readFileSync(m, 'utf-8').slice(0, 500);
|
|
1101
|
+
if (content.includes(parts[parts.length - 1])) return m;
|
|
1102
|
+
}
|
|
1103
|
+
} catch {}
|
|
1104
|
+
return '';
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
/**
|
|
1108
|
+
* Trace the shipping rate modification chain:
|
|
1109
|
+
* carriers → plugins on collectRates → ShippingRateModifier pool → totals collector
|
|
1110
|
+
*/
|
|
1111
|
+
async function traceShippingChain(carrierOrMethod) {
|
|
1112
|
+
const root = config.magentoRoot;
|
|
1113
|
+
const chain = { carriers: [], collectRatesPlugins: [], rateModifiers: [], totalsCollectors: [], fieldsets: [] };
|
|
1114
|
+
|
|
1115
|
+
// 1. Find carrier classes
|
|
1116
|
+
const carrierQuery = carrierOrMethod
|
|
1117
|
+
? `carrier shipping ${carrierOrMethod} collectRates`
|
|
1118
|
+
: 'carrier shipping collectRates AbstractCarrier';
|
|
1119
|
+
const carrierRaw = await safeSearch(carrierQuery, 30);
|
|
1120
|
+
const carriers = carrierRaw.map(normalizeResult).filter(r =>
|
|
1121
|
+
r.path?.includes('/Carrier/') || r.path?.includes('/Model/Carrier')
|
|
1122
|
+
);
|
|
1123
|
+
chain.carriers = carriers.slice(0, 10).map(r => ({
|
|
1124
|
+
path: r.path, className: r.className || null, methods: r.methods || []
|
|
1125
|
+
}));
|
|
1126
|
+
|
|
1127
|
+
// 2. Find plugins on AbstractCarrierInterface::collectRates
|
|
1128
|
+
const diFiles = await glob('**/etc/**/di.xml', { cwd: root, absolute: true, nodir: true });
|
|
1129
|
+
for (const diFile of diFiles) {
|
|
1130
|
+
let content;
|
|
1131
|
+
try { content = readFileSync(diFile, 'utf-8'); } catch { continue; }
|
|
1132
|
+
const carrierTargets = ['AbstractCarrier', 'CarrierInterface', 'AbstractCarrierInterface', 'AbstractCarrierOnline'];
|
|
1133
|
+
for (const target of carrierTargets) {
|
|
1134
|
+
if (!content.includes(target)) continue;
|
|
1135
|
+
const typeBlockRegex = /<type\s+name="([^"]*)"[^>]*>([\s\S]*?)<\/type>/g;
|
|
1136
|
+
let tm;
|
|
1137
|
+
while ((tm = typeBlockRegex.exec(content)) !== null) {
|
|
1138
|
+
if (!tm[1].includes(target)) continue;
|
|
1139
|
+
const block = tm[2];
|
|
1140
|
+
const pluginRegex = /<plugin\s+([^/>]*)\/?>/g;
|
|
1141
|
+
let pm;
|
|
1142
|
+
while ((pm = pluginRegex.exec(block)) !== null) {
|
|
1143
|
+
const attrs = {};
|
|
1144
|
+
const attrRe = /(\w+)="([^"]*)"/g;
|
|
1145
|
+
let am;
|
|
1146
|
+
while ((am = attrRe.exec(pm[1])) !== null) attrs[am[1]] = am[2];
|
|
1147
|
+
let area = 'global';
|
|
1148
|
+
const relPath = diFile.replace(root + '/', '');
|
|
1149
|
+
if (relPath.includes('/etc/adminhtml/')) area = 'adminhtml';
|
|
1150
|
+
else if (relPath.includes('/etc/frontend/')) area = 'frontend';
|
|
1151
|
+
const pluginFile = attrs.type ? findClassFile(root, attrs.type) : '';
|
|
1152
|
+
const pluginMethods = pluginFile ? extractPluginMethods(pluginFile) : [];
|
|
1153
|
+
chain.collectRatesPlugins.push({
|
|
1154
|
+
target: tm[1], pluginName: attrs.name || '', pluginClass: attrs.type || '',
|
|
1155
|
+
disabled: attrs.disabled === 'true', sortOrder: attrs.sortOrder || null,
|
|
1156
|
+
area, file: relPath, methods: pluginMethods
|
|
1157
|
+
});
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
// 3. Find ShippingRateModifier implementations
|
|
1164
|
+
const modifierRaw = await safeSearch('ShippingRateModifier shipping rate modifier', 20);
|
|
1165
|
+
const modifiers = modifierRaw.map(normalizeResult).filter(r =>
|
|
1166
|
+
r.path?.includes('Modifier') || r.path?.includes('modifier') ||
|
|
1167
|
+
r.className?.includes('Modifier')
|
|
1168
|
+
);
|
|
1169
|
+
chain.rateModifiers = modifiers.slice(0, 10).map(r => ({
|
|
1170
|
+
path: r.path, className: r.className || null, methods: r.methods || []
|
|
1171
|
+
}));
|
|
1172
|
+
|
|
1173
|
+
// 4. Find shipping totals collectors
|
|
1174
|
+
const totalsRaw = await safeSearch('shipping total collector quote address', 20);
|
|
1175
|
+
const totals = totalsRaw.map(normalizeResult).filter(r =>
|
|
1176
|
+
r.path?.includes('/Total/') || r.path?.includes('Shipping') ||
|
|
1177
|
+
r.className?.includes('Shipping')
|
|
1178
|
+
);
|
|
1179
|
+
chain.totalsCollectors = totals.slice(0, 5).map(r => ({
|
|
1180
|
+
path: r.path, className: r.className || null, methods: r.methods || []
|
|
1181
|
+
}));
|
|
1182
|
+
|
|
1183
|
+
// 5. Find relevant fieldsets
|
|
1184
|
+
const fieldsets = await parseFieldsetXml('sales_convert', null);
|
|
1185
|
+
chain.fieldsets = fieldsets;
|
|
1186
|
+
|
|
1187
|
+
return chain;
|
|
1188
|
+
}
|
|
1189
|
+
|
|
968
1190
|
function rerank(results, boosts = {}, weight = 0.3) {
|
|
969
1191
|
if (!boosts || Object.keys(boosts).length === 0) return results;
|
|
970
1192
|
|
|
@@ -1027,6 +1249,9 @@ async function traceRoute(entryPoint, depth) {
|
|
|
1027
1249
|
const best = ranked[0];
|
|
1028
1250
|
if (best) {
|
|
1029
1251
|
trace.controller = { path: best.path, className: best.className || null, methods: best.methods || [] };
|
|
1252
|
+
// Add code snippet for the controller's execute() method
|
|
1253
|
+
const execSnippet = readMethodSnippet(best.path, 'execute');
|
|
1254
|
+
if (execSnippet) trace.controller.codeSnippet = execSnippet;
|
|
1030
1255
|
}
|
|
1031
1256
|
|
|
1032
1257
|
const routeConfigs = routeRaw.map(normalizeResult).filter(r => r.path?.includes('routes.xml'));
|
|
@@ -1039,7 +1264,13 @@ async function traceRoute(entryPoint, depth) {
|
|
|
1039
1264
|
const pluginRaw = await safeSearch(`plugin interceptor ${best.className}`, 20);
|
|
1040
1265
|
const plugins = pluginRaw.map(normalizeResult).filter(r => r.isPlugin || r.path?.includes('/Plugin/') || r.path?.includes('di.xml'));
|
|
1041
1266
|
if (plugins.length > 0) {
|
|
1042
|
-
trace.plugins = plugins.slice(0, 10).map(r =>
|
|
1267
|
+
trace.plugins = plugins.slice(0, 10).map(r => {
|
|
1268
|
+
const entry = { path: r.path, className: r.className || null, methods: r.methods || [] };
|
|
1269
|
+
if (r.path && !r.path.endsWith('.xml')) {
|
|
1270
|
+
entry.pluginMethods = extractPluginMethods(r.path);
|
|
1271
|
+
}
|
|
1272
|
+
return entry;
|
|
1273
|
+
});
|
|
1043
1274
|
}
|
|
1044
1275
|
}
|
|
1045
1276
|
|
|
@@ -1075,6 +1306,13 @@ async function traceRoute(entryPoint, depth) {
|
|
|
1075
1306
|
if (templates.length > 0) {
|
|
1076
1307
|
trace.templates = templates.slice(0, 10).map(r => ({ path: r.path }));
|
|
1077
1308
|
}
|
|
1309
|
+
|
|
1310
|
+
// Fieldset discovery — look for fieldsets related to the route domain
|
|
1311
|
+
const domain = parts[0]; // e.g., "sales", "checkout", "catalog"
|
|
1312
|
+
const fieldsets = await parseFieldsetXml(domain, null);
|
|
1313
|
+
if (fieldsets.length > 0) {
|
|
1314
|
+
trace.fieldsets = fieldsets;
|
|
1315
|
+
}
|
|
1078
1316
|
}
|
|
1079
1317
|
|
|
1080
1318
|
return trace;
|
|
@@ -1103,6 +1341,12 @@ async function traceApi(entryPoint, depth) {
|
|
|
1103
1341
|
const svcs = svcRaw.map(normalizeResult).filter(r => r.className?.includes(serviceShortName));
|
|
1104
1342
|
if (svcs.length > 0) {
|
|
1105
1343
|
trace.serviceClass = { path: svcs[0].path, className: svcs[0].className || serviceClassName, methods: svcs[0].methods || [] };
|
|
1344
|
+
// Try to add code snippet for the main service method
|
|
1345
|
+
const methodMatch = (webapis[0]?.searchText || '').match(/method="([^"]+)"/);
|
|
1346
|
+
if (methodMatch) {
|
|
1347
|
+
const svcSnippet = readMethodSnippet(svcs[0].path, methodMatch[1]);
|
|
1348
|
+
if (svcSnippet) trace.serviceClass.codeSnippet = svcSnippet;
|
|
1349
|
+
}
|
|
1106
1350
|
}
|
|
1107
1351
|
}
|
|
1108
1352
|
|
|
@@ -1168,7 +1412,15 @@ async function traceEvent(entryPoint, depth) {
|
|
|
1168
1412
|
const obsRaw = await safeSearch(`event ${entryPoint} observer`, 30);
|
|
1169
1413
|
const observers = obsRaw.map(normalizeResult).filter(r => r.isObserver || r.path?.includes('/Observer/') || r.path?.includes('events.xml'));
|
|
1170
1414
|
if (observers.length > 0) {
|
|
1171
|
-
trace.observers = observers.slice(0, 15).map(r =>
|
|
1415
|
+
trace.observers = observers.slice(0, 15).map(r => {
|
|
1416
|
+
const entry = { eventName: entryPoint, path: r.path, className: r.className || null };
|
|
1417
|
+
// Add execute() snippet for observer PHP classes
|
|
1418
|
+
if (r.path && r.path.endsWith('.php')) {
|
|
1419
|
+
const snippet = readMethodSnippet(r.path, 'execute');
|
|
1420
|
+
if (snippet) entry.codeSnippet = snippet;
|
|
1421
|
+
}
|
|
1422
|
+
return entry;
|
|
1423
|
+
});
|
|
1172
1424
|
}
|
|
1173
1425
|
|
|
1174
1426
|
if (depth === 'deep') {
|
|
@@ -1180,6 +1432,13 @@ async function traceEvent(entryPoint, depth) {
|
|
|
1180
1432
|
if (origins.length > 0) {
|
|
1181
1433
|
trace.origin = { path: origins[0].path, className: origins[0].className || null, methods: origins[0].methods || [] };
|
|
1182
1434
|
}
|
|
1435
|
+
|
|
1436
|
+
// Fieldset discovery — look for fieldsets related to the event domain
|
|
1437
|
+
const domain = entryPoint.split('_').slice(0, 2).join('_'); // e.g., "sales_convert", "sales_order"
|
|
1438
|
+
const fieldsets = await parseFieldsetXml(domain, null);
|
|
1439
|
+
if (fieldsets.length > 0) {
|
|
1440
|
+
trace.fieldsets = fieldsets;
|
|
1441
|
+
}
|
|
1183
1442
|
}
|
|
1184
1443
|
|
|
1185
1444
|
return trace;
|
|
@@ -1202,6 +1461,8 @@ async function traceCron(entryPoint, depth) {
|
|
|
1202
1461
|
const handlers = handlerRaw.map(normalizeResult).filter(r => r.path?.includes('/Cron/'));
|
|
1203
1462
|
if (handlers.length > 0) {
|
|
1204
1463
|
trace.handler = { path: handlers[0].path, className: handlers[0].className || null, methods: handlers[0].methods || [] };
|
|
1464
|
+
const cronSnippet = readMethodSnippet(handlers[0].path, 'execute');
|
|
1465
|
+
if (cronSnippet) trace.handler.codeSnippet = cronSnippet;
|
|
1205
1466
|
}
|
|
1206
1467
|
|
|
1207
1468
|
if (depth === 'deep' && handlers[0]?.className) {
|
|
@@ -1307,6 +1568,32 @@ function formatSearchResults(results) {
|
|
|
1307
1568
|
: r.searchText;
|
|
1308
1569
|
}
|
|
1309
1570
|
|
|
1571
|
+
// 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'))) {
|
|
1573
|
+
if (r.methodName) {
|
|
1574
|
+
const preview = readMethodSnippet(r.path, r.methodName, 10);
|
|
1575
|
+
if (preview) entry.codePreview = preview;
|
|
1576
|
+
} else if (r.className) {
|
|
1577
|
+
// Show class declaration + first few lines
|
|
1578
|
+
const classShort = r.className.split('\\').pop();
|
|
1579
|
+
const preview = readMethodSnippet(r.path, classShort ? `class ${classShort}` : null, 10);
|
|
1580
|
+
if (!preview) {
|
|
1581
|
+
// Fallback: read file header (namespace + class line)
|
|
1582
|
+
const absP = r.path.startsWith('/') ? r.path : path.join(config.magentoRoot, r.path);
|
|
1583
|
+
try {
|
|
1584
|
+
const content = readFileSync(absP, 'utf-8');
|
|
1585
|
+
const lines = content.split('\n');
|
|
1586
|
+
const classLine = lines.findIndex(l => /^\s*(abstract\s+|final\s+)?class\s+/.test(l));
|
|
1587
|
+
if (classLine >= 0) {
|
|
1588
|
+
entry.codePreview = lines.slice(Math.max(0, classLine - 2), classLine + 8).join('\n');
|
|
1589
|
+
}
|
|
1590
|
+
} catch {}
|
|
1591
|
+
} else {
|
|
1592
|
+
entry.codePreview = preview;
|
|
1593
|
+
}
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1310
1597
|
return entry;
|
|
1311
1598
|
});
|
|
1312
1599
|
|
|
@@ -1889,6 +2176,7 @@ async function analyzeImpact(className) {
|
|
|
1889
2176
|
diXmlReferences: [],
|
|
1890
2177
|
instantiations: [],
|
|
1891
2178
|
typeHints: [],
|
|
2179
|
+
runtimeCallers: [],
|
|
1892
2180
|
total: 0
|
|
1893
2181
|
};
|
|
1894
2182
|
|
|
@@ -1907,6 +2195,7 @@ async function analyzeImpact(className) {
|
|
|
1907
2195
|
];
|
|
1908
2196
|
|
|
1909
2197
|
// Check PHP files for direct references
|
|
2198
|
+
const escapedShort = shortName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
1910
2199
|
for (const r of relatedPaths.slice(0, 40)) {
|
|
1911
2200
|
const absPath = path.join(root, r.path);
|
|
1912
2201
|
if (!existsSync(absPath) || !r.path.endsWith('.php')) continue;
|
|
@@ -1921,13 +2210,38 @@ async function analyzeImpact(className) {
|
|
|
1921
2210
|
references.instantiations.push({ file: r.path, className: r.className });
|
|
1922
2211
|
}
|
|
1923
2212
|
if (content.includes(`@var ${shortName}`) || content.includes(`@param ${shortName}`) ||
|
|
1924
|
-
content.match(new RegExp(`:\\s*${
|
|
2213
|
+
content.match(new RegExp(`:\\s*${escapedShort}\\b`))) {
|
|
1925
2214
|
references.typeHints.push({ file: r.path, className: r.className });
|
|
1926
2215
|
}
|
|
2216
|
+
|
|
2217
|
+
// Runtime callers: find $this->property->method() where property is typed as this class
|
|
2218
|
+
if (hasUse) {
|
|
2219
|
+
const ctorMatch = content.match(/function\s+__construct\s*\(([\s\S]*?)\)\s*[{:]/);
|
|
2220
|
+
if (ctorMatch) {
|
|
2221
|
+
// Find constructor params typed as the target class
|
|
2222
|
+
const paramRegex = new RegExp(`(?:${escapedShort}|${className.replace(/\\/g, '\\\\')})\\s+\\$(\\w+)`, 'g');
|
|
2223
|
+
let pm;
|
|
2224
|
+
while ((pm = paramRegex.exec(ctorMatch[1])) !== null) {
|
|
2225
|
+
const propName = pm[1];
|
|
2226
|
+
// Find all calls to this property in the file
|
|
2227
|
+
const callRegex = new RegExp(`\\$this->${propName}->(\\w+)\\s*\\(`, 'g');
|
|
2228
|
+
let cm;
|
|
2229
|
+
while ((cm = callRegex.exec(content)) !== null) {
|
|
2230
|
+
references.runtimeCallers.push({
|
|
2231
|
+
file: r.path,
|
|
2232
|
+
callerClass: r.className,
|
|
2233
|
+
property: propName,
|
|
2234
|
+
calledMethod: cm[1]
|
|
2235
|
+
});
|
|
2236
|
+
}
|
|
2237
|
+
}
|
|
2238
|
+
}
|
|
2239
|
+
}
|
|
1927
2240
|
}
|
|
1928
2241
|
|
|
1929
2242
|
references.total = references.useStatements.length + references.diXmlReferences.length +
|
|
1930
|
-
references.instantiations.length + references.typeHints.length
|
|
2243
|
+
references.instantiations.length + references.typeHints.length +
|
|
2244
|
+
references.runtimeCallers.length;
|
|
1931
2245
|
|
|
1932
2246
|
return references;
|
|
1933
2247
|
}
|
|
@@ -2617,6 +2931,25 @@ async function traceCallChain(startClass, startMethod, maxDepth = 3) {
|
|
|
2617
2931
|
};
|
|
2618
2932
|
|
|
2619
2933
|
const classFileMap = new Map();
|
|
2934
|
+
const parentClassCache = new Map();
|
|
2935
|
+
|
|
2936
|
+
// Resolve parent class from extends declaration in PHP file content
|
|
2937
|
+
function resolveParentFromContent(content) {
|
|
2938
|
+
const extendsMatch = content.match(/class\s+\w+\s+extends\s+([\w\\]+)/);
|
|
2939
|
+
if (!extendsMatch) return null;
|
|
2940
|
+
const parent = extendsMatch[1];
|
|
2941
|
+
// If it's a short name, resolve using use statements
|
|
2942
|
+
if (!parent.includes('\\')) {
|
|
2943
|
+
const useMatch = content.match(new RegExp(`use\\s+([\\w\\\\]+\\\\${parent})\\s*;`));
|
|
2944
|
+
if (useMatch) return useMatch[1];
|
|
2945
|
+
// Check namespace-relative
|
|
2946
|
+
const nsMatch = content.match(/namespace\s+([\w\\]+)/);
|
|
2947
|
+
if (nsMatch) return `${nsMatch[1]}\\${parent}`;
|
|
2948
|
+
return parent;
|
|
2949
|
+
}
|
|
2950
|
+
// Leading backslash = fully qualified
|
|
2951
|
+
return parent.replace(/^\\/, '');
|
|
2952
|
+
}
|
|
2620
2953
|
|
|
2621
2954
|
async function resolveClassFile(className) {
|
|
2622
2955
|
const shortName = className.split('\\').pop();
|
|
@@ -2722,14 +3055,46 @@ async function traceCallChain(startClass, startMethod, maxDepth = 3) {
|
|
|
2722
3055
|
const relativePath = filePath.replace(root + '/', '');
|
|
2723
3056
|
|
|
2724
3057
|
// Extract method body (brace counting)
|
|
3058
|
+
const escapedMethod = methodName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
2725
3059
|
const methodRegex = new RegExp(
|
|
2726
|
-
`function\\s+${
|
|
3060
|
+
`function\\s+${escapedMethod}\\s*\\([^)]*\\)[^{]*\\{`
|
|
2727
3061
|
);
|
|
2728
|
-
|
|
3062
|
+
let methodStart = content.search(methodRegex);
|
|
3063
|
+
|
|
3064
|
+
// If method not found in this class, walk up the inheritance chain
|
|
3065
|
+
let resolvedClass = className;
|
|
3066
|
+
let resolvedContent = content;
|
|
3067
|
+
let resolvedFilePath = filePath;
|
|
2729
3068
|
if (methodStart === -1) {
|
|
2730
|
-
|
|
2731
|
-
|
|
3069
|
+
let currentContent = content;
|
|
3070
|
+
let found = false;
|
|
3071
|
+
const visited = new Set([className]);
|
|
3072
|
+
for (let i = 0; i < 10; i++) { // max 10 parent levels
|
|
3073
|
+
const parentFqcn = resolveParentFromContent(currentContent);
|
|
3074
|
+
if (!parentFqcn || visited.has(parentFqcn)) break;
|
|
3075
|
+
visited.add(parentFqcn);
|
|
3076
|
+
const parentFile = await resolveClassFile(parentFqcn);
|
|
3077
|
+
if (!parentFile) break;
|
|
3078
|
+
let parentContent;
|
|
3079
|
+
try { parentContent = readFileSync(parentFile, 'utf-8'); } catch { break; }
|
|
3080
|
+
const parentMethodStart = parentContent.search(methodRegex);
|
|
3081
|
+
if (parentMethodStart !== -1) {
|
|
3082
|
+
resolvedClass = parentFqcn;
|
|
3083
|
+
resolvedContent = parentContent;
|
|
3084
|
+
resolvedFilePath = parentFile;
|
|
3085
|
+
methodStart = parentMethodStart;
|
|
3086
|
+
found = true;
|
|
3087
|
+
break;
|
|
3088
|
+
}
|
|
3089
|
+
currentContent = parentContent;
|
|
3090
|
+
}
|
|
3091
|
+
if (!found) {
|
|
3092
|
+
result.chain.push({ depth, class: className, method: methodName, file: relativePath, status: 'method_not_found' });
|
|
3093
|
+
return;
|
|
3094
|
+
}
|
|
2732
3095
|
}
|
|
3096
|
+
content = resolvedContent;
|
|
3097
|
+
const resolvedRelPath = resolvedFilePath.replace(root + '/', '');
|
|
2733
3098
|
|
|
2734
3099
|
let braceCount = 0;
|
|
2735
3100
|
let bodyStart = content.indexOf('{', methodStart);
|
|
@@ -2741,8 +3106,11 @@ async function traceCallChain(startClass, startMethod, maxDepth = 3) {
|
|
|
2741
3106
|
}
|
|
2742
3107
|
const methodBody = content.slice(bodyStart, bodyEnd + 1);
|
|
2743
3108
|
|
|
3109
|
+
// Show where method was actually found if inherited
|
|
3110
|
+
const inheritedFrom = (resolvedClass !== className) ? resolvedClass : null;
|
|
2744
3111
|
const chainEntry = {
|
|
2745
3112
|
depth, class: className, method: methodName, file: relativePath,
|
|
3113
|
+
...(inheritedFrom ? { inheritedFrom, resolvedFile: resolvedRelPath } : {}),
|
|
2746
3114
|
calls: [], dispatches: []
|
|
2747
3115
|
};
|
|
2748
3116
|
|
|
@@ -2762,13 +3130,20 @@ async function traceCallChain(startClass, startMethod, maxDepth = 3) {
|
|
|
2762
3130
|
while ((dc = depCallRegex.exec(methodBody)) !== null) {
|
|
2763
3131
|
const property = dc[1];
|
|
2764
3132
|
const calledMethod = dc[2];
|
|
2765
|
-
// Resolve property type from constructor
|
|
2766
|
-
const ctorMatch = content.match(/function\s+__construct\s*\(([\s\S]*?)\)\s*[{:]/);
|
|
3133
|
+
// Resolve property type from constructor — check the original class first, then the resolved (parent) class
|
|
2767
3134
|
let resolvedType = null;
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
3135
|
+
let originalContent = content;
|
|
3136
|
+
if (resolvedClass !== className) {
|
|
3137
|
+
try { originalContent = readFileSync(filePath, 'utf-8'); } catch { originalContent = content; }
|
|
3138
|
+
}
|
|
3139
|
+
const contentSources = (resolvedClass !== className) ? [originalContent, content] : [content];
|
|
3140
|
+
for (const src of contentSources) {
|
|
3141
|
+
const ctorMatch = src.match(/function\s+__construct\s*\(([\s\S]*?)\)\s*[{:]/);
|
|
3142
|
+
if (ctorMatch) {
|
|
3143
|
+
const paramRegex = new RegExp(`([\\w\\\\]+)\\s+\\$${property}\\b`);
|
|
3144
|
+
const pm = ctorMatch[1].match(paramRegex);
|
|
3145
|
+
if (pm) { resolvedType = pm[1]; break; }
|
|
3146
|
+
}
|
|
2772
3147
|
}
|
|
2773
3148
|
chainEntry.calls.push({ type: 'dependency', property, method: calledMethod, typeHint: resolvedType || null });
|
|
2774
3149
|
}
|
|
@@ -2857,6 +3232,11 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
2857
3232
|
description: 'Enable query expansion with Magento domain synonyms for better recall (default: true)',
|
|
2858
3233
|
default: true
|
|
2859
3234
|
},
|
|
3235
|
+
precise: {
|
|
3236
|
+
type: 'boolean',
|
|
3237
|
+
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.',
|
|
3238
|
+
default: false
|
|
3239
|
+
},
|
|
2860
3240
|
},
|
|
2861
3241
|
required: ['query']
|
|
2862
3242
|
}
|
|
@@ -3199,6 +3579,36 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
3199
3579
|
}
|
|
3200
3580
|
}
|
|
3201
3581
|
},
|
|
3582
|
+
{
|
|
3583
|
+
name: 'magento_find_fieldset',
|
|
3584
|
+
description: 'Find fieldset.xml definitions that control how data is copied between Magento entities (e.g., order→quote, quote→order). Shows which fields are copied for each aspect (to_order, to_edit, to_quote). Essential for understanding data conversion flows like reorder, order edit, and checkout.',
|
|
3585
|
+
inputSchema: {
|
|
3586
|
+
type: 'object',
|
|
3587
|
+
properties: {
|
|
3588
|
+
fieldset: {
|
|
3589
|
+
type: 'string',
|
|
3590
|
+
description: 'Fieldset name or partial match. Examples: "sales_copy_order", "sales_convert_quote", "sales_convert_order", "customer_account"'
|
|
3591
|
+
},
|
|
3592
|
+
aspect: {
|
|
3593
|
+
type: 'string',
|
|
3594
|
+
description: 'Aspect name filter. Examples: "to_order", "to_edit", "to_quote", "to_customer"'
|
|
3595
|
+
}
|
|
3596
|
+
}
|
|
3597
|
+
}
|
|
3598
|
+
},
|
|
3599
|
+
{
|
|
3600
|
+
name: 'magento_trace_shipping_chain',
|
|
3601
|
+
description: 'Trace the complete shipping rate calculation chain: carrier classes → plugins on collectRates() → ShippingRateModifier pool → totals collectors → fieldset copy mappings. Use this to understand how shipping prices are calculated, modified, and propagated during checkout, reorder, or order edit.',
|
|
3602
|
+
inputSchema: {
|
|
3603
|
+
type: 'object',
|
|
3604
|
+
properties: {
|
|
3605
|
+
carrier: {
|
|
3606
|
+
type: 'string',
|
|
3607
|
+
description: 'Optional carrier or shipping method to focus on. Examples: "flatrate", "freeshipping", "tablerate", "innoship", "home_delivery", "pickup"'
|
|
3608
|
+
}
|
|
3609
|
+
}
|
|
3610
|
+
}
|
|
3611
|
+
},
|
|
3202
3612
|
{
|
|
3203
3613
|
name: 'magento_trace_flow',
|
|
3204
3614
|
description: 'Trace Magento execution flow from an entry point (route, API endpoint, GraphQL mutation, event, or cron job). Chains multiple searches to map controller → plugins → observers → templates for a given request path. Use this to understand how a request is processed end-to-end.',
|
|
@@ -3442,6 +3852,29 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
3442
3852
|
required: ['eventName']
|
|
3443
3853
|
}
|
|
3444
3854
|
},
|
|
3855
|
+
{
|
|
3856
|
+
name: 'magento_batch',
|
|
3857
|
+
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).',
|
|
3858
|
+
inputSchema: {
|
|
3859
|
+
type: 'object',
|
|
3860
|
+
properties: {
|
|
3861
|
+
queries: {
|
|
3862
|
+
type: 'array',
|
|
3863
|
+
description: 'Array of tool calls to execute. Each entry has a "tool" name and "args" object.',
|
|
3864
|
+
items: {
|
|
3865
|
+
type: 'object',
|
|
3866
|
+
properties: {
|
|
3867
|
+
tool: { type: 'string', description: 'Magector tool name (e.g., "magento_find_class", "magento_find_plugin")' },
|
|
3868
|
+
args: { type: 'object', description: 'Arguments for the tool call' }
|
|
3869
|
+
},
|
|
3870
|
+
required: ['tool', 'args']
|
|
3871
|
+
},
|
|
3872
|
+
maxItems: 10
|
|
3873
|
+
}
|
|
3874
|
+
},
|
|
3875
|
+
required: ['queries']
|
|
3876
|
+
}
|
|
3877
|
+
},
|
|
3445
3878
|
]
|
|
3446
3879
|
}));
|
|
3447
3880
|
|
|
@@ -3488,12 +3921,22 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
3488
3921
|
try {
|
|
3489
3922
|
switch (name) {
|
|
3490
3923
|
case 'magento_search': {
|
|
3491
|
-
const
|
|
3492
|
-
const
|
|
3924
|
+
const precise = args.precise === true;
|
|
3925
|
+
const searchQuery = (args.expand !== false && !precise) ? expandQuery(args.query) : args.query;
|
|
3926
|
+
const raw = await rustSearchAsync(searchQuery, Math.max(args.limit || 10, precise ? 60 : 30));
|
|
3493
3927
|
const arr = Array.isArray(raw) ? raw : [];
|
|
3494
3928
|
let results = arr.map(normalizeResult);
|
|
3495
3929
|
// Hybrid BM25 rerank for better exact-match handling
|
|
3496
3930
|
results = hybridRerank(results, args.query);
|
|
3931
|
+
// Precise mode: strict post-filter — result must contain at least one significant query keyword
|
|
3932
|
+
if (precise) {
|
|
3933
|
+
const queryTerms = args.query.toLowerCase().split(/\s+/).filter(t => t.length > 2);
|
|
3934
|
+
results = results.filter(r => {
|
|
3935
|
+
const haystack = [r.searchText, r.className, r.methodName, r.path, ...(r.methods || [])]
|
|
3936
|
+
.filter(Boolean).join(' ').toLowerCase();
|
|
3937
|
+
return queryTerms.some(term => haystack.includes(term));
|
|
3938
|
+
});
|
|
3939
|
+
}
|
|
3497
3940
|
// Apply module filter if specified
|
|
3498
3941
|
if (args.moduleFilter) {
|
|
3499
3942
|
results = filterByModule(results, args.moduleFilter);
|
|
@@ -3696,6 +4139,18 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
3696
4139
|
}
|
|
3697
4140
|
}
|
|
3698
4141
|
|
|
4142
|
+
// Resolve plugin methods for DI registrations
|
|
4143
|
+
const fpRoot = args.targetClass ? config.magentoRoot : null;
|
|
4144
|
+
for (const reg of diRegistrations) {
|
|
4145
|
+
if (reg.pluginClass && fpRoot) {
|
|
4146
|
+
const pluginFile = findClassFile(fpRoot, reg.pluginClass);
|
|
4147
|
+
if (pluginFile) {
|
|
4148
|
+
reg.methods = extractPluginMethods(pluginFile);
|
|
4149
|
+
reg.resolvedFile = pluginFile.replace(fpRoot + '/', '');
|
|
4150
|
+
}
|
|
4151
|
+
}
|
|
4152
|
+
}
|
|
4153
|
+
|
|
3699
4154
|
let text = formatSearchResults(enrichedResults);
|
|
3700
4155
|
if (diRegistrations.length > 0) {
|
|
3701
4156
|
text += `\n\n### DI Plugin Registrations for ${args.targetClass} (${diRegistrations.length})\n`;
|
|
@@ -3703,6 +4158,14 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
3703
4158
|
const disabledTag = reg.disabled ? ' **[DISABLED]**' : '';
|
|
3704
4159
|
const sortTag = reg.sortOrder ? ` (sortOrder: ${reg.sortOrder})` : '';
|
|
3705
4160
|
text += `- **${reg.pluginName}** → \`${reg.pluginClass}\` [${reg.area}]${sortTag}${disabledTag} (${reg.file})\n`;
|
|
4161
|
+
if (reg.resolvedFile) {
|
|
4162
|
+
text += ` PHP: \`${reg.resolvedFile}\`\n`;
|
|
4163
|
+
}
|
|
4164
|
+
if (reg.methods?.length > 0) {
|
|
4165
|
+
for (const m of reg.methods) {
|
|
4166
|
+
text += ` - \`${m.type}\` **${m.targetMethod}** → \`${m.name}()\`\n`;
|
|
4167
|
+
}
|
|
4168
|
+
}
|
|
3706
4169
|
}
|
|
3707
4170
|
}
|
|
3708
4171
|
|
|
@@ -4222,6 +4685,93 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
4222
4685
|
}
|
|
4223
4686
|
}
|
|
4224
4687
|
|
|
4688
|
+
case 'magento_find_fieldset': {
|
|
4689
|
+
const fieldsets = await parseFieldsetXml(args.fieldset || null, args.aspect || null);
|
|
4690
|
+
if (fieldsets.length === 0) {
|
|
4691
|
+
return { content: [{ type: 'text', text: 'No fieldset.xml definitions found matching the criteria.' }] };
|
|
4692
|
+
}
|
|
4693
|
+
let text = `## Fieldset Definitions (${fieldsets.length} matches)\n\n`;
|
|
4694
|
+
for (const fs of fieldsets) {
|
|
4695
|
+
text += `### ${fs.scope ? `${fs.scope} / ` : ''}${fs.fieldset}\n`;
|
|
4696
|
+
text += `File: \`${fs.file}\`\n`;
|
|
4697
|
+
// Group fields by aspect
|
|
4698
|
+
const byAspect = {};
|
|
4699
|
+
for (const f of fs.fields) {
|
|
4700
|
+
if (!byAspect[f.aspect]) byAspect[f.aspect] = [];
|
|
4701
|
+
byAspect[f.aspect].push(f.field);
|
|
4702
|
+
}
|
|
4703
|
+
for (const [aspect, fields] of Object.entries(byAspect)) {
|
|
4704
|
+
text += `- **${aspect}**: ${fields.map(f => `\`${f}\``).join(', ')}\n`;
|
|
4705
|
+
}
|
|
4706
|
+
text += '\n';
|
|
4707
|
+
}
|
|
4708
|
+
return { content: [{ type: 'text', text }] };
|
|
4709
|
+
}
|
|
4710
|
+
|
|
4711
|
+
case 'magento_trace_shipping_chain': {
|
|
4712
|
+
const chain = await traceShippingChain(args.carrier || null);
|
|
4713
|
+
let text = `## Shipping Rate Chain\n\n`;
|
|
4714
|
+
|
|
4715
|
+
if (chain.carriers.length > 0) {
|
|
4716
|
+
text += `### Carriers (${chain.carriers.length})\n`;
|
|
4717
|
+
for (const c of chain.carriers) {
|
|
4718
|
+
text += `- \`${c.className || c.path}\` (${c.path})\n`;
|
|
4719
|
+
if (c.methods?.length > 0) text += ` Methods: ${c.methods.join(', ')}\n`;
|
|
4720
|
+
}
|
|
4721
|
+
text += '\n';
|
|
4722
|
+
}
|
|
4723
|
+
|
|
4724
|
+
if (chain.collectRatesPlugins.length > 0) {
|
|
4725
|
+
text += `### Plugins on collectRates (${chain.collectRatesPlugins.length})\n`;
|
|
4726
|
+
for (const p of chain.collectRatesPlugins) {
|
|
4727
|
+
const disabledTag = p.disabled ? ' **[DISABLED]**' : '';
|
|
4728
|
+
const sortTag = p.sortOrder ? ` (sortOrder: ${p.sortOrder})` : '';
|
|
4729
|
+
text += `- **${p.pluginName}** → \`${p.pluginClass}\` [${p.area}]${sortTag}${disabledTag}\n`;
|
|
4730
|
+
text += ` Target: \`${p.target}\` | DI: \`${p.file}\`\n`;
|
|
4731
|
+
if (p.methods?.length > 0) {
|
|
4732
|
+
for (const m of p.methods) {
|
|
4733
|
+
text += ` - \`${m.type}\` **${m.targetMethod}** → \`${m.name}()\`\n`;
|
|
4734
|
+
}
|
|
4735
|
+
}
|
|
4736
|
+
}
|
|
4737
|
+
text += '\n';
|
|
4738
|
+
}
|
|
4739
|
+
|
|
4740
|
+
if (chain.rateModifiers.length > 0) {
|
|
4741
|
+
text += `### Shipping Rate Modifiers (${chain.rateModifiers.length})\n`;
|
|
4742
|
+
for (const m of chain.rateModifiers) {
|
|
4743
|
+
text += `- \`${m.className || m.path}\` (${m.path})\n`;
|
|
4744
|
+
}
|
|
4745
|
+
text += '\n';
|
|
4746
|
+
}
|
|
4747
|
+
|
|
4748
|
+
if (chain.totalsCollectors.length > 0) {
|
|
4749
|
+
text += `### Totals Collectors (${chain.totalsCollectors.length})\n`;
|
|
4750
|
+
for (const t of chain.totalsCollectors) {
|
|
4751
|
+
text += `- \`${t.className || t.path}\` (${t.path})\n`;
|
|
4752
|
+
}
|
|
4753
|
+
text += '\n';
|
|
4754
|
+
}
|
|
4755
|
+
|
|
4756
|
+
if (chain.fieldsets.length > 0) {
|
|
4757
|
+
text += `### Related Fieldsets (${chain.fieldsets.length})\n`;
|
|
4758
|
+
for (const fs of chain.fieldsets) {
|
|
4759
|
+
const byAspect = {};
|
|
4760
|
+
for (const f of fs.fields) {
|
|
4761
|
+
if (!byAspect[f.aspect]) byAspect[f.aspect] = [];
|
|
4762
|
+
byAspect[f.aspect].push(f.field);
|
|
4763
|
+
}
|
|
4764
|
+
text += `- **${fs.scope || fs.fieldset}** (${fs.file})\n`;
|
|
4765
|
+
for (const [aspect, fields] of Object.entries(byAspect)) {
|
|
4766
|
+
text += ` - ${aspect}: ${fields.map(f => `\`${f}\``).join(', ')}\n`;
|
|
4767
|
+
}
|
|
4768
|
+
}
|
|
4769
|
+
text += '\n';
|
|
4770
|
+
}
|
|
4771
|
+
|
|
4772
|
+
return { content: [{ type: 'text', text }] };
|
|
4773
|
+
}
|
|
4774
|
+
|
|
4225
4775
|
case 'magento_trace_flow': {
|
|
4226
4776
|
const entryPoint = args.entryPoint;
|
|
4227
4777
|
const entryType = args.entryType || 'auto';
|
|
@@ -4426,6 +4976,25 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
4426
4976
|
text += '\n';
|
|
4427
4977
|
}
|
|
4428
4978
|
|
|
4979
|
+
if (impact.runtimeCallers.length > 0) {
|
|
4980
|
+
text += `### Runtime Callers (${impact.runtimeCallers.length})\n`;
|
|
4981
|
+
text += '_Classes that inject this class and call its methods at runtime:_\n';
|
|
4982
|
+
// Group by caller class for readability
|
|
4983
|
+
const grouped = new Map();
|
|
4984
|
+
for (const c of impact.runtimeCallers) {
|
|
4985
|
+
const key = c.callerClass || c.file;
|
|
4986
|
+
if (!grouped.has(key)) grouped.set(key, { file: c.file, methods: new Set() });
|
|
4987
|
+
grouped.get(key).methods.add(`$this->${c.property}->${c.calledMethod}()`);
|
|
4988
|
+
}
|
|
4989
|
+
for (const [cls, info] of grouped) {
|
|
4990
|
+
text += `- **\`${cls}\`** (\`${info.file}\`)\n`;
|
|
4991
|
+
for (const m of info.methods) {
|
|
4992
|
+
text += ` - \`${m}\`\n`;
|
|
4993
|
+
}
|
|
4994
|
+
}
|
|
4995
|
+
text += '\n';
|
|
4996
|
+
}
|
|
4997
|
+
|
|
4429
4998
|
if (impact.total === 0) {
|
|
4430
4999
|
text += '_No references found. The class may be referenced under a different name or alias._\n';
|
|
4431
5000
|
}
|
|
@@ -4633,6 +5202,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
4633
5202
|
const status = entry.status ? ` [${entry.status}]` : '';
|
|
4634
5203
|
text += `${indent}- **${entry.class}::${entry.method}**${status}`;
|
|
4635
5204
|
if (entry.file) text += ` (${entry.file})`;
|
|
5205
|
+
if (entry.inheritedFrom) text += `\n${indent} _inherited from_ \`${entry.inheritedFrom}\` (${entry.resolvedFile})`;
|
|
4636
5206
|
text += '\n';
|
|
4637
5207
|
|
|
4638
5208
|
if (entry.calls) {
|
|
@@ -4746,6 +5316,100 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
4746
5316
|
return { content: [{ type: 'text', text }] };
|
|
4747
5317
|
}
|
|
4748
5318
|
|
|
5319
|
+
case 'magento_batch': {
|
|
5320
|
+
const queries = args.queries || [];
|
|
5321
|
+
if (queries.length === 0) {
|
|
5322
|
+
return { content: [{ type: 'text', text: 'No queries provided.' }] };
|
|
5323
|
+
}
|
|
5324
|
+
if (queries.length > 10) {
|
|
5325
|
+
return { content: [{ type: 'text', text: 'Maximum 10 queries per batch.' }], isError: true };
|
|
5326
|
+
}
|
|
5327
|
+
// Run batch queries in parallel using existing standalone functions
|
|
5328
|
+
const batchResults = await Promise.all(queries.map(async (q, idx) => {
|
|
5329
|
+
try {
|
|
5330
|
+
const a = q.args || {};
|
|
5331
|
+
let text = '';
|
|
5332
|
+
switch (q.tool) {
|
|
5333
|
+
case 'magento_find_class': {
|
|
5334
|
+
const ns = a.namespace || '';
|
|
5335
|
+
const qr = `${a.className} ${ns}`.trim();
|
|
5336
|
+
const raw = await rustSearchAsync(qr, 30);
|
|
5337
|
+
const cl = a.className.toLowerCase();
|
|
5338
|
+
const res = raw.map(normalizeResult).filter(r =>
|
|
5339
|
+
r.className?.toLowerCase().includes(cl) || r.path?.toLowerCase().includes(cl.replace(/\\/g, '/'))
|
|
5340
|
+
);
|
|
5341
|
+
text = formatSearchResults(res.slice(0, 5));
|
|
5342
|
+
break;
|
|
5343
|
+
}
|
|
5344
|
+
case 'magento_find_plugin': {
|
|
5345
|
+
const qr = `plugin interceptor ${a.targetClass || ''} ${a.targetMethod || ''}`.trim();
|
|
5346
|
+
const raw = await rustSearchAsync(qr, 30);
|
|
5347
|
+
const res = raw.map(normalizeResult).filter(r =>
|
|
5348
|
+
r.magentoType === 'plugin' || r.path?.toLowerCase().includes('plugin')
|
|
5349
|
+
);
|
|
5350
|
+
text = formatSearchResults(res.slice(0, 10));
|
|
5351
|
+
break;
|
|
5352
|
+
}
|
|
5353
|
+
case 'magento_find_observer': {
|
|
5354
|
+
const flow = await traceEventFlow(a.eventName);
|
|
5355
|
+
text = `Observers: ${flow.observers.length}\n`;
|
|
5356
|
+
for (const o of flow.observers.slice(0, 10)) {
|
|
5357
|
+
text += `- ${o.name}: ${o.instance}::${o.method} (${o.file})\n`;
|
|
5358
|
+
}
|
|
5359
|
+
break;
|
|
5360
|
+
}
|
|
5361
|
+
case 'magento_trace_dependency': {
|
|
5362
|
+
const dep = await traceDependency(a.className, a.direction || 'both');
|
|
5363
|
+
text = `Preferences: ${dep.preferences.length}, Plugins: ${dep.plugins.length}, VirtualTypes: ${dep.virtualTypes.length}, Args: ${dep.argumentOverrides.length}\n`;
|
|
5364
|
+
for (const p of dep.plugins.slice(0, 5)) text += `- plugin: ${p.pluginName} → ${p.pluginClass}\n`;
|
|
5365
|
+
for (const p of dep.preferences.slice(0, 5)) text += `- pref: ${p.for} → ${p.type}\n`;
|
|
5366
|
+
break;
|
|
5367
|
+
}
|
|
5368
|
+
case 'magento_impact_analysis': {
|
|
5369
|
+
const imp = await analyzeImpact(a.className);
|
|
5370
|
+
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`;
|
|
5371
|
+
for (const c of imp.runtimeCallers.slice(0, 5)) text += `- ${c.callerClass}: $this->${c.property}->${c.calledMethod}()\n`;
|
|
5372
|
+
break;
|
|
5373
|
+
}
|
|
5374
|
+
case 'magento_search': {
|
|
5375
|
+
const precise = a.precise === true;
|
|
5376
|
+
const sq = (a.expand !== false && !precise) ? expandQuery(a.query) : a.query;
|
|
5377
|
+
const raw = await rustSearchAsync(sq, 30);
|
|
5378
|
+
let res = raw.map(normalizeResult);
|
|
5379
|
+
res = hybridRerank(res, a.query);
|
|
5380
|
+
text = formatSearchResults(res.slice(0, a.limit || 5));
|
|
5381
|
+
break;
|
|
5382
|
+
}
|
|
5383
|
+
case 'magento_find_callers': {
|
|
5384
|
+
const callers = await findCallers(a.methodName, a.className);
|
|
5385
|
+
text = `Callers: ${callers.callers?.length || 0}\n`;
|
|
5386
|
+
for (const c of (callers.callers || []).slice(0, 10)) {
|
|
5387
|
+
text += `- ${c.class}::${c.method} (${c.path}:${c.line})\n`;
|
|
5388
|
+
}
|
|
5389
|
+
break;
|
|
5390
|
+
}
|
|
5391
|
+
case 'magento_find_event_flow': {
|
|
5392
|
+
const flow = await traceEventFlow(a.eventName);
|
|
5393
|
+
text = `Dispatchers: ${flow.dispatchers.length}, Observers: ${flow.observers.length}\n`;
|
|
5394
|
+
for (const o of flow.observers.slice(0, 10)) text += `- ${o.name}: ${o.instance} (${o.file})\n`;
|
|
5395
|
+
break;
|
|
5396
|
+
}
|
|
5397
|
+
default:
|
|
5398
|
+
text = `Unsupported batch tool: ${q.tool}`;
|
|
5399
|
+
}
|
|
5400
|
+
return { idx, tool: q.tool, text };
|
|
5401
|
+
} catch (err) {
|
|
5402
|
+
return { idx, tool: q.tool, text: `Error: ${err.message}` };
|
|
5403
|
+
}
|
|
5404
|
+
}));
|
|
5405
|
+
|
|
5406
|
+
let text = `## Batch Results (${batchResults.length} queries)\n\n`;
|
|
5407
|
+
for (const br of batchResults) {
|
|
5408
|
+
text += `---\n### [${br.idx + 1}] ${br.tool}\n\n${br.text}\n\n`;
|
|
5409
|
+
}
|
|
5410
|
+
return { content: [{ type: 'text', text }] };
|
|
5411
|
+
}
|
|
5412
|
+
|
|
4749
5413
|
default:
|
|
4750
5414
|
return {
|
|
4751
5415
|
content: [{
|