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 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 45 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 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
  [![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-22.5+-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** -- 45 tools integrating with Claude Code, Cursor, and any MCP-compatible AI tool
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/>45 tools · LRU cache"]
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 45 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.
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.13)
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 (45 tools, structured JSON output)
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.14.1",
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.14.1",
37
- "@magector/cli-linux-x64": "2.14.1",
38
- "@magector/cli-linux-arm64": "2.14.1",
39
- "@magector/cli-win32-x64": "2.14.1"
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
- // An empty .semgrepignore overrides the defaults: https://semgrep.dev/docs/ignoring-files-folders-code/
3676
- const semgrepIgnorePath = path.join(targetPath, '.semgrepignore');
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 ${targetPath}`);
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
- file: r.path.replace(root + '/', ''),
3733
- line: r.start.line,
3734
- endLine: r.end.line,
3735
- snippet: (r.extra?.lines || '').trim()
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 };