magector 2.16.7 → 2.16.10
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 +32 -5
- package/package.json +5 -5
- package/src/mcp-server.js +364 -5
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,31 @@ 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
|
+
# MySQL 8.0+ with --json flag
|
|
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
|
+
# Older MySQL (no --json): pipe through python3
|
|
1036
|
+
mysql -u user -p magento_db -B -e "SELECT scope, scope_id, path, value FROM core_config_data" | \
|
|
1037
|
+
python3 -c "import sys,json; lines=sys.stdin.read().strip().split('\n'); h=lines[0].split('\t'); \
|
|
1038
|
+
rows=[dict(zip(h,l.split('\t'))) for l in lines[1:]]; [r.update({'scope_id':int(r['scope_id'])}) for r in rows]; \
|
|
1039
|
+
json.dump(rows,sys.stdout,indent=2)" > .magector/config-data/CZ-production.json
|
|
1040
|
+
|
|
1041
|
+
# Or from n8n/API/any tool that produces:
|
|
1042
|
+
# [{scope, scope_id, path, value}, ...]
|
|
1043
|
+
```
|
|
1044
|
+
|
|
1045
|
+
**File naming:** Use `{country}-{environment}.json`, e.g.:
|
|
1046
|
+
- `CZ-production.json`
|
|
1047
|
+
- `SK-staging.json`
|
|
1048
|
+
- `IT-production.json`
|
|
1049
|
+
|
|
1050
|
+
When `magento_trace_config` traces a config path, it automatically looks up values from all available exports and shows them per environment.
|
|
1051
|
+
|
|
1025
1052
|
### Model Configuration
|
|
1026
1053
|
|
|
1027
1054
|
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.10",
|
|
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.10",
|
|
37
|
+
"@magector/cli-linux-x64": "2.16.10",
|
|
38
|
+
"@magector/cli-linux-arm64": "2.16.10",
|
|
39
|
+
"@magector/cli-win32-x64": "2.16.10"
|
|
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,249 @@ 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\n**ACTION REQUIRED:** No config-data exports found. Ask the user to export `core_config_data` from their database and place the JSON file at `.magector/config-data/{country}-{environment}.json` (format: `[{scope, scope_id, path, value}, ...]`). Without this data, actual config values cannot be shown.\n\n';
|
|
3949
|
+
} else {
|
|
3950
|
+
// Show which environments ARE available so agent knows what's missing
|
|
3951
|
+
const availableEnvs = Object.keys(loadAllConfigData()).join(', ');
|
|
3952
|
+
text += `### Actual Values\nNo matching values found in available exports (${availableEnvs}). If you need values from a different environment, ask the user to provide the export.\n\n`;
|
|
3953
|
+
}
|
|
3954
|
+
}
|
|
3955
|
+
|
|
3956
|
+
// config.php static values
|
|
3957
|
+
if (result.configPhpValues.length > 0) {
|
|
3958
|
+
text += '### Static Config (app/etc/config.php)\n';
|
|
3959
|
+
for (const c of result.configPhpValues) {
|
|
3960
|
+
text += `- \`${c.configPath}\`: ${c.note}\n`;
|
|
3961
|
+
}
|
|
3962
|
+
text += '\n';
|
|
3963
|
+
}
|
|
3964
|
+
|
|
3965
|
+
return text;
|
|
3966
|
+
}
|
|
3967
|
+
|
|
3656
3968
|
// ─── MCP Server ─────────────────────────────────────────────────
|
|
3657
3969
|
|
|
3658
3970
|
const server = new Server(
|
|
@@ -3692,6 +4004,13 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
3692
4004
|
],
|
|
3693
4005
|
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
4006
|
},
|
|
4007
|
+
excludeModuleFilter: {
|
|
4008
|
+
oneOf: [
|
|
4009
|
+
{ type: 'string' },
|
|
4010
|
+
{ type: 'array', items: { type: 'string' } }
|
|
4011
|
+
],
|
|
4012
|
+
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"].'
|
|
4013
|
+
},
|
|
3695
4014
|
expand: {
|
|
3696
4015
|
type: 'boolean',
|
|
3697
4016
|
description: 'Enable query expansion with Magento domain synonyms for better recall (default: true)',
|
|
@@ -4470,6 +4789,23 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
4470
4789
|
}
|
|
4471
4790
|
}
|
|
4472
4791
|
},
|
|
4792
|
+
{
|
|
4793
|
+
name: 'magento_trace_config',
|
|
4794
|
+
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. IMPORTANT: If the output says no config-data exports are available, or if the analysis needs config values from a country/environment not yet exported, ask the user to provide the export. They can run a one-time MySQL query and place the JSON file in .magector/config-data/{country}-{environment}.json.',
|
|
4795
|
+
inputSchema: {
|
|
4796
|
+
type: 'object',
|
|
4797
|
+
properties: {
|
|
4798
|
+
configPath: {
|
|
4799
|
+
type: 'string',
|
|
4800
|
+
description: 'Exact config path to trace. Example: "acme_marketplace/payments/marketplace_payment_methods", "payment/cashondelivery/active"'
|
|
4801
|
+
},
|
|
4802
|
+
keyword: {
|
|
4803
|
+
type: 'string',
|
|
4804
|
+
description: 'Keyword to search for in system.xml fields when exact path is unknown. Example: "marketplace_payment", "cashondelivery". Returns all matching config paths.'
|
|
4805
|
+
}
|
|
4806
|
+
}
|
|
4807
|
+
}
|
|
4808
|
+
},
|
|
4473
4809
|
{
|
|
4474
4810
|
name: 'magento_read',
|
|
4475
4811
|
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 +4941,11 @@ const _callToolHandler = async (request) => {
|
|
|
4605
4941
|
case 'magento_search': {
|
|
4606
4942
|
const precise = args.precise === true;
|
|
4607
4943
|
const searchQuery = (args.expand !== false && !precise) ? expandQuery(args.query) : args.query;
|
|
4608
|
-
|
|
4944
|
+
// When moduleFilter is present, fetch more results so post-filtering has enough candidates
|
|
4945
|
+
const fetchLimit = args.moduleFilter
|
|
4946
|
+
? Math.max(args.limit || 10, 200)
|
|
4947
|
+
: Math.max(args.limit || 10, precise ? 60 : 30);
|
|
4948
|
+
const raw = await rustSearchAsync(searchQuery, fetchLimit);
|
|
4609
4949
|
const arr = Array.isArray(raw) ? raw : [];
|
|
4610
4950
|
let results = arr.map(normalizeResult);
|
|
4611
4951
|
// Hybrid BM25 rerank for better exact-match handling
|
|
@@ -4623,6 +4963,9 @@ const _callToolHandler = async (request) => {
|
|
|
4623
4963
|
if (args.moduleFilter) {
|
|
4624
4964
|
results = filterByModule(results, args.moduleFilter);
|
|
4625
4965
|
}
|
|
4966
|
+
if (args.excludeModuleFilter) {
|
|
4967
|
+
results = excludeByModule(results, args.excludeModuleFilter);
|
|
4968
|
+
}
|
|
4626
4969
|
// SONA: record search with results for follow-up tracking
|
|
4627
4970
|
sessionTracker.recordToolCall(name, args || {}, arr);
|
|
4628
4971
|
return {
|
|
@@ -6631,6 +6974,11 @@ const _callToolHandler = async (request) => {
|
|
|
6631
6974
|
}
|
|
6632
6975
|
break;
|
|
6633
6976
|
}
|
|
6977
|
+
case 'magento_trace_config': {
|
|
6978
|
+
const trResult = await traceConfig(a.configPath, a.keyword);
|
|
6979
|
+
text = formatTraceConfigResult(trResult);
|
|
6980
|
+
break;
|
|
6981
|
+
}
|
|
6634
6982
|
default:
|
|
6635
6983
|
text = `Unsupported batch tool: ${q.tool}`;
|
|
6636
6984
|
}
|
|
@@ -6883,6 +7231,17 @@ const _callToolHandler = async (request) => {
|
|
|
6883
7231
|
return { content: [{ type: 'text', text }] };
|
|
6884
7232
|
}
|
|
6885
7233
|
|
|
7234
|
+
case 'magento_trace_config': {
|
|
7235
|
+
const traceResult = await traceConfig(args.configPath, args.keyword);
|
|
7236
|
+
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}`);
|
|
7237
|
+
return {
|
|
7238
|
+
content: [{
|
|
7239
|
+
type: 'text',
|
|
7240
|
+
text: formatTraceConfigResult(traceResult)
|
|
7241
|
+
}]
|
|
7242
|
+
};
|
|
7243
|
+
}
|
|
7244
|
+
|
|
6886
7245
|
case 'magento_read': {
|
|
6887
7246
|
const root = config.magentoRoot;
|
|
6888
7247
|
if (!root) return { content: [{ type: 'text', text: 'MAGENTO_ROOT not set.' }], isError: true };
|