magector 2.2.6 → 2.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  **Technology-aware MCP server for Magento 2 and Adobe Commerce with intelligent indexing and search.**
4
4
 
5
- Magector is a Model Context Protocol (MCP) server that deeply understands Magento 2 and Adobe Commerce. It builds a semantic vector index of your entire codebase — 18,000+ files across hundreds of modules — and exposes 28 tools that let AI assistants search, navigate, and understand the code with domain-specific intelligence. Instead of grepping for keywords, your AI asks *"how are checkout totals calculated?"* and gets ranked, relevant results in under 50ms, enriched with Magento pattern detection (plugins, observers, controllers, DI preferences, layout XML, and 20+ more).
5
+ Magector is a Model Context Protocol (MCP) server that deeply understands Magento 2 and Adobe Commerce. It builds a semantic vector index of your entire codebase — 18,000+ files across hundreds of modules — and exposes 34 tools that let AI assistants search, navigate, and understand the code with domain-specific intelligence. Instead of grepping for keywords, your AI asks *"how are checkout totals calculated?"* and gets ranked, relevant results in under 50ms, enriched with Magento pattern detection (plugins, observers, controllers, DI preferences, layout XML, and 20+ more).
6
6
 
7
7
  [![Rust](https://img.shields.io/badge/rust-1.75+-orange.svg)](https://www.rust-lang.org)
8
8
  [![Node.js](https://img.shields.io/badge/node-18+-green.svg)](https://nodejs.org)
@@ -58,7 +58,7 @@ The result: your AI assistant calls one MCP tool and gets ranked, pattern-enrich
58
58
  - **Complexity analysis** -- cyclomatic complexity, function count, and hotspot detection across modules
59
59
  - **Fast** -- 10-45ms queries via persistent serve process, batched ONNX embedding with adaptive thread scaling
60
60
  - **LLM description enrichment** -- generate natural-language descriptions of di.xml files using Claude, stored in SQLite, and prepend them to embedding text so descriptions influence vector search ranking (not just post-retrieval display)
61
- - **MCP server** -- 28 tools integrating with Claude Code, Cursor, and any MCP-compatible AI tool
61
+ - **MCP server** -- 34 tools integrating with Claude Code, Cursor, and any MCP-compatible AI tool
62
62
  - **Clean architecture** -- Rust core handles all indexing/search, Node.js MCP server delegates to it
63
63
 
64
64
  ---
@@ -70,7 +70,7 @@ flowchart LR
70
70
  subgraph node ["Node.js Layer"]
71
71
  direction TB
72
72
  G["CLI<br/>init · index · search · describe"]
73
- E["MCP Server<br/>28 tools · LRU cache"]
73
+ E["MCP Server<br/>34 tools · LRU cache"]
74
74
  F["Persistent Serve Process"]
75
75
  G --> F
76
76
  E --> F
@@ -369,7 +369,7 @@ npx magector index --force
369
369
 
370
370
  ## MCP Server Tools
371
371
 
372
- The MCP server exposes 28 tools for AI-assisted Magento 2 and Adobe Commerce development. All search tools return **structured JSON** with file paths, class names, methods, role badges, and content snippets -- enabling AI clients to parse results programmatically and minimize file-read round-trips.
372
+ The MCP server exposes 34 tools for AI-assisted Magento 2 and Adobe Commerce development. All search tools return **structured JSON** with file paths, class names, methods, role badges, and content snippets -- enabling AI clients to parse results programmatically and minimize file-read round-trips.
373
373
 
374
374
  ### Output Format
375
375
 
@@ -432,7 +432,10 @@ All search tools return structured JSON:
432
432
  | `magento_trace_flow` | Trace execution flow from an entry point (route, API, GraphQL, event, cron) -- maps controller → plugins → observers → templates in one call |
433
433
  | `magento_trace_dependency` | Trace DI graph for a class/interface -- preferences, plugins, virtualTypes, argument overrides (parses all di.xml, no index needed) |
434
434
  | `magento_find_event_flow` | Trace complete event chain: dispatchers → observers → handler PHP classes (parses events.xml + vector search) |
435
+ | `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)** |
435
436
  | `magento_find_layout` | Find layout XML files by handle or content -- lists blocks, containers, and referenceBlock declarations |
437
+ | `magento_trace_data_flow` | Trace how a data attribute flows: find all setters (magic setter, setData, addData) and getters (magic getter, getData) across PHP and XML **(v2.3)** |
438
+ | `magento_trace_call_chain` | Trace internal method call chain: follows `$this->method()`, `$this->dep->method()`, and `dispatch()` calls to build an execution tree **(v2.2)** |
436
439
 
437
440
  Auto-detects entry type from pattern (`/V1/...` → API, `snake_case` → event, `camelCase` → GraphQL, `path/segments` → route), or override with `entryType`. Use `depth: "shallow"` (entry + config + plugins) or `depth: "deep"` (adds observers, layout, templates, DI preferences).
438
441
 
@@ -442,6 +445,9 @@ Auto-detects entry type from pattern (`/V1/...` → API, `snake_case` → event,
442
445
  |------|-------------|
443
446
  | `magento_impact_analysis` | Analyze impact of changing a class -- finds use statements, DI references, direct instantiations, and type hints across the codebase |
444
447
  | `magento_find_test` | Find PHPUnit tests for a given class/method -- searches Test/ directories for coverage, mocks, and assertions |
448
+ | `magento_find_implementors` | Find all classes implementing a given PHP interface -- scans `implements` keywords and di.xml `<preference>` declarations **(v2.2)** |
449
+ | `magento_find_callers` | Find all call sites of a method across PHP and XML files -- `->method()` and `::method()` calls **(v2.2)** |
450
+ | `magento_find_di_wiring` | Complete DI picture for a class: preferences, plugins, constructor args, virtual types, and argument overrides from di.xml **(v2.2)** |
445
451
 
446
452
  ### Diagnostics
447
453
 
@@ -470,9 +476,22 @@ Auto-detects entry type from pattern (`/V1/...` → API, `snake_case` → event,
470
476
 
471
477
  - **Hybrid BM25+vector search** -- combines text frequency scoring with semantic vector similarity for better exact class name matches
472
478
  - **Query expansion** -- automatically expands queries with Magento domain synonyms (plugin → interceptor, checkout → cart/quote/totals, etc.)
473
- - **Module filtering** -- `moduleFilter` parameter on `magento_search` to limit results by vendor/module pattern (supports wildcards, e.g., `"Vendor_*"`)
479
+ - **Module filtering** -- `moduleFilter` parameter on `magento_search` to limit results by vendor/module pattern. Accepts a single string or array of strings. Supports wildcards, e.g., `"Vendor_*"` or `["Acme_PaymentGateway", "Acme_FreeShipping"]`
474
480
  - **Non-blocking reindex** -- old index stays usable during background rebuild; new index is built to a temp path and swapped in atomically on completion
475
481
 
482
+ ### Deep Code Analysis (v2.2)
483
+
484
+ - **`magento_find_implementors`** -- find all classes implementing a PHP interface (PHP `implements` + di.xml `<preference>`)
485
+ - **`magento_find_callers`** -- find all call sites of a method across PHP and XML files
486
+ - **`magento_find_di_wiring`** -- complete DI picture: preferences, plugins, constructor args, virtual types, argument overrides
487
+ - **`magento_trace_call_chain`** -- trace internal method execution chain: `$this->method()`, `$this->dep->method()`, and `dispatch()` calls with event→observer resolution
488
+
489
+ ### Data Flow & Event Tracing (v2.3)
490
+
491
+ - **`magento_trace_data_flow`** -- trace all setters and getters for a data attribute (magic methods, setData/getData, addData, constants, XML references). Answers "who writes/reads `custom_discounted_price_incl_tax` on `Quote\Address`?"
492
+ - **`magento_find_event_dispatchers`** -- grep-based exact search for all PHP locations dispatching a specific event, with method context and surrounding code. Complements `magento_find_event_flow` with higher precision.
493
+ - **`magento_find_plugin` area context** -- enriched output shows DI area (frontend/adminhtml/global/graphql) and explicit di.xml plugin registrations when `targetClass` is provided
494
+
476
495
  ### Tool Cross-References
477
496
 
478
497
  Each tool description includes "See also" hints to help AI clients chain tools effectively:
@@ -549,6 +568,12 @@ magento_trace_flow({ entryPoint: "checkout/cart/add", depth: "deep" })
549
568
  magento_trace_flow({ entryPoint: "/V1/products" })
550
569
  magento_trace_flow({ entryPoint: "placeOrder", entryType: "graphql" })
551
570
  magento_trace_flow({ entryPoint: "sales_order_place_after" })
571
+ magento_trace_data_flow({ attributeKey: "custom_discounted_price_incl_tax", modelClass: "Quote\\Address" })
572
+ magento_find_event_dispatchers({ eventName: "custom_discount_rule_validation_before" })
573
+ magento_find_implementors({ interfaceName: "ProductRepositoryInterface" })
574
+ magento_find_callers({ methodName: "collectTotals", className: "TotalsCollector" })
575
+ magento_find_di_wiring({ className: "CartManagementInterface" })
576
+ magento_trace_call_chain({ className: "Magento\\Quote\\Model\\QuoteManagement", methodName: "submit" })
552
577
  ```
553
578
 
554
579
  ---
@@ -629,7 +654,7 @@ cd rust-core && cargo run --release -- validate -m ./magento2 --skip-index
629
654
  magector/
630
655
  ├── src/ # Node.js source
631
656
  │ ├── cli.js # CLI entry point (npx magector <command>)
632
- │ ├── mcp-server.js # MCP server (20 tools, structured JSON output)
657
+ │ ├── mcp-server.js # MCP server (34 tools, structured JSON output)
633
658
  │ ├── binary.js # Platform binary resolver
634
659
  │ ├── model.js # ONNX model resolver/downloader
635
660
  │ ├── init.js # Full init command (index + IDE config)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "magector",
3
- "version": "2.2.6",
3
+ "version": "2.3.1",
4
4
  "description": "Semantic code search for Magento 2 — index, search, MCP server",
5
5
  "type": "module",
6
6
  "main": "src/mcp-server.js",
@@ -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.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"
36
+ "@magector/cli-darwin-arm64": "2.3.1",
37
+ "@magector/cli-linux-x64": "2.3.1",
38
+ "@magector/cli-linux-arm64": "2.3.1",
39
+ "@magector/cli-win32-x64": "2.3.1"
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
- type: 'string',
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.'
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_*", ["Acme_PaymentGateway", "Acme_FreeShipping"], "Magento_Catalog".'
2611
2852
  },
2612
2853
  expand: {
2613
2854
  type: 'boolean',
@@ -3089,7 +3330,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
3089
3330
  properties: {
3090
3331
  methodName: {
3091
3332
  type: 'string',
3092
- description: 'Method name to find callers for. Examples: "execute", "save", "collectTotals", "copySalesRuleIdsFromParentToDrmaxQuote"'
3333
+ description: 'Method name to find callers for. Examples: "execute", "save", "collectTotals", "copySalesRuleIdsFromParentToChildQuote"'
3093
3334
  },
3094
3335
  className: {
3095
3336
  type: 'string',
@@ -3121,7 +3362,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
3121
3362
  properties: {
3122
3363
  className: {
3123
3364
  type: 'string',
3124
- description: 'Full PHP class name (FQCN) to start tracing from. Examples: "DrmaxMarketplace\\OrderSplit\\Model\\CreateOrder\\CreateChildOrder", "Magento\\Quote\\Model\\QuoteManagement"'
3365
+ description: 'Full PHP class name (FQCN) to start tracing from. Examples: "Vendor\\OrderSplit\\Model\\CreateOrder\\CreateChildOrder", "Magento\\Quote\\Model\\QuoteManagement"'
3125
3366
  },
3126
3367
  methodName: {
3127
3368
  type: 'string',
@@ -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 custom_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: "custom_discounted_price_incl_tax", "base_grand_total", "custom_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", "custom_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
- return {
3334
- content: [{
3335
- type: 'text',
3336
- text: formatSearchResults(results.slice(0, 15))
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: [{