magector 1.3.5 → 1.4.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 +23 -4
- package/package.json +5 -5
- package/src/init.js +1 -1
- package/src/mcp-server.js +321 -0
package/README.md
CHANGED
|
@@ -67,7 +67,7 @@ Without Magector, asking Claude Code or Cursor *"how are checkout totals calcula
|
|
|
67
67
|
- **Diff analysis** -- risk scoring and change classification for git commits and staged changes
|
|
68
68
|
- **Complexity analysis** -- cyclomatic complexity, function count, and hotspot detection across modules
|
|
69
69
|
- **Fast** -- 10-45ms queries via persistent serve process, batched ONNX embedding with adaptive thread scaling
|
|
70
|
-
- **MCP server** --
|
|
70
|
+
- **MCP server** -- 20 tools integrating with Claude Code, Cursor, and any MCP-compatible AI tool
|
|
71
71
|
- **Clean architecture** -- Rust core handles all indexing/search, Node.js MCP server delegates to it
|
|
72
72
|
|
|
73
73
|
---
|
|
@@ -84,7 +84,7 @@ flowchart TD
|
|
|
84
84
|
A --> B --> C --> D
|
|
85
85
|
end
|
|
86
86
|
subgraph node ["Node.js Layer"]
|
|
87
|
-
E["MCP Server ·
|
|
87
|
+
E["MCP Server · 20 tools"]
|
|
88
88
|
F["Persistent Serve"]
|
|
89
89
|
G["CLI · init/index/search"]
|
|
90
90
|
E --> F
|
|
@@ -279,7 +279,7 @@ npx magector help # Show help
|
|
|
279
279
|
|
|
280
280
|
## MCP Server Tools
|
|
281
281
|
|
|
282
|
-
The MCP server exposes
|
|
282
|
+
The MCP server exposes 20 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.
|
|
283
283
|
|
|
284
284
|
### Output Format
|
|
285
285
|
|
|
@@ -335,6 +335,14 @@ All search tools return structured JSON:
|
|
|
335
335
|
| `magento_find_cron` | Find cron job definitions in crontab.xml |
|
|
336
336
|
| `magento_find_db_schema` | Find database table definitions in db_schema.xml (declarative schema) |
|
|
337
337
|
|
|
338
|
+
### Flow Tracing
|
|
339
|
+
|
|
340
|
+
| Tool | Description |
|
|
341
|
+
|------|-------------|
|
|
342
|
+
| `magento_trace_flow` | Trace execution flow from an entry point (route, API, GraphQL, event, cron) -- maps controller → plugins → observers → templates in one call |
|
|
343
|
+
|
|
344
|
+
Auto-detects entry type from pattern (`/V1/...` → API, `snake_case` → event, `camelCase` → GraphQL, `path/segments` → route), or override with `entryType`. Use `depth: "shallow"` (entry + config + plugins) or `depth: "deep"` (adds observers, layout, templates, DI preferences).
|
|
345
|
+
|
|
338
346
|
### Analysis Tools
|
|
339
347
|
|
|
340
348
|
| Tool | Description |
|
|
@@ -371,6 +379,12 @@ graph TD
|
|
|
371
379
|
gql["find_graphql"] --> cls
|
|
372
380
|
gql --> mtd
|
|
373
381
|
ctl["find_controller"] --> cfg
|
|
382
|
+
trc["trace_flow"] -.-> ctl
|
|
383
|
+
trc -.-> plg
|
|
384
|
+
trc -.-> obs
|
|
385
|
+
trc -.-> tpl
|
|
386
|
+
trc -.-> api
|
|
387
|
+
trc -.-> gql
|
|
374
388
|
|
|
375
389
|
style cls fill:#4a90d9,color:#fff
|
|
376
390
|
style mtd fill:#4a90d9,color:#fff
|
|
@@ -384,6 +398,7 @@ graph TD
|
|
|
384
398
|
style dbs fill:#9b59b6,color:#fff
|
|
385
399
|
style gql fill:#9b59b6,color:#fff
|
|
386
400
|
style ctl fill:#4a90d9,color:#fff
|
|
401
|
+
style trc fill:#2ecc71,color:#000
|
|
387
402
|
```
|
|
388
403
|
|
|
389
404
|
### Query Examples
|
|
@@ -406,6 +421,10 @@ magento_find_block("cart totals")
|
|
|
406
421
|
magento_find_template("minicart")
|
|
407
422
|
magento_analyze_diff({ commitHash: "abc123" })
|
|
408
423
|
magento_complexity({ module: "Magento_Catalog", threshold: 10 })
|
|
424
|
+
magento_trace_flow({ entryPoint: "checkout/cart/add", depth: "deep" })
|
|
425
|
+
magento_trace_flow({ entryPoint: "/V1/products" })
|
|
426
|
+
magento_trace_flow({ entryPoint: "placeOrder", entryType: "graphql" })
|
|
427
|
+
magento_trace_flow({ entryPoint: "sales_order_place_after" })
|
|
409
428
|
```
|
|
410
429
|
|
|
411
430
|
---
|
|
@@ -502,7 +521,7 @@ cd rust-core && cargo run --release -- validate -m ./magento2 --skip-index
|
|
|
502
521
|
magector/
|
|
503
522
|
├── src/ # Node.js source
|
|
504
523
|
│ ├── cli.js # CLI entry point (npx magector <command>)
|
|
505
|
-
│ ├── mcp-server.js # MCP server (
|
|
524
|
+
│ ├── mcp-server.js # MCP server (20 tools, structured JSON output)
|
|
506
525
|
│ ├── binary.js # Platform binary resolver
|
|
507
526
|
│ ├── model.js # ONNX model resolver/downloader
|
|
508
527
|
│ ├── init.js # Full init command (index + IDE config)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "magector",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.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": "1.
|
|
37
|
-
"@magector/cli-linux-x64": "1.
|
|
38
|
-
"@magector/cli-linux-arm64": "1.
|
|
39
|
-
"@magector/cli-win32-x64": "1.
|
|
36
|
+
"@magector/cli-darwin-arm64": "1.4.1",
|
|
37
|
+
"@magector/cli-linux-x64": "1.4.1",
|
|
38
|
+
"@magector/cli-linux-arm64": "1.4.1",
|
|
39
|
+
"@magector/cli-win32-x64": "1.4.1"
|
|
40
40
|
},
|
|
41
41
|
"keywords": [
|
|
42
42
|
"magento",
|
package/src/init.js
CHANGED
|
@@ -54,7 +54,7 @@ function detectIDEs(projectPath) {
|
|
|
54
54
|
function writeMcpConfig(projectPath, ides, dbPath) {
|
|
55
55
|
const mcpEntry = {
|
|
56
56
|
command: 'npx',
|
|
57
|
-
args: ['-y', 'magector', 'mcp'],
|
|
57
|
+
args: ['-y', 'magector@latest', 'mcp'],
|
|
58
58
|
env: {
|
|
59
59
|
MAGENTO_ROOT: projectPath,
|
|
60
60
|
MAGECTOR_DB: dbPath
|
package/src/mcp-server.js
CHANGED
|
@@ -302,6 +302,285 @@ function rerank(results, boosts = {}, weight = 0.3) {
|
|
|
302
302
|
}).sort((a, b) => b.score - a.score);
|
|
303
303
|
}
|
|
304
304
|
|
|
305
|
+
// ─── Trace Flow helpers ─────────────────────────────────────────
|
|
306
|
+
|
|
307
|
+
function detectEntryType(entryPoint) {
|
|
308
|
+
if (/^\/?V\d/.test(entryPoint)) return 'api';
|
|
309
|
+
if (!entryPoint.includes('/') && /^[a-z][a-z0-9]*(_[a-z0-9]+)+$/.test(entryPoint)) return 'event';
|
|
310
|
+
if (!entryPoint.includes('/') && /^[a-z]/.test(entryPoint) && /[A-Z]/.test(entryPoint)) return 'graphql';
|
|
311
|
+
if (entryPoint.includes('/')) return 'route';
|
|
312
|
+
return 'route';
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/** Wrapper that never throws — returns [] on failure so trace steps are independent. */
|
|
316
|
+
async function safeSearch(query, limit = 10) {
|
|
317
|
+
try {
|
|
318
|
+
return await rustSearchAsync(query, limit);
|
|
319
|
+
} catch {
|
|
320
|
+
return [];
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
async function traceRoute(entryPoint, depth) {
|
|
325
|
+
const parts = entryPoint.replace(/^\//, '').split('/');
|
|
326
|
+
const nameParts = parts.map(p => p.charAt(0).toUpperCase() + p.slice(1));
|
|
327
|
+
const trace = {};
|
|
328
|
+
|
|
329
|
+
// Controller + route config (independent, run in parallel)
|
|
330
|
+
const [controllerRaw, routeRaw] = await Promise.all([
|
|
331
|
+
safeSearch(`${nameParts.join(' ')} controller execute action`, 30),
|
|
332
|
+
safeSearch(`routes.xml ${parts[0]}`, 20)
|
|
333
|
+
]);
|
|
334
|
+
const controllers = controllerRaw.map(normalizeResult).filter(r => r.path?.includes('/Controller/'));
|
|
335
|
+
// Boost results matching route path segments
|
|
336
|
+
const ranked = controllers.map(r => {
|
|
337
|
+
const bonus = nameParts.filter(p => r.path?.includes(p)).length * 0.3;
|
|
338
|
+
return { ...r, score: (r.score || 0) + bonus };
|
|
339
|
+
}).sort((a, b) => b.score - a.score);
|
|
340
|
+
const best = ranked[0];
|
|
341
|
+
if (best) {
|
|
342
|
+
trace.controller = { path: best.path, className: best.className || null, methods: best.methods || [] };
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const routeConfigs = routeRaw.map(normalizeResult).filter(r => r.path?.includes('routes.xml'));
|
|
346
|
+
if (routeConfigs.length > 0) {
|
|
347
|
+
trace.routeConfig = routeConfigs.slice(0, 5).map(r => ({ path: r.path, snippet: (r.searchText || '').slice(0, 200) }));
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Plugins on controller
|
|
351
|
+
if (best?.className) {
|
|
352
|
+
const pluginRaw = await safeSearch(`plugin interceptor ${best.className}`, 20);
|
|
353
|
+
const plugins = pluginRaw.map(normalizeResult).filter(r => r.isPlugin || r.path?.includes('/Plugin/') || r.path?.includes('di.xml'));
|
|
354
|
+
if (plugins.length > 0) {
|
|
355
|
+
trace.plugins = plugins.slice(0, 10).map(r => ({ path: r.path, className: r.className || null, methods: r.methods || [] }));
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
if (depth === 'deep') {
|
|
360
|
+
const moduleName = best?.module || nameParts[0];
|
|
361
|
+
const eventName = parts.join('_');
|
|
362
|
+
const layoutHandle = parts.join('_');
|
|
363
|
+
|
|
364
|
+
// All deep searches are independent — run in parallel
|
|
365
|
+
const [diRaw, obsRaw, layoutRaw, tplRaw] = await Promise.all([
|
|
366
|
+
safeSearch(`di.xml preference ${moduleName}`, 20),
|
|
367
|
+
safeSearch(`event ${eventName} observer`, 20),
|
|
368
|
+
safeSearch(`layout ${layoutHandle}`, 20),
|
|
369
|
+
safeSearch(`${nameParts.join(' ')} template phtml`, 20)
|
|
370
|
+
]);
|
|
371
|
+
|
|
372
|
+
const prefs = diRaw.map(normalizeResult).filter(r => r.path?.includes('di.xml'));
|
|
373
|
+
if (prefs.length > 0) {
|
|
374
|
+
trace.preferences = prefs.slice(0, 10).map(r => ({ path: r.path, snippet: (r.searchText || '').slice(0, 200) }));
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const observers = obsRaw.map(normalizeResult).filter(r => r.isObserver || r.path?.includes('/Observer/') || r.path?.includes('events.xml'));
|
|
378
|
+
if (observers.length > 0) {
|
|
379
|
+
trace.observers = observers.slice(0, 10).map(r => ({ eventName: r.searchText?.match(/event\s+name="([^"]+)"/)?.[1] || eventName, path: r.path, className: r.className || null }));
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
const layouts = layoutRaw.map(normalizeResult).filter(r => r.path?.includes('/layout/'));
|
|
383
|
+
if (layouts.length > 0) {
|
|
384
|
+
trace.layout = layouts.slice(0, 10).map(r => ({ path: r.path }));
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const templates = tplRaw.map(normalizeResult).filter(r => r.path?.includes('.phtml'));
|
|
388
|
+
if (templates.length > 0) {
|
|
389
|
+
trace.templates = templates.slice(0, 10).map(r => ({ path: r.path }));
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
return trace;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
async function traceApi(entryPoint, depth) {
|
|
397
|
+
const trace = {};
|
|
398
|
+
|
|
399
|
+
// webapi.xml
|
|
400
|
+
const webapiRaw = await safeSearch(`webapi route ${entryPoint}`, 20);
|
|
401
|
+
const webapis = webapiRaw.map(normalizeResult).filter(r => r.path?.includes('webapi.xml'));
|
|
402
|
+
if (webapis.length > 0) {
|
|
403
|
+
trace.webapiConfig = webapis.slice(0, 5).map(r => ({ path: r.path, snippet: (r.searchText || '').slice(0, 300) }));
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// Service class — extract from webapi searchText
|
|
407
|
+
let serviceClassName = null;
|
|
408
|
+
for (const w of webapis) {
|
|
409
|
+
const match = (w.searchText || '').match(/service\s+class="([^"]+)"/);
|
|
410
|
+
if (match) { serviceClassName = match[1]; break; }
|
|
411
|
+
}
|
|
412
|
+
let serviceShortName = null;
|
|
413
|
+
if (serviceClassName) {
|
|
414
|
+
serviceShortName = serviceClassName.split('\\').pop();
|
|
415
|
+
const svcRaw = await safeSearch(serviceShortName, 10);
|
|
416
|
+
const svcs = svcRaw.map(normalizeResult).filter(r => r.className?.includes(serviceShortName));
|
|
417
|
+
if (svcs.length > 0) {
|
|
418
|
+
trace.serviceClass = { path: svcs[0].path, className: svcs[0].className || serviceClassName, methods: svcs[0].methods || [] };
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
if (depth === 'deep') {
|
|
423
|
+
const resource = entryPoint.replace(/^\/?V\d+\//, '').split('/')[0];
|
|
424
|
+
|
|
425
|
+
// Plugins + observers are independent — run in parallel
|
|
426
|
+
const searches = [safeSearch(`event ${resource} observer`, 20)];
|
|
427
|
+
if (serviceClassName) {
|
|
428
|
+
searches.push(safeSearch(`plugin interceptor ${serviceClassName}`, 20));
|
|
429
|
+
}
|
|
430
|
+
const [obsRaw, pluginRaw] = await Promise.all(searches);
|
|
431
|
+
|
|
432
|
+
if (pluginRaw) {
|
|
433
|
+
const plugins = pluginRaw.map(normalizeResult).filter(r => r.isPlugin || r.path?.includes('/Plugin/') || r.path?.includes('di.xml'));
|
|
434
|
+
if (plugins.length > 0) {
|
|
435
|
+
trace.plugins = plugins.slice(0, 10).map(r => ({ path: r.path, className: r.className || null, methods: r.methods || [] }));
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
const observers = obsRaw.map(normalizeResult).filter(r => r.isObserver || r.path?.includes('/Observer/') || r.path?.includes('events.xml'));
|
|
440
|
+
if (observers.length > 0) {
|
|
441
|
+
trace.observers = observers.slice(0, 10).map(r => ({ eventName: resource, path: r.path, className: r.className || null }));
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
return trace;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
async function traceGraphql(entryPoint, depth) {
|
|
449
|
+
const trace = {};
|
|
450
|
+
|
|
451
|
+
// Schema + resolver (independent, run in parallel)
|
|
452
|
+
const [schemaRaw, resolverRaw] = await Promise.all([
|
|
453
|
+
safeSearch(`graphql ${entryPoint} mutation query`, 20),
|
|
454
|
+
safeSearch(`${entryPoint} resolver`, 20)
|
|
455
|
+
]);
|
|
456
|
+
const schemas = schemaRaw.map(normalizeResult).filter(r => r.path?.includes('.graphqls') || r.type === 'graphql');
|
|
457
|
+
if (schemas.length > 0) {
|
|
458
|
+
trace.schema = schemas.slice(0, 5).map(r => ({ path: r.path, snippet: (r.searchText || '').slice(0, 300) }));
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
const resolvers = resolverRaw.map(normalizeResult).filter(r => r.isResolver || r.path?.includes('/Resolver/'));
|
|
462
|
+
if (resolvers.length > 0) {
|
|
463
|
+
trace.resolver = { path: resolvers[0].path, className: resolvers[0].className || null, methods: resolvers[0].methods || [] };
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
if (depth === 'deep' && resolvers[0]?.className) {
|
|
467
|
+
const pluginRaw = await safeSearch(`plugin interceptor ${resolvers[0].className}`, 20);
|
|
468
|
+
const plugins = pluginRaw.map(normalizeResult).filter(r => r.isPlugin || r.path?.includes('/Plugin/') || r.path?.includes('di.xml'));
|
|
469
|
+
if (plugins.length > 0) {
|
|
470
|
+
trace.plugins = plugins.slice(0, 10).map(r => ({ path: r.path, className: r.className || null, methods: r.methods || [] }));
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
return trace;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
async function traceEvent(entryPoint, depth) {
|
|
478
|
+
const trace = {};
|
|
479
|
+
|
|
480
|
+
// Observers
|
|
481
|
+
const obsRaw = await safeSearch(`event ${entryPoint} observer`, 30);
|
|
482
|
+
const observers = obsRaw.map(normalizeResult).filter(r => r.isObserver || r.path?.includes('/Observer/') || r.path?.includes('events.xml'));
|
|
483
|
+
if (observers.length > 0) {
|
|
484
|
+
trace.observers = observers.slice(0, 15).map(r => ({ eventName: entryPoint, path: r.path, className: r.className || null }));
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
if (depth === 'deep') {
|
|
488
|
+
// Origin — infer source model from event prefix
|
|
489
|
+
const prefix = entryPoint.split('_').slice(0, 2).join('_');
|
|
490
|
+
const originParts = prefix.split('_').map(p => p.charAt(0).toUpperCase() + p.slice(1));
|
|
491
|
+
const originRaw = await safeSearch(`${originParts.join(' ')} model`, 10);
|
|
492
|
+
const origins = originRaw.map(normalizeResult).filter(r => r.isModel || r.path?.includes('/Model/'));
|
|
493
|
+
if (origins.length > 0) {
|
|
494
|
+
trace.origin = { path: origins[0].path, className: origins[0].className || null, methods: origins[0].methods || [] };
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
return trace;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
async function traceCron(entryPoint, depth) {
|
|
502
|
+
const trace = {};
|
|
503
|
+
|
|
504
|
+
// crontab.xml + handler class (independent, run in parallel)
|
|
505
|
+
const handlerParts = entryPoint.split('_').map(p => p.charAt(0).toUpperCase() + p.slice(1));
|
|
506
|
+
const [cronRaw, handlerRaw] = await Promise.all([
|
|
507
|
+
safeSearch(`cron job ${entryPoint}`, 20),
|
|
508
|
+
safeSearch(`${handlerParts.join(' ')} cron`, 20)
|
|
509
|
+
]);
|
|
510
|
+
const cronConfigs = cronRaw.map(normalizeResult).filter(r => r.path?.includes('crontab.xml'));
|
|
511
|
+
if (cronConfigs.length > 0) {
|
|
512
|
+
trace.cronConfig = cronConfigs.slice(0, 5).map(r => ({ path: r.path, snippet: (r.searchText || '').slice(0, 200) }));
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
const handlers = handlerRaw.map(normalizeResult).filter(r => r.path?.includes('/Cron/'));
|
|
516
|
+
if (handlers.length > 0) {
|
|
517
|
+
trace.handler = { path: handlers[0].path, className: handlers[0].className || null, methods: handlers[0].methods || [] };
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
if (depth === 'deep' && handlers[0]?.className) {
|
|
521
|
+
const pluginRaw = await safeSearch(`plugin interceptor ${handlers[0].className}`, 20);
|
|
522
|
+
const plugins = pluginRaw.map(normalizeResult).filter(r => r.isPlugin || r.path?.includes('/Plugin/') || r.path?.includes('di.xml'));
|
|
523
|
+
if (plugins.length > 0) {
|
|
524
|
+
trace.plugins = plugins.slice(0, 10).map(r => ({ path: r.path, className: r.className || null, methods: r.methods || [] }));
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
return trace;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
async function traceFlow(entryPoint, entryType, depth) {
|
|
532
|
+
const type = entryType === 'auto' ? detectEntryType(entryPoint) : entryType;
|
|
533
|
+
|
|
534
|
+
let trace;
|
|
535
|
+
switch (type) {
|
|
536
|
+
case 'route': trace = await traceRoute(entryPoint, depth); break;
|
|
537
|
+
case 'api': trace = await traceApi(entryPoint, depth); break;
|
|
538
|
+
case 'graphql': trace = await traceGraphql(entryPoint, depth); break;
|
|
539
|
+
case 'event': trace = await traceEvent(entryPoint, depth); break;
|
|
540
|
+
case 'cron': trace = await traceCron(entryPoint, depth); break;
|
|
541
|
+
default: trace = await traceRoute(entryPoint, depth); break;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
return { entryPoint, entryType: type, trace };
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
function buildTraceSummary(result) {
|
|
548
|
+
const { entryPoint, entryType, trace } = result;
|
|
549
|
+
const parts = [];
|
|
550
|
+
|
|
551
|
+
switch (entryType) {
|
|
552
|
+
case 'route':
|
|
553
|
+
parts.push(`Route ${entryPoint}`);
|
|
554
|
+
if (trace.controller) parts.push(trace.controller.className ? `${trace.controller.className}::execute()` : trace.controller.path);
|
|
555
|
+
break;
|
|
556
|
+
case 'api':
|
|
557
|
+
parts.push(`API ${entryPoint}`);
|
|
558
|
+
if (trace.serviceClass) parts.push(trace.serviceClass.className || trace.serviceClass.path);
|
|
559
|
+
break;
|
|
560
|
+
case 'graphql':
|
|
561
|
+
parts.push(`GraphQL ${entryPoint}`);
|
|
562
|
+
if (trace.resolver) parts.push(trace.resolver.className || trace.resolver.path);
|
|
563
|
+
break;
|
|
564
|
+
case 'event':
|
|
565
|
+
parts.push(`Event ${entryPoint}`);
|
|
566
|
+
break;
|
|
567
|
+
case 'cron':
|
|
568
|
+
parts.push(`Cron ${entryPoint}`);
|
|
569
|
+
if (trace.handler) parts.push(trace.handler.className || trace.handler.path);
|
|
570
|
+
break;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
const counts = [];
|
|
574
|
+
if (trace.plugins?.length) counts.push(`${trace.plugins.length} plugin${trace.plugins.length > 1 ? 's' : ''}`);
|
|
575
|
+
if (trace.observers?.length) counts.push(`${trace.observers.length} observer${trace.observers.length > 1 ? 's' : ''}`);
|
|
576
|
+
if (trace.templates?.length) counts.push(`${trace.templates.length} template${trace.templates.length > 1 ? 's' : ''}`);
|
|
577
|
+
if (trace.layout?.length) counts.push(`${trace.layout.length} layout${trace.layout.length > 1 ? 's' : ''}`);
|
|
578
|
+
if (trace.preferences?.length) counts.push(`${trace.preferences.length} preference${trace.preferences.length > 1 ? 's' : ''}`);
|
|
579
|
+
if (counts.length > 0) parts.push(counts.join(', '));
|
|
580
|
+
|
|
581
|
+
return parts.join(' → ');
|
|
582
|
+
}
|
|
583
|
+
|
|
305
584
|
function formatSearchResults(results) {
|
|
306
585
|
if (!results || results.length === 0) {
|
|
307
586
|
return JSON.stringify({ results: [], count: 0 });
|
|
@@ -674,6 +953,32 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
674
953
|
}
|
|
675
954
|
}
|
|
676
955
|
}
|
|
956
|
+
},
|
|
957
|
+
{
|
|
958
|
+
name: 'magento_trace_flow',
|
|
959
|
+
description: 'Trace Magento execution flow from an entry point (route, API endpoint, GraphQL mutation, event, or cron job). Chains multiple searches to map controller → plugins → observers → templates for a given request path. Use this to understand how a request is processed end-to-end.',
|
|
960
|
+
inputSchema: {
|
|
961
|
+
type: 'object',
|
|
962
|
+
properties: {
|
|
963
|
+
entryPoint: {
|
|
964
|
+
type: 'string',
|
|
965
|
+
description: 'The entry point to trace. Examples: "checkout/cart/add" (route), "/V1/products" (API), "placeOrder" (GraphQL), "sales_order_place_after" (event), "catalog_product_reindex" (cron)'
|
|
966
|
+
},
|
|
967
|
+
entryType: {
|
|
968
|
+
type: 'string',
|
|
969
|
+
enum: ['auto', 'route', 'api', 'graphql', 'event', 'cron'],
|
|
970
|
+
description: 'Type of entry point. "auto" detects from the pattern (default). Override when auto-detection is wrong.',
|
|
971
|
+
default: 'auto'
|
|
972
|
+
},
|
|
973
|
+
depth: {
|
|
974
|
+
type: 'string',
|
|
975
|
+
enum: ['shallow', 'deep'],
|
|
976
|
+
description: 'Trace depth. "shallow" traces entry point + config + direct plugins (faster). "deep" adds observers, layout, templates, and DI preferences (more complete). Default: shallow.',
|
|
977
|
+
default: 'shallow'
|
|
978
|
+
}
|
|
979
|
+
},
|
|
980
|
+
required: ['entryPoint']
|
|
981
|
+
}
|
|
677
982
|
}
|
|
678
983
|
]
|
|
679
984
|
}));
|
|
@@ -1134,6 +1439,22 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1134
1439
|
return { content: [{ type: 'text', text }] };
|
|
1135
1440
|
}
|
|
1136
1441
|
|
|
1442
|
+
case 'magento_trace_flow': {
|
|
1443
|
+
const entryPoint = args.entryPoint;
|
|
1444
|
+
const entryType = args.entryType || 'auto';
|
|
1445
|
+
const depth = args.depth || 'shallow';
|
|
1446
|
+
|
|
1447
|
+
const result = await traceFlow(entryPoint, entryType, depth);
|
|
1448
|
+
result.summary = buildTraceSummary(result);
|
|
1449
|
+
|
|
1450
|
+
return {
|
|
1451
|
+
content: [{
|
|
1452
|
+
type: 'text',
|
|
1453
|
+
text: JSON.stringify(result)
|
|
1454
|
+
}]
|
|
1455
|
+
};
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1137
1458
|
default:
|
|
1138
1459
|
return {
|
|
1139
1460
|
content: [{
|