magector 2.2.4 → 2.2.6
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 +789 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "magector",
|
|
3
|
-
"version": "2.2.
|
|
3
|
+
"version": "2.2.6",
|
|
4
4
|
"description": "Semantic code search for Magento 2 — index, search, MCP server",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/mcp-server.js",
|
|
@@ -39,10 +39,10 @@
|
|
|
39
39
|
"ruvector": "^0.1.96"
|
|
40
40
|
},
|
|
41
41
|
"optionalDependencies": {
|
|
42
|
-
"@magector/cli-darwin-arm64": "2.2.
|
|
43
|
-
"@magector/cli-linux-x64": "2.2.
|
|
44
|
-
"@magector/cli-linux-arm64": "2.2.
|
|
45
|
-
"@magector/cli-win32-x64": "2.2.
|
|
42
|
+
"@magector/cli-darwin-arm64": "2.2.6",
|
|
43
|
+
"@magector/cli-linux-x64": "2.2.6",
|
|
44
|
+
"@magector/cli-linux-arm64": "2.2.6",
|
|
45
|
+
"@magector/cli-win32-x64": "2.2.6"
|
|
46
46
|
},
|
|
47
47
|
"keywords": [
|
|
48
48
|
"magento",
|
package/src/mcp-server.js
CHANGED
|
@@ -1773,6 +1773,11 @@ function expandQuery(query) {
|
|
|
1773
1773
|
|
|
1774
1774
|
// ─── Module Filtering ───────────────────────────────────────────
|
|
1775
1775
|
// Filter search results by vendor/module namespace pattern.
|
|
1776
|
+
// Module values in the index use composer format: "vendor_package"
|
|
1777
|
+
// (e.g. "some-vendor_module-name"). Users may write patterns with
|
|
1778
|
+
// "/" or "_" as separator, and may use a vendor prefix that matches
|
|
1779
|
+
// only the start of the full vendor name (e.g. "acme/*" matching
|
|
1780
|
+
// "acme-extensions_module-foo").
|
|
1776
1781
|
|
|
1777
1782
|
function filterByModule(results, moduleFilter) {
|
|
1778
1783
|
if (!moduleFilter) return results;
|
|
@@ -1782,11 +1787,43 @@ function filterByModule(results, moduleFilter) {
|
|
|
1782
1787
|
const filePath = r.path || '';
|
|
1783
1788
|
return patterns.some(pat => {
|
|
1784
1789
|
if (pat.includes('*')) {
|
|
1785
|
-
|
|
1786
|
-
|
|
1790
|
+
// 1. Exact regex match — treat / and _ as interchangeable separators
|
|
1791
|
+
const normalized = pat.replace(/[/_]/g, '[/_]');
|
|
1792
|
+
const regex = new RegExp('^' + normalized.replace(/\*/g, '.*') + '$', 'i');
|
|
1793
|
+
if (regex.test(mod) || regex.test(filePath)) return true;
|
|
1794
|
+
|
|
1795
|
+
// 2. Vendor prefix match: "vendor/*" should also match "vendor-extended_*"
|
|
1796
|
+
// Extract the vendor part (everything before the first separator or wildcard)
|
|
1797
|
+
const vendorPrefix = pat.split(/[/*_]/)[0];
|
|
1798
|
+
if (vendorPrefix) {
|
|
1799
|
+
const pfx = vendorPrefix.toLowerCase();
|
|
1800
|
+
// Module field starts with the vendor prefix
|
|
1801
|
+
if (mod.toLowerCase().startsWith(pfx)) return true;
|
|
1802
|
+
// File path contains vendor/<prefix> (covers vendor/ directory)
|
|
1803
|
+
if (filePath.toLowerCase().includes('vendor/' + pfx) ||
|
|
1804
|
+
filePath.toLowerCase().includes('app/code/' + pfx)) return true;
|
|
1805
|
+
}
|
|
1806
|
+
return false;
|
|
1807
|
+
}
|
|
1808
|
+
// Non-wildcard: substring match on module and path, with separator normalization
|
|
1809
|
+
const patLower = pat.toLowerCase();
|
|
1810
|
+
if (mod.toLowerCase().includes(patLower) || filePath.toLowerCase().includes(patLower)) return true;
|
|
1811
|
+
// Structured match: split "Vendor_Module" into parts and match against
|
|
1812
|
+
// index format "vendor_module-name" (composer packages use "module-" prefix)
|
|
1813
|
+
const patParts = patLower.split(/[/_]/);
|
|
1814
|
+
if (patParts.length >= 2) {
|
|
1815
|
+
const modLower = mod.toLowerCase();
|
|
1816
|
+
const modParts = modLower.split(/[/_]/);
|
|
1817
|
+
if (modParts.length >= 2) {
|
|
1818
|
+
const vendorMatch = modParts[0].startsWith(patParts[0]) || patParts[0].startsWith(modParts[0]);
|
|
1819
|
+
const modulePart = modParts.slice(1).join('-');
|
|
1820
|
+
const patModule = patParts.slice(1).join('-');
|
|
1821
|
+
// "module-catalog" contains "catalog", or "catalog" matches after stripping "module-"
|
|
1822
|
+
const moduleMatch = modulePart.includes(patModule) || modulePart.replace(/^module-/, '').includes(patModule);
|
|
1823
|
+
if (vendorMatch && moduleMatch) return true;
|
|
1824
|
+
}
|
|
1787
1825
|
}
|
|
1788
|
-
return
|
|
1789
|
-
filePath.toLowerCase().includes(pat.toLowerCase());
|
|
1826
|
+
return false;
|
|
1790
1827
|
});
|
|
1791
1828
|
});
|
|
1792
1829
|
}
|
|
@@ -2037,6 +2074,505 @@ async function findTests(className, methodName) {
|
|
|
2037
2074
|
return result;
|
|
2038
2075
|
}
|
|
2039
2076
|
|
|
2077
|
+
// ─── Find Implementors ──────────────────────────────────────────
|
|
2078
|
+
// Find all classes implementing a given interface: PHP `implements` + DI preferences
|
|
2079
|
+
|
|
2080
|
+
async function findImplementors(interfaceName) {
|
|
2081
|
+
const root = config.magentoRoot;
|
|
2082
|
+
const shortName = interfaceName.split('\\').pop();
|
|
2083
|
+
|
|
2084
|
+
const result = {
|
|
2085
|
+
interface: interfaceName,
|
|
2086
|
+
implementors: [],
|
|
2087
|
+
diPreferences: []
|
|
2088
|
+
};
|
|
2089
|
+
|
|
2090
|
+
// 1. Search DI preferences for this interface
|
|
2091
|
+
const diFiles = await glob('**/etc/**/di.xml', { cwd: root, absolute: true, nodir: true });
|
|
2092
|
+
for (const diFile of diFiles) {
|
|
2093
|
+
let content;
|
|
2094
|
+
try { content = readFileSync(diFile, 'utf-8'); } catch { continue; }
|
|
2095
|
+
const relativePath = diFile.replace(root + '/', '');
|
|
2096
|
+
|
|
2097
|
+
const prefRegex = /<preference\s+for="([^"]+)"\s+type="([^"]+)"\s*\/?>/g;
|
|
2098
|
+
let m;
|
|
2099
|
+
while ((m = prefRegex.exec(content)) !== null) {
|
|
2100
|
+
const forClass = m[1];
|
|
2101
|
+
if (forClass === interfaceName || forClass.endsWith('\\' + shortName) ||
|
|
2102
|
+
forClass.toLowerCase() === interfaceName.toLowerCase()) {
|
|
2103
|
+
result.diPreferences.push({ for: forClass, type: m[2], file: relativePath });
|
|
2104
|
+
}
|
|
2105
|
+
}
|
|
2106
|
+
}
|
|
2107
|
+
|
|
2108
|
+
// 2. Grep PHP files for `implements ...InterfaceName`
|
|
2109
|
+
const phpFiles = await glob('**/*.php', {
|
|
2110
|
+
cwd: root, absolute: true, nodir: true,
|
|
2111
|
+
ignore: ['**/test/**', '**/tests/**', '**/Test/**', '**/Tests/**']
|
|
2112
|
+
});
|
|
2113
|
+
|
|
2114
|
+
const implementsRegex = new RegExp(
|
|
2115
|
+
`implements\\s+[^{]*\\b${shortName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`, 'i'
|
|
2116
|
+
);
|
|
2117
|
+
|
|
2118
|
+
for (const phpFile of phpFiles) {
|
|
2119
|
+
let content;
|
|
2120
|
+
try { content = readFileSync(phpFile, 'utf-8'); } catch { continue; }
|
|
2121
|
+
if (!implementsRegex.test(content)) continue;
|
|
2122
|
+
|
|
2123
|
+
const relativePath = phpFile.replace(root + '/', '');
|
|
2124
|
+
const classMatch = content.match(/(?:class|abstract\s+class)\s+(\w+)/);
|
|
2125
|
+
const nsMatch = content.match(/namespace\s+([\w\\]+)/);
|
|
2126
|
+
const className = classMatch ? classMatch[1] : path.basename(phpFile, '.php');
|
|
2127
|
+
const fqcn = nsMatch ? `${nsMatch[1]}\\${className}` : className;
|
|
2128
|
+
|
|
2129
|
+
// Verify it references the right interface (not just a name collision)
|
|
2130
|
+
const useStatements = content.match(/use\s+([\w\\]+);/g) || [];
|
|
2131
|
+
const usesFullInterface = useStatements.some(u =>
|
|
2132
|
+
u.includes(interfaceName) || u.endsWith(shortName + ';')
|
|
2133
|
+
);
|
|
2134
|
+
|
|
2135
|
+
if (usesFullInterface || content.includes(interfaceName)) {
|
|
2136
|
+
result.implementors.push({ class: fqcn, file: relativePath });
|
|
2137
|
+
} else {
|
|
2138
|
+
result.implementors.push({ class: fqcn, file: relativePath, matchType: 'shortName' });
|
|
2139
|
+
}
|
|
2140
|
+
}
|
|
2141
|
+
|
|
2142
|
+
return result;
|
|
2143
|
+
}
|
|
2144
|
+
|
|
2145
|
+
// ─── Find Callers ───────────────────────────────────────────────
|
|
2146
|
+
// Find all call sites of a method across PHP and XML files
|
|
2147
|
+
|
|
2148
|
+
async function findCallers(methodName, className) {
|
|
2149
|
+
const root = config.magentoRoot;
|
|
2150
|
+
const shortClass = className ? className.split('\\').pop() : null;
|
|
2151
|
+
|
|
2152
|
+
const result = {
|
|
2153
|
+
method: methodName,
|
|
2154
|
+
className: className || null,
|
|
2155
|
+
phpCallers: [],
|
|
2156
|
+
xmlReferences: [],
|
|
2157
|
+
totalScanned: 0
|
|
2158
|
+
};
|
|
2159
|
+
|
|
2160
|
+
// 1. PHP call sites: ->methodName( and ::methodName(
|
|
2161
|
+
const phpFiles = await glob('**/*.php', { cwd: root, absolute: true, nodir: true });
|
|
2162
|
+
result.totalScanned = phpFiles.length;
|
|
2163
|
+
|
|
2164
|
+
const methodRegex = new RegExp(
|
|
2165
|
+
`(?:->|::)${methodName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s*\\(`, 'g'
|
|
2166
|
+
);
|
|
2167
|
+
|
|
2168
|
+
for (const phpFile of phpFiles) {
|
|
2169
|
+
let content;
|
|
2170
|
+
try { content = readFileSync(phpFile, 'utf-8'); } catch { continue; }
|
|
2171
|
+
if (!methodRegex.test(content)) continue;
|
|
2172
|
+
methodRegex.lastIndex = 0;
|
|
2173
|
+
|
|
2174
|
+
const relativePath = phpFile.replace(root + '/', '');
|
|
2175
|
+
const lines = content.split('\n');
|
|
2176
|
+
const callSites = [];
|
|
2177
|
+
|
|
2178
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2179
|
+
if (methodRegex.test(lines[i])) {
|
|
2180
|
+
if (shortClass) {
|
|
2181
|
+
const usesClass = content.includes(shortClass);
|
|
2182
|
+
if (!usesClass) continue;
|
|
2183
|
+
}
|
|
2184
|
+
callSites.push({ line: i + 1, snippet: lines[i].trim().slice(0, 150) });
|
|
2185
|
+
}
|
|
2186
|
+
methodRegex.lastIndex = 0;
|
|
2187
|
+
}
|
|
2188
|
+
|
|
2189
|
+
if (callSites.length > 0) {
|
|
2190
|
+
const nsMatch = content.match(/namespace\s+([\w\\]+)/);
|
|
2191
|
+
const classMatch = content.match(/(?:class|trait)\s+(\w+)/);
|
|
2192
|
+
const callerClass = nsMatch && classMatch ? `${nsMatch[1]}\\${classMatch[1]}` : null;
|
|
2193
|
+
|
|
2194
|
+
result.phpCallers.push({
|
|
2195
|
+
file: relativePath, callerClass,
|
|
2196
|
+
calls: callSites.slice(0, 5)
|
|
2197
|
+
});
|
|
2198
|
+
}
|
|
2199
|
+
}
|
|
2200
|
+
|
|
2201
|
+
// 2. XML references
|
|
2202
|
+
const xmlFiles = await glob('**/etc/**/*.xml', { cwd: root, absolute: true, nodir: true });
|
|
2203
|
+
for (const xmlFile of xmlFiles) {
|
|
2204
|
+
let content;
|
|
2205
|
+
try { content = readFileSync(xmlFile, 'utf-8'); } catch { continue; }
|
|
2206
|
+
if (!content.includes(methodName)) continue;
|
|
2207
|
+
|
|
2208
|
+
const relativePath = xmlFile.replace(root + '/', '');
|
|
2209
|
+
const lines = content.split('\n');
|
|
2210
|
+
const refs = [];
|
|
2211
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2212
|
+
if (lines[i].includes(methodName)) {
|
|
2213
|
+
refs.push({ line: i + 1, snippet: lines[i].trim().slice(0, 150) });
|
|
2214
|
+
}
|
|
2215
|
+
}
|
|
2216
|
+
if (refs.length > 0) {
|
|
2217
|
+
result.xmlReferences.push({ file: relativePath, refs: refs.slice(0, 5) });
|
|
2218
|
+
}
|
|
2219
|
+
}
|
|
2220
|
+
|
|
2221
|
+
// Sort: files referencing the class get priority
|
|
2222
|
+
if (shortClass) {
|
|
2223
|
+
result.phpCallers.sort((a, b) => {
|
|
2224
|
+
const aHas = a.callerClass?.includes(shortClass) ? 0 : 1;
|
|
2225
|
+
const bHas = b.callerClass?.includes(shortClass) ? 0 : 1;
|
|
2226
|
+
return aHas - bHas;
|
|
2227
|
+
});
|
|
2228
|
+
}
|
|
2229
|
+
|
|
2230
|
+
return result;
|
|
2231
|
+
}
|
|
2232
|
+
|
|
2233
|
+
// ─── Find DI Wiring ─────────────────────────────────────────────
|
|
2234
|
+
// Complete DI picture: preferences, plugins, constructor args, virtual types
|
|
2235
|
+
|
|
2236
|
+
async function findDiWiring(className) {
|
|
2237
|
+
const root = config.magentoRoot;
|
|
2238
|
+
const shortName = className.split('\\').pop();
|
|
2239
|
+
const shortLower = shortName.toLowerCase();
|
|
2240
|
+
|
|
2241
|
+
const result = {
|
|
2242
|
+
className,
|
|
2243
|
+
preferences: [],
|
|
2244
|
+
plugins: [],
|
|
2245
|
+
constructorArguments: [],
|
|
2246
|
+
virtualTypes: [],
|
|
2247
|
+
typeArguments: [],
|
|
2248
|
+
totalDiFiles: 0
|
|
2249
|
+
};
|
|
2250
|
+
|
|
2251
|
+
const diFiles = await glob('**/etc/**/di.xml', { cwd: root, absolute: true, nodir: true });
|
|
2252
|
+
result.totalDiFiles = diFiles.length;
|
|
2253
|
+
|
|
2254
|
+
for (const diFile of diFiles) {
|
|
2255
|
+
let content;
|
|
2256
|
+
try { content = readFileSync(diFile, 'utf-8'); } catch { continue; }
|
|
2257
|
+
const relativePath = diFile.replace(root + '/', '');
|
|
2258
|
+
const contentLower = content.toLowerCase();
|
|
2259
|
+
|
|
2260
|
+
if (!contentLower.includes(shortLower)) continue;
|
|
2261
|
+
|
|
2262
|
+
// 1. Preferences where this class is the "for" or "type"
|
|
2263
|
+
const prefRegex = /<preference\s+for="([^"]+)"\s+type="([^"]+)"\s*\/?>/g;
|
|
2264
|
+
let m;
|
|
2265
|
+
while ((m = prefRegex.exec(content)) !== null) {
|
|
2266
|
+
const forLower = m[1].toLowerCase();
|
|
2267
|
+
const typeLower = m[2].toLowerCase();
|
|
2268
|
+
if (forLower.includes(shortLower) || typeLower.includes(shortLower)) {
|
|
2269
|
+
result.preferences.push({ for: m[1], type: m[2], file: relativePath });
|
|
2270
|
+
}
|
|
2271
|
+
}
|
|
2272
|
+
|
|
2273
|
+
// 2. Plugins and type arguments
|
|
2274
|
+
const typeBlockRegex = /<type\s+name="([^"]+)"[^>]*>([\s\S]*?)<\/type>/g;
|
|
2275
|
+
let typeMatch;
|
|
2276
|
+
while ((typeMatch = typeBlockRegex.exec(content)) !== null) {
|
|
2277
|
+
const typeName = typeMatch[1];
|
|
2278
|
+
const typeNameLower = typeName.toLowerCase();
|
|
2279
|
+
const typeBlock = typeMatch[2];
|
|
2280
|
+
|
|
2281
|
+
if (!typeNameLower.includes(shortLower)) continue;
|
|
2282
|
+
|
|
2283
|
+
// Plugins
|
|
2284
|
+
const pluginRegex = /<plugin\s+name="([^"]+)"[^>]*type="([^"]+)"[^>]*/g;
|
|
2285
|
+
let pMatch;
|
|
2286
|
+
while ((pMatch = pluginRegex.exec(typeBlock)) !== null) {
|
|
2287
|
+
result.plugins.push({
|
|
2288
|
+
target: typeName, pluginName: pMatch[1],
|
|
2289
|
+
pluginClass: pMatch[2], file: relativePath
|
|
2290
|
+
});
|
|
2291
|
+
}
|
|
2292
|
+
|
|
2293
|
+
// Simple arguments
|
|
2294
|
+
const argSimpleRegex = /<argument\s+name="([^"]+)"[^>]*xsi:type="([^"]+)"[^>]*>([^<]*)<\/argument>/g;
|
|
2295
|
+
let aMatch;
|
|
2296
|
+
while ((aMatch = argSimpleRegex.exec(typeBlock)) !== null) {
|
|
2297
|
+
result.typeArguments.push({
|
|
2298
|
+
target: typeName, name: aMatch[1],
|
|
2299
|
+
type: aMatch[2], value: aMatch[3].trim().slice(0, 200), file: relativePath
|
|
2300
|
+
});
|
|
2301
|
+
}
|
|
2302
|
+
|
|
2303
|
+
// Array arguments
|
|
2304
|
+
const argArrayRegex = /<argument\s+name="([^"]+)"[^>]*xsi:type="array"[^>]*>([\s\S]*?)<\/argument>/g;
|
|
2305
|
+
let arrMatch;
|
|
2306
|
+
while ((arrMatch = argArrayRegex.exec(typeBlock)) !== null) {
|
|
2307
|
+
const argName = arrMatch[1];
|
|
2308
|
+
const argBlock = arrMatch[2];
|
|
2309
|
+
const itemRegex = /<item\s+name="([^"]+)"[^>]*xsi:type="([^"]+)"[^>]*>([^<]*)<\/item>/g;
|
|
2310
|
+
let iMatch;
|
|
2311
|
+
const items = [];
|
|
2312
|
+
while ((iMatch = itemRegex.exec(argBlock)) !== null) {
|
|
2313
|
+
items.push({ name: iMatch[1], type: iMatch[2], value: iMatch[3].trim().slice(0, 200) });
|
|
2314
|
+
}
|
|
2315
|
+
if (items.length > 0) {
|
|
2316
|
+
result.typeArguments.push({
|
|
2317
|
+
target: typeName, name: argName,
|
|
2318
|
+
type: 'array', items, file: relativePath
|
|
2319
|
+
});
|
|
2320
|
+
}
|
|
2321
|
+
}
|
|
2322
|
+
}
|
|
2323
|
+
|
|
2324
|
+
// 3. Virtual types extending this class
|
|
2325
|
+
const vtRegex = /<virtualType\s+name="([^"]+)"[^>]*type="([^"]+)"[^>]*(?:\/>|>([\s\S]*?)<\/virtualType>)/g;
|
|
2326
|
+
while ((m = vtRegex.exec(content)) !== null) {
|
|
2327
|
+
const vtType = m[2];
|
|
2328
|
+
if (!vtType.toLowerCase().includes(shortLower)) continue;
|
|
2329
|
+
const vtEntry = { name: m[1], type: vtType, file: relativePath };
|
|
2330
|
+
if (m[3]) {
|
|
2331
|
+
const argRegex = /<argument\s+name="([^"]+)"[^>]*xsi:type="([^"]+)"[^>]*>([^<]*)<\/argument>/g;
|
|
2332
|
+
const vtArgs = [];
|
|
2333
|
+
let vam;
|
|
2334
|
+
while ((vam = argRegex.exec(m[3])) !== null) {
|
|
2335
|
+
vtArgs.push({ name: vam[1], type: vam[2], value: vam[3].trim().slice(0, 200) });
|
|
2336
|
+
}
|
|
2337
|
+
if (vtArgs.length > 0) vtEntry.arguments = vtArgs;
|
|
2338
|
+
}
|
|
2339
|
+
result.virtualTypes.push(vtEntry);
|
|
2340
|
+
}
|
|
2341
|
+
}
|
|
2342
|
+
|
|
2343
|
+
// 4. Constructor arguments from PHP class
|
|
2344
|
+
const phpFiles = await glob(`**/${shortName}.php`, { cwd: root, absolute: true, nodir: true });
|
|
2345
|
+
for (const phpFile of phpFiles) {
|
|
2346
|
+
let content;
|
|
2347
|
+
try { content = readFileSync(phpFile, 'utf-8'); } catch { continue; }
|
|
2348
|
+
const classMatch = content.match(/(?:class|abstract\s+class)\s+(\w+)/);
|
|
2349
|
+
if (!classMatch || classMatch[1] !== shortName) continue;
|
|
2350
|
+
|
|
2351
|
+
const ctorMatch = content.match(/function\s+__construct\s*\(([\s\S]*?)\)\s*[{:]/);
|
|
2352
|
+
if (ctorMatch) {
|
|
2353
|
+
const paramRegex = /(?:([\w\\]+)\s+)?(\$\w+)/g;
|
|
2354
|
+
let pm;
|
|
2355
|
+
while ((pm = paramRegex.exec(ctorMatch[1])) !== null) {
|
|
2356
|
+
result.constructorArguments.push({ typeHint: pm[1] || null, variable: pm[2] });
|
|
2357
|
+
}
|
|
2358
|
+
}
|
|
2359
|
+
break;
|
|
2360
|
+
}
|
|
2361
|
+
|
|
2362
|
+
return result;
|
|
2363
|
+
}
|
|
2364
|
+
|
|
2365
|
+
// ─── Trace Call Chain ───────────────────────────────────────────
|
|
2366
|
+
// Trace internal method execution: method calls, dispatched events, observers
|
|
2367
|
+
|
|
2368
|
+
async function traceCallChain(startClass, startMethod, maxDepth = 3) {
|
|
2369
|
+
const root = config.magentoRoot;
|
|
2370
|
+
|
|
2371
|
+
const result = {
|
|
2372
|
+
entryPoint: `${startClass}::${startMethod}`,
|
|
2373
|
+
chain: [],
|
|
2374
|
+
events: [],
|
|
2375
|
+
observers: [],
|
|
2376
|
+
depth: 0
|
|
2377
|
+
};
|
|
2378
|
+
|
|
2379
|
+
const classFileMap = new Map();
|
|
2380
|
+
|
|
2381
|
+
async function resolveClassFile(className) {
|
|
2382
|
+
const shortName = className.split('\\').pop();
|
|
2383
|
+
if (classFileMap.has(className)) return classFileMap.get(className);
|
|
2384
|
+
|
|
2385
|
+
// Try common Magento path patterns
|
|
2386
|
+
const nsPath = className.replace(/\\/g, '/') + '.php';
|
|
2387
|
+
const candidates = [
|
|
2388
|
+
`app/code/${nsPath}`,
|
|
2389
|
+
`vendor/${nsPath}`
|
|
2390
|
+
];
|
|
2391
|
+
|
|
2392
|
+
for (const c of candidates) {
|
|
2393
|
+
const full = path.join(root, c);
|
|
2394
|
+
if (existsSync(full)) {
|
|
2395
|
+
classFileMap.set(className, full);
|
|
2396
|
+
return full;
|
|
2397
|
+
}
|
|
2398
|
+
}
|
|
2399
|
+
|
|
2400
|
+
// Fallback: glob for the class file
|
|
2401
|
+
const matches = await glob(`**/${shortName}.php`, {
|
|
2402
|
+
cwd: root, absolute: true, nodir: true,
|
|
2403
|
+
ignore: ['**/test/**', '**/tests/**', '**/Test/**', '**/Tests/**']
|
|
2404
|
+
});
|
|
2405
|
+
|
|
2406
|
+
for (const match of matches) {
|
|
2407
|
+
let content;
|
|
2408
|
+
try { content = readFileSync(match, 'utf-8'); } catch { continue; }
|
|
2409
|
+
const nsMatch = content.match(/namespace\s+([\w\\]+)/);
|
|
2410
|
+
const classMatch = content.match(/(?:class|abstract\s+class|trait)\s+(\w+)/);
|
|
2411
|
+
if (classMatch && classMatch[1] === shortName) {
|
|
2412
|
+
const fqcn = nsMatch ? `${nsMatch[1]}\\${classMatch[1]}` : classMatch[1];
|
|
2413
|
+
classFileMap.set(fqcn, match);
|
|
2414
|
+
if (fqcn === className || classMatch[1] === shortName) {
|
|
2415
|
+
classFileMap.set(className, match);
|
|
2416
|
+
return match;
|
|
2417
|
+
}
|
|
2418
|
+
}
|
|
2419
|
+
}
|
|
2420
|
+
|
|
2421
|
+
classFileMap.set(className, null);
|
|
2422
|
+
return null;
|
|
2423
|
+
}
|
|
2424
|
+
|
|
2425
|
+
// Load events.xml index
|
|
2426
|
+
const eventObserverMap = new Map();
|
|
2427
|
+
const eventFiles = await glob('**/etc/**/events.xml', { cwd: root, absolute: true, nodir: true });
|
|
2428
|
+
for (const evFile of eventFiles) {
|
|
2429
|
+
let content;
|
|
2430
|
+
try { content = readFileSync(evFile, 'utf-8'); } catch { continue; }
|
|
2431
|
+
const relativePath = evFile.replace(root + '/', '');
|
|
2432
|
+
|
|
2433
|
+
const eventRegex = /<event\s+name="([^"]+)"[^>]*>([\s\S]*?)<\/event>/g;
|
|
2434
|
+
let em;
|
|
2435
|
+
while ((em = eventRegex.exec(content)) !== null) {
|
|
2436
|
+
const eventName = em[1];
|
|
2437
|
+
const eventBlock = em[2];
|
|
2438
|
+
const obsRegex = /<observer\s+[^>]*name="([^"]+)"[^>]*instance="([^"]+)"[^>]*/g;
|
|
2439
|
+
let om;
|
|
2440
|
+
while ((om = obsRegex.exec(eventBlock)) !== null) {
|
|
2441
|
+
if (!eventObserverMap.has(eventName)) eventObserverMap.set(eventName, []);
|
|
2442
|
+
eventObserverMap.get(eventName).push({
|
|
2443
|
+
name: om[1], class: om[2], file: relativePath
|
|
2444
|
+
});
|
|
2445
|
+
}
|
|
2446
|
+
}
|
|
2447
|
+
}
|
|
2448
|
+
|
|
2449
|
+
// Resolve DI preference for an interface
|
|
2450
|
+
const prefCache = new Map();
|
|
2451
|
+
async function resolvePreference(interfaceName) {
|
|
2452
|
+
if (prefCache.has(interfaceName)) return prefCache.get(interfaceName);
|
|
2453
|
+
const shortName = interfaceName.split('\\').pop();
|
|
2454
|
+
const diXmlFiles = await glob('**/etc/di.xml', { cwd: root, absolute: true, nodir: true });
|
|
2455
|
+
for (const diFile of diXmlFiles) {
|
|
2456
|
+
let content;
|
|
2457
|
+
try { content = readFileSync(diFile, 'utf-8'); } catch { continue; }
|
|
2458
|
+
const prefRegex = /<preference\s+for="([^"]+)"\s+type="([^"]+)"\s*\/?>/g;
|
|
2459
|
+
let m;
|
|
2460
|
+
while ((m = prefRegex.exec(content)) !== null) {
|
|
2461
|
+
if (m[1] === interfaceName || m[1].endsWith('\\' + shortName)) {
|
|
2462
|
+
prefCache.set(interfaceName, m[2]);
|
|
2463
|
+
return m[2];
|
|
2464
|
+
}
|
|
2465
|
+
}
|
|
2466
|
+
}
|
|
2467
|
+
prefCache.set(interfaceName, null);
|
|
2468
|
+
return null;
|
|
2469
|
+
}
|
|
2470
|
+
|
|
2471
|
+
async function traceMethod(className, methodName, depth) {
|
|
2472
|
+
if (depth > maxDepth) return;
|
|
2473
|
+
|
|
2474
|
+
const filePath = await resolveClassFile(className);
|
|
2475
|
+
if (!filePath) {
|
|
2476
|
+
result.chain.push({ depth, class: className, method: methodName, status: 'unresolved' });
|
|
2477
|
+
return;
|
|
2478
|
+
}
|
|
2479
|
+
|
|
2480
|
+
let content;
|
|
2481
|
+
try { content = readFileSync(filePath, 'utf-8'); } catch { return; }
|
|
2482
|
+
const relativePath = filePath.replace(root + '/', '');
|
|
2483
|
+
|
|
2484
|
+
// Extract method body (brace counting)
|
|
2485
|
+
const methodRegex = new RegExp(
|
|
2486
|
+
`function\\s+${methodName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s*\\([^)]*\\)[^{]*\\{`
|
|
2487
|
+
);
|
|
2488
|
+
const methodStart = content.search(methodRegex);
|
|
2489
|
+
if (methodStart === -1) {
|
|
2490
|
+
result.chain.push({ depth, class: className, method: methodName, file: relativePath, status: 'method_not_found' });
|
|
2491
|
+
return;
|
|
2492
|
+
}
|
|
2493
|
+
|
|
2494
|
+
let braceCount = 0;
|
|
2495
|
+
let bodyStart = content.indexOf('{', methodStart);
|
|
2496
|
+
let bodyEnd = bodyStart;
|
|
2497
|
+
for (let i = bodyStart; i < content.length; i++) {
|
|
2498
|
+
if (content[i] === '{') braceCount++;
|
|
2499
|
+
if (content[i] === '}') braceCount--;
|
|
2500
|
+
if (braceCount === 0) { bodyEnd = i; break; }
|
|
2501
|
+
}
|
|
2502
|
+
const methodBody = content.slice(bodyStart, bodyEnd + 1);
|
|
2503
|
+
|
|
2504
|
+
const chainEntry = {
|
|
2505
|
+
depth, class: className, method: methodName, file: relativePath,
|
|
2506
|
+
calls: [], dispatches: []
|
|
2507
|
+
};
|
|
2508
|
+
|
|
2509
|
+
// $this->method( calls
|
|
2510
|
+
const selfCallRegex = /\$this->(\w+)\s*\(/g;
|
|
2511
|
+
let sc;
|
|
2512
|
+
while ((sc = selfCallRegex.exec(methodBody)) !== null) {
|
|
2513
|
+
const calledMethod = sc[1];
|
|
2514
|
+
if (calledMethod !== methodName && calledMethod !== '__construct') {
|
|
2515
|
+
chainEntry.calls.push({ type: 'self', method: calledMethod });
|
|
2516
|
+
}
|
|
2517
|
+
}
|
|
2518
|
+
|
|
2519
|
+
// $this->dependency->method( calls
|
|
2520
|
+
const depCallRegex = /\$this->(\w+)->(\w+)\s*\(/g;
|
|
2521
|
+
let dc;
|
|
2522
|
+
while ((dc = depCallRegex.exec(methodBody)) !== null) {
|
|
2523
|
+
const property = dc[1];
|
|
2524
|
+
const calledMethod = dc[2];
|
|
2525
|
+
// Resolve property type from constructor
|
|
2526
|
+
const ctorMatch = content.match(/function\s+__construct\s*\(([\s\S]*?)\)\s*[{:]/);
|
|
2527
|
+
let resolvedType = null;
|
|
2528
|
+
if (ctorMatch) {
|
|
2529
|
+
const paramRegex = new RegExp(`([\\w\\\\]+)\\s+\\$${property}\\b`);
|
|
2530
|
+
const pm = ctorMatch[1].match(paramRegex);
|
|
2531
|
+
if (pm) resolvedType = pm[1];
|
|
2532
|
+
}
|
|
2533
|
+
chainEntry.calls.push({ type: 'dependency', property, method: calledMethod, typeHint: resolvedType || null });
|
|
2534
|
+
}
|
|
2535
|
+
|
|
2536
|
+
// eventManager->dispatch calls
|
|
2537
|
+
const dispatchRegex = /(?:eventManager|_eventManager)->dispatch\s*\(\s*['"]([^'"]+)['"]/g;
|
|
2538
|
+
let dm;
|
|
2539
|
+
while ((dm = dispatchRegex.exec(methodBody)) !== null) {
|
|
2540
|
+
const eventName = dm[1];
|
|
2541
|
+
chainEntry.dispatches.push(eventName);
|
|
2542
|
+
const observers = eventObserverMap.get(eventName) || [];
|
|
2543
|
+
result.events.push({ event: eventName, dispatchedIn: `${className}::${methodName}`, observers });
|
|
2544
|
+
for (const obs of observers) {
|
|
2545
|
+
result.observers.push({ event: eventName, ...obs });
|
|
2546
|
+
}
|
|
2547
|
+
}
|
|
2548
|
+
|
|
2549
|
+
result.chain.push(chainEntry);
|
|
2550
|
+
result.depth = Math.max(result.depth, depth);
|
|
2551
|
+
|
|
2552
|
+
// Recurse into $this-> calls
|
|
2553
|
+
for (const call of chainEntry.calls) {
|
|
2554
|
+
if (call.type === 'self') {
|
|
2555
|
+
await traceMethod(className, call.method, depth + 1);
|
|
2556
|
+
}
|
|
2557
|
+
}
|
|
2558
|
+
|
|
2559
|
+
// Recurse into dependency calls (within depth limit)
|
|
2560
|
+
if (depth < maxDepth - 1) {
|
|
2561
|
+
for (const call of chainEntry.calls) {
|
|
2562
|
+
if (call.type === 'dependency' && call.typeHint) {
|
|
2563
|
+
const impl = await resolvePreference(call.typeHint);
|
|
2564
|
+
const targetClass = impl || call.typeHint;
|
|
2565
|
+
await traceMethod(targetClass, call.method, depth + 1);
|
|
2566
|
+
}
|
|
2567
|
+
}
|
|
2568
|
+
}
|
|
2569
|
+
}
|
|
2570
|
+
|
|
2571
|
+
await traceMethod(startClass, startMethod, 0);
|
|
2572
|
+
|
|
2573
|
+
return result;
|
|
2574
|
+
}
|
|
2575
|
+
|
|
2040
2576
|
// ─── MCP Server ─────────────────────────────────────────────────
|
|
2041
2577
|
|
|
2042
2578
|
const server = new Server(
|
|
@@ -2071,7 +2607,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
2071
2607
|
},
|
|
2072
2608
|
moduleFilter: {
|
|
2073
2609
|
type: 'string',
|
|
2074
|
-
description: 'Filter results by vendor/module pattern. Supports wildcards. Examples: "Vendor_*" to show
|
|
2610
|
+
description: 'Filter results by vendor/module pattern. Supports wildcards and vendor prefix matching. Uses "/" or "_" interchangeably as separator. Examples: "Vendor_*" or "Vendor/*" to show modules from that vendor (also matches vendor-extended names like "Vendor-Extra_*"), "Magento_Catalog" for specific module. Omit to search all modules.'
|
|
2075
2611
|
},
|
|
2076
2612
|
expand: {
|
|
2077
2613
|
type: 'boolean',
|
|
@@ -2531,6 +3067,75 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
2531
3067
|
required: ['className']
|
|
2532
3068
|
}
|
|
2533
3069
|
},
|
|
3070
|
+
{
|
|
3071
|
+
name: 'magento_find_implementors',
|
|
3072
|
+
description: 'Find all classes that implement a given PHP interface. Scans PHP files for `implements` keyword and di.xml for `<preference>` declarations. Use this to discover all concrete implementations of an interface across the codebase.',
|
|
3073
|
+
inputSchema: {
|
|
3074
|
+
type: 'object',
|
|
3075
|
+
properties: {
|
|
3076
|
+
interfaceName: {
|
|
3077
|
+
type: 'string',
|
|
3078
|
+
description: 'Full or short PHP interface name. Examples: "OrderRepositoryInterface", "Magento\\Sales\\Api\\OrderRepositoryInterface", "ChildOrderValidatorInterface"'
|
|
3079
|
+
}
|
|
3080
|
+
},
|
|
3081
|
+
required: ['interfaceName']
|
|
3082
|
+
}
|
|
3083
|
+
},
|
|
3084
|
+
{
|
|
3085
|
+
name: 'magento_find_callers',
|
|
3086
|
+
description: 'Find all call sites of a PHP method across the codebase. Searches for ->method() and ::method() patterns in PHP files and method references in XML config files. Use this to understand where a method is used and trace data flow.',
|
|
3087
|
+
inputSchema: {
|
|
3088
|
+
type: 'object',
|
|
3089
|
+
properties: {
|
|
3090
|
+
methodName: {
|
|
3091
|
+
type: 'string',
|
|
3092
|
+
description: 'Method name to find callers for. Examples: "execute", "save", "collectTotals", "copySalesRuleIdsFromParentToDrmaxQuote"'
|
|
3093
|
+
},
|
|
3094
|
+
className: {
|
|
3095
|
+
type: 'string',
|
|
3096
|
+
description: 'Optional: class that owns the method — narrows results to files that reference this class. Examples: "SalesRuleManagement", "CartRepository"'
|
|
3097
|
+
}
|
|
3098
|
+
},
|
|
3099
|
+
required: ['methodName']
|
|
3100
|
+
}
|
|
3101
|
+
},
|
|
3102
|
+
{
|
|
3103
|
+
name: 'magento_find_di_wiring',
|
|
3104
|
+
description: 'Get the complete DI wiring picture for a PHP class: preferences (interface→implementation), plugins (interceptors), constructor arguments from di.xml, virtual types, and argument overrides. Also extracts the PHP constructor signature. Use this to understand how a class is configured and extended across all modules.',
|
|
3105
|
+
inputSchema: {
|
|
3106
|
+
type: 'object',
|
|
3107
|
+
properties: {
|
|
3108
|
+
className: {
|
|
3109
|
+
type: 'string',
|
|
3110
|
+
description: 'Full or short PHP class/interface name. Examples: "ChildOrderValidatorChain", "Magento\\SalesRule\\Model\\Rule", "CartManagementInterface"'
|
|
3111
|
+
}
|
|
3112
|
+
},
|
|
3113
|
+
required: ['className']
|
|
3114
|
+
}
|
|
3115
|
+
},
|
|
3116
|
+
{
|
|
3117
|
+
name: 'magento_trace_call_chain',
|
|
3118
|
+
description: 'Trace the internal method call chain starting from a specific class::method. Follows $this->method() calls (same class), $this->dependency->method() calls (resolves DI types), and eventManager->dispatch() calls (maps to observers from events.xml). Returns a call tree showing the execution path through the code.',
|
|
3119
|
+
inputSchema: {
|
|
3120
|
+
type: 'object',
|
|
3121
|
+
properties: {
|
|
3122
|
+
className: {
|
|
3123
|
+
type: 'string',
|
|
3124
|
+
description: 'Full PHP class name (FQCN) to start tracing from. Examples: "DrmaxMarketplace\\OrderSplit\\Model\\CreateOrder\\CreateChildOrder", "Magento\\Quote\\Model\\QuoteManagement"'
|
|
3125
|
+
},
|
|
3126
|
+
methodName: {
|
|
3127
|
+
type: 'string',
|
|
3128
|
+
description: 'Method name to start tracing. Examples: "execute", "submit", "collectTotals"'
|
|
3129
|
+
},
|
|
3130
|
+
maxDepth: {
|
|
3131
|
+
type: 'number',
|
|
3132
|
+
description: 'Maximum recursion depth for tracing (default: 3). Higher values trace deeper but take longer.',
|
|
3133
|
+
default: 3
|
|
3134
|
+
}
|
|
3135
|
+
},
|
|
3136
|
+
required: ['className', 'methodName']
|
|
3137
|
+
}
|
|
3138
|
+
},
|
|
2534
3139
|
]
|
|
2535
3140
|
}));
|
|
2536
3141
|
|
|
@@ -3376,6 +3981,185 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
3376
3981
|
return { content: [{ type: 'text', text }] };
|
|
3377
3982
|
}
|
|
3378
3983
|
|
|
3984
|
+
case 'magento_find_implementors': {
|
|
3985
|
+
const implResult = await findImplementors(args.interfaceName);
|
|
3986
|
+
|
|
3987
|
+
let text = `## Implementors of: ${implResult.interface}\n\n`;
|
|
3988
|
+
|
|
3989
|
+
if (implResult.diPreferences.length > 0) {
|
|
3990
|
+
text += `### DI Preferences (${implResult.diPreferences.length})\n`;
|
|
3991
|
+
for (const p of implResult.diPreferences) {
|
|
3992
|
+
text += `- \`${p.for}\` → \`${p.type}\` (${p.file})\n`;
|
|
3993
|
+
}
|
|
3994
|
+
text += '\n';
|
|
3995
|
+
}
|
|
3996
|
+
|
|
3997
|
+
if (implResult.implementors.length > 0) {
|
|
3998
|
+
text += `### PHP Implementors (${implResult.implementors.length})\n`;
|
|
3999
|
+
for (const impl of implResult.implementors) {
|
|
4000
|
+
const suffix = impl.matchType === 'shortName' ? ' _(short name match)_' : '';
|
|
4001
|
+
text += `- \`${impl.class}\` (${impl.file})${suffix}\n`;
|
|
4002
|
+
}
|
|
4003
|
+
text += '\n';
|
|
4004
|
+
}
|
|
4005
|
+
|
|
4006
|
+
if (implResult.diPreferences.length === 0 && implResult.implementors.length === 0) {
|
|
4007
|
+
text += '_No implementors found._\n';
|
|
4008
|
+
}
|
|
4009
|
+
|
|
4010
|
+
return { content: [{ type: 'text', text }] };
|
|
4011
|
+
}
|
|
4012
|
+
|
|
4013
|
+
case 'magento_find_callers': {
|
|
4014
|
+
const callResult = await findCallers(args.methodName, args.className);
|
|
4015
|
+
|
|
4016
|
+
let text = `## Callers of: ${args.methodName}${args.className ? ` (${args.className})` : ''}\n\n`;
|
|
4017
|
+
text += `Scanned ${callResult.totalScanned} PHP files.\n\n`;
|
|
4018
|
+
|
|
4019
|
+
if (callResult.phpCallers.length > 0) {
|
|
4020
|
+
text += `### PHP Call Sites (${callResult.phpCallers.length})\n`;
|
|
4021
|
+
for (const caller of callResult.phpCallers.slice(0, 30)) {
|
|
4022
|
+
text += `- **${caller.file}**${caller.callerClass ? ` (\`${caller.callerClass}\`)` : ''}\n`;
|
|
4023
|
+
for (const c of caller.calls) {
|
|
4024
|
+
text += ` - L${c.line}: \`${c.snippet}\`\n`;
|
|
4025
|
+
}
|
|
4026
|
+
}
|
|
4027
|
+
text += '\n';
|
|
4028
|
+
}
|
|
4029
|
+
|
|
4030
|
+
if (callResult.xmlReferences.length > 0) {
|
|
4031
|
+
text += `### XML References (${callResult.xmlReferences.length})\n`;
|
|
4032
|
+
for (const ref of callResult.xmlReferences.slice(0, 15)) {
|
|
4033
|
+
text += `- **${ref.file}**\n`;
|
|
4034
|
+
for (const r of ref.refs) {
|
|
4035
|
+
text += ` - L${r.line}: \`${r.snippet}\`\n`;
|
|
4036
|
+
}
|
|
4037
|
+
}
|
|
4038
|
+
text += '\n';
|
|
4039
|
+
}
|
|
4040
|
+
|
|
4041
|
+
if (callResult.phpCallers.length === 0 && callResult.xmlReferences.length === 0) {
|
|
4042
|
+
text += '_No callers found._\n';
|
|
4043
|
+
}
|
|
4044
|
+
|
|
4045
|
+
return { content: [{ type: 'text', text }] };
|
|
4046
|
+
}
|
|
4047
|
+
|
|
4048
|
+
case 'magento_find_di_wiring': {
|
|
4049
|
+
const diResult = await findDiWiring(args.className);
|
|
4050
|
+
|
|
4051
|
+
let text = `## DI Wiring: ${diResult.className}\n\n`;
|
|
4052
|
+
text += `Scanned ${diResult.totalDiFiles} di.xml files.\n\n`;
|
|
4053
|
+
|
|
4054
|
+
if (diResult.constructorArguments.length > 0) {
|
|
4055
|
+
text += `### Constructor Signature\n`;
|
|
4056
|
+
for (const arg of diResult.constructorArguments) {
|
|
4057
|
+
text += `- ${arg.typeHint || 'mixed'} ${arg.variable}\n`;
|
|
4058
|
+
}
|
|
4059
|
+
text += '\n';
|
|
4060
|
+
}
|
|
4061
|
+
|
|
4062
|
+
if (diResult.preferences.length > 0) {
|
|
4063
|
+
text += `### Preferences (${diResult.preferences.length})\n`;
|
|
4064
|
+
for (const p of diResult.preferences) {
|
|
4065
|
+
text += `- \`${p.for}\` → \`${p.type}\` (${p.file})\n`;
|
|
4066
|
+
}
|
|
4067
|
+
text += '\n';
|
|
4068
|
+
}
|
|
4069
|
+
|
|
4070
|
+
if (diResult.plugins.length > 0) {
|
|
4071
|
+
text += `### Plugins (${diResult.plugins.length})\n`;
|
|
4072
|
+
for (const p of diResult.plugins) {
|
|
4073
|
+
text += `- \`${p.pluginName}\`: \`${p.pluginClass}\` on \`${p.target}\` (${p.file})\n`;
|
|
4074
|
+
}
|
|
4075
|
+
text += '\n';
|
|
4076
|
+
}
|
|
4077
|
+
|
|
4078
|
+
if (diResult.typeArguments.length > 0) {
|
|
4079
|
+
text += `### DI Arguments (${diResult.typeArguments.length})\n`;
|
|
4080
|
+
for (const a of diResult.typeArguments) {
|
|
4081
|
+
if (a.type === 'array' && a.items) {
|
|
4082
|
+
text += `- \`${a.target}\`.${a.name} (array, ${a.items.length} items) (${a.file})\n`;
|
|
4083
|
+
for (const item of a.items.slice(0, 10)) {
|
|
4084
|
+
text += ` - ${item.name}: \`${item.value}\`\n`;
|
|
4085
|
+
}
|
|
4086
|
+
} else {
|
|
4087
|
+
text += `- \`${a.target}\`.${a.name} (${a.type}) = \`${a.value}\` (${a.file})\n`;
|
|
4088
|
+
}
|
|
4089
|
+
}
|
|
4090
|
+
text += '\n';
|
|
4091
|
+
}
|
|
4092
|
+
|
|
4093
|
+
if (diResult.virtualTypes.length > 0) {
|
|
4094
|
+
text += `### Virtual Types (${diResult.virtualTypes.length})\n`;
|
|
4095
|
+
for (const v of diResult.virtualTypes) {
|
|
4096
|
+
text += `- \`${v.name}\` extends \`${v.type}\` (${v.file})\n`;
|
|
4097
|
+
if (v.arguments) {
|
|
4098
|
+
for (const a of v.arguments) {
|
|
4099
|
+
text += ` - ${a.name}: \`${a.value}\`\n`;
|
|
4100
|
+
}
|
|
4101
|
+
}
|
|
4102
|
+
}
|
|
4103
|
+
text += '\n';
|
|
4104
|
+
}
|
|
4105
|
+
|
|
4106
|
+
return { content: [{ type: 'text', text }] };
|
|
4107
|
+
}
|
|
4108
|
+
|
|
4109
|
+
case 'magento_trace_call_chain': {
|
|
4110
|
+
const traceResult = await traceCallChain(args.className, args.methodName, args.maxDepth || 3);
|
|
4111
|
+
|
|
4112
|
+
let text = `## Call Chain: ${traceResult.entryPoint}\n\n`;
|
|
4113
|
+
text += `Max depth reached: ${traceResult.depth}\n\n`;
|
|
4114
|
+
|
|
4115
|
+
if (traceResult.chain.length > 0) {
|
|
4116
|
+
text += `### Execution Chain (${traceResult.chain.length} methods)\n`;
|
|
4117
|
+
for (const entry of traceResult.chain) {
|
|
4118
|
+
const indent = ' '.repeat(entry.depth);
|
|
4119
|
+
const status = entry.status ? ` [${entry.status}]` : '';
|
|
4120
|
+
text += `${indent}- **${entry.class}::${entry.method}**${status}`;
|
|
4121
|
+
if (entry.file) text += ` (${entry.file})`;
|
|
4122
|
+
text += '\n';
|
|
4123
|
+
|
|
4124
|
+
if (entry.calls) {
|
|
4125
|
+
for (const call of entry.calls) {
|
|
4126
|
+
if (call.type === 'self') {
|
|
4127
|
+
text += `${indent} → \`$this->${call.method}()\`\n`;
|
|
4128
|
+
} else {
|
|
4129
|
+
text += `${indent} → \`$this->${call.property}->${call.method}()\``;
|
|
4130
|
+
if (call.typeHint) text += ` [${call.typeHint}]`;
|
|
4131
|
+
text += '\n';
|
|
4132
|
+
}
|
|
4133
|
+
}
|
|
4134
|
+
}
|
|
4135
|
+
|
|
4136
|
+
if (entry.dispatches) {
|
|
4137
|
+
for (const evt of entry.dispatches) {
|
|
4138
|
+
text += `${indent} ⚡ dispatch(\`${evt}\`)\n`;
|
|
4139
|
+
}
|
|
4140
|
+
}
|
|
4141
|
+
}
|
|
4142
|
+
text += '\n';
|
|
4143
|
+
}
|
|
4144
|
+
|
|
4145
|
+
if (traceResult.events.length > 0) {
|
|
4146
|
+
text += `### Events Dispatched (${traceResult.events.length})\n`;
|
|
4147
|
+
for (const evt of traceResult.events) {
|
|
4148
|
+
text += `- **${evt.event}** (from ${evt.dispatchedIn})\n`;
|
|
4149
|
+
if (evt.observers.length > 0) {
|
|
4150
|
+
for (const obs of evt.observers) {
|
|
4151
|
+
text += ` - \`${obs.name}\`: \`${obs.class}\` (${obs.file})\n`;
|
|
4152
|
+
}
|
|
4153
|
+
} else {
|
|
4154
|
+
text += ' - _no observers registered_\n';
|
|
4155
|
+
}
|
|
4156
|
+
}
|
|
4157
|
+
text += '\n';
|
|
4158
|
+
}
|
|
4159
|
+
|
|
4160
|
+
return { content: [{ type: 'text', text }] };
|
|
4161
|
+
}
|
|
4162
|
+
|
|
3379
4163
|
default:
|
|
3380
4164
|
return {
|
|
3381
4165
|
content: [{
|