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 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 46 tools that let AI assistants search, navigate, and understand the code with domain-specific intelligence. Instead of grepping for keywords, your AI asks *"how are checkout totals calculated?"* and gets ranked, relevant results in under 50ms, enriched with Magento pattern detection (plugins, observers, controllers, DI preferences, layout XML, and 20+ more).
5
+ Magector is a Model Context Protocol (MCP) server that deeply understands Magento 2 and Adobe Commerce. It builds a semantic vector index of your entire codebase — 18,000+ files across hundreds of modules — and exposes 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
  [![Rust](https://img.shields.io/badge/rust-1.75+-orange.svg)](https://www.rust-lang.org)
8
8
  [![Node.js](https://img.shields.io/badge/node-18+-green.svg)](https://nodejs.org)
@@ -58,7 +58,7 @@ The result: your AI assistant calls one MCP tool and gets ranked, pattern-enrich
58
58
  - **Complexity analysis** -- cyclomatic complexity, function count, and hotspot detection across modules
59
59
  - **Fast** -- 10-45ms queries via persistent serve process, batched ONNX embedding with adaptive thread scaling
60
60
  - **LLM description enrichment** -- generate natural-language descriptions of di.xml files using Claude, stored in SQLite, and prepend them to embedding text so descriptions influence vector search ranking (not just post-retrieval display)
61
- - **MCP server** -- 46 tools integrating with Claude Code, Cursor, and any MCP-compatible AI tool
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/>46 tools · LRU cache"]
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 46 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
+ 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 (46 tools, structured JSON output)
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.7",
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.7",
37
- "@magector/cli-linux-x64": "2.16.7",
38
- "@magector/cli-linux-arm64": "2.16.7",
39
- "@magector/cli-win32-x64": "2.16.7"
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
- const raw = await rustSearchAsync(searchQuery, Math.max(args.limit || 10, precise ? 60 : 30));
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 };