magector 2.14.1 → 2.15.0
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 +7 -6
- package/package.json +5 -5
- package/src/mcp-server.js +78 -11
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 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).
|
|
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** -- 46 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/>46 tools · LRU cache"]
|
|
74
74
|
F["Persistent Serve Process"]
|
|
75
75
|
G --> F
|
|
76
76
|
E --> F
|
|
@@ -371,7 +371,7 @@ npx magector index --force
|
|
|
371
371
|
|
|
372
372
|
## MCP Server Tools
|
|
373
373
|
|
|
374
|
-
The MCP server exposes
|
|
374
|
+
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.
|
|
375
375
|
|
|
376
376
|
### Output Format
|
|
377
377
|
|
|
@@ -482,13 +482,14 @@ Auto-detects entry type from pattern (`/V1/...` → API, `snake_case` → event,
|
|
|
482
482
|
| `magento_find_trigger` | Find database triggers across the codebase |
|
|
483
483
|
| `magento_find_table_usage` | Find all PHP code referencing a specific database table |
|
|
484
484
|
|
|
485
|
-
### Null-Safety Analysis (v2.12–v2.
|
|
485
|
+
### Null-Safety Analysis (v2.12–v2.15)
|
|
486
486
|
|
|
487
487
|
| Tool | Description |
|
|
488
488
|
|------|-------------|
|
|
489
489
|
| `magento_ast_search` | Structural PHP code search using [semgrep](https://semgrep.dev). Understands PHP AST — matches by structure regardless of variable names, ignores comments/strings. Pattern syntax: `$X` = any expression, `$Y` = any identifier, `...` = any args. Example: `$X->getPayment()->$Y(...)`. Requires `semgrep`. **(v2.12)** |
|
|
490
490
|
| `magento_enrich` | Build the method-chain enrichment index. Scans all `vendor/` PHP files for `->firstMethod()->secondMethod()` chains and detects null guards in surrounding code. Stores results in `.magector/enrichment.db` (SQLite, `node:sqlite`). Runs automatically after `magento_index`. **(v2.13)** |
|
|
491
491
|
| `magento_find_null_risks` | Query the enrichment index for method chains without null guards. O(1) SQLite query instead of file scanning. Pass `firstMethod` to filter (e.g., `"getPayment"` → all `->getPayment()->anything()` without null guard). Requires `magento_enrich`. **(v2.13)** |
|
|
492
|
+
| `magento_find_dataobject_issues` | Detect `setX(null)` anti-pattern on Magento `DataObject` subclasses. `setX(null)` stores `['x' => null]` in `_data` — `hasX()` (via `array_key_exists`) returns `true` even when the value is `null`, creating false-positive guard conditions. Use during field-lifecycle audits or when debugging "value persists but shouldn't" bugs. Requires `semgrep`. **(v2.15)** |
|
|
492
493
|
|
|
493
494
|
### Search Enhancements (v2.1)
|
|
494
495
|
|
|
@@ -672,7 +673,7 @@ cd rust-core && cargo run --release -- validate -m ./magento2 --skip-index
|
|
|
672
673
|
magector/
|
|
673
674
|
├── src/ # Node.js source
|
|
674
675
|
│ ├── cli.js # CLI entry point (npx magector <command>)
|
|
675
|
-
│ ├── mcp-server.js # MCP server (
|
|
676
|
+
│ ├── mcp-server.js # MCP server (46 tools, structured JSON output)
|
|
676
677
|
│ ├── binary.js # Platform binary resolver
|
|
677
678
|
│ ├── model.js # ONNX model resolver/downloader
|
|
678
679
|
│ ├── init.js # Full init command (index + IDE config)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "magector",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.15.0",
|
|
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.
|
|
37
|
-
"@magector/cli-linux-x64": "2.
|
|
38
|
-
"@magector/cli-linux-arm64": "2.
|
|
39
|
-
"@magector/cli-win32-x64": "2.
|
|
36
|
+
"@magector/cli-darwin-arm64": "2.15.0",
|
|
37
|
+
"@magector/cli-linux-x64": "2.15.0",
|
|
38
|
+
"@magector/cli-linux-arm64": "2.15.0",
|
|
39
|
+
"@magector/cli-win32-x64": "2.15.0"
|
|
40
40
|
},
|
|
41
41
|
"keywords": [
|
|
42
42
|
"magento",
|
package/src/mcp-server.js
CHANGED
|
@@ -3670,16 +3670,16 @@ async function astSearch(pattern, searchPath, lang, maxResults) {
|
|
|
3670
3670
|
logToFile('INFO', `ast_search: pattern="${pattern}" path="${searchPath || '.'}" lang=${semgrepLang} limit=${limit}`);
|
|
3671
3671
|
const astStart = Date.now();
|
|
3672
3672
|
|
|
3673
|
-
// Create a temporary empty .semgrepignore in the target directory if none exists.
|
|
3674
3673
|
// Semgrep's default ignore list includes "vendor/" which is exactly what we need to scan.
|
|
3675
|
-
//
|
|
3676
|
-
|
|
3674
|
+
// Semgrep resolves .semgrepignore from the git repo root, NOT the scan directory.
|
|
3675
|
+
// An empty .semgrepignore at root overrides the defaults: https://semgrep.dev/docs/ignoring-files-folders-code/
|
|
3676
|
+
const semgrepIgnorePath = path.join(root, '.semgrepignore');
|
|
3677
3677
|
let createdSemgrepIgnore = false;
|
|
3678
3678
|
if (!existsSync(semgrepIgnorePath)) {
|
|
3679
3679
|
try {
|
|
3680
3680
|
writeFileSync(semgrepIgnorePath, '# Magector: scan vendor/ and all project files\n');
|
|
3681
3681
|
createdSemgrepIgnore = true;
|
|
3682
|
-
logToFile('INFO', `ast_search: created temporary .semgrepignore at ${
|
|
3682
|
+
logToFile('INFO', `ast_search: created temporary .semgrepignore at ${root}`);
|
|
3683
3683
|
} catch (err) {
|
|
3684
3684
|
logToFile('WARN', `ast_search: failed to create .semgrepignore: ${err.message}`);
|
|
3685
3685
|
}
|
|
@@ -3728,12 +3728,34 @@ async function astSearch(pattern, searchPath, lang, maxResults) {
|
|
|
3728
3728
|
if (parsed.errors && parsed.errors.length > 0) {
|
|
3729
3729
|
logToFile('WARN', `ast_search: semgrep reported ${parsed.errors.length} error(s): ${parsed.errors.slice(0, 3).map(e => e.message || e.type || JSON.stringify(e)).join('; ')}`);
|
|
3730
3730
|
}
|
|
3731
|
-
return findings.map(r =>
|
|
3732
|
-
|
|
3733
|
-
|
|
3734
|
-
|
|
3735
|
-
snippet
|
|
3736
|
-
|
|
3731
|
+
return findings.map(r => {
|
|
3732
|
+
// semgrep >=1.100 may return "requires login" in r.extra.lines for unlicensed installs.
|
|
3733
|
+
// Fall back to r.extra.message which contains the matched expression (always available).
|
|
3734
|
+
const rawLines = r.extra?.lines || '';
|
|
3735
|
+
const snippet = (rawLines && rawLines !== 'requires login')
|
|
3736
|
+
? rawLines.trim()
|
|
3737
|
+
: (r.extra?.message || '').trim();
|
|
3738
|
+
return {
|
|
3739
|
+
file: r.path.replace(root + '/', ''),
|
|
3740
|
+
line: r.start.line,
|
|
3741
|
+
endLine: r.end.line,
|
|
3742
|
+
snippet
|
|
3743
|
+
};
|
|
3744
|
+
});
|
|
3745
|
+
}
|
|
3746
|
+
|
|
3747
|
+
// ─── DataObject set-null Anti-pattern Detection ─────────────────
|
|
3748
|
+
|
|
3749
|
+
async function findDataObjectIssues(searchPath, maxResults) {
|
|
3750
|
+
// Detects DataObject::setX(null) anti-pattern:
|
|
3751
|
+
// setX(null) stores ['x' => null] in _data — key EXISTS with null value.
|
|
3752
|
+
// hasX() / hasData('x') calls array_key_exists() → returns true even for null.
|
|
3753
|
+
// This causes false-positive guard conditions: hasX() passes, getX() returns null.
|
|
3754
|
+
// Correct way to fully clear: unsetData('x') removes the key entirely.
|
|
3755
|
+
const allResults = await astSearch('$X->$SETTER(null)', searchPath, 'php', 500);
|
|
3756
|
+
const setterNullRegex = /->set[A-Z]\w+\s*\(\s*null\s*\)/;
|
|
3757
|
+
const limit = Math.min(maxResults || 100, 500);
|
|
3758
|
+
return allResults.filter(r => setterNullRegex.test(r.snippet)).slice(0, limit);
|
|
3737
3759
|
}
|
|
3738
3760
|
|
|
3739
3761
|
// ─── MCP Server ─────────────────────────────────────────────────
|
|
@@ -4494,6 +4516,24 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
4494
4516
|
required: ['pattern']
|
|
4495
4517
|
}
|
|
4496
4518
|
},
|
|
4519
|
+
{
|
|
4520
|
+
name: 'magento_find_dataobject_issues',
|
|
4521
|
+
description: 'Detect DataObject::setX(null) anti-pattern calls. In Magento, classes extending DataObject store values in a _data array. Calling setX(null) stores the key with a null value — so hasX()/hasData(\'x\') (which use array_key_exists) return true even though the value is null. Downstream guard conditions silently pass, but getX() returns null. The correct way to clear is unsetData(\'x\'). Use this during field-lifecycle audits or when debugging "value persists but shouldn\'t" bugs. ⚡ For multi-query workflows use magento_batch.',
|
|
4522
|
+
inputSchema: {
|
|
4523
|
+
type: 'object',
|
|
4524
|
+
properties: {
|
|
4525
|
+
path: {
|
|
4526
|
+
type: 'string',
|
|
4527
|
+
description: 'Subdirectory to search (relative to MAGENTO_ROOT). Default: entire codebase. Example: "vendor/acme/"'
|
|
4528
|
+
},
|
|
4529
|
+
maxResults: {
|
|
4530
|
+
type: 'number',
|
|
4531
|
+
description: 'Maximum matches to return (default: 100, max: 500)',
|
|
4532
|
+
default: 100
|
|
4533
|
+
}
|
|
4534
|
+
}
|
|
4535
|
+
}
|
|
4536
|
+
},
|
|
4497
4537
|
{
|
|
4498
4538
|
name: 'magento_enrich',
|
|
4499
4539
|
description: 'Build the method-chain enrichment index. Scans all vendor/ PHP files for two-step method chains (->firstMethod()->secondMethod()) and analyses whether each call has a null guard in surrounding code. Results stored in .magector/enrichment.db. Run this once after magento_index, then use magento_find_null_risks for instant O(1) null-safety queries instead of 20+ grep calls. Also runs automatically after magento_index completes.',
|
|
@@ -4583,7 +4623,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
4583
4623
|
// These tools have filesystem/di.xml fallbacks — work without serve process
|
|
4584
4624
|
'magento_find_class', 'magento_find_method', 'magento_find_plugin',
|
|
4585
4625
|
'magento_find_observer', 'magento_find_di_wiring', 'magento_module_structure',
|
|
4586
|
-
'magento_batch', 'magento_find_config', 'magento_find_callers', 'magento_grep', 'magento_read', 'magento_trace_api', 'magento_ast_search', 'magento_find_null_risks'];
|
|
4626
|
+
'magento_batch', 'magento_find_config', 'magento_find_callers', 'magento_grep', 'magento_read', 'magento_trace_api', 'magento_ast_search', 'magento_find_null_risks', 'magento_find_dataobject_issues'];
|
|
4587
4627
|
if (warmupInProgress && !indexFreeTools.includes(name)) {
|
|
4588
4628
|
logToFile('REQ', `${name} → blocked (warmup: loading index)`);
|
|
4589
4629
|
return {
|
|
@@ -6485,6 +6525,16 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
6485
6525
|
}
|
|
6486
6526
|
break;
|
|
6487
6527
|
}
|
|
6528
|
+
case 'magento_find_dataobject_issues': {
|
|
6529
|
+
const doResults = await findDataObjectIssues(a.path, a.maxResults);
|
|
6530
|
+
if (doResults.length === 0) {
|
|
6531
|
+
text = 'No DataObject setX(null) anti-pattern calls found.';
|
|
6532
|
+
} else {
|
|
6533
|
+
text = `Found ${doResults.length} setX(null) call(s) — review for DataObject anti-pattern:\n\n`;
|
|
6534
|
+
for (const r of doResults) text += `${r.file}:${r.line}: ${r.snippet}\n`;
|
|
6535
|
+
}
|
|
6536
|
+
break;
|
|
6537
|
+
}
|
|
6488
6538
|
case 'magento_find_null_risks': {
|
|
6489
6539
|
const bRoot = config.magentoRoot;
|
|
6490
6540
|
const bLimit = Math.min(a.limit || 100, 500);
|
|
@@ -6762,6 +6812,23 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
6762
6812
|
return { content: [{ type: 'text', text }] };
|
|
6763
6813
|
}
|
|
6764
6814
|
|
|
6815
|
+
case 'magento_find_dataobject_issues': {
|
|
6816
|
+
const doResults = await findDataObjectIssues(args.path, args.maxResults);
|
|
6817
|
+
if (doResults.length === 0) {
|
|
6818
|
+
return { content: [{ type: 'text', text: '## magento_find_dataobject_issues\n\nNo `setX(null)` calls found.' }] };
|
|
6819
|
+
}
|
|
6820
|
+
let doText = `## magento_find_dataobject_issues\n`;
|
|
6821
|
+
doText += `Found **${doResults.length}** \`setX(null)\` call(s)\n\n`;
|
|
6822
|
+
doText += `> ⚠️ **DataObject anti-pattern**: \`setX(null)\` stores \`['x' => null]\` in \`_data\` — \`hasX()\` returns \`true\` via \`array_key_exists\` even for \`null\`. `;
|
|
6823
|
+
doText += `Downstream \`hasX()\` guards silently pass while \`getX()\` returns \`null\`. `;
|
|
6824
|
+
doText += `**Fix**: use \`unsetData('x')\` to remove the key entirely.\n\n`;
|
|
6825
|
+
for (const r of doResults) {
|
|
6826
|
+
const lineInfo = r.endLine && r.endLine !== r.line ? `${r.line}-${r.endLine}` : String(r.line);
|
|
6827
|
+
doText += `**${r.file}:${lineInfo}**\n\`\`\`php\n${r.snippet}\n\`\`\`\n\n`;
|
|
6828
|
+
}
|
|
6829
|
+
return { content: [{ type: 'text', text: doText }] };
|
|
6830
|
+
}
|
|
6831
|
+
|
|
6765
6832
|
case 'magento_enrich': {
|
|
6766
6833
|
const root = config.magentoRoot;
|
|
6767
6834
|
if (!root) return { content: [{ type: 'text', text: 'MAGENTO_ROOT not set.' }], isError: true };
|