magector 2.4.0 → 2.5.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.
- package/README.md +4 -2
- package/package.json +5 -5
- package/src/mcp-server.js +445 -10
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.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.
|
|
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.0",
|
|
37
|
+
"@magector/cli-linux-x64": "2.5.0",
|
|
38
|
+
"@magector/cli-linux-arm64": "2.5.0",
|
|
39
|
+
"@magector/cli-win32-x64": "2.5.0"
|
|
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
|
|
|
@@ -3199,6 +3486,36 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
3199
3486
|
}
|
|
3200
3487
|
}
|
|
3201
3488
|
},
|
|
3489
|
+
{
|
|
3490
|
+
name: 'magento_find_fieldset',
|
|
3491
|
+
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.',
|
|
3492
|
+
inputSchema: {
|
|
3493
|
+
type: 'object',
|
|
3494
|
+
properties: {
|
|
3495
|
+
fieldset: {
|
|
3496
|
+
type: 'string',
|
|
3497
|
+
description: 'Fieldset name or partial match. Examples: "sales_copy_order", "sales_convert_quote", "sales_convert_order", "customer_account"'
|
|
3498
|
+
},
|
|
3499
|
+
aspect: {
|
|
3500
|
+
type: 'string',
|
|
3501
|
+
description: 'Aspect name filter. Examples: "to_order", "to_edit", "to_quote", "to_customer"'
|
|
3502
|
+
}
|
|
3503
|
+
}
|
|
3504
|
+
}
|
|
3505
|
+
},
|
|
3506
|
+
{
|
|
3507
|
+
name: 'magento_trace_shipping_chain',
|
|
3508
|
+
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.',
|
|
3509
|
+
inputSchema: {
|
|
3510
|
+
type: 'object',
|
|
3511
|
+
properties: {
|
|
3512
|
+
carrier: {
|
|
3513
|
+
type: 'string',
|
|
3514
|
+
description: 'Optional carrier or shipping method to focus on. Examples: "flatrate", "freeshipping", "tablerate", "innoship", "home_delivery", "pickup"'
|
|
3515
|
+
}
|
|
3516
|
+
}
|
|
3517
|
+
}
|
|
3518
|
+
},
|
|
3202
3519
|
{
|
|
3203
3520
|
name: 'magento_trace_flow',
|
|
3204
3521
|
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.',
|
|
@@ -3655,30 +3972,39 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
3655
3972
|
if (args.targetClass) {
|
|
3656
3973
|
const fpRoot = config.magentoRoot;
|
|
3657
3974
|
const diFiles = await glob('**/etc/**/di.xml', { cwd: fpRoot, absolute: true, nodir: true });
|
|
3658
|
-
|
|
3975
|
+
// Normalize target class for matching (both \ and \\)
|
|
3976
|
+
const normalizedTarget = args.targetClass.replace(/\\\\/g, '\\');
|
|
3659
3977
|
for (const diFile of diFiles) {
|
|
3660
3978
|
let content;
|
|
3661
3979
|
try { content = readFileSync(diFile, 'utf-8'); } catch { continue; }
|
|
3662
|
-
if (!content.includes(
|
|
3980
|
+
if (!content.includes(normalizedTarget)) continue;
|
|
3663
3981
|
const relPath = diFile.replace(fpRoot + '/', '');
|
|
3664
3982
|
// Find plugin registrations for this target
|
|
3665
3983
|
const typeBlockRegex = /<type\s+name="([^"]+)"[^>]*>([\s\S]*?)<\/type>/g;
|
|
3666
3984
|
let tm;
|
|
3667
3985
|
while ((tm = typeBlockRegex.exec(content)) !== null) {
|
|
3668
|
-
const typeName = tm[1];
|
|
3669
|
-
if (
|
|
3986
|
+
const typeName = tm[1].replace(/\\\\/g, '\\');
|
|
3987
|
+
if (typeName !== normalizedTarget) continue;
|
|
3670
3988
|
const block = tm[2];
|
|
3671
|
-
const pluginRegex = /<plugin\s+
|
|
3989
|
+
const pluginRegex = /<plugin\s+([^/>]*)\/?>/g;
|
|
3672
3990
|
let pm;
|
|
3673
3991
|
while ((pm = pluginRegex.exec(block)) !== null) {
|
|
3992
|
+
const attrs = {};
|
|
3993
|
+
const localAttrRe = /(\w+)="([^"]*)"/g;
|
|
3994
|
+
let am;
|
|
3995
|
+
while ((am = localAttrRe.exec(pm[1])) !== null) {
|
|
3996
|
+
attrs[am[1]] = am[2];
|
|
3997
|
+
}
|
|
3674
3998
|
let area = 'global';
|
|
3675
3999
|
if (relPath.includes('/etc/adminhtml/')) area = 'adminhtml';
|
|
3676
4000
|
else if (relPath.includes('/etc/frontend/')) area = 'frontend';
|
|
3677
4001
|
else if (relPath.includes('/etc/graphql/')) area = 'graphql';
|
|
3678
4002
|
diRegistrations.push({
|
|
3679
4003
|
target: typeName,
|
|
3680
|
-
pluginName:
|
|
3681
|
-
pluginClass:
|
|
4004
|
+
pluginName: attrs.name || '',
|
|
4005
|
+
pluginClass: attrs.type || '',
|
|
4006
|
+
disabled: attrs.disabled === 'true',
|
|
4007
|
+
sortOrder: attrs.sortOrder || null,
|
|
3682
4008
|
area,
|
|
3683
4009
|
file: relPath
|
|
3684
4010
|
});
|
|
@@ -3687,11 +4013,33 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
3687
4013
|
}
|
|
3688
4014
|
}
|
|
3689
4015
|
|
|
4016
|
+
// Resolve plugin methods for DI registrations
|
|
4017
|
+
const fpRoot = args.targetClass ? config.magentoRoot : null;
|
|
4018
|
+
for (const reg of diRegistrations) {
|
|
4019
|
+
if (reg.pluginClass && fpRoot) {
|
|
4020
|
+
const pluginFile = findClassFile(fpRoot, reg.pluginClass);
|
|
4021
|
+
if (pluginFile) {
|
|
4022
|
+
reg.methods = extractPluginMethods(pluginFile);
|
|
4023
|
+
reg.resolvedFile = pluginFile.replace(fpRoot + '/', '');
|
|
4024
|
+
}
|
|
4025
|
+
}
|
|
4026
|
+
}
|
|
4027
|
+
|
|
3690
4028
|
let text = formatSearchResults(enrichedResults);
|
|
3691
4029
|
if (diRegistrations.length > 0) {
|
|
3692
4030
|
text += `\n\n### DI Plugin Registrations for ${args.targetClass} (${diRegistrations.length})\n`;
|
|
3693
4031
|
for (const reg of diRegistrations) {
|
|
3694
|
-
|
|
4032
|
+
const disabledTag = reg.disabled ? ' **[DISABLED]**' : '';
|
|
4033
|
+
const sortTag = reg.sortOrder ? ` (sortOrder: ${reg.sortOrder})` : '';
|
|
4034
|
+
text += `- **${reg.pluginName}** → \`${reg.pluginClass}\` [${reg.area}]${sortTag}${disabledTag} (${reg.file})\n`;
|
|
4035
|
+
if (reg.resolvedFile) {
|
|
4036
|
+
text += ` PHP: \`${reg.resolvedFile}\`\n`;
|
|
4037
|
+
}
|
|
4038
|
+
if (reg.methods?.length > 0) {
|
|
4039
|
+
for (const m of reg.methods) {
|
|
4040
|
+
text += ` - \`${m.type}\` **${m.targetMethod}** → \`${m.name}()\`\n`;
|
|
4041
|
+
}
|
|
4042
|
+
}
|
|
3695
4043
|
}
|
|
3696
4044
|
}
|
|
3697
4045
|
|
|
@@ -4211,6 +4559,93 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
4211
4559
|
}
|
|
4212
4560
|
}
|
|
4213
4561
|
|
|
4562
|
+
case 'magento_find_fieldset': {
|
|
4563
|
+
const fieldsets = await parseFieldsetXml(args.fieldset || null, args.aspect || null);
|
|
4564
|
+
if (fieldsets.length === 0) {
|
|
4565
|
+
return { content: [{ type: 'text', text: 'No fieldset.xml definitions found matching the criteria.' }] };
|
|
4566
|
+
}
|
|
4567
|
+
let text = `## Fieldset Definitions (${fieldsets.length} matches)\n\n`;
|
|
4568
|
+
for (const fs of fieldsets) {
|
|
4569
|
+
text += `### ${fs.scope ? `${fs.scope} / ` : ''}${fs.fieldset}\n`;
|
|
4570
|
+
text += `File: \`${fs.file}\`\n`;
|
|
4571
|
+
// Group fields by aspect
|
|
4572
|
+
const byAspect = {};
|
|
4573
|
+
for (const f of fs.fields) {
|
|
4574
|
+
if (!byAspect[f.aspect]) byAspect[f.aspect] = [];
|
|
4575
|
+
byAspect[f.aspect].push(f.field);
|
|
4576
|
+
}
|
|
4577
|
+
for (const [aspect, fields] of Object.entries(byAspect)) {
|
|
4578
|
+
text += `- **${aspect}**: ${fields.map(f => `\`${f}\``).join(', ')}\n`;
|
|
4579
|
+
}
|
|
4580
|
+
text += '\n';
|
|
4581
|
+
}
|
|
4582
|
+
return { content: [{ type: 'text', text }] };
|
|
4583
|
+
}
|
|
4584
|
+
|
|
4585
|
+
case 'magento_trace_shipping_chain': {
|
|
4586
|
+
const chain = await traceShippingChain(args.carrier || null);
|
|
4587
|
+
let text = `## Shipping Rate Chain\n\n`;
|
|
4588
|
+
|
|
4589
|
+
if (chain.carriers.length > 0) {
|
|
4590
|
+
text += `### Carriers (${chain.carriers.length})\n`;
|
|
4591
|
+
for (const c of chain.carriers) {
|
|
4592
|
+
text += `- \`${c.className || c.path}\` (${c.path})\n`;
|
|
4593
|
+
if (c.methods?.length > 0) text += ` Methods: ${c.methods.join(', ')}\n`;
|
|
4594
|
+
}
|
|
4595
|
+
text += '\n';
|
|
4596
|
+
}
|
|
4597
|
+
|
|
4598
|
+
if (chain.collectRatesPlugins.length > 0) {
|
|
4599
|
+
text += `### Plugins on collectRates (${chain.collectRatesPlugins.length})\n`;
|
|
4600
|
+
for (const p of chain.collectRatesPlugins) {
|
|
4601
|
+
const disabledTag = p.disabled ? ' **[DISABLED]**' : '';
|
|
4602
|
+
const sortTag = p.sortOrder ? ` (sortOrder: ${p.sortOrder})` : '';
|
|
4603
|
+
text += `- **${p.pluginName}** → \`${p.pluginClass}\` [${p.area}]${sortTag}${disabledTag}\n`;
|
|
4604
|
+
text += ` Target: \`${p.target}\` | DI: \`${p.file}\`\n`;
|
|
4605
|
+
if (p.methods?.length > 0) {
|
|
4606
|
+
for (const m of p.methods) {
|
|
4607
|
+
text += ` - \`${m.type}\` **${m.targetMethod}** → \`${m.name}()\`\n`;
|
|
4608
|
+
}
|
|
4609
|
+
}
|
|
4610
|
+
}
|
|
4611
|
+
text += '\n';
|
|
4612
|
+
}
|
|
4613
|
+
|
|
4614
|
+
if (chain.rateModifiers.length > 0) {
|
|
4615
|
+
text += `### Shipping Rate Modifiers (${chain.rateModifiers.length})\n`;
|
|
4616
|
+
for (const m of chain.rateModifiers) {
|
|
4617
|
+
text += `- \`${m.className || m.path}\` (${m.path})\n`;
|
|
4618
|
+
}
|
|
4619
|
+
text += '\n';
|
|
4620
|
+
}
|
|
4621
|
+
|
|
4622
|
+
if (chain.totalsCollectors.length > 0) {
|
|
4623
|
+
text += `### Totals Collectors (${chain.totalsCollectors.length})\n`;
|
|
4624
|
+
for (const t of chain.totalsCollectors) {
|
|
4625
|
+
text += `- \`${t.className || t.path}\` (${t.path})\n`;
|
|
4626
|
+
}
|
|
4627
|
+
text += '\n';
|
|
4628
|
+
}
|
|
4629
|
+
|
|
4630
|
+
if (chain.fieldsets.length > 0) {
|
|
4631
|
+
text += `### Related Fieldsets (${chain.fieldsets.length})\n`;
|
|
4632
|
+
for (const fs of chain.fieldsets) {
|
|
4633
|
+
const byAspect = {};
|
|
4634
|
+
for (const f of fs.fields) {
|
|
4635
|
+
if (!byAspect[f.aspect]) byAspect[f.aspect] = [];
|
|
4636
|
+
byAspect[f.aspect].push(f.field);
|
|
4637
|
+
}
|
|
4638
|
+
text += `- **${fs.scope || fs.fieldset}** (${fs.file})\n`;
|
|
4639
|
+
for (const [aspect, fields] of Object.entries(byAspect)) {
|
|
4640
|
+
text += ` - ${aspect}: ${fields.map(f => `\`${f}\``).join(', ')}\n`;
|
|
4641
|
+
}
|
|
4642
|
+
}
|
|
4643
|
+
text += '\n';
|
|
4644
|
+
}
|
|
4645
|
+
|
|
4646
|
+
return { content: [{ type: 'text', text }] };
|
|
4647
|
+
}
|
|
4648
|
+
|
|
4214
4649
|
case 'magento_trace_flow': {
|
|
4215
4650
|
const entryPoint = args.entryPoint;
|
|
4216
4651
|
const entryType = args.entryType || 'auto';
|