magector 2.16.6 → 2.16.9
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 +26 -5
- package/package.json +5 -5
- package/src/mcp-server.js +362 -5
- package/src/templates/claude-md.js +8 -0
- package/src/templates/cursor-rules-mdc.js +5 -0
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 47 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** -- 47 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/>47 tools · LRU cache"]
|
|
74
74
|
F["Persistent Serve Process"]
|
|
75
75
|
G --> F
|
|
76
76
|
E --> F
|
|
@@ -132,6 +132,7 @@ flowchart LR
|
|
|
132
132
|
| Unified metadata | rusqlite (bundled SQLite) | LLM descriptions, method-chain enrichment, process state, cache — all in .magector/data.db |
|
|
133
133
|
| SONA | Custom Rust | Feedback learning with MicroLoRA + EWC++ |
|
|
134
134
|
| MCP server | `@modelcontextprotocol/sdk` | AI tool integration with structured JSON output |
|
|
135
|
+
| Config data | JSON exports in `.magector/config-data/` | One-time `core_config_data` exports per environment for config tracing |
|
|
135
136
|
|
|
136
137
|
---
|
|
137
138
|
|
|
@@ -400,7 +401,7 @@ npx magector index --force
|
|
|
400
401
|
|
|
401
402
|
## MCP Server Tools
|
|
402
403
|
|
|
403
|
-
The MCP server exposes
|
|
404
|
+
The MCP server exposes 47 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.
|
|
404
405
|
|
|
405
406
|
### Output Format
|
|
406
407
|
|
|
@@ -508,6 +509,7 @@ Auto-detects entry type from pattern (`/V1/...` → API, `snake_case` → event,
|
|
|
508
509
|
| `magento_grep` | Exact text/regex search across PHP/XML/PHTML files (`grep -rn -E` internally). Supports `filesOnly` mode (like `grep -l`), `context` lines, `ignoreCase`, `include` patterns. **(v2.9)** |
|
|
509
510
|
| `magento_read` | Read a specific file with optional `methodName` extraction (~10× fewer tokens than reading the whole file) and `startLine`/`endLine` range. **(v2.10)** |
|
|
510
511
|
| `magento_trace_api` | Trace REST/GraphQL API endpoint from URL to implementation: webapi.xml → service interface → DI preference → method body. One call replaces 4-5 grep+read steps. **(v2.11)** |
|
|
512
|
+
| `magento_trace_config` | Trace a config path end-to-end: system.xml admin definition → PHP classes that consume the value → actual DB values from config-data exports. Accepts exact path or keyword search. **(v2.17)** |
|
|
511
513
|
| `magento_find_trigger` | Find database triggers across the codebase |
|
|
512
514
|
| `magento_find_table_usage` | Find all PHP code referencing a specific database table |
|
|
513
515
|
|
|
@@ -702,7 +704,7 @@ cd rust-core && cargo run --release -- validate -m ./magento2 --skip-index
|
|
|
702
704
|
magector/
|
|
703
705
|
├── src/ # Node.js source
|
|
704
706
|
│ ├── cli.js # CLI entry point (npx magector <command>)
|
|
705
|
-
│ ├── mcp-server.js # MCP server (
|
|
707
|
+
│ ├── mcp-server.js # MCP server (47 tools, structured JSON output)
|
|
706
708
|
│ ├── binary.js # Platform binary resolver
|
|
707
709
|
│ ├── model.js # ONNX model resolver/downloader
|
|
708
710
|
│ ├── init.js # Full init command (index + IDE config)
|
|
@@ -1022,6 +1024,25 @@ lib/internal
|
|
|
1022
1024
|
- Patterns without `/` match directory names anywhere in the tree
|
|
1023
1025
|
- Patterns with `/` match relative paths from the project root
|
|
1024
1026
|
|
|
1027
|
+
### Config Data (core_config_data exports)
|
|
1028
|
+
|
|
1029
|
+
The `magento_trace_config` tool can show actual database config values alongside code analysis. Export your `core_config_data` table as JSON and place files in `.magector/config-data/`:
|
|
1030
|
+
|
|
1031
|
+
```bash
|
|
1032
|
+
# Export from MySQL (one-time per environment)
|
|
1033
|
+
mysql -u user -p magento_db -e "SELECT scope, scope_id, path, value FROM core_config_data" --json > .magector/config-data/CZ-production.json
|
|
1034
|
+
|
|
1035
|
+
# Or from n8n/API/any tool that produces:
|
|
1036
|
+
# [{scope, scope_id, path, value}, ...]
|
|
1037
|
+
```
|
|
1038
|
+
|
|
1039
|
+
**File naming:** Use `{country}-{environment}.json`, e.g.:
|
|
1040
|
+
- `CZ-production.json`
|
|
1041
|
+
- `SK-staging.json`
|
|
1042
|
+
- `IT-production.json`
|
|
1043
|
+
|
|
1044
|
+
When `magento_trace_config` traces a config path, it automatically looks up values from all available exports and shows them per environment.
|
|
1045
|
+
|
|
1025
1046
|
### Model Configuration
|
|
1026
1047
|
|
|
1027
1048
|
The ONNX model (`all-MiniLM-L6-v2`) is automatically downloaded on first run to `~/.magector/models/`. To use a different location:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "magector",
|
|
3
|
-
"version": "2.16.
|
|
3
|
+
"version": "2.16.9",
|
|
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.16.
|
|
37
|
-
"@magector/cli-linux-x64": "2.16.
|
|
38
|
-
"@magector/cli-linux-arm64": "2.16.
|
|
39
|
-
"@magector/cli-win32-x64": "2.16.
|
|
36
|
+
"@magector/cli-darwin-arm64": "2.16.9",
|
|
37
|
+
"@magector/cli-linux-x64": "2.16.9",
|
|
38
|
+
"@magector/cli-linux-arm64": "2.16.9",
|
|
39
|
+
"@magector/cli-win32-x64": "2.16.9"
|
|
40
40
|
},
|
|
41
41
|
"keywords": [
|
|
42
42
|
"magento",
|
package/src/mcp-server.js
CHANGED
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
import { execFileSync, spawn } from 'child_process';
|
|
18
18
|
import { createInterface } from 'readline';
|
|
19
19
|
import { createServer as createNetServer, createConnection } from 'net';
|
|
20
|
-
import { existsSync, statSync, unlinkSync, copyFileSync, renameSync, appendFileSync, writeFileSync, readFileSync, mkdirSync, openSync, closeSync, chmodSync, constants as fsConstants } from 'fs';
|
|
20
|
+
import { existsSync, statSync, unlinkSync, copyFileSync, renameSync, appendFileSync, writeFileSync, readFileSync, readdirSync, mkdirSync, openSync, closeSync, chmodSync, constants as fsConstants } from 'fs';
|
|
21
21
|
import { stat } from 'fs/promises';
|
|
22
22
|
import { glob } from 'glob';
|
|
23
23
|
import path from 'path';
|
|
@@ -67,6 +67,68 @@ async function loadDescriptions() {
|
|
|
67
67
|
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
+
// ─── Config Data (core_config_data exports) ─────────────────────
|
|
71
|
+
// One-time JSON exports from core_config_data, stored per environment.
|
|
72
|
+
// Directory: .magector/config-data/
|
|
73
|
+
// Files: {label}.json, e.g. "CZ-production.json", "SK-staging.json"
|
|
74
|
+
// Format: [{scope, scope_id, path, value}, ...]
|
|
75
|
+
|
|
76
|
+
let configDataCache = null;
|
|
77
|
+
|
|
78
|
+
function getConfigDataDir() {
|
|
79
|
+
return path.join(config.magentoRoot, '.magector', 'config-data');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function loadAllConfigData() {
|
|
83
|
+
if (configDataCache) return configDataCache;
|
|
84
|
+
const dir = getConfigDataDir();
|
|
85
|
+
configDataCache = {};
|
|
86
|
+
try {
|
|
87
|
+
if (!existsSync(dir)) return configDataCache;
|
|
88
|
+
const files = readdirSync(dir).filter(f => f.endsWith('.json'));
|
|
89
|
+
for (const file of files) {
|
|
90
|
+
const label = file.replace(/\.json$/, '');
|
|
91
|
+
try {
|
|
92
|
+
const data = JSON.parse(readFileSync(path.join(dir, file), 'utf-8'));
|
|
93
|
+
configDataCache[label] = Array.isArray(data) ? data : [];
|
|
94
|
+
} catch {
|
|
95
|
+
// Skip malformed files
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
} catch {
|
|
99
|
+
// Directory doesn't exist yet — that's fine
|
|
100
|
+
}
|
|
101
|
+
return configDataCache;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function lookupConfigValues(configPath) {
|
|
105
|
+
const allData = loadAllConfigData();
|
|
106
|
+
const results = [];
|
|
107
|
+
for (const [env, rows] of Object.entries(allData)) {
|
|
108
|
+
const matches = rows.filter(r => r.path === configPath);
|
|
109
|
+
if (matches.length > 0) {
|
|
110
|
+
results.push({ environment: env, values: matches });
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return results;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function searchConfigData(keyword) {
|
|
117
|
+
const allData = loadAllConfigData();
|
|
118
|
+
const kw = keyword.toLowerCase();
|
|
119
|
+
const results = [];
|
|
120
|
+
for (const [env, rows] of Object.entries(allData)) {
|
|
121
|
+
const matches = rows.filter(r =>
|
|
122
|
+
(r.path && r.path.toLowerCase().includes(kw)) ||
|
|
123
|
+
(r.value && String(r.value).toLowerCase().includes(kw))
|
|
124
|
+
);
|
|
125
|
+
if (matches.length > 0) {
|
|
126
|
+
results.push({ environment: env, values: matches });
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return results;
|
|
130
|
+
}
|
|
131
|
+
|
|
70
132
|
// ─── Logging ─────────────────────────────────────────────────────
|
|
71
133
|
// All activity is logged to .magector/magector.log.
|
|
72
134
|
|
|
@@ -933,7 +995,7 @@ function tryConnectSocket() {
|
|
|
933
995
|
if (socketBusy || socketQueryQueue.length === 0) return;
|
|
934
996
|
socketBusy = true;
|
|
935
997
|
const { command, params, timeoutMs, resolve: qResolve, reject: qReject } = socketQueryQueue.shift();
|
|
936
|
-
const timer = setTimeout(() => { pendingResolve = null; qReject(new Error('Socket query timeout')); socketBusy = false; processSocketQueue(); }, timeoutMs);
|
|
998
|
+
const timer = setTimeout(() => { pendingResolve = null; qReject(new Error('Socket query timeout')); socketBusy = false; processSocketQueue(); }, timeoutMs || 30000);
|
|
937
999
|
pendingResolve = (resp) => { clearTimeout(timer); qResolve(resp); socketBusy = false; processSocketQueue(); };
|
|
938
1000
|
try {
|
|
939
1001
|
conn.write(JSON.stringify({ command, params, timeout: timeoutMs }) + '\n');
|
|
@@ -947,8 +1009,8 @@ function tryConnectSocket() {
|
|
|
947
1009
|
}
|
|
948
1010
|
|
|
949
1011
|
// Replace the global serveQuery with socket-based version
|
|
950
|
-
globalServeQuery = (command, params, timeoutMs) => new Promise((res, rej) => {
|
|
951
|
-
socketQueryQueue.push({ command, params, timeoutMs, resolve: res, reject: rej });
|
|
1012
|
+
globalServeQuery = (command, params, timeoutMs = 30000) => new Promise((res, rej) => {
|
|
1013
|
+
socketQueryQueue.push({ command, params, timeoutMs: timeoutMs || 30000, resolve: res, reject: rej });
|
|
952
1014
|
processSocketQueue();
|
|
953
1015
|
});
|
|
954
1016
|
|
|
@@ -2486,6 +2548,13 @@ function filterByModule(results, moduleFilter) {
|
|
|
2486
2548
|
});
|
|
2487
2549
|
}
|
|
2488
2550
|
|
|
2551
|
+
function excludeByModule(results, excludeFilter) {
|
|
2552
|
+
if (!excludeFilter) return results;
|
|
2553
|
+
// Use filterByModule to find what TO exclude, then remove those
|
|
2554
|
+
const toExclude = new Set(filterByModule(results, excludeFilter).map(r => r.path));
|
|
2555
|
+
return results.filter(r => !toExclude.has(r.path));
|
|
2556
|
+
}
|
|
2557
|
+
|
|
2489
2558
|
// ─── Layout XML Search ──────────────────────────────────────────
|
|
2490
2559
|
|
|
2491
2560
|
async function findLayout(query, handle) {
|
|
@@ -3653,6 +3722,247 @@ async function findDataObjectIssues(searchPath, maxResults) {
|
|
|
3653
3722
|
return astSearch('dataobject-set-null', searchPath, maxResults || 100);
|
|
3654
3723
|
}
|
|
3655
3724
|
|
|
3725
|
+
// ─── Config Tracing ─────────────────────────────────────────────
|
|
3726
|
+
// Trace a config path end-to-end: system.xml definition → PHP consumers → actual DB values
|
|
3727
|
+
|
|
3728
|
+
async function traceConfig(configPath, keyword) {
|
|
3729
|
+
const root = config.magentoRoot;
|
|
3730
|
+
if (!root) return { error: 'MAGENTO_ROOT not set' };
|
|
3731
|
+
|
|
3732
|
+
const result = {
|
|
3733
|
+
configPath: configPath || null,
|
|
3734
|
+
keyword: keyword || null,
|
|
3735
|
+
systemXml: [],
|
|
3736
|
+
phpConsumers: [],
|
|
3737
|
+
configValues: [],
|
|
3738
|
+
configPhpValues: []
|
|
3739
|
+
};
|
|
3740
|
+
|
|
3741
|
+
// If only keyword given, try to find matching config paths first
|
|
3742
|
+
let pathsToTrace = [];
|
|
3743
|
+
if (configPath) {
|
|
3744
|
+
pathsToTrace = [configPath];
|
|
3745
|
+
} else if (keyword) {
|
|
3746
|
+
// Search system.xml for the keyword
|
|
3747
|
+
const kw = keyword.toLowerCase();
|
|
3748
|
+
try {
|
|
3749
|
+
const sysXmlFiles = await glob('**/etc/adminhtml/system.xml', { cwd: root, absolute: true, nodir: true });
|
|
3750
|
+
for (const f of sysXmlFiles) {
|
|
3751
|
+
try {
|
|
3752
|
+
const content = readFileSync(f, 'utf-8');
|
|
3753
|
+
if (content.toLowerCase().includes(kw)) {
|
|
3754
|
+
// Extract config paths (section/group/field ids) using nested regex
|
|
3755
|
+
const sectionRegex = /<section\s+id="([^"]+)"[^>]*>([\s\S]*?)<\/section>/g;
|
|
3756
|
+
let secMatch;
|
|
3757
|
+
while ((secMatch = sectionRegex.exec(content)) !== null) {
|
|
3758
|
+
const secId = secMatch[1];
|
|
3759
|
+
const secBlock = secMatch[2];
|
|
3760
|
+
const groupRegex = /<group\s+id="([^"]+)"[^>]*>([\s\S]*?)<\/group>/g;
|
|
3761
|
+
let grpMatch;
|
|
3762
|
+
while ((grpMatch = groupRegex.exec(secBlock)) !== null) {
|
|
3763
|
+
const grpId = grpMatch[1];
|
|
3764
|
+
const grpBlock = grpMatch[2];
|
|
3765
|
+
const fieldRegex = /<field\s+id="([^"]+)"/g;
|
|
3766
|
+
let fldMatch;
|
|
3767
|
+
while ((fldMatch = fieldRegex.exec(grpBlock)) !== null) {
|
|
3768
|
+
const fullPath = `${secId}/${grpId}/${fldMatch[1]}`;
|
|
3769
|
+
if (fullPath.toLowerCase().includes(kw) || fldMatch[1].toLowerCase().includes(kw)) {
|
|
3770
|
+
if (!pathsToTrace.includes(fullPath)) pathsToTrace.push(fullPath);
|
|
3771
|
+
}
|
|
3772
|
+
}
|
|
3773
|
+
}
|
|
3774
|
+
}
|
|
3775
|
+
}
|
|
3776
|
+
} catch { /* skip unreadable */ }
|
|
3777
|
+
}
|
|
3778
|
+
} catch { /* glob error */ }
|
|
3779
|
+
}
|
|
3780
|
+
|
|
3781
|
+
// For each config path, find system.xml definition
|
|
3782
|
+
for (const cp of pathsToTrace) {
|
|
3783
|
+
const parts = cp.split('/');
|
|
3784
|
+
const fieldId = parts[parts.length - 1];
|
|
3785
|
+
|
|
3786
|
+
// 1. Find system.xml definition
|
|
3787
|
+
try {
|
|
3788
|
+
const sysXmlFiles = await glob('**/etc/adminhtml/system.xml', { cwd: root, absolute: true, nodir: true });
|
|
3789
|
+
for (const f of sysXmlFiles) {
|
|
3790
|
+
try {
|
|
3791
|
+
const content = readFileSync(f, 'utf-8');
|
|
3792
|
+
if (!content.includes(`id="${fieldId}"`)) continue;
|
|
3793
|
+
// Check if this is the right section/group
|
|
3794
|
+
const hasSection = parts.length < 2 || content.includes(`id="${parts[0]}"`);
|
|
3795
|
+
const hasGroup = parts.length < 3 || content.includes(`id="${parts[1]}"`);
|
|
3796
|
+
if (!hasSection || !hasGroup) continue;
|
|
3797
|
+
|
|
3798
|
+
const relPath = path.relative(root, f);
|
|
3799
|
+
const entry = { file: relPath, configPath: cp };
|
|
3800
|
+
|
|
3801
|
+
// Extract field details
|
|
3802
|
+
const fieldRegex = new RegExp(`<field\\s+id="${fieldId}"[^>]*>([\\s\\S]*?)</field>`, 'g');
|
|
3803
|
+
const fieldMatch = fieldRegex.exec(content);
|
|
3804
|
+
if (fieldMatch) {
|
|
3805
|
+
const block = fieldMatch[1];
|
|
3806
|
+
const labelMatch = block.match(/<label>(.*?)<\/label>/);
|
|
3807
|
+
const typeMatch = content.match(new RegExp(`<field\\s+id="${fieldId}"[^>]*type="([^"]+)"`));
|
|
3808
|
+
const sourceMatch = block.match(/<source_model>(.*?)<\/source_model>/);
|
|
3809
|
+
const commentMatch = block.match(/<comment>(?:<!\[CDATA\[)?(.*?)(?:\]\]>)?<\/comment>/s);
|
|
3810
|
+
if (labelMatch) entry.label = labelMatch[1];
|
|
3811
|
+
if (typeMatch) entry.type = typeMatch[1];
|
|
3812
|
+
if (sourceMatch) entry.sourceModel = sourceMatch[1];
|
|
3813
|
+
if (commentMatch) entry.comment = commentMatch[1].trim();
|
|
3814
|
+
}
|
|
3815
|
+
result.systemXml.push(entry);
|
|
3816
|
+
} catch { /* skip */ }
|
|
3817
|
+
}
|
|
3818
|
+
} catch { /* glob error */ }
|
|
3819
|
+
|
|
3820
|
+
// 2. Find PHP consumers — classes that read this config path
|
|
3821
|
+
const escapedPath = cp.replace(/[/]/g, '\\/');
|
|
3822
|
+
try {
|
|
3823
|
+
// Search for the config path string in PHP files (argv-array, no shell)
|
|
3824
|
+
const grepArgs = ['-rn', '-l', '--include=*.php', '--', cp, root];
|
|
3825
|
+
let grepOut;
|
|
3826
|
+
try {
|
|
3827
|
+
grepOut = execFileSync('grep', grepArgs, { encoding: 'utf-8', timeout: 15000, maxBuffer: 5 * 1024 * 1024, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
3828
|
+
} catch (err) { grepOut = err.stdout || ''; }
|
|
3829
|
+
|
|
3830
|
+
const phpFiles = grepOut.trim().split('\n').filter(Boolean);
|
|
3831
|
+
for (const f of phpFiles.slice(0, 20)) {
|
|
3832
|
+
try {
|
|
3833
|
+
const content = readFileSync(f, 'utf-8');
|
|
3834
|
+
const relPath = path.relative(root, f);
|
|
3835
|
+
const entry = { file: relPath };
|
|
3836
|
+
|
|
3837
|
+
// Extract class name
|
|
3838
|
+
const nsMatch = content.match(/namespace\s+([\w\\]+)/);
|
|
3839
|
+
const classMatch = content.match(/class\s+(\w+)/);
|
|
3840
|
+
if (nsMatch && classMatch) entry.className = nsMatch[1] + '\\' + classMatch[1];
|
|
3841
|
+
|
|
3842
|
+
// Find the constant or property that holds this path
|
|
3843
|
+
const constMatch = content.match(new RegExp(`(\\w+)\\s*=\\s*'${escapedPath}'`));
|
|
3844
|
+
if (constMatch) entry.constant = constMatch[1];
|
|
3845
|
+
|
|
3846
|
+
// Find methods that use this config value
|
|
3847
|
+
const methods = [];
|
|
3848
|
+
const methodRegex = /(?:public|protected|private)\s+(?:static\s+)?function\s+(\w+)/g;
|
|
3849
|
+
let mm;
|
|
3850
|
+
while ((mm = methodRegex.exec(content)) !== null) {
|
|
3851
|
+
const methodName = mm[1];
|
|
3852
|
+
const body = readFullMethodBody(f, methodName);
|
|
3853
|
+
if (body && (body.includes(cp) || (constMatch && body.includes(constMatch[1])))) {
|
|
3854
|
+
methods.push(methodName);
|
|
3855
|
+
}
|
|
3856
|
+
}
|
|
3857
|
+
if (methods.length > 0) entry.methods = methods;
|
|
3858
|
+
|
|
3859
|
+
result.phpConsumers.push(entry);
|
|
3860
|
+
} catch { /* skip */ }
|
|
3861
|
+
}
|
|
3862
|
+
} catch { /* grep error */ }
|
|
3863
|
+
|
|
3864
|
+
// 3. Look up actual config values from imported data
|
|
3865
|
+
const dbValues = lookupConfigValues(cp);
|
|
3866
|
+
if (dbValues.length > 0) {
|
|
3867
|
+
result.configValues.push({ configPath: cp, environments: dbValues });
|
|
3868
|
+
}
|
|
3869
|
+
}
|
|
3870
|
+
|
|
3871
|
+
// 4. Also check config.php for statically set values
|
|
3872
|
+
if (pathsToTrace.length > 0) {
|
|
3873
|
+
const configPhpPath = path.join(root, 'app/etc/config.php');
|
|
3874
|
+
if (existsSync(configPhpPath)) {
|
|
3875
|
+
try {
|
|
3876
|
+
const configPhpContent = readFileSync(configPhpPath, 'utf-8');
|
|
3877
|
+
for (const cp of pathsToTrace) {
|
|
3878
|
+
const parts = cp.split('/');
|
|
3879
|
+
// Check if any part of the path appears in config.php
|
|
3880
|
+
if (parts.some(p => configPhpContent.includes(`'${p}'`))) {
|
|
3881
|
+
result.configPhpValues.push({ configPath: cp, note: 'Path segments found in app/etc/config.php — may contain static overrides' });
|
|
3882
|
+
}
|
|
3883
|
+
}
|
|
3884
|
+
} catch { /* skip */ }
|
|
3885
|
+
}
|
|
3886
|
+
}
|
|
3887
|
+
|
|
3888
|
+
return result;
|
|
3889
|
+
}
|
|
3890
|
+
|
|
3891
|
+
function formatTraceConfigResult(result) {
|
|
3892
|
+
let text = '## Config Trace';
|
|
3893
|
+
if (result.configPath) text += `: \`${result.configPath}\``;
|
|
3894
|
+
if (result.keyword) text += ` (keyword: "${result.keyword}")`;
|
|
3895
|
+
text += '\n\n';
|
|
3896
|
+
|
|
3897
|
+
if (result.error) return text + `Error: ${result.error}\n`;
|
|
3898
|
+
|
|
3899
|
+
// System.xml definitions
|
|
3900
|
+
if (result.systemXml.length > 0) {
|
|
3901
|
+
text += '### Admin Configuration (system.xml)\n\n';
|
|
3902
|
+
for (const s of result.systemXml) {
|
|
3903
|
+
text += `**${s.label || s.configPath}**`;
|
|
3904
|
+
if (s.type) text += ` (type: ${s.type})`;
|
|
3905
|
+
text += `\n- File: \`${s.file}\`\n- Config path: \`${s.configPath}\`\n`;
|
|
3906
|
+
if (s.sourceModel) text += `- Source model: \`${s.sourceModel}\`\n`;
|
|
3907
|
+
if (s.comment) text += `- Comment: ${s.comment}\n`;
|
|
3908
|
+
text += '\n';
|
|
3909
|
+
}
|
|
3910
|
+
} else {
|
|
3911
|
+
text += '### Admin Configuration (system.xml)\nNo system.xml definition found.\n\n';
|
|
3912
|
+
}
|
|
3913
|
+
|
|
3914
|
+
// PHP consumers
|
|
3915
|
+
if (result.phpConsumers.length > 0) {
|
|
3916
|
+
text += '### PHP Consumers\n\n';
|
|
3917
|
+
for (const c of result.phpConsumers) {
|
|
3918
|
+
text += `- \`${c.className || c.file}\``;
|
|
3919
|
+
if (c.constant) text += ` (const \`${c.constant}\`)`;
|
|
3920
|
+
if (c.methods?.length > 0) text += ` — methods: ${c.methods.map(m => '`' + m + '()`').join(', ')}`;
|
|
3921
|
+
text += `\n File: \`${c.file}\`\n`;
|
|
3922
|
+
}
|
|
3923
|
+
text += '\n';
|
|
3924
|
+
} else {
|
|
3925
|
+
text += '### PHP Consumers\nNo PHP files reference this config path.\n\n';
|
|
3926
|
+
}
|
|
3927
|
+
|
|
3928
|
+
// Actual DB values
|
|
3929
|
+
if (result.configValues.length > 0) {
|
|
3930
|
+
text += '### Actual Values (from config-data exports)\n\n';
|
|
3931
|
+
for (const cv of result.configValues) {
|
|
3932
|
+
text += `**\`${cv.configPath}\`**\n`;
|
|
3933
|
+
for (const env of cv.environments) {
|
|
3934
|
+
text += `\n**${env.environment}:**\n`;
|
|
3935
|
+
for (const v of env.values) {
|
|
3936
|
+
const scope = v.scope === 'default' ? 'default' : `${v.scope} (id: ${v.scope_id})`;
|
|
3937
|
+
const val = v.value === null ? '*(null)*' : (String(v.value).length > 200 ? String(v.value).slice(0, 200) + '...' : String(v.value));
|
|
3938
|
+
text += `- [${scope}] = \`${val}\`\n`;
|
|
3939
|
+
}
|
|
3940
|
+
}
|
|
3941
|
+
text += '\n';
|
|
3942
|
+
}
|
|
3943
|
+
} else {
|
|
3944
|
+
const dataDir = getConfigDataDir();
|
|
3945
|
+
let hasData = false;
|
|
3946
|
+
try { hasData = existsSync(dataDir) && readdirSync(dataDir).some(f => f.endsWith('.json')); } catch {}
|
|
3947
|
+
if (!hasData) {
|
|
3948
|
+
text += '### Actual Values\nNo config-data exports found. Export `core_config_data` to `.magector/config-data/{env}.json` (format: `[{scope, scope_id, path, value}, ...]`).\n\n';
|
|
3949
|
+
} else {
|
|
3950
|
+
text += '### Actual Values\nNo matching values found in config-data exports.\n\n';
|
|
3951
|
+
}
|
|
3952
|
+
}
|
|
3953
|
+
|
|
3954
|
+
// config.php static values
|
|
3955
|
+
if (result.configPhpValues.length > 0) {
|
|
3956
|
+
text += '### Static Config (app/etc/config.php)\n';
|
|
3957
|
+
for (const c of result.configPhpValues) {
|
|
3958
|
+
text += `- \`${c.configPath}\`: ${c.note}\n`;
|
|
3959
|
+
}
|
|
3960
|
+
text += '\n';
|
|
3961
|
+
}
|
|
3962
|
+
|
|
3963
|
+
return text;
|
|
3964
|
+
}
|
|
3965
|
+
|
|
3656
3966
|
// ─── MCP Server ─────────────────────────────────────────────────
|
|
3657
3967
|
|
|
3658
3968
|
const server = new Server(
|
|
@@ -3692,6 +4002,13 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
3692
4002
|
],
|
|
3693
4003
|
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".'
|
|
3694
4004
|
},
|
|
4005
|
+
excludeModuleFilter: {
|
|
4006
|
+
oneOf: [
|
|
4007
|
+
{ type: 'string' },
|
|
4008
|
+
{ type: 'array', items: { type: 'string' } }
|
|
4009
|
+
],
|
|
4010
|
+
description: 'Exclude results from vendor/module pattern(s). Inverse of moduleFilter — removes matching modules from results. Same pattern syntax as moduleFilter. Useful when you know which modules are irrelevant. Examples: "Vendor_MarketplaceBase", ["Vendor_ModuleA", "Vendor_ModuleB"].'
|
|
4011
|
+
},
|
|
3695
4012
|
expand: {
|
|
3696
4013
|
type: 'boolean',
|
|
3697
4014
|
description: 'Enable query expansion with Magento domain synonyms for better recall (default: true)',
|
|
@@ -4470,6 +4787,23 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
4470
4787
|
}
|
|
4471
4788
|
}
|
|
4472
4789
|
},
|
|
4790
|
+
{
|
|
4791
|
+
name: 'magento_trace_config',
|
|
4792
|
+
description: 'Trace a Magento config path end-to-end: finds system.xml admin definition (label, type, source_model), PHP classes that read the value, and actual DB values from config-data exports. Use when investigating config-driven behavior ("why is this feature enabled/disabled?", "what controls marketplace payment methods?"). Accepts either an exact config path or a keyword to search for.',
|
|
4793
|
+
inputSchema: {
|
|
4794
|
+
type: 'object',
|
|
4795
|
+
properties: {
|
|
4796
|
+
configPath: {
|
|
4797
|
+
type: 'string',
|
|
4798
|
+
description: 'Exact config path to trace. Example: "acme_marketplace/payments/marketplace_payment_methods", "payment/cashondelivery/active"'
|
|
4799
|
+
},
|
|
4800
|
+
keyword: {
|
|
4801
|
+
type: 'string',
|
|
4802
|
+
description: 'Keyword to search for in system.xml fields when exact path is unknown. Example: "marketplace_payment", "cashondelivery". Returns all matching config paths.'
|
|
4803
|
+
}
|
|
4804
|
+
}
|
|
4805
|
+
}
|
|
4806
|
+
},
|
|
4473
4807
|
{
|
|
4474
4808
|
name: 'magento_read',
|
|
4475
4809
|
description: 'Read a file from the Magento codebase. Use in magento_batch to read multiple files in a single MCP call (e.g., grep finds 5 files → read all 5 in one batch). Supports line ranges and method extraction.',
|
|
@@ -4605,7 +4939,11 @@ const _callToolHandler = async (request) => {
|
|
|
4605
4939
|
case 'magento_search': {
|
|
4606
4940
|
const precise = args.precise === true;
|
|
4607
4941
|
const searchQuery = (args.expand !== false && !precise) ? expandQuery(args.query) : args.query;
|
|
4608
|
-
|
|
4942
|
+
// When moduleFilter is present, fetch more results so post-filtering has enough candidates
|
|
4943
|
+
const fetchLimit = args.moduleFilter
|
|
4944
|
+
? Math.max(args.limit || 10, 200)
|
|
4945
|
+
: Math.max(args.limit || 10, precise ? 60 : 30);
|
|
4946
|
+
const raw = await rustSearchAsync(searchQuery, fetchLimit);
|
|
4609
4947
|
const arr = Array.isArray(raw) ? raw : [];
|
|
4610
4948
|
let results = arr.map(normalizeResult);
|
|
4611
4949
|
// Hybrid BM25 rerank for better exact-match handling
|
|
@@ -4623,6 +4961,9 @@ const _callToolHandler = async (request) => {
|
|
|
4623
4961
|
if (args.moduleFilter) {
|
|
4624
4962
|
results = filterByModule(results, args.moduleFilter);
|
|
4625
4963
|
}
|
|
4964
|
+
if (args.excludeModuleFilter) {
|
|
4965
|
+
results = excludeByModule(results, args.excludeModuleFilter);
|
|
4966
|
+
}
|
|
4626
4967
|
// SONA: record search with results for follow-up tracking
|
|
4627
4968
|
sessionTracker.recordToolCall(name, args || {}, arr);
|
|
4628
4969
|
return {
|
|
@@ -6631,6 +6972,11 @@ const _callToolHandler = async (request) => {
|
|
|
6631
6972
|
}
|
|
6632
6973
|
break;
|
|
6633
6974
|
}
|
|
6975
|
+
case 'magento_trace_config': {
|
|
6976
|
+
const trResult = await traceConfig(a.configPath, a.keyword);
|
|
6977
|
+
text = formatTraceConfigResult(trResult);
|
|
6978
|
+
break;
|
|
6979
|
+
}
|
|
6634
6980
|
default:
|
|
6635
6981
|
text = `Unsupported batch tool: ${q.tool}`;
|
|
6636
6982
|
}
|
|
@@ -6883,6 +7229,17 @@ const _callToolHandler = async (request) => {
|
|
|
6883
7229
|
return { content: [{ type: 'text', text }] };
|
|
6884
7230
|
}
|
|
6885
7231
|
|
|
7232
|
+
case 'magento_trace_config': {
|
|
7233
|
+
const traceResult = await traceConfig(args.configPath, args.keyword);
|
|
7234
|
+
logToFile('RES', `trace_config: path=${args.configPath || 'none'} keyword=${args.keyword || 'none'} sysxml=${traceResult.systemXml?.length || 0} php=${traceResult.phpConsumers?.length || 0} values=${traceResult.configValues?.length || 0}`);
|
|
7235
|
+
return {
|
|
7236
|
+
content: [{
|
|
7237
|
+
type: 'text',
|
|
7238
|
+
text: formatTraceConfigResult(traceResult)
|
|
7239
|
+
}]
|
|
7240
|
+
};
|
|
7241
|
+
}
|
|
7242
|
+
|
|
6886
7243
|
case 'magento_read': {
|
|
6887
7244
|
const root = config.magentoRoot;
|
|
6888
7245
|
if (!root) return { content: [{ type: 'text', text: 'MAGENTO_ROOT not set.' }], isError: true };
|
|
@@ -38,6 +38,14 @@ This project is indexed with Magector. Use the MCP tools below to search the cod
|
|
|
38
38
|
- Include Magento terms: "plugin for save", "observer for order place"
|
|
39
39
|
- Be specific: "customer address validation before checkout" not just "validation"
|
|
40
40
|
|
|
41
|
+
## Analysis Patterns
|
|
42
|
+
|
|
43
|
+
### Negative match audit
|
|
44
|
+
When \`magento_grep\` finds a bug pattern in only SOME files of a known group (e.g. \`orderRepository->save()\` in 5 of 8 transition handlers), always read the files that DON'T match to understand why they differ. This narrows the fix scope — files with a different code path may not need fixing.
|
|
45
|
+
|
|
46
|
+
### Follow up DI listings with code reads
|
|
47
|
+
When \`magento_trace_dependency\` or \`magento_find_plugin\` returns a list of plugins/preferences, don't stop at the names. Use \`magento_read\` or \`Read\` to inspect the actual implementation of each — the root cause is often in a specific condition inside the plugin code, not in the DI wiring itself.
|
|
48
|
+
|
|
41
49
|
## Re-indexing
|
|
42
50
|
|
|
43
51
|
After significant code changes, re-index:
|
|
@@ -40,6 +40,11 @@ Before reading files manually, ALWAYS use Magector MCP tools to find relevant co
|
|
|
40
40
|
- Include Magento terms: "plugin for save", "observer for order place", "checkout totals collector"
|
|
41
41
|
- Be specific: "customer address validation before checkout" not just "validation"
|
|
42
42
|
|
|
43
|
+
## Analysis Patterns
|
|
44
|
+
|
|
45
|
+
- **Negative match audit:** When \`magento_grep\` finds a bug pattern in only SOME files of a group, always read the non-matching files to understand why they differ. This narrows the fix scope.
|
|
46
|
+
- **Follow up DI listings with code reads:** When \`magento_trace_dependency\` or \`magento_find_plugin\` returns plugin/preference names, always read the actual implementation. The root cause is often in a condition inside the code, not in the DI wiring.
|
|
47
|
+
|
|
43
48
|
## Magento Development Patterns
|
|
44
49
|
|
|
45
50
|
- Always check for existing plugins before modifying core behavior
|