magector 2.7.0 → 2.8.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/package.json +5 -5
- package/src/mcp-server.js +158 -54
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "magector",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.8.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.8.0",
|
|
37
|
+
"@magector/cli-linux-x64": "2.8.0",
|
|
38
|
+
"@magector/cli-linux-arm64": "2.8.0",
|
|
39
|
+
"@magector/cli-win32-x64": "2.8.0"
|
|
40
40
|
},
|
|
41
41
|
"keywords": [
|
|
42
42
|
"magento",
|
package/src/mcp-server.js
CHANGED
|
@@ -2303,6 +2303,33 @@ async function analyzeImpact(className) {
|
|
|
2303
2303
|
...diTrace.virtualTypes.map(v => ({ type: 'virtualType', file: v.file, detail: `${v.name} extends ${v.type}` })),
|
|
2304
2304
|
...diTrace.argumentOverrides.map(a => ({ type: 'argument', file: a.file, detail: `${a.target}.${a.argumentName}` }))
|
|
2305
2305
|
];
|
|
2306
|
+
// Also find where this class is used AS a plugin/preference/virtualType implementation
|
|
2307
|
+
if (references.diXmlReferences.length === 0 && root) {
|
|
2308
|
+
const diFiles = await getDiXmlFiles(root);
|
|
2309
|
+
for (const { content, relPath } of diFiles) {
|
|
2310
|
+
if (!content.toLowerCase().includes(shortName.toLowerCase())) continue;
|
|
2311
|
+
// Check if class appears as plugin type
|
|
2312
|
+
const pluginTypeRegex = /<plugin\s+[^>]*type="([^"]+)"[^>]*\/?>/g;
|
|
2313
|
+
let pm;
|
|
2314
|
+
while ((pm = pluginTypeRegex.exec(content)) !== null) {
|
|
2315
|
+
if (pm[1].toLowerCase().includes(shortName.toLowerCase())) {
|
|
2316
|
+
// Find parent <type> to know the target
|
|
2317
|
+
const beforePlugin = content.slice(0, pm.index);
|
|
2318
|
+
const typeMatch = beforePlugin.match(/<type\s+name="([^"]+)"[^>]*>\s*$/m);
|
|
2319
|
+
const target = typeMatch ? typeMatch[1] : 'unknown';
|
|
2320
|
+
references.diXmlReferences.push({ type: 'registered-as-plugin', file: relPath, detail: `plugin on ${target} → ${pm[1]}` });
|
|
2321
|
+
}
|
|
2322
|
+
}
|
|
2323
|
+
// Check if class appears as preference type
|
|
2324
|
+
const prefRegex = /<preference\s+for="([^"]+)"\s+type="([^"]+)"\s*\/?>/g;
|
|
2325
|
+
let prm;
|
|
2326
|
+
while ((prm = prefRegex.exec(content)) !== null) {
|
|
2327
|
+
if (prm[2].toLowerCase().includes(shortName.toLowerCase())) {
|
|
2328
|
+
references.diXmlReferences.push({ type: 'registered-as-preference', file: relPath, detail: `preference for ${prm[1]} → ${prm[2]}` });
|
|
2329
|
+
}
|
|
2330
|
+
}
|
|
2331
|
+
}
|
|
2332
|
+
}
|
|
2306
2333
|
|
|
2307
2334
|
// Check PHP files for direct references
|
|
2308
2335
|
const escapedShort = shortName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
@@ -4016,11 +4043,17 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
4016
4043
|
const reqStart = Date.now();
|
|
4017
4044
|
logToFile('REQ', `${name}(${JSON.stringify(args || {})})`);
|
|
4018
4045
|
|
|
4019
|
-
// ── Warmup guard:
|
|
4046
|
+
// ── Warmup guard: only block if no DB exists at all (nothing to search) ──
|
|
4047
|
+
// Tools with filesystem fallback work without the serve process, so we
|
|
4048
|
+
// don't block them during warmup. Only block if the DB doesn't exist.
|
|
4020
4049
|
const indexFreeTools = ['magento_stats', 'magento_analyze_diff', 'magento_complexity',
|
|
4021
4050
|
'magento_trace_dependency', 'magento_error_parser', 'magento_find_layout',
|
|
4022
4051
|
'magento_impact_analysis', 'magento_find_event_flow', 'magento_find_test',
|
|
4023
|
-
'magento_trace_data_flow', 'magento_find_event_dispatchers'
|
|
4052
|
+
'magento_trace_data_flow', 'magento_find_event_dispatchers',
|
|
4053
|
+
// These tools have filesystem/di.xml fallbacks — work without serve process
|
|
4054
|
+
'magento_find_class', 'magento_find_method', 'magento_find_plugin',
|
|
4055
|
+
'magento_find_observer', 'magento_find_di_wiring', 'magento_module_structure',
|
|
4056
|
+
'magento_batch', 'magento_find_config', 'magento_find_callers'];
|
|
4024
4057
|
if (warmupInProgress && !indexFreeTools.includes(name)) {
|
|
4025
4058
|
logToFile('REQ', `${name} → blocked (warmup: loading index)`);
|
|
4026
4059
|
return {
|
|
@@ -5640,18 +5673,22 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
5640
5673
|
const res = raw.map(normalizeResult).filter(r =>
|
|
5641
5674
|
r.magentoType === 'plugin' || r.path?.toLowerCase().includes('plugin')
|
|
5642
5675
|
);
|
|
5643
|
-
text = formatSearchResults(res.slice(0,
|
|
5644
|
-
// DI registrations + method bodies (
|
|
5676
|
+
text = formatSearchResults(res.slice(0, 3));
|
|
5677
|
+
// DI registrations + method bodies (compact: only targetMethod bodies)
|
|
5645
5678
|
if (a.targetClass) {
|
|
5646
5679
|
const diFiles = await getDiXmlFiles(config.magentoRoot);
|
|
5647
5680
|
const normalizedTarget = a.targetClass.replace(/\\\\/g, '\\');
|
|
5648
5681
|
const isFqcn = normalizedTarget.includes('\\');
|
|
5649
5682
|
const shortTarget = normalizedTarget.split('\\').pop().toLowerCase();
|
|
5683
|
+
let regCount = 0;
|
|
5684
|
+
text += '\n\n### DI Registrations\n';
|
|
5650
5685
|
for (const { content: diContent, relPath } of diFiles) {
|
|
5686
|
+
if (regCount >= 8) break;
|
|
5651
5687
|
if (!diContent.includes(isFqcn ? normalizedTarget : a.targetClass)) continue;
|
|
5652
5688
|
const typeBlockRegex = /<type\s+name="([^"]+)"[^>]*>([\s\S]*?)<\/type>/g;
|
|
5653
5689
|
let tm;
|
|
5654
5690
|
while ((tm = typeBlockRegex.exec(diContent)) !== null) {
|
|
5691
|
+
if (regCount >= 8) break;
|
|
5655
5692
|
const typeName = tm[1].replace(/\\\\/g, '\\');
|
|
5656
5693
|
const typeMatches = isFqcn ? typeName === normalizedTarget : typeName.split('\\').pop().toLowerCase() === shortTarget;
|
|
5657
5694
|
if (!typeMatches) continue;
|
|
@@ -5659,22 +5696,27 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
5659
5696
|
const pluginRegex = /<plugin\s+([^/>]*)\/?>/g;
|
|
5660
5697
|
let pm;
|
|
5661
5698
|
while ((pm = pluginRegex.exec(block)) !== null) {
|
|
5699
|
+
if (regCount >= 8) break;
|
|
5662
5700
|
const attrs = {};
|
|
5663
5701
|
const localAttrRe = /(\w+)="([^"]*)"/g;
|
|
5664
|
-
let
|
|
5665
|
-
while ((
|
|
5666
|
-
|
|
5667
|
-
|
|
5702
|
+
let am2;
|
|
5703
|
+
while ((am2 = localAttrRe.exec(pm[1])) !== null) attrs[am2[1]] = am2[2];
|
|
5704
|
+
const disabled = attrs.disabled === 'true' ? ' [DISABLED]' : '';
|
|
5705
|
+
text += `- **${attrs.name || '?'}** → \`${attrs.type || '?'}\`${disabled} (${relPath})\n`;
|
|
5706
|
+
// Only read method bodies for plugins that intercept the targetMethod
|
|
5707
|
+
if (attrs.type && a.targetMethod) {
|
|
5668
5708
|
const pFile = findClassFile(config.magentoRoot, attrs.type);
|
|
5669
5709
|
if (pFile) {
|
|
5670
5710
|
const methods = extractPluginMethods(pFile);
|
|
5671
|
-
|
|
5711
|
+
const relevant = methods.filter(m => m.targetMethod === a.targetMethod);
|
|
5712
|
+
for (const m of relevant) {
|
|
5672
5713
|
const body = readFullMethodBody(pFile, m.name);
|
|
5673
|
-
text +=
|
|
5674
|
-
if (body) text += '
|
|
5714
|
+
text += ` - \`${m.type}\` **${m.targetMethod}** → \`${m.name}()\`\n`;
|
|
5715
|
+
if (body) text += ' ' + '```php\n ' + body.split('\n').join('\n ') + '\n ' + '```\n';
|
|
5675
5716
|
}
|
|
5676
5717
|
}
|
|
5677
5718
|
}
|
|
5719
|
+
regCount++;
|
|
5678
5720
|
}
|
|
5679
5721
|
}
|
|
5680
5722
|
}
|
|
@@ -5740,9 +5782,37 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
5740
5782
|
let res = raw.map(normalizeResult).filter(r =>
|
|
5741
5783
|
r.methodName?.toLowerCase() === ml || r.methods?.some(m => m.toLowerCase() === ml)
|
|
5742
5784
|
);
|
|
5785
|
+
// Filesystem fallback: grep -rl for method signature
|
|
5786
|
+
if (res.length === 0 && config.magentoRoot) {
|
|
5787
|
+
const methodSig = `function ${a.methodName}(`;
|
|
5788
|
+
const classShort = a.className ? a.className.split('\\').pop() : null;
|
|
5789
|
+
try {
|
|
5790
|
+
let files = [];
|
|
5791
|
+
if (classShort) {
|
|
5792
|
+
files = await glob(`**/${classShort}.php`, { cwd: config.magentoRoot, absolute: false, nodir: true });
|
|
5793
|
+
} else {
|
|
5794
|
+
const grepResult = execFileSync('grep', ['-rl', '--include=*.php', methodSig, '.'],
|
|
5795
|
+
{ cwd: config.magentoRoot, encoding: 'utf-8', timeout: 15000, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
5796
|
+
files = grepResult.trim().split('\n').filter(Boolean).map(f => f.replace(/^\.\//, ''));
|
|
5797
|
+
}
|
|
5798
|
+
for (const f of files.slice(0, 10)) {
|
|
5799
|
+
const absP = f.startsWith('/') ? f : path.join(config.magentoRoot, f);
|
|
5800
|
+
let content;
|
|
5801
|
+
try { content = readFileSync(absP, 'utf-8'); } catch { continue; }
|
|
5802
|
+
if (!content.includes(methodSig)) continue;
|
|
5803
|
+
let cn = null;
|
|
5804
|
+
const nsM = content.match(/namespace\s+([\w\\]+)/);
|
|
5805
|
+
const clM = content.match(/class\s+(\w+)/);
|
|
5806
|
+
if (nsM && clM) cn = nsM[1] + '\\' + clM[1];
|
|
5807
|
+
const body = readFullMethodBody(absP, a.methodName);
|
|
5808
|
+
res.push({ path: f, className: cn, methodName: a.methodName, score: 0.5, fullMethodBody: body || undefined });
|
|
5809
|
+
if (res.length >= 5) break;
|
|
5810
|
+
}
|
|
5811
|
+
} catch {}
|
|
5812
|
+
}
|
|
5743
5813
|
for (const r of res.slice(0, 5)) {
|
|
5744
|
-
if (r.path?.endsWith('.php')) {
|
|
5745
|
-
const body = readFullMethodBody(r.path, a.methodName);
|
|
5814
|
+
if (r.path?.endsWith('.php') && !r.fullMethodBody) {
|
|
5815
|
+
const body = readFullMethodBody(path.join(config.magentoRoot, r.path), a.methodName);
|
|
5746
5816
|
if (body) r.fullMethodBody = body;
|
|
5747
5817
|
}
|
|
5748
5818
|
}
|
|
@@ -5752,13 +5822,34 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
5752
5822
|
case 'magento_module_structure': {
|
|
5753
5823
|
const raw = await rustSearchAsync(a.moduleName, 200);
|
|
5754
5824
|
const modulePath = a.moduleName.replace('_', '/') + '/';
|
|
5755
|
-
const
|
|
5756
|
-
|
|
5757
|
-
const
|
|
5825
|
+
const mParts = a.moduleName.split('_');
|
|
5826
|
+
// Hyphenate camelCase for vendor path: OrderSplit → order-split
|
|
5827
|
+
const vendorPath = mParts.length === 2
|
|
5828
|
+
? `module-${mParts[1].replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase()}/`
|
|
5829
|
+
: '';
|
|
5830
|
+
let res = raw.map(normalizeResult).filter(r => {
|
|
5758
5831
|
const p = r.path || '';
|
|
5759
5832
|
const mod = r.module || '';
|
|
5760
5833
|
return mod === a.moduleName || p.includes(modulePath) || (vendorPath && p.toLowerCase().includes(vendorPath));
|
|
5761
5834
|
});
|
|
5835
|
+
// Filesystem fallback
|
|
5836
|
+
if (res.length === 0 && config.magentoRoot && vendorPath) {
|
|
5837
|
+
try {
|
|
5838
|
+
const vendorGlob = `**/${vendorPath}**/*.{php,xml,phtml}`;
|
|
5839
|
+
const files = await glob(vendorGlob, { cwd: config.magentoRoot, absolute: false, nodir: true });
|
|
5840
|
+
for (const f of files.slice(0, 100)) {
|
|
5841
|
+
const entry = { path: f, score: 0.5 };
|
|
5842
|
+
if (f.includes('/Controller/')) entry.isController = true;
|
|
5843
|
+
if (f.includes('/Model/')) entry.isModel = true;
|
|
5844
|
+
if (f.includes('/Plugin/')) entry.isPlugin = true;
|
|
5845
|
+
if (f.includes('/Observer/')) entry.isObserver = true;
|
|
5846
|
+
if (f.endsWith('.xml')) entry.type = 'xml';
|
|
5847
|
+
const phpMatch = f.match(/\/([A-Z]\w+)\.php$/);
|
|
5848
|
+
if (phpMatch) entry.className = phpMatch[1];
|
|
5849
|
+
res.push(entry);
|
|
5850
|
+
}
|
|
5851
|
+
} catch {}
|
|
5852
|
+
}
|
|
5762
5853
|
text = `Module: ${a.moduleName} (${res.length} files)\n`;
|
|
5763
5854
|
const cats = { controllers: '/Controller/', models: '/Model/', plugins: '/Plugin/', observers: '/Observer/', api: '/Api/' };
|
|
5764
5855
|
for (const [cat, pattern] of Object.entries(cats)) {
|
|
@@ -5875,8 +5966,9 @@ async function main() {
|
|
|
5875
5966
|
logToFile('WARN', `Serve process version mismatch: ${staleVersion} vs ${__pkg.version} — killing stale process`);
|
|
5876
5967
|
console.error(`Killing stale serve process (version ${staleVersion}, current ${__pkg.version})`);
|
|
5877
5968
|
killStaleServeProcess();
|
|
5878
|
-
// Remove stale socket so we don't connect to
|
|
5969
|
+
// Remove stale socket and lock so we don't connect to dead process
|
|
5879
5970
|
try { if (existsSync(SOCK_PATH)) unlinkSync(SOCK_PATH); } catch {}
|
|
5971
|
+
releasePrimaryLock();
|
|
5880
5972
|
}
|
|
5881
5973
|
|
|
5882
5974
|
const connected = await tryConnectSocket();
|
|
@@ -5887,47 +5979,53 @@ async function main() {
|
|
|
5887
5979
|
role = 'primary';
|
|
5888
5980
|
logToFile('INFO', 'Acquired primary lock — this instance owns the serve process');
|
|
5889
5981
|
|
|
5890
|
-
//
|
|
5891
|
-
|
|
5892
|
-
|
|
5893
|
-
|
|
5894
|
-
startBackgroundReindex();
|
|
5895
|
-
} else {
|
|
5896
|
-
logToFile('INFO', 'Existing database is compatible — reusing index');
|
|
5897
|
-
}
|
|
5898
|
-
} else if (config.magentoRoot && existsSync(config.magentoRoot)) {
|
|
5899
|
-
logToFile('INFO', 'No index database found — scheduling background index');
|
|
5900
|
-
startBackgroundReindex();
|
|
5901
|
-
}
|
|
5902
|
-
|
|
5903
|
-
const canStartServe = !reindexInProgress || (existsSync(config.dbPath) && (() => { try { return statSync(config.dbPath).size > 100; } catch { return false; } })());
|
|
5904
|
-
if (canStartServe) {
|
|
5982
|
+
// Start serve process in background — don't block tool availability
|
|
5983
|
+
// Tools with filesystem fallbacks work immediately via execFileSync.
|
|
5984
|
+
// Serve process provides faster search once ready.
|
|
5985
|
+
(async () => {
|
|
5905
5986
|
try {
|
|
5906
|
-
|
|
5907
|
-
if (
|
|
5908
|
-
|
|
5909
|
-
|
|
5910
|
-
|
|
5911
|
-
]);
|
|
5912
|
-
if (ready) {
|
|
5913
|
-
logToFile('INFO', 'Serve process ready (primary)');
|
|
5914
|
-
console.error('Serve process ready (primary)');
|
|
5915
|
-
startSocketProxy();
|
|
5987
|
+
// Check DB format (uses cache → instant if already validated)
|
|
5988
|
+
if (existsSync(config.dbPath)) {
|
|
5989
|
+
if (!(await checkDbFormat())) {
|
|
5990
|
+
logToFile('WARN', 'Database format incompatible — scheduling background re-index');
|
|
5991
|
+
startBackgroundReindex();
|
|
5916
5992
|
} else {
|
|
5917
|
-
logToFile('
|
|
5918
|
-
|
|
5993
|
+
logToFile('INFO', 'Existing database is compatible — reusing index');
|
|
5994
|
+
}
|
|
5995
|
+
} else if (config.magentoRoot && existsSync(config.magentoRoot)) {
|
|
5996
|
+
logToFile('INFO', 'No index database found — scheduling background index');
|
|
5997
|
+
startBackgroundReindex();
|
|
5998
|
+
}
|
|
5999
|
+
|
|
6000
|
+
const canStartServe = !reindexInProgress || (existsSync(config.dbPath) && (() => { try { return statSync(config.dbPath).size > 100; } catch { return false; } })());
|
|
6001
|
+
if (canStartServe) {
|
|
6002
|
+
startServeProcess();
|
|
6003
|
+
if (serveReadyPromise) {
|
|
6004
|
+
const ready = await Promise.race([
|
|
6005
|
+
serveReadyPromise,
|
|
6006
|
+
new Promise(r => setTimeout(() => r(false), 60000))
|
|
6007
|
+
]);
|
|
6008
|
+
if (ready) {
|
|
6009
|
+
logToFile('INFO', 'Serve process ready (primary)');
|
|
6010
|
+
console.error('Serve process ready (primary)');
|
|
6011
|
+
startSocketProxy();
|
|
6012
|
+
} else {
|
|
6013
|
+
logToFile('WARN', 'Serve process not ready in time, will use fallback');
|
|
6014
|
+
console.error('Serve process not ready in time, will use fallback');
|
|
6015
|
+
}
|
|
5919
6016
|
}
|
|
5920
6017
|
}
|
|
5921
6018
|
} catch {
|
|
5922
6019
|
// Non-fatal: falls back to execFileSync per query
|
|
5923
6020
|
}
|
|
5924
|
-
}
|
|
6021
|
+
})();
|
|
5925
6022
|
} else {
|
|
5926
|
-
// Another instance is starting up —
|
|
5927
|
-
logToFile('INFO', 'Another instance is primary —
|
|
5928
|
-
console.error('
|
|
5929
|
-
|
|
5930
|
-
|
|
6023
|
+
// Another instance is starting up — try socket briefly, then fall through
|
|
6024
|
+
logToFile('INFO', 'Another instance is primary — trying socket...');
|
|
6025
|
+
console.error('Trying to join existing serve process...');
|
|
6026
|
+
// Quick check: 3 attempts at 2s intervals (6s max), then give up and use fallback
|
|
6027
|
+
for (let i = 0; i < 3; i++) {
|
|
6028
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
5931
6029
|
if (await tryConnectSocket()) {
|
|
5932
6030
|
logToFile('INFO', 'Connected to socket after waiting (secondary)');
|
|
5933
6031
|
console.error('Joined existing serve process (secondary)');
|
|
@@ -5935,15 +6033,21 @@ async function main() {
|
|
|
5935
6033
|
}
|
|
5936
6034
|
}
|
|
5937
6035
|
if (!globalServeQuery) {
|
|
5938
|
-
logToFile('
|
|
6036
|
+
logToFile('INFO', 'Socket not available — tools will use cold-start fallback');
|
|
5939
6037
|
}
|
|
5940
6038
|
}
|
|
5941
6039
|
|
|
5942
|
-
|
|
5943
|
-
} finally {
|
|
6040
|
+
// Mark tools as available ASAP — filesystem fallbacks work without serve process
|
|
5944
6041
|
warmupInProgress = false;
|
|
5945
|
-
logToFile('INFO', '
|
|
6042
|
+
logToFile('INFO', 'Tools available (serve process loading in background)');
|
|
5946
6043
|
console.error('Warmup complete — all tools available');
|
|
6044
|
+
|
|
6045
|
+
// Load descriptions in background (non-blocking)
|
|
6046
|
+
loadDescriptions().catch(() => {});
|
|
6047
|
+
} catch (err) {
|
|
6048
|
+
warmupInProgress = false;
|
|
6049
|
+
logToFile('WARN', `Startup error (tools still available via fallback): ${err.message}`);
|
|
6050
|
+
console.error('Warmup complete — all tools available (with fallbacks)');
|
|
5947
6051
|
}
|
|
5948
6052
|
}
|
|
5949
6053
|
|