magector 2.11.1 → 2.13.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 +372 -13
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "magector",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.13.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.13.0",
|
|
37
|
+
"@magector/cli-linux-x64": "2.13.0",
|
|
38
|
+
"@magector/cli-linux-arm64": "2.13.0",
|
|
39
|
+
"@magector/cli-win32-x64": "2.13.0"
|
|
40
40
|
},
|
|
41
41
|
"keywords": [
|
|
42
42
|
"magento",
|
package/src/mcp-server.js
CHANGED
|
@@ -3425,6 +3425,206 @@ async function traceCallChain(startClass, startMethod, maxDepth = 3) {
|
|
|
3425
3425
|
return result;
|
|
3426
3426
|
}
|
|
3427
3427
|
|
|
3428
|
+
// ─── Method Chain Enrichment ────────────────────────────────────
|
|
3429
|
+
// Scans PHP files for two-step method chains (->first()->second()) and detects
|
|
3430
|
+
// null guards in surrounding code. Results stored in SQLite enrichment.db for
|
|
3431
|
+
// instant O(1) queries — eliminates 20+ grep calls for null-risk analyses.
|
|
3432
|
+
|
|
3433
|
+
const ENRICHMENT_DB_PATH = (root) => path.join(root, '.magector', 'enrichment.db');
|
|
3434
|
+
|
|
3435
|
+
/**
|
|
3436
|
+
* Detect null guard for a chained call in surrounding lines.
|
|
3437
|
+
* Checks ±guardRadius lines for: null checks, ?->, ??, isset()
|
|
3438
|
+
*/
|
|
3439
|
+
function hasNullGuard(lines, matchLineIdx, receiverExpr, guardRadius = 6) {
|
|
3440
|
+
const start = Math.max(0, matchLineIdx - guardRadius);
|
|
3441
|
+
const end = Math.min(lines.length - 1, matchLineIdx + guardRadius);
|
|
3442
|
+
const window = lines.slice(start, end + 1).join('\n');
|
|
3443
|
+
|
|
3444
|
+
if (window.includes('?->')) return true;
|
|
3445
|
+
if (/\?\?|\?:/.test(window)) return true;
|
|
3446
|
+
|
|
3447
|
+
if (receiverExpr) {
|
|
3448
|
+
const esc = receiverExpr.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
3449
|
+
if (new RegExp(`(?:is_null\\s*\\(\\s*${esc}|${esc}\\s*(?:===|!==)\\s*null|!\\s*${esc}\\s*[,)]|isset\\s*\\(\\s*${esc})`, 'i').test(window)) return true;
|
|
3450
|
+
}
|
|
3451
|
+
return false;
|
|
3452
|
+
}
|
|
3453
|
+
|
|
3454
|
+
/**
|
|
3455
|
+
* Scan vendor/ PHP files for ->first()->second() chains and store null-safety
|
|
3456
|
+
* analysis in enrichment.db. Called by magento_enrich and after magento_index.
|
|
3457
|
+
*/
|
|
3458
|
+
async function enrichMethodChains(root, options = {}) {
|
|
3459
|
+
const dbPath = ENRICHMENT_DB_PATH(root);
|
|
3460
|
+
|
|
3461
|
+
// Use node:sqlite (built-in, no deps)
|
|
3462
|
+
let DatabaseSync;
|
|
3463
|
+
try {
|
|
3464
|
+
({ DatabaseSync } = await import('node:sqlite'));
|
|
3465
|
+
} catch {
|
|
3466
|
+
throw new Error('node:sqlite not available — requires Node.js 22.5+');
|
|
3467
|
+
}
|
|
3468
|
+
|
|
3469
|
+
const db = new DatabaseSync(dbPath);
|
|
3470
|
+
db.exec('PRAGMA journal_mode = WAL');
|
|
3471
|
+
db.exec(`
|
|
3472
|
+
CREATE TABLE IF NOT EXISTS method_chains (
|
|
3473
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
3474
|
+
file TEXT NOT NULL,
|
|
3475
|
+
line INTEGER NOT NULL,
|
|
3476
|
+
chain TEXT NOT NULL,
|
|
3477
|
+
first_method TEXT NOT NULL,
|
|
3478
|
+
second_method TEXT NOT NULL,
|
|
3479
|
+
has_null_guard INTEGER NOT NULL DEFAULT 0,
|
|
3480
|
+
updated_at INTEGER NOT NULL
|
|
3481
|
+
);
|
|
3482
|
+
CREATE INDEX IF NOT EXISTS idx_first_method ON method_chains (first_method);
|
|
3483
|
+
CREATE INDEX IF NOT EXISTS idx_null_guard ON method_chains (has_null_guard, first_method);
|
|
3484
|
+
`);
|
|
3485
|
+
|
|
3486
|
+
// Two-step chain: $var->firstMethod(...)->secondMethod(
|
|
3487
|
+
// Captures: receiver ($var), firstMethod, secondMethod
|
|
3488
|
+
const chainRegex = /(\$\w+)\s*->\s*(\w+)\s*\([^)]{0,60}\)\s*->\s*(\w+)\s*\(/g;
|
|
3489
|
+
const now = Date.now();
|
|
3490
|
+
|
|
3491
|
+
const phpFiles = await glob('vendor/**/*.php', { cwd: root, absolute: true, nodir: true });
|
|
3492
|
+
let scanned = 0;
|
|
3493
|
+
let chains = 0;
|
|
3494
|
+
|
|
3495
|
+
const insertStmt = db.prepare(
|
|
3496
|
+
'INSERT INTO method_chains (file, line, chain, first_method, second_method, has_null_guard, updated_at) VALUES (?,?,?,?,?,?,?)'
|
|
3497
|
+
);
|
|
3498
|
+
const deleteFile = db.prepare('DELETE FROM method_chains WHERE file = ?');
|
|
3499
|
+
|
|
3500
|
+
// Process files in batches for memory efficiency
|
|
3501
|
+
for (const phpFile of phpFiles) {
|
|
3502
|
+
let content;
|
|
3503
|
+
try { content = readFileSync(phpFile, 'utf-8'); } catch { continue; }
|
|
3504
|
+
if (!content.includes('->')) continue;
|
|
3505
|
+
|
|
3506
|
+
const relPath = phpFile.replace(root + '/', '');
|
|
3507
|
+
const lines = content.split('\n');
|
|
3508
|
+
const rows = [];
|
|
3509
|
+
|
|
3510
|
+
chainRegex.lastIndex = 0;
|
|
3511
|
+
let m;
|
|
3512
|
+
while ((m = chainRegex.exec(content)) !== null) {
|
|
3513
|
+
const lineNum = content.slice(0, m.index).split('\n').length;
|
|
3514
|
+
rows.push({
|
|
3515
|
+
file: relPath, line: lineNum,
|
|
3516
|
+
chain: `->${m[2]}()->${m[3]}()`,
|
|
3517
|
+
firstMethod: m[2], secondMethod: m[3],
|
|
3518
|
+
hasNullGuard: hasNullGuard(lines, lineNum - 1, m[1]) ? 1 : 0
|
|
3519
|
+
});
|
|
3520
|
+
chains++;
|
|
3521
|
+
}
|
|
3522
|
+
|
|
3523
|
+
if (rows.length > 0) {
|
|
3524
|
+
deleteFile.run(relPath);
|
|
3525
|
+
for (const r of rows) {
|
|
3526
|
+
insertStmt.run(r.file, r.line, r.chain, r.firstMethod, r.secondMethod, r.hasNullGuard, now);
|
|
3527
|
+
}
|
|
3528
|
+
}
|
|
3529
|
+
scanned++;
|
|
3530
|
+
}
|
|
3531
|
+
|
|
3532
|
+
db.close();
|
|
3533
|
+
return { scanned, chains };
|
|
3534
|
+
}
|
|
3535
|
+
|
|
3536
|
+
/**
|
|
3537
|
+
* Query enrichment.db for unsafe method chains (no null guard).
|
|
3538
|
+
*/
|
|
3539
|
+
async function queryNullRisks(root, firstMethod, limit = 100) {
|
|
3540
|
+
const dbPath = ENRICHMENT_DB_PATH(root);
|
|
3541
|
+
if (!existsSync(dbPath)) return null;
|
|
3542
|
+
|
|
3543
|
+
let DatabaseSync;
|
|
3544
|
+
try {
|
|
3545
|
+
({ DatabaseSync } = await import('node:sqlite'));
|
|
3546
|
+
} catch {
|
|
3547
|
+
return null;
|
|
3548
|
+
}
|
|
3549
|
+
|
|
3550
|
+
const db = new DatabaseSync(dbPath, { open: true });
|
|
3551
|
+
let rows;
|
|
3552
|
+
try {
|
|
3553
|
+
if (firstMethod) {
|
|
3554
|
+
rows = db.prepare(
|
|
3555
|
+
'SELECT file, line, chain, second_method FROM method_chains WHERE has_null_guard = 0 AND first_method = ? ORDER BY file, line LIMIT ?'
|
|
3556
|
+
).all(firstMethod, limit);
|
|
3557
|
+
} else {
|
|
3558
|
+
rows = db.prepare(
|
|
3559
|
+
'SELECT file, line, chain, first_method, second_method FROM method_chains WHERE has_null_guard = 0 ORDER BY first_method, file, line LIMIT ?'
|
|
3560
|
+
).all(limit);
|
|
3561
|
+
}
|
|
3562
|
+
} finally {
|
|
3563
|
+
db.close();
|
|
3564
|
+
}
|
|
3565
|
+
return rows;
|
|
3566
|
+
}
|
|
3567
|
+
|
|
3568
|
+
// ─── AST Search (semgrep) ───────────────────────────────────────
|
|
3569
|
+
|
|
3570
|
+
async function astSearch(pattern, searchPath, lang, maxResults) {
|
|
3571
|
+
const root = config.magentoRoot;
|
|
3572
|
+
if (!root) throw new Error('MAGENTO_ROOT not set');
|
|
3573
|
+
|
|
3574
|
+
const targetPath = searchPath ? path.join(root, searchPath) : root;
|
|
3575
|
+
const semgrepLang = lang || 'php';
|
|
3576
|
+
const limit = Math.min(maxResults || 50, 200);
|
|
3577
|
+
|
|
3578
|
+
// Create a temporary empty .semgrepignore in the target directory if none exists.
|
|
3579
|
+
// Semgrep's default ignore list includes "vendor/" which is exactly what we need to scan.
|
|
3580
|
+
// An empty .semgrepignore overrides the defaults: https://semgrep.dev/docs/ignoring-files-folders-code/
|
|
3581
|
+
const semgrepIgnorePath = path.join(targetPath, '.semgrepignore');
|
|
3582
|
+
let createdSemgrepIgnore = false;
|
|
3583
|
+
if (!existsSync(semgrepIgnorePath)) {
|
|
3584
|
+
try { writeFileSync(semgrepIgnorePath, '# Magector: scan vendor/ and all project files\n'); createdSemgrepIgnore = true; } catch { /* best effort */ }
|
|
3585
|
+
}
|
|
3586
|
+
|
|
3587
|
+
const semgrepArgs = [
|
|
3588
|
+
'--pattern', pattern,
|
|
3589
|
+
'--lang', semgrepLang,
|
|
3590
|
+
'--json',
|
|
3591
|
+
'--no-git-ignore',
|
|
3592
|
+
targetPath
|
|
3593
|
+
];
|
|
3594
|
+
|
|
3595
|
+
let rawOutput;
|
|
3596
|
+
try {
|
|
3597
|
+
rawOutput = execFileSync('semgrep', semgrepArgs, {
|
|
3598
|
+
encoding: 'utf-8',
|
|
3599
|
+
timeout: 60000,
|
|
3600
|
+
maxBuffer: 20 * 1024 * 1024,
|
|
3601
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
3602
|
+
env: { ...process.env, PATH: process.env.PATH + ':/home/swed/.local/bin' }
|
|
3603
|
+
});
|
|
3604
|
+
} catch (err) {
|
|
3605
|
+
// semgrep exits non-zero when it has findings — stdout still contains valid JSON
|
|
3606
|
+
rawOutput = err.stdout || '';
|
|
3607
|
+
if (!rawOutput) throw new Error(`semgrep failed: ${(err.stderr || err.message || '').slice(0, 500)}`);
|
|
3608
|
+
} finally {
|
|
3609
|
+
if (createdSemgrepIgnore) { try { unlinkSync(semgrepIgnorePath); } catch { /* best effort */ } }
|
|
3610
|
+
}
|
|
3611
|
+
|
|
3612
|
+
let parsed;
|
|
3613
|
+
try {
|
|
3614
|
+
parsed = JSON.parse(rawOutput);
|
|
3615
|
+
} catch {
|
|
3616
|
+
throw new Error(`Failed to parse semgrep output. First 300 chars: ${rawOutput.slice(0, 300)}`);
|
|
3617
|
+
}
|
|
3618
|
+
|
|
3619
|
+
const findings = (parsed.results || []).slice(0, limit);
|
|
3620
|
+
return findings.map(r => ({
|
|
3621
|
+
file: r.path.replace(root + '/', ''),
|
|
3622
|
+
line: r.start.line,
|
|
3623
|
+
endLine: r.end.line,
|
|
3624
|
+
snippet: (r.extra?.lines || '').trim()
|
|
3625
|
+
}));
|
|
3626
|
+
}
|
|
3627
|
+
|
|
3428
3628
|
// ─── MCP Server ─────────────────────────────────────────────────
|
|
3429
3629
|
|
|
3430
3630
|
const server = new Server(
|
|
@@ -4143,13 +4343,69 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
4143
4343
|
},
|
|
4144
4344
|
context: {
|
|
4145
4345
|
type: 'number',
|
|
4146
|
-
description: 'Lines of context around each match (default:
|
|
4147
|
-
default:
|
|
4346
|
+
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.',
|
|
4347
|
+
default: 4
|
|
4348
|
+
},
|
|
4349
|
+
filesOnly: {
|
|
4350
|
+
type: 'boolean',
|
|
4351
|
+
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.',
|
|
4352
|
+
default: false
|
|
4148
4353
|
}
|
|
4149
4354
|
},
|
|
4150
4355
|
required: ['pattern']
|
|
4151
4356
|
}
|
|
4152
4357
|
},
|
|
4358
|
+
{
|
|
4359
|
+
name: 'magento_ast_search',
|
|
4360
|
+
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.',
|
|
4361
|
+
inputSchema: {
|
|
4362
|
+
type: 'object',
|
|
4363
|
+
properties: {
|
|
4364
|
+
pattern: {
|
|
4365
|
+
type: 'string',
|
|
4366
|
+
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(...)"'
|
|
4367
|
+
},
|
|
4368
|
+
path: {
|
|
4369
|
+
type: 'string',
|
|
4370
|
+
description: 'Subdirectory to search (relative to MAGENTO_ROOT). Default: entire codebase. Example: "vendor/acme/"'
|
|
4371
|
+
},
|
|
4372
|
+
lang: {
|
|
4373
|
+
type: 'string',
|
|
4374
|
+
description: 'Language to search (default: php). Options: php, xml, js.',
|
|
4375
|
+
default: 'php'
|
|
4376
|
+
},
|
|
4377
|
+
maxResults: {
|
|
4378
|
+
type: 'number',
|
|
4379
|
+
description: 'Maximum matches to return (default: 50, max: 200)',
|
|
4380
|
+
default: 50
|
|
4381
|
+
}
|
|
4382
|
+
},
|
|
4383
|
+
required: ['pattern']
|
|
4384
|
+
}
|
|
4385
|
+
},
|
|
4386
|
+
{
|
|
4387
|
+
name: 'magento_enrich',
|
|
4388
|
+
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.',
|
|
4389
|
+
inputSchema: { type: 'object', properties: {} }
|
|
4390
|
+
},
|
|
4391
|
+
{
|
|
4392
|
+
name: 'magento_find_null_risks',
|
|
4393
|
+
description: 'Find method chains without null guards using the pre-built enrichment index. Returns all ->firstMethod()->secondMethod() calls where no null check (=== null, !== null, ?->, ??, isset, is_null) was detected in surrounding code. Requires magento_enrich to have been run first. 100× faster than grep — O(1) SQLite query vs O(n) file scan. Use firstMethod to filter (e.g., "getPayment" finds all ->getPayment()->anything() without null guard). ⚡ For multi-query workflows use magento_batch.',
|
|
4394
|
+
inputSchema: {
|
|
4395
|
+
type: 'object',
|
|
4396
|
+
properties: {
|
|
4397
|
+
firstMethod: {
|
|
4398
|
+
type: 'string',
|
|
4399
|
+
description: 'Filter by first method name. Example: "getPayment" returns all ->getPayment()->$X() without null guard. Omit to get all unsafe chains.'
|
|
4400
|
+
},
|
|
4401
|
+
limit: {
|
|
4402
|
+
type: 'number',
|
|
4403
|
+
description: 'Maximum results (default: 100, max: 500)',
|
|
4404
|
+
default: 100
|
|
4405
|
+
}
|
|
4406
|
+
}
|
|
4407
|
+
}
|
|
4408
|
+
},
|
|
4153
4409
|
{
|
|
4154
4410
|
name: 'magento_trace_api',
|
|
4155
4411
|
description: 'Trace a REST or GraphQL API endpoint from URL to implementation. Parses webapi.xml to find the service interface, resolves the DI preference to the concrete class, reads the execute/method body, and checks di.xml for constructor arguments. Returns the complete chain in one call.',
|
|
@@ -4216,7 +4472,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
4216
4472
|
// These tools have filesystem/di.xml fallbacks — work without serve process
|
|
4217
4473
|
'magento_find_class', 'magento_find_method', 'magento_find_plugin',
|
|
4218
4474
|
'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'];
|
|
4475
|
+
'magento_batch', 'magento_find_config', 'magento_find_callers', 'magento_grep', 'magento_read', 'magento_trace_api', 'magento_ast_search', 'magento_find_null_risks'];
|
|
4220
4476
|
if (warmupInProgress && !indexFreeTools.includes(name)) {
|
|
4221
4477
|
logToFile('REQ', `${name} → blocked (warmup: loading index)`);
|
|
4222
4478
|
return {
|
|
@@ -4479,10 +4735,16 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
4479
4735
|
case 'magento_index': {
|
|
4480
4736
|
const root = args.path || config.magentoRoot;
|
|
4481
4737
|
const output = rustIndex(root);
|
|
4738
|
+
// Auto-enrich after indexing: runs in background, doesn't block response
|
|
4739
|
+
enrichMethodChains(root, { verbose: true }).then(({ scanned, chains }) => {
|
|
4740
|
+
logToFile('INFO', `Auto-enrich complete: ${scanned} files, ${chains} chains`);
|
|
4741
|
+
}).catch(err => {
|
|
4742
|
+
logToFile('WARN', `Auto-enrich failed: ${err.message}`);
|
|
4743
|
+
});
|
|
4482
4744
|
return {
|
|
4483
4745
|
content: [{
|
|
4484
4746
|
type: 'text',
|
|
4485
|
-
text: `Indexing complete (Rust core).\n\n${output}`
|
|
4747
|
+
text: `Indexing complete (Rust core).\n\n${output}\n\n_Method-chain enrichment index is being built in the background. Use \`magento_enrich\` to run it manually or wait ~30-60s._`
|
|
4486
4748
|
}]
|
|
4487
4749
|
};
|
|
4488
4750
|
}
|
|
@@ -6078,10 +6340,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
6078
6340
|
const searchPath = a.path || '.';
|
|
6079
6341
|
const include = a.include || '*.php';
|
|
6080
6342
|
const maxRes = Math.min(a.maxResults || 30, 100);
|
|
6081
|
-
const batchCtx = a.context !== undefined ? a.context :
|
|
6343
|
+
const batchCtx = a.context !== undefined ? a.context : 4;
|
|
6344
|
+
const batchFilesOnly = a.filesOnly || false;
|
|
6082
6345
|
const gArgs = ['-rn', '-E'];
|
|
6346
|
+
if (batchFilesOnly) { gArgs[0] = '-rl'; gArgs.splice(1, 1); } // -rl = recursive + files-only, drop -n
|
|
6083
6347
|
if (a.ignoreCase) gArgs.push('-i');
|
|
6084
|
-
if (batchCtx > 0) gArgs.push('-C', String(batchCtx));
|
|
6348
|
+
if (!batchFilesOnly && batchCtx > 0) gArgs.push('-C', String(batchCtx));
|
|
6085
6349
|
for (const pat of include.split(',').map(p => p.trim())) gArgs.push('--include=' + pat);
|
|
6086
6350
|
gArgs.push('--', a.pattern, searchPath);
|
|
6087
6351
|
let out;
|
|
@@ -6089,8 +6353,36 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
6089
6353
|
out = execFileSync('grep', gArgs, { cwd: config.magentoRoot, encoding: 'utf-8', timeout: 15000, maxBuffer: 5 * 1024 * 1024, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
6090
6354
|
} catch (err) { out = err.stdout || ''; }
|
|
6091
6355
|
const gLines = out.trim().split('\n').filter(Boolean);
|
|
6092
|
-
|
|
6093
|
-
|
|
6356
|
+
if (batchFilesOnly) {
|
|
6357
|
+
text = `Files matching \`${a.pattern}\` (${gLines.length}):\n`;
|
|
6358
|
+
for (const gl of gLines.slice(0, maxRes)) text += gl + '\n';
|
|
6359
|
+
} else {
|
|
6360
|
+
text = `Found ${gLines.length} matches${gLines.length > maxRes ? ` (showing ${maxRes})` : ''}:\n`;
|
|
6361
|
+
for (const gl of gLines.slice(0, maxRes)) text += gl + '\n';
|
|
6362
|
+
}
|
|
6363
|
+
break;
|
|
6364
|
+
}
|
|
6365
|
+
case 'magento_ast_search': {
|
|
6366
|
+
const astResults = await astSearch(a.pattern, a.path, a.lang, a.maxResults);
|
|
6367
|
+
if (astResults.length === 0) {
|
|
6368
|
+
text = `No matches for pattern: \`${a.pattern}\``;
|
|
6369
|
+
} else {
|
|
6370
|
+
text = `Found ${astResults.length} match(es) for \`${a.pattern}\`:\n\n`;
|
|
6371
|
+
for (const r of astResults) text += `${r.file}:${r.line}: ${r.snippet}\n`;
|
|
6372
|
+
}
|
|
6373
|
+
break;
|
|
6374
|
+
}
|
|
6375
|
+
case 'magento_find_null_risks': {
|
|
6376
|
+
const bRoot = config.magentoRoot;
|
|
6377
|
+
const bLimit = Math.min(a.limit || 100, 500);
|
|
6378
|
+
const bRows = bRoot ? await queryNullRisks(bRoot, a.firstMethod || null, bLimit) : null;
|
|
6379
|
+
if (!bRows) { text = '⚠️ Run magento_enrich first.'; break; }
|
|
6380
|
+
if (bRows.length === 0) { text = 'No unsafe chains found.'; break; }
|
|
6381
|
+
text = `Found ${bRows.length} unsafe chain(s):\n`;
|
|
6382
|
+
for (const r of bRows.slice(0, 50)) {
|
|
6383
|
+
const chain = r.chain || `->${r.first_method}()->${r.second_method}()`;
|
|
6384
|
+
text += `${r.file}:${r.line}: ${chain}\n`;
|
|
6385
|
+
}
|
|
6094
6386
|
break;
|
|
6095
6387
|
}
|
|
6096
6388
|
default:
|
|
@@ -6115,10 +6407,11 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
6115
6407
|
const searchPath = args.path || '.';
|
|
6116
6408
|
const include = args.include || '*.php';
|
|
6117
6409
|
const maxResults = Math.min(args.maxResults || 50, 200);
|
|
6118
|
-
const ctxLines = args.context !== undefined ? args.context :
|
|
6119
|
-
const
|
|
6410
|
+
const ctxLines = args.context !== undefined ? args.context : 4;
|
|
6411
|
+
const filesOnly = args.filesOnly || false;
|
|
6412
|
+
const grepArgs = filesOnly ? ['-rl', '-E'] : ['-rn', '-E'];
|
|
6120
6413
|
if (args.ignoreCase) grepArgs.push('-i');
|
|
6121
|
-
if (ctxLines > 0) grepArgs.push('-C', String(ctxLines));
|
|
6414
|
+
if (!filesOnly && ctxLines > 0) grepArgs.push('-C', String(ctxLines));
|
|
6122
6415
|
// Support multiple include patterns (e.g., "*.{php,xml}")
|
|
6123
6416
|
for (const pat of include.split(',').map(p => p.trim())) {
|
|
6124
6417
|
grepArgs.push('--include=' + pat);
|
|
@@ -6138,8 +6431,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
6138
6431
|
const lines = output.trim().split('\n').filter(Boolean);
|
|
6139
6432
|
const total = lines.length;
|
|
6140
6433
|
const truncated = lines.slice(0, maxResults);
|
|
6141
|
-
let text =
|
|
6142
|
-
|
|
6434
|
+
let text = filesOnly
|
|
6435
|
+
? `## 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`
|
|
6436
|
+
: `## grep: \`${args.pattern}\`\nFound **${total}** matches${total > maxResults ? ` (showing first ${maxResults})` : ''}\n\n`;
|
|
6143
6437
|
for (const line of truncated) {
|
|
6144
6438
|
text += line + '\n';
|
|
6145
6439
|
}
|
|
@@ -6324,10 +6618,75 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
6324
6618
|
const numbered = sliced.map((line, i) => `${start + i + 1}\t${line}`).join('\n');
|
|
6325
6619
|
let text = `## ${args.path}`;
|
|
6326
6620
|
if (args.startLine || args.endLine) text += ` (lines ${start + 1}-${end})`;
|
|
6621
|
+
// Hint: large file read without method extraction wastes tokens
|
|
6622
|
+
if (!args.startLine && !args.endLine && allLines.length > 100) {
|
|
6623
|
+
const methodMatches = content.match(/(?:public|protected|private)\s+(?:static\s+)?function\s+(\w+)/g) || [];
|
|
6624
|
+
const methodNames = methodMatches.slice(0, 8).map(m => m.replace(/.*function\s+/, '')).join(', ');
|
|
6625
|
+
text += `\n> 💡 **${allLines.length} lines** — add \`methodName\` to extract a single method (~10× fewer tokens). Methods: ${methodNames}${methodMatches.length > 8 ? ', ...' : ''}`;
|
|
6626
|
+
}
|
|
6327
6627
|
text += `\n\n${numbered}`;
|
|
6328
6628
|
return { content: [{ type: 'text', text }] };
|
|
6329
6629
|
}
|
|
6330
6630
|
|
|
6631
|
+
case 'magento_ast_search': {
|
|
6632
|
+
const astResults = await astSearch(args.pattern, args.path, args.lang, args.maxResults);
|
|
6633
|
+
if (astResults.length === 0) {
|
|
6634
|
+
return { content: [{ type: 'text', text: `## magento_ast_search: \`${args.pattern}\`\n\nNo matches found.` }] };
|
|
6635
|
+
}
|
|
6636
|
+
let text = `## magento_ast_search: \`${args.pattern}\`\n`;
|
|
6637
|
+
text += `Found **${astResults.length}** match(es)\n\n`;
|
|
6638
|
+
for (const r of astResults) {
|
|
6639
|
+
const lineInfo = r.endLine && r.endLine !== r.line ? `${r.line}-${r.endLine}` : String(r.line);
|
|
6640
|
+
text += `**${r.file}:${lineInfo}**\n\`\`\`php\n${r.snippet}\n\`\`\`\n\n`;
|
|
6641
|
+
}
|
|
6642
|
+
return { content: [{ type: 'text', text }] };
|
|
6643
|
+
}
|
|
6644
|
+
|
|
6645
|
+
case 'magento_enrich': {
|
|
6646
|
+
const root = config.magentoRoot;
|
|
6647
|
+
if (!root) return { content: [{ type: 'text', text: 'MAGENTO_ROOT not set.' }], isError: true };
|
|
6648
|
+
let text = `## magento_enrich\n\nScanning vendor/ PHP files for method chains...\n`;
|
|
6649
|
+
try {
|
|
6650
|
+
const { scanned, chains } = await enrichMethodChains(root, { verbose: true });
|
|
6651
|
+
text += `\n✅ **Done**\n- Files scanned: ${scanned}\n- Method chains indexed: ${chains}\n- Null-risk index saved to: \`.magector/enrichment.db\`\n\nUse \`magento_find_null_risks\` to query unsafe chains.`;
|
|
6652
|
+
} catch (err) {
|
|
6653
|
+
text += `\n❌ Error: ${err.message}`;
|
|
6654
|
+
}
|
|
6655
|
+
return { content: [{ type: 'text', text }] };
|
|
6656
|
+
}
|
|
6657
|
+
|
|
6658
|
+
case 'magento_find_null_risks': {
|
|
6659
|
+
const root = config.magentoRoot;
|
|
6660
|
+
if (!root) return { content: [{ type: 'text', text: 'MAGENTO_ROOT not set.' }], isError: true };
|
|
6661
|
+
const limit = Math.min(args.limit || 100, 500);
|
|
6662
|
+
const rows = await queryNullRisks(root, args.firstMethod || null, limit);
|
|
6663
|
+
if (rows === null) {
|
|
6664
|
+
return { content: [{ type: 'text', text: `## magento_find_null_risks\n\n⚠️ Enrichment index not found. Run \`magento_enrich\` first to build the method-chain index.` }] };
|
|
6665
|
+
}
|
|
6666
|
+
if (rows.length === 0) {
|
|
6667
|
+
const filter = args.firstMethod ? ` for \`->${args.firstMethod}()\`` : '';
|
|
6668
|
+
return { content: [{ type: 'text', text: `## magento_find_null_risks${filter}\n\nNo unsafe chains found. All detected chains have null guards.` }] };
|
|
6669
|
+
}
|
|
6670
|
+
const filter = args.firstMethod ? ` for \`->${args.firstMethod}()\`` : '';
|
|
6671
|
+
let text = `## magento_find_null_risks${filter}\n\nFound **${rows.length}** chain(s) without null guard:\n\n`;
|
|
6672
|
+
// Group by chain type for readability
|
|
6673
|
+
const byChain = {};
|
|
6674
|
+
for (const r of rows) {
|
|
6675
|
+
const key = r.chain || `->${r.first_method}()->${r.second_method}()`;
|
|
6676
|
+
if (!byChain[key]) byChain[key] = [];
|
|
6677
|
+
byChain[key].push(r);
|
|
6678
|
+
}
|
|
6679
|
+
for (const [chain, sites] of Object.entries(byChain)) {
|
|
6680
|
+
text += `### \`${chain}\` (${sites.length} site${sites.length > 1 ? 's' : ''})\n`;
|
|
6681
|
+
for (const s of sites.slice(0, 20)) {
|
|
6682
|
+
text += `- \`${s.file}:${s.line}\`\n`;
|
|
6683
|
+
}
|
|
6684
|
+
if (sites.length > 20) text += `- ... and ${sites.length - 20} more\n`;
|
|
6685
|
+
text += '\n';
|
|
6686
|
+
}
|
|
6687
|
+
return { content: [{ type: 'text', text }] };
|
|
6688
|
+
}
|
|
6689
|
+
|
|
6331
6690
|
default:
|
|
6332
6691
|
return {
|
|
6333
6692
|
content: [{
|