magector 2.11.1 → 2.12.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/package.json +5 -5
- package/src/mcp-server.js +144 -12
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "magector",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.12.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.12.0",
|
|
37
|
+
"@magector/cli-linux-x64": "2.12.0",
|
|
38
|
+
"@magector/cli-linux-arm64": "2.12.0",
|
|
39
|
+
"@magector/cli-win32-x64": "2.12.0"
|
|
40
40
|
},
|
|
41
41
|
"keywords": [
|
|
42
42
|
"magento",
|
package/src/mcp-server.js
CHANGED
|
@@ -3425,6 +3425,66 @@ async function traceCallChain(startClass, startMethod, maxDepth = 3) {
|
|
|
3425
3425
|
return result;
|
|
3426
3426
|
}
|
|
3427
3427
|
|
|
3428
|
+
// ─── AST Search (semgrep) ───────────────────────────────────────
|
|
3429
|
+
|
|
3430
|
+
async function astSearch(pattern, searchPath, lang, maxResults) {
|
|
3431
|
+
const root = config.magentoRoot;
|
|
3432
|
+
if (!root) throw new Error('MAGENTO_ROOT not set');
|
|
3433
|
+
|
|
3434
|
+
const targetPath = searchPath ? path.join(root, searchPath) : root;
|
|
3435
|
+
const semgrepLang = lang || 'php';
|
|
3436
|
+
const limit = Math.min(maxResults || 50, 200);
|
|
3437
|
+
|
|
3438
|
+
// Create a temporary empty .semgrepignore in the target directory if none exists.
|
|
3439
|
+
// Semgrep's default ignore list includes "vendor/" which is exactly what we need to scan.
|
|
3440
|
+
// An empty .semgrepignore overrides the defaults: https://semgrep.dev/docs/ignoring-files-folders-code/
|
|
3441
|
+
const semgrepIgnorePath = path.join(targetPath, '.semgrepignore');
|
|
3442
|
+
let createdSemgrepIgnore = false;
|
|
3443
|
+
if (!existsSync(semgrepIgnorePath)) {
|
|
3444
|
+
try { writeFileSync(semgrepIgnorePath, '# Magector: scan vendor/ and all project files\n'); createdSemgrepIgnore = true; } catch { /* best effort */ }
|
|
3445
|
+
}
|
|
3446
|
+
|
|
3447
|
+
const semgrepArgs = [
|
|
3448
|
+
'--pattern', pattern,
|
|
3449
|
+
'--lang', semgrepLang,
|
|
3450
|
+
'--json',
|
|
3451
|
+
'--no-git-ignore',
|
|
3452
|
+
targetPath
|
|
3453
|
+
];
|
|
3454
|
+
|
|
3455
|
+
let rawOutput;
|
|
3456
|
+
try {
|
|
3457
|
+
rawOutput = execFileSync('semgrep', semgrepArgs, {
|
|
3458
|
+
encoding: 'utf-8',
|
|
3459
|
+
timeout: 60000,
|
|
3460
|
+
maxBuffer: 20 * 1024 * 1024,
|
|
3461
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
3462
|
+
env: { ...process.env, PATH: process.env.PATH + ':/home/swed/.local/bin' }
|
|
3463
|
+
});
|
|
3464
|
+
} catch (err) {
|
|
3465
|
+
// semgrep exits non-zero when it has findings — stdout still contains valid JSON
|
|
3466
|
+
rawOutput = err.stdout || '';
|
|
3467
|
+
if (!rawOutput) throw new Error(`semgrep failed: ${(err.stderr || err.message || '').slice(0, 500)}`);
|
|
3468
|
+
} finally {
|
|
3469
|
+
if (createdSemgrepIgnore) { try { unlinkSync(semgrepIgnorePath); } catch { /* best effort */ } }
|
|
3470
|
+
}
|
|
3471
|
+
|
|
3472
|
+
let parsed;
|
|
3473
|
+
try {
|
|
3474
|
+
parsed = JSON.parse(rawOutput);
|
|
3475
|
+
} catch {
|
|
3476
|
+
throw new Error(`Failed to parse semgrep output. First 300 chars: ${rawOutput.slice(0, 300)}`);
|
|
3477
|
+
}
|
|
3478
|
+
|
|
3479
|
+
const findings = (parsed.results || []).slice(0, limit);
|
|
3480
|
+
return findings.map(r => ({
|
|
3481
|
+
file: r.path.replace(root + '/', ''),
|
|
3482
|
+
line: r.start.line,
|
|
3483
|
+
endLine: r.end.line,
|
|
3484
|
+
snippet: (r.extra?.lines || '').trim()
|
|
3485
|
+
}));
|
|
3486
|
+
}
|
|
3487
|
+
|
|
3428
3488
|
// ─── MCP Server ─────────────────────────────────────────────────
|
|
3429
3489
|
|
|
3430
3490
|
const server = new Server(
|
|
@@ -4143,8 +4203,41 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
4143
4203
|
},
|
|
4144
4204
|
context: {
|
|
4145
4205
|
type: 'number',
|
|
4146
|
-
description: 'Lines of context around each match (default:
|
|
4147
|
-
default:
|
|
4206
|
+
description: 'Lines of context around each match (default: 4). Like grep -C. Use 0 for broad scans with many matches, then batch-read specific files.',
|
|
4207
|
+
default: 4
|
|
4208
|
+
},
|
|
4209
|
+
filesOnly: {
|
|
4210
|
+
type: 'boolean',
|
|
4211
|
+
description: 'Return only file paths (like grep -l). No content, no context. Use for discovery: first find which files match, then batch-read them with magento_read. Dramatically reduces tokens when pattern matches many files.',
|
|
4212
|
+
default: false
|
|
4213
|
+
}
|
|
4214
|
+
},
|
|
4215
|
+
required: ['pattern']
|
|
4216
|
+
}
|
|
4217
|
+
},
|
|
4218
|
+
{
|
|
4219
|
+
name: 'magento_ast_search',
|
|
4220
|
+
description: 'Structural PHP code search using semgrep patterns. Unlike magento_grep (text-based), this understands PHP AST — matches code structure regardless of variable names, ignores comments/strings, understands operator precedence. Use when grep gives false positives or you need structural awareness. Pattern syntax: $X = any expression/variable, $Y = any identifier, ... = any arguments. Examples: "$ORDER->getPayment()->$M(...)" finds all method calls on payment regardless of variable name; "$X->getPayment()->$Y(...)" finds all two-step chains involving getPayment. ⚡ For multi-query workflows use magento_batch.',
|
|
4221
|
+
inputSchema: {
|
|
4222
|
+
type: 'object',
|
|
4223
|
+
properties: {
|
|
4224
|
+
pattern: {
|
|
4225
|
+
type: 'string',
|
|
4226
|
+
description: 'Semgrep PHP pattern. $X = any expr, $Y = any identifier, ... = any args. Examples: "$X->getPayment()->$Y(...)", "if ($X !== null) { ... $X->$Y(...) }", "$X = $Y->getPayment(); ... $X->$Z(...)"'
|
|
4227
|
+
},
|
|
4228
|
+
path: {
|
|
4229
|
+
type: 'string',
|
|
4230
|
+
description: 'Subdirectory to search (relative to MAGENTO_ROOT). Default: entire codebase. Example: "vendor/acme/"'
|
|
4231
|
+
},
|
|
4232
|
+
lang: {
|
|
4233
|
+
type: 'string',
|
|
4234
|
+
description: 'Language to search (default: php). Options: php, xml, js.',
|
|
4235
|
+
default: 'php'
|
|
4236
|
+
},
|
|
4237
|
+
maxResults: {
|
|
4238
|
+
type: 'number',
|
|
4239
|
+
description: 'Maximum matches to return (default: 50, max: 200)',
|
|
4240
|
+
default: 50
|
|
4148
4241
|
}
|
|
4149
4242
|
},
|
|
4150
4243
|
required: ['pattern']
|
|
@@ -4216,7 +4309,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
4216
4309
|
// These tools have filesystem/di.xml fallbacks — work without serve process
|
|
4217
4310
|
'magento_find_class', 'magento_find_method', 'magento_find_plugin',
|
|
4218
4311
|
'magento_find_observer', 'magento_find_di_wiring', 'magento_module_structure',
|
|
4219
|
-
'magento_batch', 'magento_find_config', 'magento_find_callers', 'magento_grep', 'magento_read', 'magento_trace_api'];
|
|
4312
|
+
'magento_batch', 'magento_find_config', 'magento_find_callers', 'magento_grep', 'magento_read', 'magento_trace_api', 'magento_ast_search'];
|
|
4220
4313
|
if (warmupInProgress && !indexFreeTools.includes(name)) {
|
|
4221
4314
|
logToFile('REQ', `${name} → blocked (warmup: loading index)`);
|
|
4222
4315
|
return {
|
|
@@ -6078,10 +6171,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
6078
6171
|
const searchPath = a.path || '.';
|
|
6079
6172
|
const include = a.include || '*.php';
|
|
6080
6173
|
const maxRes = Math.min(a.maxResults || 30, 100);
|
|
6081
|
-
const batchCtx = a.context !== undefined ? a.context :
|
|
6174
|
+
const batchCtx = a.context !== undefined ? a.context : 4;
|
|
6175
|
+
const batchFilesOnly = a.filesOnly || false;
|
|
6082
6176
|
const gArgs = ['-rn', '-E'];
|
|
6177
|
+
if (batchFilesOnly) { gArgs[0] = '-rl'; gArgs.splice(1, 1); } // -rl = recursive + files-only, drop -n
|
|
6083
6178
|
if (a.ignoreCase) gArgs.push('-i');
|
|
6084
|
-
if (batchCtx > 0) gArgs.push('-C', String(batchCtx));
|
|
6179
|
+
if (!batchFilesOnly && batchCtx > 0) gArgs.push('-C', String(batchCtx));
|
|
6085
6180
|
for (const pat of include.split(',').map(p => p.trim())) gArgs.push('--include=' + pat);
|
|
6086
6181
|
gArgs.push('--', a.pattern, searchPath);
|
|
6087
6182
|
let out;
|
|
@@ -6089,8 +6184,23 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
6089
6184
|
out = execFileSync('grep', gArgs, { cwd: config.magentoRoot, encoding: 'utf-8', timeout: 15000, maxBuffer: 5 * 1024 * 1024, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
6090
6185
|
} catch (err) { out = err.stdout || ''; }
|
|
6091
6186
|
const gLines = out.trim().split('\n').filter(Boolean);
|
|
6092
|
-
|
|
6093
|
-
|
|
6187
|
+
if (batchFilesOnly) {
|
|
6188
|
+
text = `Files matching \`${a.pattern}\` (${gLines.length}):\n`;
|
|
6189
|
+
for (const gl of gLines.slice(0, maxRes)) text += gl + '\n';
|
|
6190
|
+
} else {
|
|
6191
|
+
text = `Found ${gLines.length} matches${gLines.length > maxRes ? ` (showing ${maxRes})` : ''}:\n`;
|
|
6192
|
+
for (const gl of gLines.slice(0, maxRes)) text += gl + '\n';
|
|
6193
|
+
}
|
|
6194
|
+
break;
|
|
6195
|
+
}
|
|
6196
|
+
case 'magento_ast_search': {
|
|
6197
|
+
const astResults = await astSearch(a.pattern, a.path, a.lang, a.maxResults);
|
|
6198
|
+
if (astResults.length === 0) {
|
|
6199
|
+
text = `No matches for pattern: \`${a.pattern}\``;
|
|
6200
|
+
} else {
|
|
6201
|
+
text = `Found ${astResults.length} match(es) for \`${a.pattern}\`:\n\n`;
|
|
6202
|
+
for (const r of astResults) text += `${r.file}:${r.line}: ${r.snippet}\n`;
|
|
6203
|
+
}
|
|
6094
6204
|
break;
|
|
6095
6205
|
}
|
|
6096
6206
|
default:
|
|
@@ -6115,10 +6225,11 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
6115
6225
|
const searchPath = args.path || '.';
|
|
6116
6226
|
const include = args.include || '*.php';
|
|
6117
6227
|
const maxResults = Math.min(args.maxResults || 50, 200);
|
|
6118
|
-
const ctxLines = args.context !== undefined ? args.context :
|
|
6119
|
-
const
|
|
6228
|
+
const ctxLines = args.context !== undefined ? args.context : 4;
|
|
6229
|
+
const filesOnly = args.filesOnly || false;
|
|
6230
|
+
const grepArgs = filesOnly ? ['-rl', '-E'] : ['-rn', '-E'];
|
|
6120
6231
|
if (args.ignoreCase) grepArgs.push('-i');
|
|
6121
|
-
if (ctxLines > 0) grepArgs.push('-C', String(ctxLines));
|
|
6232
|
+
if (!filesOnly && ctxLines > 0) grepArgs.push('-C', String(ctxLines));
|
|
6122
6233
|
// Support multiple include patterns (e.g., "*.{php,xml}")
|
|
6123
6234
|
for (const pat of include.split(',').map(p => p.trim())) {
|
|
6124
6235
|
grepArgs.push('--include=' + pat);
|
|
@@ -6138,8 +6249,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
6138
6249
|
const lines = output.trim().split('\n').filter(Boolean);
|
|
6139
6250
|
const total = lines.length;
|
|
6140
6251
|
const truncated = lines.slice(0, maxResults);
|
|
6141
|
-
let text =
|
|
6142
|
-
|
|
6252
|
+
let text = filesOnly
|
|
6253
|
+
? `## grep (files only): \`${args.pattern}\`\nFound **${total}** file(s)${total > maxResults ? ` (showing first ${maxResults})` : ''}. Use magento_read with methodName to read specific methods.\n\n`
|
|
6254
|
+
: `## grep: \`${args.pattern}\`\nFound **${total}** matches${total > maxResults ? ` (showing first ${maxResults})` : ''}\n\n`;
|
|
6143
6255
|
for (const line of truncated) {
|
|
6144
6256
|
text += line + '\n';
|
|
6145
6257
|
}
|
|
@@ -6324,10 +6436,30 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
6324
6436
|
const numbered = sliced.map((line, i) => `${start + i + 1}\t${line}`).join('\n');
|
|
6325
6437
|
let text = `## ${args.path}`;
|
|
6326
6438
|
if (args.startLine || args.endLine) text += ` (lines ${start + 1}-${end})`;
|
|
6439
|
+
// Hint: large file read without method extraction wastes tokens
|
|
6440
|
+
if (!args.startLine && !args.endLine && allLines.length > 100) {
|
|
6441
|
+
const methodMatches = content.match(/(?:public|protected|private)\s+(?:static\s+)?function\s+(\w+)/g) || [];
|
|
6442
|
+
const methodNames = methodMatches.slice(0, 8).map(m => m.replace(/.*function\s+/, '')).join(', ');
|
|
6443
|
+
text += `\n> 💡 **${allLines.length} lines** — add \`methodName\` to extract a single method (~10× fewer tokens). Methods: ${methodNames}${methodMatches.length > 8 ? ', ...' : ''}`;
|
|
6444
|
+
}
|
|
6327
6445
|
text += `\n\n${numbered}`;
|
|
6328
6446
|
return { content: [{ type: 'text', text }] };
|
|
6329
6447
|
}
|
|
6330
6448
|
|
|
6449
|
+
case 'magento_ast_search': {
|
|
6450
|
+
const astResults = await astSearch(args.pattern, args.path, args.lang, args.maxResults);
|
|
6451
|
+
if (astResults.length === 0) {
|
|
6452
|
+
return { content: [{ type: 'text', text: `## magento_ast_search: \`${args.pattern}\`\n\nNo matches found.` }] };
|
|
6453
|
+
}
|
|
6454
|
+
let text = `## magento_ast_search: \`${args.pattern}\`\n`;
|
|
6455
|
+
text += `Found **${astResults.length}** match(es)\n\n`;
|
|
6456
|
+
for (const r of astResults) {
|
|
6457
|
+
const lineInfo = r.endLine && r.endLine !== r.line ? `${r.line}-${r.endLine}` : String(r.line);
|
|
6458
|
+
text += `**${r.file}:${lineInfo}**\n\`\`\`php\n${r.snippet}\n\`\`\`\n\n`;
|
|
6459
|
+
}
|
|
6460
|
+
return { content: [{ type: 'text', text }] };
|
|
6461
|
+
}
|
|
6462
|
+
|
|
6331
6463
|
default:
|
|
6332
6464
|
return {
|
|
6333
6465
|
content: [{
|