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 +31 -6
- package/package.json +6 -12
- package/src/mcp-server.js +410 -11
- package/src/validation/test-data-generator.js +0 -672
- package/src/validation/test-queries.js +0 -326
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
|
|
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
|
[](https://www.rust-lang.org)
|
|
8
8
|
[](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** --
|
|
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/>
|
|
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
|
|
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
|
|
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 (
|
|
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.
|
|
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.
|
|
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.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
|
-
|
|
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_*", ["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", "
|
|
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: "
|
|
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
|
-
|
|
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: [{
|