magector 2.14.2 → 2.15.1
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 +38 -6
- package/package.json +5 -5
- package/src/mcp-server.js +141 -14
- package/src/update.js +18 -3
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
|
|
@@ -136,6 +136,37 @@ flowchart LR
|
|
|
136
136
|
|
|
137
137
|
---
|
|
138
138
|
|
|
139
|
+
## Security
|
|
140
|
+
|
|
141
|
+
Magector operates on source code indexed from potentially-untrusted `vendor/` dependencies and is driven by an LLM that may be manipulated via prompt injection in indexed comments, docblocks, or markdown. The following hardening applies as of **v2.15.1**:
|
|
142
|
+
|
|
143
|
+
### Path traversal protection
|
|
144
|
+
|
|
145
|
+
All tools that accept a `path` argument (`magento_read`, `magento_grep`, `magento_ast_search`, `magento_find_dataobject_issues`) route the input through `safePath()` / `safeRelPath()` helpers in `src/mcp-server.js`. These:
|
|
146
|
+
|
|
147
|
+
1. Resolve the argument against `MAGENTO_ROOT` with `path.resolve()` (normalizes `..`, symlinks are not followed during validation).
|
|
148
|
+
2. Reject any resolved path that does not lie inside `MAGENTO_ROOT`.
|
|
149
|
+
|
|
150
|
+
This prevents a hostile `vendor/` comment from instructing the LLM to e.g. `magento_read` `../../home/user/.ssh/id_rsa`. Both the standalone case handlers and their `magento_batch` counterparts share the same chokepoint.
|
|
151
|
+
|
|
152
|
+
### Shell injection hardening in auto-update
|
|
153
|
+
|
|
154
|
+
`src/update.js` fetches the `latest` field from the npm registry and re-execs itself with the new version string. Previously this was interpolated into a shell command; a tampered registry response could inject shell metacharacters. As of v2.15.1:
|
|
155
|
+
|
|
156
|
+
- The re-exec passes argv as an **array** to a no-shell spawner (no intermediate shell).
|
|
157
|
+
- A semver-strict `isSafeVersion()` validator rejects any version string containing metacharacters or that does not match `X.Y.Z` / `X.Y.Z-prerelease` form.
|
|
158
|
+
- Fails closed: the auto-update is silently skipped rather than run a malformed version.
|
|
159
|
+
|
|
160
|
+
### Unix socket permissions
|
|
161
|
+
|
|
162
|
+
The serve-proxy Unix socket at `.magector/serve.sock` is created with `chmod 0600` immediately after `listen()`. On multi-user systems, another local account can no longer connect and query the vector index (which would leak indexed source snippets). The chmod is best-effort on platforms that don't support it (logged to `.magector/magector.log`).
|
|
163
|
+
|
|
164
|
+
### Reporting vulnerabilities
|
|
165
|
+
|
|
166
|
+
If you find a security issue, please open an issue on the GitHub repo and mark it as security-related. Do not post reproducers that leak actual source contents from private codebases.
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
139
170
|
## Quick Start
|
|
140
171
|
|
|
141
172
|
### Prerequisites
|
|
@@ -371,7 +402,7 @@ npx magector index --force
|
|
|
371
402
|
|
|
372
403
|
## MCP Server Tools
|
|
373
404
|
|
|
374
|
-
The MCP server exposes
|
|
405
|
+
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
406
|
|
|
376
407
|
### Output Format
|
|
377
408
|
|
|
@@ -482,13 +513,14 @@ Auto-detects entry type from pattern (`/V1/...` → API, `snake_case` → event,
|
|
|
482
513
|
| `magento_find_trigger` | Find database triggers across the codebase |
|
|
483
514
|
| `magento_find_table_usage` | Find all PHP code referencing a specific database table |
|
|
484
515
|
|
|
485
|
-
### Null-Safety Analysis (v2.12–v2.
|
|
516
|
+
### Null-Safety Analysis (v2.12–v2.15)
|
|
486
517
|
|
|
487
518
|
| Tool | Description |
|
|
488
519
|
|------|-------------|
|
|
489
520
|
| `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
521
|
| `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
522
|
| `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)** |
|
|
523
|
+
| `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
524
|
|
|
493
525
|
### Search Enhancements (v2.1)
|
|
494
526
|
|
|
@@ -672,7 +704,7 @@ cd rust-core && cargo run --release -- validate -m ./magento2 --skip-index
|
|
|
672
704
|
magector/
|
|
673
705
|
├── src/ # Node.js source
|
|
674
706
|
│ ├── cli.js # CLI entry point (npx magector <command>)
|
|
675
|
-
│ ├── mcp-server.js # MCP server (
|
|
707
|
+
│ ├── mcp-server.js # MCP server (46 tools, structured JSON output)
|
|
676
708
|
│ ├── binary.js # Platform binary resolver
|
|
677
709
|
│ ├── model.js # ONNX model resolver/downloader
|
|
678
710
|
│ ├── 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.1",
|
|
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.1",
|
|
37
|
+
"@magector/cli-linux-x64": "2.15.1",
|
|
38
|
+
"@magector/cli-linux-arm64": "2.15.1",
|
|
39
|
+
"@magector/cli-win32-x64": "2.15.1"
|
|
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, constants as fsConstants } from 'fs';
|
|
20
|
+
import { existsSync, statSync, unlinkSync, copyFileSync, renameSync, appendFileSync, writeFileSync, readFileSync, 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';
|
|
@@ -149,6 +149,38 @@ const SOCK_PATH = path.join(config.magentoRoot, '.magector', 'serve.sock');
|
|
|
149
149
|
const FORMAT_CACHE_PATH = path.join(config.magentoRoot, '.magector', 'format-ok.json');
|
|
150
150
|
const PRIMARY_LOCK_PATH = path.join(config.magentoRoot, '.magector', 'primary.lock');
|
|
151
151
|
|
|
152
|
+
// ─── Path Safety ────────────────────────────────────────────────
|
|
153
|
+
// All tool handlers accept user-supplied paths relative to MAGENTO_ROOT.
|
|
154
|
+
// Without validation, `../../../etc/passwd` would escape the project.
|
|
155
|
+
// A hostile indexed file could prompt-inject the LLM into requesting such a
|
|
156
|
+
// path and leak host files. These helpers are the single chokepoint.
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Resolve a user-supplied path against a trusted root and verify it stays
|
|
160
|
+
* inside the root. Returns the absolute resolved path or null on escape.
|
|
161
|
+
*/
|
|
162
|
+
function safePath(root, rel) {
|
|
163
|
+
if (rel === undefined || rel === null) return null;
|
|
164
|
+
const rootAbs = path.resolve(root);
|
|
165
|
+
const joined = path.resolve(rootAbs, String(rel));
|
|
166
|
+
if (joined !== rootAbs && !joined.startsWith(rootAbs + path.sep)) {
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
return joined;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Like safePath but returns the relative form (used for tools that invoke
|
|
174
|
+
* external processes with cwd=root and want a relative path argument).
|
|
175
|
+
* '.' means "the root itself".
|
|
176
|
+
*/
|
|
177
|
+
function safeRelPath(root, rel) {
|
|
178
|
+
const abs = safePath(root, rel);
|
|
179
|
+
if (!abs) return null;
|
|
180
|
+
const r = path.relative(path.resolve(root), abs);
|
|
181
|
+
return r === '' ? '.' : r;
|
|
182
|
+
}
|
|
183
|
+
|
|
152
184
|
/**
|
|
153
185
|
* Expand brace patterns in include globs for GNU grep compatibility.
|
|
154
186
|
* GNU grep --include does NOT support brace expansion (that's a shell feature).
|
|
@@ -737,7 +769,13 @@ function startSocketProxy() {
|
|
|
737
769
|
logToFile('WARN', `Socket proxy error: ${err.message}`);
|
|
738
770
|
});
|
|
739
771
|
socketServer.listen(SOCK_PATH, () => {
|
|
740
|
-
|
|
772
|
+
// Restrict socket to the owning user — without this, on multi-user
|
|
773
|
+
// systems any local account could connect to the serve proxy and query
|
|
774
|
+
// the index (leaking indexed code snippets to other local users).
|
|
775
|
+
try { chmodSync(SOCK_PATH, 0o600); } catch (err) {
|
|
776
|
+
logToFile('WARN', `Failed to chmod socket to 0600: ${err.message}`);
|
|
777
|
+
}
|
|
778
|
+
logToFile('INFO', `Socket proxy listening on ${SOCK_PATH} (mode 0600)`);
|
|
741
779
|
});
|
|
742
780
|
}
|
|
743
781
|
|
|
@@ -3663,7 +3701,11 @@ async function astSearch(pattern, searchPath, lang, maxResults) {
|
|
|
3663
3701
|
const root = config.magentoRoot;
|
|
3664
3702
|
if (!root) throw new Error('MAGENTO_ROOT not set');
|
|
3665
3703
|
|
|
3666
|
-
const targetPath = searchPath ?
|
|
3704
|
+
const targetPath = searchPath ? safePath(root, searchPath) : path.resolve(root);
|
|
3705
|
+
if (!targetPath) {
|
|
3706
|
+
logToFile('WARN', `ast_search: rejected path traversal attempt: "${searchPath}"`);
|
|
3707
|
+
throw new Error(`Path escapes project root: ${searchPath}`);
|
|
3708
|
+
}
|
|
3667
3709
|
const semgrepLang = lang || 'php';
|
|
3668
3710
|
const limit = Math.min(maxResults || 50, 200);
|
|
3669
3711
|
|
|
@@ -3728,12 +3770,34 @@ async function astSearch(pattern, searchPath, lang, maxResults) {
|
|
|
3728
3770
|
if (parsed.errors && parsed.errors.length > 0) {
|
|
3729
3771
|
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
3772
|
}
|
|
3731
|
-
return findings.map(r =>
|
|
3732
|
-
|
|
3733
|
-
|
|
3734
|
-
|
|
3735
|
-
snippet
|
|
3736
|
-
|
|
3773
|
+
return findings.map(r => {
|
|
3774
|
+
// semgrep >=1.100 may return "requires login" in r.extra.lines for unlicensed installs.
|
|
3775
|
+
// Fall back to r.extra.message which contains the matched expression (always available).
|
|
3776
|
+
const rawLines = r.extra?.lines || '';
|
|
3777
|
+
const snippet = (rawLines && rawLines !== 'requires login')
|
|
3778
|
+
? rawLines.trim()
|
|
3779
|
+
: (r.extra?.message || '').trim();
|
|
3780
|
+
return {
|
|
3781
|
+
file: r.path.replace(root + '/', ''),
|
|
3782
|
+
line: r.start.line,
|
|
3783
|
+
endLine: r.end.line,
|
|
3784
|
+
snippet
|
|
3785
|
+
};
|
|
3786
|
+
});
|
|
3787
|
+
}
|
|
3788
|
+
|
|
3789
|
+
// ─── DataObject set-null Anti-pattern Detection ─────────────────
|
|
3790
|
+
|
|
3791
|
+
async function findDataObjectIssues(searchPath, maxResults) {
|
|
3792
|
+
// Detects DataObject::setX(null) anti-pattern:
|
|
3793
|
+
// setX(null) stores ['x' => null] in _data — key EXISTS with null value.
|
|
3794
|
+
// hasX() / hasData('x') calls array_key_exists() → returns true even for null.
|
|
3795
|
+
// This causes false-positive guard conditions: hasX() passes, getX() returns null.
|
|
3796
|
+
// Correct way to fully clear: unsetData('x') removes the key entirely.
|
|
3797
|
+
const allResults = await astSearch('$X->$SETTER(null)', searchPath, 'php', 500);
|
|
3798
|
+
const setterNullRegex = /->set[A-Z]\w+\s*\(\s*null\s*\)/;
|
|
3799
|
+
const limit = Math.min(maxResults || 100, 500);
|
|
3800
|
+
return allResults.filter(r => setterNullRegex.test(r.snippet)).slice(0, limit);
|
|
3737
3801
|
}
|
|
3738
3802
|
|
|
3739
3803
|
// ─── MCP Server ─────────────────────────────────────────────────
|
|
@@ -4494,6 +4558,24 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
4494
4558
|
required: ['pattern']
|
|
4495
4559
|
}
|
|
4496
4560
|
},
|
|
4561
|
+
{
|
|
4562
|
+
name: 'magento_find_dataobject_issues',
|
|
4563
|
+
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.',
|
|
4564
|
+
inputSchema: {
|
|
4565
|
+
type: 'object',
|
|
4566
|
+
properties: {
|
|
4567
|
+
path: {
|
|
4568
|
+
type: 'string',
|
|
4569
|
+
description: 'Subdirectory to search (relative to MAGENTO_ROOT). Default: entire codebase. Example: "vendor/acme/"'
|
|
4570
|
+
},
|
|
4571
|
+
maxResults: {
|
|
4572
|
+
type: 'number',
|
|
4573
|
+
description: 'Maximum matches to return (default: 100, max: 500)',
|
|
4574
|
+
default: 100
|
|
4575
|
+
}
|
|
4576
|
+
}
|
|
4577
|
+
}
|
|
4578
|
+
},
|
|
4497
4579
|
{
|
|
4498
4580
|
name: 'magento_enrich',
|
|
4499
4581
|
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 +4665,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
4583
4665
|
// These tools have filesystem/di.xml fallbacks — work without serve process
|
|
4584
4666
|
'magento_find_class', 'magento_find_method', 'magento_find_plugin',
|
|
4585
4667
|
'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'];
|
|
4668
|
+
'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
4669
|
if (warmupInProgress && !indexFreeTools.includes(name)) {
|
|
4588
4670
|
logToFile('REQ', `${name} → blocked (warmup: loading index)`);
|
|
4589
4671
|
return {
|
|
@@ -6426,7 +6508,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
6426
6508
|
break;
|
|
6427
6509
|
}
|
|
6428
6510
|
case 'magento_read': {
|
|
6429
|
-
const filePath =
|
|
6511
|
+
const filePath = safePath(config.magentoRoot, a.path);
|
|
6512
|
+
if (!filePath) {
|
|
6513
|
+
logToFile('WARN', `batch read: rejected path traversal attempt: "${a.path}"`);
|
|
6514
|
+
text = `Path escapes project root: ${a.path}`;
|
|
6515
|
+
break;
|
|
6516
|
+
}
|
|
6430
6517
|
let fileContent;
|
|
6431
6518
|
try { fileContent = readFileSync(filePath, 'utf-8'); } catch {
|
|
6432
6519
|
text = `File not found: ${a.path}`;
|
|
@@ -6451,7 +6538,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
6451
6538
|
break;
|
|
6452
6539
|
}
|
|
6453
6540
|
case 'magento_grep': {
|
|
6454
|
-
const searchPath = a.path || '.';
|
|
6541
|
+
const searchPath = safeRelPath(config.magentoRoot, a.path || '.');
|
|
6542
|
+
if (!searchPath) {
|
|
6543
|
+
logToFile('WARN', `batch grep: rejected path traversal attempt: "${a.path}"`);
|
|
6544
|
+
text = `Path escapes project root: ${a.path}`;
|
|
6545
|
+
break;
|
|
6546
|
+
}
|
|
6455
6547
|
const include = a.include || '*.php';
|
|
6456
6548
|
const maxRes = Math.min(a.maxResults || 30, 100);
|
|
6457
6549
|
const batchCtx = a.context !== undefined ? a.context : 4;
|
|
@@ -6485,6 +6577,16 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
6485
6577
|
}
|
|
6486
6578
|
break;
|
|
6487
6579
|
}
|
|
6580
|
+
case 'magento_find_dataobject_issues': {
|
|
6581
|
+
const doResults = await findDataObjectIssues(a.path, a.maxResults);
|
|
6582
|
+
if (doResults.length === 0) {
|
|
6583
|
+
text = 'No DataObject setX(null) anti-pattern calls found.';
|
|
6584
|
+
} else {
|
|
6585
|
+
text = `Found ${doResults.length} setX(null) call(s) — review for DataObject anti-pattern:\n\n`;
|
|
6586
|
+
for (const r of doResults) text += `${r.file}:${r.line}: ${r.snippet}\n`;
|
|
6587
|
+
}
|
|
6588
|
+
break;
|
|
6589
|
+
}
|
|
6488
6590
|
case 'magento_find_null_risks': {
|
|
6489
6591
|
const bRoot = config.magentoRoot;
|
|
6490
6592
|
const bLimit = Math.min(a.limit || 100, 500);
|
|
@@ -6519,7 +6621,11 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
6519
6621
|
case 'magento_grep': {
|
|
6520
6622
|
const root = config.magentoRoot;
|
|
6521
6623
|
if (!root) return { content: [{ type: 'text', text: 'MAGENTO_ROOT not set.' }], isError: true };
|
|
6522
|
-
const searchPath = args.path || '.';
|
|
6624
|
+
const searchPath = safeRelPath(root, args.path || '.');
|
|
6625
|
+
if (!searchPath) {
|
|
6626
|
+
logToFile('WARN', `grep: rejected path traversal attempt: "${args.path}"`);
|
|
6627
|
+
return { content: [{ type: 'text', text: `Path escapes project root: ${args.path}` }], isError: true };
|
|
6628
|
+
}
|
|
6523
6629
|
const include = args.include || '*.php';
|
|
6524
6630
|
const maxResults = Math.min(args.maxResults || 50, 200);
|
|
6525
6631
|
const ctxLines = args.context !== undefined ? args.context : 4;
|
|
@@ -6706,7 +6812,11 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
6706
6812
|
case 'magento_read': {
|
|
6707
6813
|
const root = config.magentoRoot;
|
|
6708
6814
|
if (!root) return { content: [{ type: 'text', text: 'MAGENTO_ROOT not set.' }], isError: true };
|
|
6709
|
-
const filePath =
|
|
6815
|
+
const filePath = safePath(root, args.path);
|
|
6816
|
+
if (!filePath) {
|
|
6817
|
+
logToFile('WARN', `read: rejected path traversal attempt: "${args.path}"`);
|
|
6818
|
+
return { content: [{ type: 'text', text: `Path escapes project root: ${args.path}` }], isError: true };
|
|
6819
|
+
}
|
|
6710
6820
|
let content;
|
|
6711
6821
|
try { content = readFileSync(filePath, 'utf-8'); } catch (err) {
|
|
6712
6822
|
logToFile('WARN', `read: file not found: ${args.path} (${err.code || err.message})`);
|
|
@@ -6762,6 +6872,23 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
6762
6872
|
return { content: [{ type: 'text', text }] };
|
|
6763
6873
|
}
|
|
6764
6874
|
|
|
6875
|
+
case 'magento_find_dataobject_issues': {
|
|
6876
|
+
const doResults = await findDataObjectIssues(args.path, args.maxResults);
|
|
6877
|
+
if (doResults.length === 0) {
|
|
6878
|
+
return { content: [{ type: 'text', text: '## magento_find_dataobject_issues\n\nNo `setX(null)` calls found.' }] };
|
|
6879
|
+
}
|
|
6880
|
+
let doText = `## magento_find_dataobject_issues\n`;
|
|
6881
|
+
doText += `Found **${doResults.length}** \`setX(null)\` call(s)\n\n`;
|
|
6882
|
+
doText += `> ⚠️ **DataObject anti-pattern**: \`setX(null)\` stores \`['x' => null]\` in \`_data\` — \`hasX()\` returns \`true\` via \`array_key_exists\` even for \`null\`. `;
|
|
6883
|
+
doText += `Downstream \`hasX()\` guards silently pass while \`getX()\` returns \`null\`. `;
|
|
6884
|
+
doText += `**Fix**: use \`unsetData('x')\` to remove the key entirely.\n\n`;
|
|
6885
|
+
for (const r of doResults) {
|
|
6886
|
+
const lineInfo = r.endLine && r.endLine !== r.line ? `${r.line}-${r.endLine}` : String(r.line);
|
|
6887
|
+
doText += `**${r.file}:${lineInfo}**\n\`\`\`php\n${r.snippet}\n\`\`\`\n\n`;
|
|
6888
|
+
}
|
|
6889
|
+
return { content: [{ type: 'text', text: doText }] };
|
|
6890
|
+
}
|
|
6891
|
+
|
|
6765
6892
|
case 'magento_enrich': {
|
|
6766
6893
|
const root = config.magentoRoot;
|
|
6767
6894
|
if (!root) return { content: [{ type: 'text', text: 'MAGENTO_ROOT not set.' }], isError: true };
|
package/src/update.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* Never blocks the CLI on failure — network errors are silently ignored.
|
|
9
9
|
*/
|
|
10
10
|
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
11
|
-
import {
|
|
11
|
+
import { execFileSync } from 'child_process';
|
|
12
12
|
import { homedir } from 'os';
|
|
13
13
|
import path from 'path';
|
|
14
14
|
import { fileURLToPath } from 'url';
|
|
@@ -140,14 +140,29 @@ export async function checkForUpdate(command, originalArgs) {
|
|
|
140
140
|
}
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
+
/**
|
|
144
|
+
* Validate a semver string to prevent shell injection via malicious registry
|
|
145
|
+
* responses. Only digits, dots, dashes and alphanumerics allowed (semver prerelease).
|
|
146
|
+
* Example: "1.2.3", "1.2.3-beta.1", "2.0.0-rc.9" — yes. "1; rm -rf ~" — no.
|
|
147
|
+
*/
|
|
148
|
+
function isSafeVersion(v) {
|
|
149
|
+
return typeof v === 'string' && /^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$/.test(v);
|
|
150
|
+
}
|
|
151
|
+
|
|
143
152
|
/**
|
|
144
153
|
* Re-exec the current command with the latest version.
|
|
145
154
|
*/
|
|
146
155
|
function reExec(current, latest, originalArgs) {
|
|
156
|
+
// Defensive: reject anything that doesn't look like a real semver so a
|
|
157
|
+
// compromised npm registry response can't inject shell metacharacters.
|
|
158
|
+
if (!isSafeVersion(latest)) {
|
|
159
|
+
return; // silently skip — never block CLI on update check
|
|
160
|
+
}
|
|
147
161
|
console.log(`\n⬆ Updating magector: v${current} → v${latest}...\n`);
|
|
148
162
|
try {
|
|
149
|
-
|
|
150
|
-
|
|
163
|
+
// execFileSync with an argv array (no shell) — originalArgs are passed as
|
|
164
|
+
// individual argv entries, so spaces/metachars in them can't expand.
|
|
165
|
+
execFileSync('npx', ['-y', `magector@${latest}`, ...originalArgs], {
|
|
151
166
|
stdio: 'inherit',
|
|
152
167
|
env: { ...process.env, MAGECTOR_NO_UPDATE: '1' }
|
|
153
168
|
});
|