magector 2.11.0 → 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 +149 -14
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 {
|
|
@@ -4578,8 +4671,11 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
4578
4671
|
if (pluginFile) {
|
|
4579
4672
|
reg.methods = extractPluginMethods(pluginFile);
|
|
4580
4673
|
reg.resolvedFile = pluginFile.replace(fpRoot2 + '/', '');
|
|
4581
|
-
// Read
|
|
4582
|
-
|
|
4674
|
+
// Read method bodies — only for targetMethod if specified (reduces token bloat)
|
|
4675
|
+
const methodsToRead = args.targetMethod
|
|
4676
|
+
? reg.methods.filter(m => m.targetMethod === args.targetMethod)
|
|
4677
|
+
: reg.methods;
|
|
4678
|
+
for (const m of methodsToRead) {
|
|
4583
4679
|
const body = readFullMethodBody(pluginFile, m.name);
|
|
4584
4680
|
if (body) m.body = body;
|
|
4585
4681
|
}
|
|
@@ -6075,10 +6171,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
6075
6171
|
const searchPath = a.path || '.';
|
|
6076
6172
|
const include = a.include || '*.php';
|
|
6077
6173
|
const maxRes = Math.min(a.maxResults || 30, 100);
|
|
6078
|
-
const batchCtx = a.context !== undefined ? a.context :
|
|
6174
|
+
const batchCtx = a.context !== undefined ? a.context : 4;
|
|
6175
|
+
const batchFilesOnly = a.filesOnly || false;
|
|
6079
6176
|
const gArgs = ['-rn', '-E'];
|
|
6177
|
+
if (batchFilesOnly) { gArgs[0] = '-rl'; gArgs.splice(1, 1); } // -rl = recursive + files-only, drop -n
|
|
6080
6178
|
if (a.ignoreCase) gArgs.push('-i');
|
|
6081
|
-
if (batchCtx > 0) gArgs.push('-C', String(batchCtx));
|
|
6179
|
+
if (!batchFilesOnly && batchCtx > 0) gArgs.push('-C', String(batchCtx));
|
|
6082
6180
|
for (const pat of include.split(',').map(p => p.trim())) gArgs.push('--include=' + pat);
|
|
6083
6181
|
gArgs.push('--', a.pattern, searchPath);
|
|
6084
6182
|
let out;
|
|
@@ -6086,8 +6184,23 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
6086
6184
|
out = execFileSync('grep', gArgs, { cwd: config.magentoRoot, encoding: 'utf-8', timeout: 15000, maxBuffer: 5 * 1024 * 1024, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
6087
6185
|
} catch (err) { out = err.stdout || ''; }
|
|
6088
6186
|
const gLines = out.trim().split('\n').filter(Boolean);
|
|
6089
|
-
|
|
6090
|
-
|
|
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
|
+
}
|
|
6091
6204
|
break;
|
|
6092
6205
|
}
|
|
6093
6206
|
default:
|
|
@@ -6112,10 +6225,11 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
6112
6225
|
const searchPath = args.path || '.';
|
|
6113
6226
|
const include = args.include || '*.php';
|
|
6114
6227
|
const maxResults = Math.min(args.maxResults || 50, 200);
|
|
6115
|
-
const ctxLines = args.context !== undefined ? args.context :
|
|
6116
|
-
const
|
|
6228
|
+
const ctxLines = args.context !== undefined ? args.context : 4;
|
|
6229
|
+
const filesOnly = args.filesOnly || false;
|
|
6230
|
+
const grepArgs = filesOnly ? ['-rl', '-E'] : ['-rn', '-E'];
|
|
6117
6231
|
if (args.ignoreCase) grepArgs.push('-i');
|
|
6118
|
-
if (ctxLines > 0) grepArgs.push('-C', String(ctxLines));
|
|
6232
|
+
if (!filesOnly && ctxLines > 0) grepArgs.push('-C', String(ctxLines));
|
|
6119
6233
|
// Support multiple include patterns (e.g., "*.{php,xml}")
|
|
6120
6234
|
for (const pat of include.split(',').map(p => p.trim())) {
|
|
6121
6235
|
grepArgs.push('--include=' + pat);
|
|
@@ -6135,8 +6249,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
6135
6249
|
const lines = output.trim().split('\n').filter(Boolean);
|
|
6136
6250
|
const total = lines.length;
|
|
6137
6251
|
const truncated = lines.slice(0, maxResults);
|
|
6138
|
-
let text =
|
|
6139
|
-
|
|
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`;
|
|
6140
6255
|
for (const line of truncated) {
|
|
6141
6256
|
text += line + '\n';
|
|
6142
6257
|
}
|
|
@@ -6321,10 +6436,30 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
6321
6436
|
const numbered = sliced.map((line, i) => `${start + i + 1}\t${line}`).join('\n');
|
|
6322
6437
|
let text = `## ${args.path}`;
|
|
6323
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
|
+
}
|
|
6324
6445
|
text += `\n\n${numbered}`;
|
|
6325
6446
|
return { content: [{ type: 'text', text }] };
|
|
6326
6447
|
}
|
|
6327
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
|
+
|
|
6328
6463
|
default:
|
|
6329
6464
|
return {
|
|
6330
6465
|
content: [{
|