magector 2.2.6 → 2.3.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 +6 -12
- package/src/mcp-server.js +408 -9
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "magector",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.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",
|
|
@@ -23,13 +23,7 @@
|
|
|
23
23
|
"test": "node tests/unit.test.js && node tests/mcp-server.test.js",
|
|
24
24
|
"test:unit": "node tests/unit.test.js",
|
|
25
25
|
"test:integration": "node tests/mcp-server.test.js",
|
|
26
|
-
"test:no-index": "node tests/mcp-server.test.js --no-index"
|
|
27
|
-
"test:accuracy": "node tests/mcp-accuracy.test.js",
|
|
28
|
-
"test:accuracy:verbose": "node tests/mcp-accuracy.test.js --verbose",
|
|
29
|
-
"test:sona-eval": "node tests/mcp-sona-eval.test.js",
|
|
30
|
-
"test:sona-eval:verbose": "node tests/mcp-sona-eval.test.js --verbose",
|
|
31
|
-
"test:describe-eval": "node tests/describe-benefit-eval.test.js",
|
|
32
|
-
"test:describe-eval:verbose": "node tests/describe-benefit-eval.test.js --verbose"
|
|
26
|
+
"test:no-index": "node tests/mcp-server.test.js --no-index"
|
|
33
27
|
},
|
|
34
28
|
"dependencies": {
|
|
35
29
|
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
@@ -39,10 +33,10 @@
|
|
|
39
33
|
"ruvector": "^0.1.96"
|
|
40
34
|
},
|
|
41
35
|
"optionalDependencies": {
|
|
42
|
-
"@magector/cli-darwin-arm64": "2.
|
|
43
|
-
"@magector/cli-linux-x64": "2.
|
|
44
|
-
"@magector/cli-linux-arm64": "2.
|
|
45
|
-
"@magector/cli-win32-x64": "2.
|
|
36
|
+
"@magector/cli-darwin-arm64": "2.3.0",
|
|
37
|
+
"@magector/cli-linux-x64": "2.3.0",
|
|
38
|
+
"@magector/cli-linux-arm64": "2.3.0",
|
|
39
|
+
"@magector/cli-win32-x64": "2.3.0"
|
|
46
40
|
},
|
|
47
41
|
"keywords": [
|
|
48
42
|
"magento",
|
package/src/mcp-server.js
CHANGED
|
@@ -2362,6 +2362,244 @@ async function findDiWiring(className) {
|
|
|
2362
2362
|
return result;
|
|
2363
2363
|
}
|
|
2364
2364
|
|
|
2365
|
+
// ─── Trace Data Flow ───────────────────────────────────────────
|
|
2366
|
+
// Find all setters and getters for a data attribute across the codebase
|
|
2367
|
+
|
|
2368
|
+
async function traceDataFlow(attributeKey, modelClass) {
|
|
2369
|
+
const root = config.magentoRoot;
|
|
2370
|
+
|
|
2371
|
+
// Convert snake_case to PascalCase for magic method names
|
|
2372
|
+
const pascal = attributeKey.split('_').map(p => p.charAt(0).toUpperCase() + p.slice(1)).join('');
|
|
2373
|
+
const setterMethod = `set${pascal}`;
|
|
2374
|
+
const getterMethod = `get${pascal}`;
|
|
2375
|
+
|
|
2376
|
+
const result = {
|
|
2377
|
+
attributeKey,
|
|
2378
|
+
setterMethod,
|
|
2379
|
+
getterMethod,
|
|
2380
|
+
setters: [],
|
|
2381
|
+
getters: [],
|
|
2382
|
+
xmlReferences: []
|
|
2383
|
+
};
|
|
2384
|
+
|
|
2385
|
+
// 1. Search PHP files for setters/getters
|
|
2386
|
+
const phpFiles = await glob('**/*.php', {
|
|
2387
|
+
cwd: root, absolute: true, nodir: true,
|
|
2388
|
+
ignore: ['**/test/**', '**/tests/**', '**/Test/**', '**/Tests/**']
|
|
2389
|
+
});
|
|
2390
|
+
|
|
2391
|
+
const setterRegex = new RegExp(
|
|
2392
|
+
`(?:->|::)${setterMethod}\\s*\\(|` +
|
|
2393
|
+
`setData\\s*\\(\\s*['"]${attributeKey.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}['"]`
|
|
2394
|
+
);
|
|
2395
|
+
const getterRegex = new RegExp(
|
|
2396
|
+
`(?:->|::)${getterMethod}\\s*\\(|` +
|
|
2397
|
+
`getData\\s*\\(\\s*['"]${attributeKey.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}['"]`
|
|
2398
|
+
);
|
|
2399
|
+
// Also match addData calls that could set this attribute
|
|
2400
|
+
const addDataRegex = new RegExp(
|
|
2401
|
+
`addData\\s*\\(.*${attributeKey.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`
|
|
2402
|
+
);
|
|
2403
|
+
// Match constant definitions for this key
|
|
2404
|
+
const constRegex = new RegExp(
|
|
2405
|
+
`(?:const\\s+\\w+\\s*=\\s*['"]${attributeKey.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}['"])`
|
|
2406
|
+
);
|
|
2407
|
+
|
|
2408
|
+
const shortModel = modelClass ? modelClass.split('\\').pop() : null;
|
|
2409
|
+
|
|
2410
|
+
for (const phpFile of phpFiles) {
|
|
2411
|
+
let content;
|
|
2412
|
+
try { content = readFileSync(phpFile, 'utf-8'); } catch { continue; }
|
|
2413
|
+
|
|
2414
|
+
// Quick pre-check
|
|
2415
|
+
if (!content.includes(setterMethod) && !content.includes(getterMethod) &&
|
|
2416
|
+
!content.includes(attributeKey)) continue;
|
|
2417
|
+
|
|
2418
|
+
// If modelClass specified, prioritize files that reference it
|
|
2419
|
+
const refsModel = !shortModel || content.includes(shortModel);
|
|
2420
|
+
|
|
2421
|
+
const relativePath = phpFile.replace(root + '/', '');
|
|
2422
|
+
const classMatch = content.match(/(?:class|abstract\s+class|trait)\s+(\w+)/);
|
|
2423
|
+
const nsMatch = content.match(/namespace\s+([\w\\]+)/);
|
|
2424
|
+
const className = classMatch ? classMatch[1] : path.basename(phpFile, '.php');
|
|
2425
|
+
const fqcn = nsMatch ? `${nsMatch[1]}\\${className}` : className;
|
|
2426
|
+
const lines = content.split('\n');
|
|
2427
|
+
|
|
2428
|
+
// Determine area from path
|
|
2429
|
+
const area = relativePath.includes('/frontend/') ? 'frontend'
|
|
2430
|
+
: relativePath.includes('/adminhtml/') ? 'adminhtml'
|
|
2431
|
+
: relativePath.includes('/graphql/') ? 'graphql' : 'global';
|
|
2432
|
+
|
|
2433
|
+
// Find setter lines
|
|
2434
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2435
|
+
const line = lines[i];
|
|
2436
|
+
if (setterRegex.test(line) || (addDataRegex.test(line) && line.includes(attributeKey))) {
|
|
2437
|
+
// Find enclosing method
|
|
2438
|
+
let methodName = null;
|
|
2439
|
+
for (let j = i; j >= Math.max(0, i - 30); j--) {
|
|
2440
|
+
const mMatch = lines[j].match(/(?:public|protected|private|static)\s+function\s+(\w+)/);
|
|
2441
|
+
if (mMatch) { methodName = mMatch[1]; break; }
|
|
2442
|
+
}
|
|
2443
|
+
result.setters.push({
|
|
2444
|
+
path: relativePath,
|
|
2445
|
+
class: fqcn,
|
|
2446
|
+
method: methodName,
|
|
2447
|
+
line: i + 1,
|
|
2448
|
+
snippet: line.trim().slice(0, 200),
|
|
2449
|
+
referencesModel: refsModel,
|
|
2450
|
+
area
|
|
2451
|
+
});
|
|
2452
|
+
break; // one entry per file for setters
|
|
2453
|
+
}
|
|
2454
|
+
}
|
|
2455
|
+
|
|
2456
|
+
// Find getter lines
|
|
2457
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2458
|
+
const line = lines[i];
|
|
2459
|
+
if (getterRegex.test(line)) {
|
|
2460
|
+
let methodName = null;
|
|
2461
|
+
for (let j = i; j >= Math.max(0, i - 30); j--) {
|
|
2462
|
+
const mMatch = lines[j].match(/(?:public|protected|private|static)\s+function\s+(\w+)/);
|
|
2463
|
+
if (mMatch) { methodName = mMatch[1]; break; }
|
|
2464
|
+
}
|
|
2465
|
+
result.getters.push({
|
|
2466
|
+
path: relativePath,
|
|
2467
|
+
class: fqcn,
|
|
2468
|
+
method: methodName,
|
|
2469
|
+
line: i + 1,
|
|
2470
|
+
snippet: line.trim().slice(0, 200),
|
|
2471
|
+
referencesModel: refsModel,
|
|
2472
|
+
area
|
|
2473
|
+
});
|
|
2474
|
+
break; // one entry per file for getters
|
|
2475
|
+
}
|
|
2476
|
+
}
|
|
2477
|
+
|
|
2478
|
+
// Find constant definitions
|
|
2479
|
+
if (constRegex.test(content)) {
|
|
2480
|
+
const constLine = lines.findIndex(l => constRegex.test(l));
|
|
2481
|
+
if (constLine >= 0) {
|
|
2482
|
+
result.setters.push({
|
|
2483
|
+
path: relativePath,
|
|
2484
|
+
class: fqcn,
|
|
2485
|
+
method: null,
|
|
2486
|
+
line: constLine + 1,
|
|
2487
|
+
snippet: lines[constLine].trim().slice(0, 200),
|
|
2488
|
+
type: 'constant',
|
|
2489
|
+
area
|
|
2490
|
+
});
|
|
2491
|
+
}
|
|
2492
|
+
}
|
|
2493
|
+
}
|
|
2494
|
+
|
|
2495
|
+
// 2. Search XML files (sales.xml, extension_attributes.xml) for attribute references
|
|
2496
|
+
const xmlFiles = await glob('**/etc/**/*.xml', { cwd: root, absolute: true, nodir: true });
|
|
2497
|
+
const escapedKey = attributeKey.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
2498
|
+
const xmlRegex = new RegExp(escapedKey);
|
|
2499
|
+
|
|
2500
|
+
for (const xmlFile of xmlFiles) {
|
|
2501
|
+
let content;
|
|
2502
|
+
try { content = readFileSync(xmlFile, 'utf-8'); } catch { continue; }
|
|
2503
|
+
if (!xmlRegex.test(content)) continue;
|
|
2504
|
+
|
|
2505
|
+
const relativePath = xmlFile.replace(root + '/', '');
|
|
2506
|
+
const lines = content.split('\n');
|
|
2507
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2508
|
+
if (xmlRegex.test(lines[i])) {
|
|
2509
|
+
result.xmlReferences.push({
|
|
2510
|
+
path: relativePath,
|
|
2511
|
+
line: i + 1,
|
|
2512
|
+
snippet: lines[i].trim().slice(0, 200)
|
|
2513
|
+
});
|
|
2514
|
+
break;
|
|
2515
|
+
}
|
|
2516
|
+
}
|
|
2517
|
+
}
|
|
2518
|
+
|
|
2519
|
+
// Sort: files referencing the model get priority
|
|
2520
|
+
if (shortModel) {
|
|
2521
|
+
result.setters.sort((a, b) => (b.referencesModel ? 1 : 0) - (a.referencesModel ? 1 : 0));
|
|
2522
|
+
result.getters.sort((a, b) => (b.referencesModel ? 1 : 0) - (a.referencesModel ? 1 : 0));
|
|
2523
|
+
}
|
|
2524
|
+
|
|
2525
|
+
return result;
|
|
2526
|
+
}
|
|
2527
|
+
|
|
2528
|
+
// ─── Find Event Dispatchers ────────────────────────────────────
|
|
2529
|
+
// Find all PHP locations where a specific Magento event is dispatched
|
|
2530
|
+
|
|
2531
|
+
async function findEventDispatchers(eventName) {
|
|
2532
|
+
const root = config.magentoRoot;
|
|
2533
|
+
|
|
2534
|
+
const result = {
|
|
2535
|
+
eventName,
|
|
2536
|
+
dispatchers: [],
|
|
2537
|
+
observerCount: 0
|
|
2538
|
+
};
|
|
2539
|
+
|
|
2540
|
+
const escaped = eventName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
2541
|
+
// Match eventManager->dispatch('event_name' and similar patterns
|
|
2542
|
+
const dispatchRegex = new RegExp(
|
|
2543
|
+
`dispatch\\s*\\(\\s*['"]${escaped}['"]`
|
|
2544
|
+
);
|
|
2545
|
+
|
|
2546
|
+
// 1. Grep PHP files for exact dispatch calls
|
|
2547
|
+
const phpFiles = await glob('**/*.php', {
|
|
2548
|
+
cwd: root, absolute: true, nodir: true,
|
|
2549
|
+
ignore: ['**/test/**', '**/tests/**', '**/Test/**', '**/Tests/**']
|
|
2550
|
+
});
|
|
2551
|
+
|
|
2552
|
+
for (const phpFile of phpFiles) {
|
|
2553
|
+
let content;
|
|
2554
|
+
try { content = readFileSync(phpFile, 'utf-8'); } catch { continue; }
|
|
2555
|
+
if (!content.includes(eventName)) continue;
|
|
2556
|
+
|
|
2557
|
+
const relativePath = phpFile.replace(root + '/', '');
|
|
2558
|
+
const lines = content.split('\n');
|
|
2559
|
+
const classMatch = content.match(/(?:class|abstract\s+class|trait)\s+(\w+)/);
|
|
2560
|
+
const nsMatch = content.match(/namespace\s+([\w\\]+)/);
|
|
2561
|
+
const className = classMatch ? classMatch[1] : path.basename(phpFile, '.php');
|
|
2562
|
+
const fqcn = nsMatch ? `${nsMatch[1]}\\${className}` : className;
|
|
2563
|
+
|
|
2564
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2565
|
+
if (dispatchRegex.test(lines[i])) {
|
|
2566
|
+
// Find enclosing method
|
|
2567
|
+
let methodName = null;
|
|
2568
|
+
for (let j = i; j >= Math.max(0, i - 30); j--) {
|
|
2569
|
+
const mMatch = lines[j].match(/(?:public|protected|private|static)\s+function\s+(\w+)/);
|
|
2570
|
+
if (mMatch) { methodName = mMatch[1]; break; }
|
|
2571
|
+
}
|
|
2572
|
+
|
|
2573
|
+
// Get surrounding context (2 lines before and after)
|
|
2574
|
+
const ctxStart = Math.max(0, i - 2);
|
|
2575
|
+
const ctxEnd = Math.min(lines.length - 1, i + 2);
|
|
2576
|
+
const context = lines.slice(ctxStart, ctxEnd + 1).map(l => l.trimEnd()).join('\n');
|
|
2577
|
+
|
|
2578
|
+
result.dispatchers.push({
|
|
2579
|
+
path: relativePath,
|
|
2580
|
+
class: fqcn,
|
|
2581
|
+
method: methodName,
|
|
2582
|
+
line: i + 1,
|
|
2583
|
+
snippet: lines[i].trim().slice(0, 200),
|
|
2584
|
+
context: context.slice(0, 500)
|
|
2585
|
+
});
|
|
2586
|
+
}
|
|
2587
|
+
}
|
|
2588
|
+
}
|
|
2589
|
+
|
|
2590
|
+
// 2. Count registered observers for context
|
|
2591
|
+
const eventsFiles = await glob('**/etc/**/events.xml', { cwd: root, absolute: true, nodir: true });
|
|
2592
|
+
for (const file of eventsFiles) {
|
|
2593
|
+
let content;
|
|
2594
|
+
try { content = readFileSync(file, 'utf-8'); } catch { continue; }
|
|
2595
|
+
if (!content.includes(eventName)) continue;
|
|
2596
|
+
const obsMatches = content.match(/<observer\s+/g);
|
|
2597
|
+
if (obsMatches) result.observerCount += obsMatches.length;
|
|
2598
|
+
}
|
|
2599
|
+
|
|
2600
|
+
return result;
|
|
2601
|
+
}
|
|
2602
|
+
|
|
2365
2603
|
// ─── Trace Call Chain ───────────────────────────────────────────
|
|
2366
2604
|
// Trace internal method execution: method calls, dispatched events, observers
|
|
2367
2605
|
|
|
@@ -2606,8 +2844,11 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
2606
2844
|
default: 10
|
|
2607
2845
|
},
|
|
2608
2846
|
moduleFilter: {
|
|
2609
|
-
|
|
2610
|
-
|
|
2847
|
+
oneOf: [
|
|
2848
|
+
{ type: 'string' },
|
|
2849
|
+
{ type: 'array', items: { type: 'string' } }
|
|
2850
|
+
],
|
|
2851
|
+
description: 'Filter results by vendor/module pattern(s). Accepts a single string or array of strings. Supports wildcards and vendor prefix matching. Uses "/" or "_" interchangeably as separator. Examples: "Vendor_*", ["drmax_paymentrestrictions", "drmax_module-free-shipping"], "Magento_Catalog".'
|
|
2611
2852
|
},
|
|
2612
2853
|
expand: {
|
|
2613
2854
|
type: 'boolean',
|
|
@@ -3136,6 +3377,38 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
3136
3377
|
required: ['className', 'methodName']
|
|
3137
3378
|
}
|
|
3138
3379
|
},
|
|
3380
|
+
{
|
|
3381
|
+
name: 'magento_trace_data_flow',
|
|
3382
|
+
description: 'Trace how a data attribute flows through the Magento codebase: find all PHP files that set (via magic setter, setData, addData) and get (via magic getter, getData) a specific attribute key. Shows which classes write vs read the attribute, in which methods, and whether XML configs reference it. Use this to understand data dependencies — e.g., who sets drmax_discounted_price_incl_tax on Quote\\Address and who reads it.',
|
|
3383
|
+
inputSchema: {
|
|
3384
|
+
type: 'object',
|
|
3385
|
+
properties: {
|
|
3386
|
+
attributeKey: {
|
|
3387
|
+
type: 'string',
|
|
3388
|
+
description: 'The snake_case data attribute key to trace. Examples: "drmax_discounted_price_incl_tax", "base_grand_total", "drmax_free_shipping_price", "subtotal_with_discount"'
|
|
3389
|
+
},
|
|
3390
|
+
modelClass: {
|
|
3391
|
+
type: 'string',
|
|
3392
|
+
description: 'Optional: model class name to prioritize results that reference this class. Examples: "Quote\\Address", "Order", "Product"'
|
|
3393
|
+
}
|
|
3394
|
+
},
|
|
3395
|
+
required: ['attributeKey']
|
|
3396
|
+
}
|
|
3397
|
+
},
|
|
3398
|
+
{
|
|
3399
|
+
name: 'magento_find_event_dispatchers',
|
|
3400
|
+
description: 'Find all PHP locations where a specific Magento event is dispatched via eventManager->dispatch(). Unlike magento_find_event_flow (which shows the full chain: dispatchers+observers+handlers), this tool focuses exclusively on finding WHERE an event is triggered — with exact grep matching, method context, and surrounding code. Use this to answer "does class X dispatch event Y?" or "who triggers this event?".',
|
|
3401
|
+
inputSchema: {
|
|
3402
|
+
type: 'object',
|
|
3403
|
+
properties: {
|
|
3404
|
+
eventName: {
|
|
3405
|
+
type: 'string',
|
|
3406
|
+
description: 'Magento event name to find dispatchers for. Examples: "sales_order_place_after", "drmax_discount_rule_validation_before", "checkout_cart_add_product_complete"'
|
|
3407
|
+
}
|
|
3408
|
+
},
|
|
3409
|
+
required: ['eventName']
|
|
3410
|
+
}
|
|
3411
|
+
},
|
|
3139
3412
|
]
|
|
3140
3413
|
}));
|
|
3141
3414
|
|
|
@@ -3147,7 +3420,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
3147
3420
|
// ── Warmup guard: index compatibility check or serve process still loading ──
|
|
3148
3421
|
const indexFreeTools = ['magento_stats', 'magento_analyze_diff', 'magento_complexity',
|
|
3149
3422
|
'magento_trace_dependency', 'magento_error_parser', 'magento_find_layout',
|
|
3150
|
-
'magento_impact_analysis', 'magento_find_event_flow', 'magento_find_test'
|
|
3423
|
+
'magento_impact_analysis', 'magento_find_event_flow', 'magento_find_test',
|
|
3424
|
+
'magento_trace_data_flow', 'magento_find_event_dispatchers'];
|
|
3151
3425
|
if (warmupInProgress && !indexFreeTools.includes(name)) {
|
|
3152
3426
|
logToFile('REQ', `${name} → blocked (warmup: loading index)`);
|
|
3153
3427
|
return {
|
|
@@ -3330,12 +3604,65 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
3330
3604
|
);
|
|
3331
3605
|
results = rerank(results, { isPlugin: true, pathContains: ['/Plugin/', 'di.xml'] });
|
|
3332
3606
|
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
|
|
3336
|
-
|
|
3337
|
-
|
|
3338
|
-
|
|
3607
|
+
// Enrich results with di.xml registration area (frontend/adminhtml/global)
|
|
3608
|
+
const enrichedResults = results.slice(0, 15).map(r => {
|
|
3609
|
+
if (!r.path) return r;
|
|
3610
|
+
let diArea = 'global';
|
|
3611
|
+
if (r.path.includes('/etc/adminhtml/')) diArea = 'adminhtml';
|
|
3612
|
+
else if (r.path.includes('/etc/frontend/')) diArea = 'frontend';
|
|
3613
|
+
else if (r.path.includes('/etc/graphql/')) diArea = 'graphql';
|
|
3614
|
+
else if (r.path.includes('/etc/webapi_rest/')) diArea = 'webapi_rest';
|
|
3615
|
+
else if (r.path.includes('/etc/webapi_soap/')) diArea = 'webapi_soap';
|
|
3616
|
+
else if (r.path.includes('/etc/crontab/')) diArea = 'crontab';
|
|
3617
|
+
return { ...r, diArea };
|
|
3618
|
+
});
|
|
3619
|
+
|
|
3620
|
+
// If targetClass provided, also scan di.xml for explicit registrations
|
|
3621
|
+
let diRegistrations = [];
|
|
3622
|
+
if (args.targetClass) {
|
|
3623
|
+
const fpRoot = config.magentoRoot;
|
|
3624
|
+
const diFiles = await glob('**/etc/**/di.xml', { cwd: fpRoot, absolute: true, nodir: true });
|
|
3625
|
+
const shortTarget = args.targetClass.split('\\').pop();
|
|
3626
|
+
for (const diFile of diFiles) {
|
|
3627
|
+
let content;
|
|
3628
|
+
try { content = readFileSync(diFile, 'utf-8'); } catch { continue; }
|
|
3629
|
+
if (!content.includes(shortTarget)) continue;
|
|
3630
|
+
const relPath = diFile.replace(fpRoot + '/', '');
|
|
3631
|
+
// Find plugin registrations for this target
|
|
3632
|
+
const typeBlockRegex = /<type\s+name="([^"]+)"[^>]*>([\s\S]*?)<\/type>/g;
|
|
3633
|
+
let tm;
|
|
3634
|
+
while ((tm = typeBlockRegex.exec(content)) !== null) {
|
|
3635
|
+
const typeName = tm[1];
|
|
3636
|
+
if (!typeName.includes(shortTarget)) continue;
|
|
3637
|
+
const block = tm[2];
|
|
3638
|
+
const pluginRegex = /<plugin\s+name="([^"]+)"[^>]*type="([^"]+)"[^>]*/g;
|
|
3639
|
+
let pm;
|
|
3640
|
+
while ((pm = pluginRegex.exec(block)) !== null) {
|
|
3641
|
+
let area = 'global';
|
|
3642
|
+
if (relPath.includes('/etc/adminhtml/')) area = 'adminhtml';
|
|
3643
|
+
else if (relPath.includes('/etc/frontend/')) area = 'frontend';
|
|
3644
|
+
else if (relPath.includes('/etc/graphql/')) area = 'graphql';
|
|
3645
|
+
diRegistrations.push({
|
|
3646
|
+
target: typeName,
|
|
3647
|
+
pluginName: pm[1],
|
|
3648
|
+
pluginClass: pm[2],
|
|
3649
|
+
area,
|
|
3650
|
+
file: relPath
|
|
3651
|
+
});
|
|
3652
|
+
}
|
|
3653
|
+
}
|
|
3654
|
+
}
|
|
3655
|
+
}
|
|
3656
|
+
|
|
3657
|
+
let text = formatSearchResults(enrichedResults);
|
|
3658
|
+
if (diRegistrations.length > 0) {
|
|
3659
|
+
text += `\n\n### DI Plugin Registrations for ${args.targetClass} (${diRegistrations.length})\n`;
|
|
3660
|
+
for (const reg of diRegistrations) {
|
|
3661
|
+
text += `- **${reg.pluginName}** → \`${reg.pluginClass}\` [${reg.area}] (${reg.file})\n`;
|
|
3662
|
+
}
|
|
3663
|
+
}
|
|
3664
|
+
|
|
3665
|
+
return { content: [{ type: 'text', text }] };
|
|
3339
3666
|
}
|
|
3340
3667
|
|
|
3341
3668
|
case 'magento_find_observer': {
|
|
@@ -4160,6 +4487,78 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
4160
4487
|
return { content: [{ type: 'text', text }] };
|
|
4161
4488
|
}
|
|
4162
4489
|
|
|
4490
|
+
case 'magento_trace_data_flow': {
|
|
4491
|
+
const flowResult = await traceDataFlow(args.attributeKey, args.modelClass);
|
|
4492
|
+
|
|
4493
|
+
let text = `## Data Flow: \`${flowResult.attributeKey}\`\n`;
|
|
4494
|
+
text += `Magic setter: \`${flowResult.setterMethod}()\` · Magic getter: \`${flowResult.getterMethod}()\`\n\n`;
|
|
4495
|
+
|
|
4496
|
+
if (flowResult.setters.length > 0) {
|
|
4497
|
+
text += `### Setters (${flowResult.setters.length})\n`;
|
|
4498
|
+
for (const s of flowResult.setters) {
|
|
4499
|
+
const typeTag = s.type === 'constant' ? ' [const]' : '';
|
|
4500
|
+
const methodTag = s.method ? `::${s.method}` : '';
|
|
4501
|
+
text += `- \`${s.class}${methodTag}\`${typeTag} (${s.path}:${s.line})\n`;
|
|
4502
|
+
if (s.snippet) text += ` \`${s.snippet}\`\n`;
|
|
4503
|
+
}
|
|
4504
|
+
text += '\n';
|
|
4505
|
+
} else {
|
|
4506
|
+
text += '### Setters\n_No setters found._\n\n';
|
|
4507
|
+
}
|
|
4508
|
+
|
|
4509
|
+
if (flowResult.getters.length > 0) {
|
|
4510
|
+
text += `### Getters (${flowResult.getters.length})\n`;
|
|
4511
|
+
for (const g of flowResult.getters) {
|
|
4512
|
+
const methodTag = g.method ? `::${g.method}` : '';
|
|
4513
|
+
text += `- \`${g.class}${methodTag}\` (${g.path}:${g.line})\n`;
|
|
4514
|
+
if (g.snippet) text += ` \`${g.snippet}\`\n`;
|
|
4515
|
+
}
|
|
4516
|
+
text += '\n';
|
|
4517
|
+
} else {
|
|
4518
|
+
text += '### Getters\n_No getters found._\n\n';
|
|
4519
|
+
}
|
|
4520
|
+
|
|
4521
|
+
if (flowResult.xmlReferences.length > 0) {
|
|
4522
|
+
text += `### XML References (${flowResult.xmlReferences.length})\n`;
|
|
4523
|
+
for (const x of flowResult.xmlReferences) {
|
|
4524
|
+
text += `- ${x.path}:${x.line}\n \`${x.snippet}\`\n`;
|
|
4525
|
+
}
|
|
4526
|
+
}
|
|
4527
|
+
|
|
4528
|
+
return { content: [{ type: 'text', text }] };
|
|
4529
|
+
}
|
|
4530
|
+
|
|
4531
|
+
case 'magento_find_event_dispatchers': {
|
|
4532
|
+
const dispResult = await findEventDispatchers(args.eventName);
|
|
4533
|
+
|
|
4534
|
+
let text = `## Event Dispatchers: \`${dispResult.eventName}\`\n\n`;
|
|
4535
|
+
|
|
4536
|
+
if (dispResult.dispatchers.length > 0) {
|
|
4537
|
+
text += `Found ${dispResult.dispatchers.length} dispatch location(s)`;
|
|
4538
|
+
if (dispResult.observerCount > 0) {
|
|
4539
|
+
text += ` (${dispResult.observerCount} observer(s) registered)`;
|
|
4540
|
+
}
|
|
4541
|
+
text += '.\n\n';
|
|
4542
|
+
|
|
4543
|
+
for (const d of dispResult.dispatchers) {
|
|
4544
|
+
const methodTag = d.method ? `::${d.method}` : '';
|
|
4545
|
+
text += `### \`${d.class}${methodTag}\`\n`;
|
|
4546
|
+
text += `**File:** ${d.path}:${d.line}\n`;
|
|
4547
|
+
if (d.context) {
|
|
4548
|
+
text += '```php\n' + d.context + '\n```\n';
|
|
4549
|
+
}
|
|
4550
|
+
text += '\n';
|
|
4551
|
+
}
|
|
4552
|
+
} else {
|
|
4553
|
+
text += '_No dispatch() calls found for this event._\n';
|
|
4554
|
+
if (dispResult.observerCount > 0) {
|
|
4555
|
+
text += `\nNote: ${dispResult.observerCount} observer(s) are registered for this event — it may be dispatched by Magento core or a module not in the scanned path.\n`;
|
|
4556
|
+
}
|
|
4557
|
+
}
|
|
4558
|
+
|
|
4559
|
+
return { content: [{ type: 'text', text }] };
|
|
4560
|
+
}
|
|
4561
|
+
|
|
4163
4562
|
default:
|
|
4164
4563
|
return {
|
|
4165
4564
|
content: [{
|