magector 1.0.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.
@@ -0,0 +1,915 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Magector MCP Server
4
+ *
5
+ * Search tools delegate to the Rust core binary (magector-core).
6
+ * Analysis tools (diff, complexity) use ruvector JS modules directly.
7
+ * No JS indexer — Rust core is the single source of truth for search/index.
8
+ */
9
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
10
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
11
+ import {
12
+ CallToolRequestSchema,
13
+ ListToolsRequestSchema,
14
+ ListResourcesRequestSchema,
15
+ ReadResourceRequestSchema
16
+ } from '@modelcontextprotocol/sdk/types.js';
17
+ import { execFileSync } from 'child_process';
18
+ import { existsSync } from 'fs';
19
+ import { stat } from 'fs/promises';
20
+ import { glob } from 'glob';
21
+ import path from 'path';
22
+ import {
23
+ analyzeCommit,
24
+ getStagedDiff,
25
+ analyzeFileDiff
26
+ } from 'ruvector/dist/core/diff-embeddings.js';
27
+ import {
28
+ analyzeFiles as analyzeComplexityFiles,
29
+ getComplexityRating
30
+ } from 'ruvector/dist/analysis/complexity.js';
31
+ import { resolveBinary } from './binary.js';
32
+ import { resolveModels } from './model.js';
33
+
34
+ const config = {
35
+ dbPath: process.env.MAGECTOR_DB || './magector.db',
36
+ magentoRoot: process.env.MAGENTO_ROOT || process.cwd(),
37
+ get rustBinary() { return resolveBinary(); },
38
+ get modelCache() { return resolveModels() || process.env.MAGECTOR_MODELS || './models'; }
39
+ };
40
+
41
+ // ─── Rust Core Integration ──────────────────────────────────────
42
+
43
+ function rustSearch(query, limit = 10) {
44
+ const result = execFileSync(config.rustBinary, [
45
+ 'search', query,
46
+ '-d', config.dbPath,
47
+ '-c', config.modelCache,
48
+ '-l', String(limit),
49
+ '-f', 'json'
50
+ ], { encoding: 'utf-8', timeout: 30000, stdio: ['pipe', 'pipe', 'pipe'] });
51
+ return JSON.parse(result);
52
+ }
53
+
54
+ function rustIndex(magentoRoot) {
55
+ const result = execFileSync(config.rustBinary, [
56
+ 'index',
57
+ '-m', magentoRoot,
58
+ '-d', config.dbPath,
59
+ '-c', config.modelCache
60
+ ], { encoding: 'utf-8', timeout: 600000, stdio: ['pipe', 'pipe', 'pipe'] });
61
+ return result;
62
+ }
63
+
64
+ function rustStats() {
65
+ const result = execFileSync(config.rustBinary, [
66
+ 'stats',
67
+ '-d', config.dbPath
68
+ ], { encoding: 'utf-8', timeout: 10000, stdio: ['pipe', 'pipe', 'pipe'] });
69
+ // Parse text output: "Total vectors: N" and "Embedding dim: N"
70
+ const vectors = result.match(/Total vectors:\s*(\d+)/)?.[1] || '0';
71
+ const dim = result.match(/Embedding dim:\s*(\d+)/)?.[1] || '384';
72
+ return { totalVectors: parseInt(vectors), embeddingDim: parseInt(dim), dbPath: config.dbPath };
73
+ }
74
+
75
+ // ─── Analysis (ruvector JS) ─────────────────────────────────────
76
+
77
+ async function analyzeDiff(options = {}) {
78
+ if (options.commitHash) {
79
+ return analyzeCommit(options.commitHash);
80
+ }
81
+ const diff = getStagedDiff();
82
+ if (!diff || diff.trim() === '') {
83
+ return { files: [], totalAdditions: 0, totalDeletions: 0, riskScore: 0, message: 'No staged changes found' };
84
+ }
85
+ const fileSections = diff.split(/^diff --git /m).filter(Boolean);
86
+ const files = [];
87
+ let totalAdditions = 0;
88
+ let totalDeletions = 0;
89
+
90
+ for (const section of fileSections) {
91
+ const fileMatch = section.match(/a\/(.+?)\s/);
92
+ if (!fileMatch) continue;
93
+ const filePath = fileMatch[1];
94
+ const analysis = await analyzeFileDiff(filePath, section);
95
+ files.push(analysis);
96
+ totalAdditions += analysis.totalAdditions || 0;
97
+ totalDeletions += analysis.totalDeletions || 0;
98
+ }
99
+
100
+ const maxRisk = files.length > 0 ? Math.max(...files.map(f => f.riskScore || 0)) : 0;
101
+ return { files, totalAdditions, totalDeletions, riskScore: maxRisk };
102
+ }
103
+
104
+ async function analyzeComplexity(paths) {
105
+ const results = analyzeComplexityFiles(paths);
106
+ return results.map(r => ({
107
+ ...r,
108
+ rating: getComplexityRating(r.cyclomaticComplexity)
109
+ }));
110
+ }
111
+
112
+ // ─── Result formatting helpers ──────────────────────────────────
113
+
114
+ function normalizeResult(r) {
115
+ const meta = r.metadata || r;
116
+ return {
117
+ path: meta.path,
118
+ module: meta.module,
119
+ type: meta.file_type || meta.type,
120
+ magentoType: meta.magento_type || meta.magentoType,
121
+ className: meta.class_name || meta.className,
122
+ methodName: meta.method_name || meta.methodName,
123
+ namespace: meta.namespace,
124
+ isPlugin: meta.is_plugin || meta.isPlugin,
125
+ isController: meta.is_controller || meta.isController,
126
+ isObserver: meta.is_observer || meta.isObserver,
127
+ isRepository: meta.is_repository || meta.isRepository,
128
+ isResolver: meta.is_resolver || meta.isResolver,
129
+ isModel: meta.is_model || meta.isModel,
130
+ isBlock: meta.is_block || meta.isBlock,
131
+ area: meta.area,
132
+ score: r.score
133
+ };
134
+ }
135
+
136
+ function formatSearchResults(results) {
137
+ if (!results || results.length === 0) {
138
+ return 'No results found.';
139
+ }
140
+
141
+ return results.map((r, i) => {
142
+ const header = `## Result ${i + 1} (score: ${r.score?.toFixed(3) || 'N/A'})`;
143
+
144
+ const meta = [
145
+ `**Path:** ${r.path || 'unknown'}`,
146
+ r.module ? `**Module:** ${r.module}` : null,
147
+ r.magentoType ? `**Magento Type:** ${r.magentoType}` : null,
148
+ r.area && r.area !== 'global' ? `**Area:** ${r.area}` : null,
149
+ r.className ? `**Class:** ${r.className}` : null,
150
+ r.namespace ? `**Namespace:** ${r.namespace}` : null,
151
+ r.methodName ? `**Method:** ${r.methodName}` : null,
152
+ r.type ? `**File Type:** ${r.type}` : null,
153
+ ].filter(Boolean).join('\n');
154
+
155
+ let badges = '';
156
+ if (r.isPlugin) badges += ' `plugin`';
157
+ if (r.isController) badges += ' `controller`';
158
+ if (r.isObserver) badges += ' `observer`';
159
+ if (r.isRepository) badges += ' `repository`';
160
+ if (r.isResolver) badges += ' `graphql-resolver`';
161
+
162
+ return `${header}\n${meta}${badges}`;
163
+ }).join('\n\n---\n\n');
164
+ }
165
+
166
+ // ─── MCP Server ─────────────────────────────────────────────────
167
+
168
+ const server = new Server(
169
+ {
170
+ name: 'magector',
171
+ version: '1.0.0'
172
+ },
173
+ {
174
+ capabilities: {
175
+ tools: {},
176
+ resources: {}
177
+ }
178
+ }
179
+ );
180
+
181
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
182
+ tools: [
183
+ {
184
+ name: 'magento_search',
185
+ description: 'Search Magento codebase semantically. Find classes, methods, configurations, templates by describing what you need.',
186
+ inputSchema: {
187
+ type: 'object',
188
+ properties: {
189
+ query: {
190
+ type: 'string',
191
+ description: 'Natural language search query (e.g., "product price calculation", "checkout controller", "customer authentication")'
192
+ },
193
+ limit: {
194
+ type: 'number',
195
+ description: 'Maximum results to return (default: 10)',
196
+ default: 10
197
+ }
198
+ },
199
+ required: ['query']
200
+ }
201
+ },
202
+ {
203
+ name: 'magento_find_class',
204
+ description: 'Find a specific PHP class, interface, or trait in Magento',
205
+ inputSchema: {
206
+ type: 'object',
207
+ properties: {
208
+ className: {
209
+ type: 'string',
210
+ description: 'Class name to find (e.g., "ProductRepository", "AbstractModel")'
211
+ },
212
+ namespace: {
213
+ type: 'string',
214
+ description: 'Optional namespace filter'
215
+ }
216
+ },
217
+ required: ['className']
218
+ }
219
+ },
220
+ {
221
+ name: 'magento_find_method',
222
+ description: 'Find implementations of a specific method across Magento',
223
+ inputSchema: {
224
+ type: 'object',
225
+ properties: {
226
+ methodName: {
227
+ type: 'string',
228
+ description: 'Method name to find (e.g., "execute", "getPrice", "save")'
229
+ },
230
+ className: {
231
+ type: 'string',
232
+ description: 'Optional class name filter'
233
+ }
234
+ },
235
+ required: ['methodName']
236
+ }
237
+ },
238
+ {
239
+ name: 'magento_find_config',
240
+ description: 'Find XML configuration files and nodes in Magento',
241
+ inputSchema: {
242
+ type: 'object',
243
+ properties: {
244
+ query: {
245
+ type: 'string',
246
+ description: 'Configuration to find (e.g., "di.xml preference", "routes.xml", "system.xml field")'
247
+ },
248
+ configType: {
249
+ type: 'string',
250
+ enum: ['di', 'routes', 'system', 'events', 'webapi', 'module', 'layout', 'other'],
251
+ description: 'Type of configuration'
252
+ }
253
+ },
254
+ required: ['query']
255
+ }
256
+ },
257
+ {
258
+ name: 'magento_find_template',
259
+ description: 'Find PHTML templates in Magento',
260
+ inputSchema: {
261
+ type: 'object',
262
+ properties: {
263
+ query: {
264
+ type: 'string',
265
+ description: 'Template description (e.g., "product listing", "checkout form", "customer account")'
266
+ },
267
+ area: {
268
+ type: 'string',
269
+ enum: ['frontend', 'adminhtml', 'base'],
270
+ description: 'Magento area'
271
+ }
272
+ },
273
+ required: ['query']
274
+ }
275
+ },
276
+ {
277
+ name: 'magento_index',
278
+ description: 'Index or re-index Magento codebase for semantic search (uses Rust core)',
279
+ inputSchema: {
280
+ type: 'object',
281
+ properties: {
282
+ path: {
283
+ type: 'string',
284
+ description: 'Path to Magento root (uses configured path if not specified)'
285
+ },
286
+ }
287
+ }
288
+ },
289
+ {
290
+ name: 'magento_stats',
291
+ description: 'Get index statistics from Rust core',
292
+ inputSchema: {
293
+ type: 'object',
294
+ properties: {}
295
+ }
296
+ },
297
+ {
298
+ name: 'magento_find_plugin',
299
+ description: 'Find plugins (interceptors) for a class or method - before/after/around methods',
300
+ inputSchema: {
301
+ type: 'object',
302
+ properties: {
303
+ targetClass: {
304
+ type: 'string',
305
+ description: 'Class being intercepted (e.g., "ProductRepository", "CartManagement")'
306
+ },
307
+ targetMethod: {
308
+ type: 'string',
309
+ description: 'Method being intercepted (e.g., "save", "getList")'
310
+ }
311
+ }
312
+ }
313
+ },
314
+ {
315
+ name: 'magento_find_observer',
316
+ description: 'Find observers for a specific event',
317
+ inputSchema: {
318
+ type: 'object',
319
+ properties: {
320
+ eventName: {
321
+ type: 'string',
322
+ description: 'Event name (e.g., "checkout_cart_add_product_complete", "sales_order_place_after")'
323
+ }
324
+ },
325
+ required: ['eventName']
326
+ }
327
+ },
328
+ {
329
+ name: 'magento_find_preference',
330
+ description: 'Find DI preference overrides for an interface or class',
331
+ inputSchema: {
332
+ type: 'object',
333
+ properties: {
334
+ interfaceName: {
335
+ type: 'string',
336
+ description: 'Interface or class name to find preferences for (e.g., "ProductRepositoryInterface")'
337
+ }
338
+ },
339
+ required: ['interfaceName']
340
+ }
341
+ },
342
+ {
343
+ name: 'magento_find_api',
344
+ description: 'Find REST/SOAP API endpoints and their implementations',
345
+ inputSchema: {
346
+ type: 'object',
347
+ properties: {
348
+ query: {
349
+ type: 'string',
350
+ description: 'API endpoint URL pattern or service method (e.g., "/V1/products", "getList")'
351
+ },
352
+ method: {
353
+ type: 'string',
354
+ enum: ['GET', 'POST', 'PUT', 'DELETE'],
355
+ description: 'HTTP method filter'
356
+ }
357
+ },
358
+ required: ['query']
359
+ }
360
+ },
361
+ {
362
+ name: 'magento_find_controller',
363
+ description: 'Find controllers by route or action',
364
+ inputSchema: {
365
+ type: 'object',
366
+ properties: {
367
+ route: {
368
+ type: 'string',
369
+ description: 'Route path (e.g., "catalog/product/view", "checkout/cart/add")'
370
+ },
371
+ area: {
372
+ type: 'string',
373
+ enum: ['frontend', 'adminhtml'],
374
+ description: 'Magento area'
375
+ }
376
+ },
377
+ required: ['route']
378
+ }
379
+ },
380
+ {
381
+ name: 'magento_find_block',
382
+ description: 'Find Block classes by name or template',
383
+ inputSchema: {
384
+ type: 'object',
385
+ properties: {
386
+ query: {
387
+ type: 'string',
388
+ description: 'Block class name or functionality (e.g., "Product\\View", "cart totals")'
389
+ }
390
+ },
391
+ required: ['query']
392
+ }
393
+ },
394
+ {
395
+ name: 'magento_find_cron',
396
+ description: 'Find cron jobs by name or schedule',
397
+ inputSchema: {
398
+ type: 'object',
399
+ properties: {
400
+ jobName: {
401
+ type: 'string',
402
+ description: 'Cron job name or pattern (e.g., "catalog_product", "indexer")'
403
+ }
404
+ },
405
+ required: ['jobName']
406
+ }
407
+ },
408
+ {
409
+ name: 'magento_find_graphql',
410
+ description: 'Find GraphQL types, queries, mutations, or resolvers',
411
+ inputSchema: {
412
+ type: 'object',
413
+ properties: {
414
+ query: {
415
+ type: 'string',
416
+ description: 'GraphQL type, query, or mutation name (e.g., "products", "createCustomer", "CartItemInterface")'
417
+ },
418
+ schemaType: {
419
+ type: 'string',
420
+ enum: ['type', 'query', 'mutation', 'interface', 'resolver'],
421
+ description: 'Type of GraphQL schema element'
422
+ }
423
+ },
424
+ required: ['query']
425
+ }
426
+ },
427
+ {
428
+ name: 'magento_find_db_schema',
429
+ description: 'Find database table definitions and columns',
430
+ inputSchema: {
431
+ type: 'object',
432
+ properties: {
433
+ tableName: {
434
+ type: 'string',
435
+ description: 'Table name pattern (e.g., "catalog_product", "sales_order")'
436
+ }
437
+ },
438
+ required: ['tableName']
439
+ }
440
+ },
441
+ {
442
+ name: 'magento_module_structure',
443
+ description: 'Get complete structure of a Magento module - all its classes, configs, templates',
444
+ inputSchema: {
445
+ type: 'object',
446
+ properties: {
447
+ moduleName: {
448
+ type: 'string',
449
+ description: 'Full module name (e.g., "Magento_Catalog", "Vendor_CustomModule")'
450
+ }
451
+ },
452
+ required: ['moduleName']
453
+ }
454
+ },
455
+ {
456
+ name: 'magento_analyze_diff',
457
+ description: 'Analyze git diffs for risk scoring, change classification, and per-file analysis. Works on commits or staged changes.',
458
+ inputSchema: {
459
+ type: 'object',
460
+ properties: {
461
+ commitHash: {
462
+ type: 'string',
463
+ description: 'Git commit hash to analyze. If omitted, analyzes staged changes.'
464
+ },
465
+ staged: {
466
+ type: 'boolean',
467
+ description: 'Analyze staged (git add) changes instead of a commit',
468
+ default: true
469
+ }
470
+ }
471
+ }
472
+ },
473
+ {
474
+ name: 'magento_complexity',
475
+ description: 'Analyze code complexity (cyclomatic complexity, function count, lines) for Magento files. Finds complex hotspots.',
476
+ inputSchema: {
477
+ type: 'object',
478
+ properties: {
479
+ module: {
480
+ type: 'string',
481
+ description: 'Magento module to analyze (e.g., "Magento_Catalog"). Finds all PHP files in the module.'
482
+ },
483
+ path: {
484
+ type: 'string',
485
+ description: 'Specific file or directory path to analyze'
486
+ },
487
+ threshold: {
488
+ type: 'number',
489
+ description: 'Minimum cyclomatic complexity to report (default: 0, show all)',
490
+ default: 0
491
+ }
492
+ }
493
+ }
494
+ }
495
+ ]
496
+ }));
497
+
498
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
499
+ const { name, arguments: args } = request.params;
500
+
501
+ try {
502
+ switch (name) {
503
+ case 'magento_search': {
504
+ const raw = rustSearch(args.query, args.limit || 10);
505
+ const results = raw.map(normalizeResult);
506
+ return {
507
+ content: [{
508
+ type: 'text',
509
+ text: formatSearchResults(results)
510
+ }]
511
+ };
512
+ }
513
+
514
+ case 'magento_find_class': {
515
+ const query = `class ${args.className} ${args.namespace || ''}`.trim();
516
+ const raw = rustSearch(query, 10);
517
+ const results = raw.map(normalizeResult).filter(r =>
518
+ r.className?.toLowerCase().includes(args.className.toLowerCase())
519
+ );
520
+ return {
521
+ content: [{
522
+ type: 'text',
523
+ text: formatSearchResults(results.slice(0, 5))
524
+ }]
525
+ };
526
+ }
527
+
528
+ case 'magento_find_method': {
529
+ const query = `function ${args.methodName} ${args.className || ''}`.trim();
530
+ const raw = rustSearch(query, 20);
531
+ const results = raw.map(normalizeResult).filter(r =>
532
+ r.methodName?.toLowerCase() === args.methodName.toLowerCase() ||
533
+ r.path?.toLowerCase().includes(args.methodName.toLowerCase())
534
+ );
535
+ return {
536
+ content: [{
537
+ type: 'text',
538
+ text: formatSearchResults(results.slice(0, 10))
539
+ }]
540
+ };
541
+ }
542
+
543
+ case 'magento_find_config': {
544
+ let query = args.query;
545
+ if (args.configType && args.configType !== 'other') {
546
+ query = `${args.configType}.xml ${args.query}`;
547
+ }
548
+ const raw = rustSearch(query, 10);
549
+ const results = raw.map(normalizeResult);
550
+ return {
551
+ content: [{
552
+ type: 'text',
553
+ text: formatSearchResults(results)
554
+ }]
555
+ };
556
+ }
557
+
558
+ case 'magento_find_template': {
559
+ let query = args.query;
560
+ if (args.area) query = `${args.area} ${query}`;
561
+ query += ' template phtml';
562
+ const raw = rustSearch(query, 10);
563
+ const results = raw.map(normalizeResult);
564
+ return {
565
+ content: [{
566
+ type: 'text',
567
+ text: formatSearchResults(results)
568
+ }]
569
+ };
570
+ }
571
+
572
+ case 'magento_index': {
573
+ const root = args.path || config.magentoRoot;
574
+ const output = rustIndex(root);
575
+ return {
576
+ content: [{
577
+ type: 'text',
578
+ text: `Indexing complete (Rust core).\n\n${output}`
579
+ }]
580
+ };
581
+ }
582
+
583
+ case 'magento_stats': {
584
+ const stats = rustStats();
585
+ return {
586
+ content: [{
587
+ type: 'text',
588
+ text: `Magector Stats (Rust core):\n- Total indexed vectors: ${stats.totalVectors}\n- Embedding dimensions: ${stats.embeddingDim}\n- Database path: ${stats.dbPath}`
589
+ }]
590
+ };
591
+ }
592
+
593
+ case 'magento_find_plugin': {
594
+ let query = 'plugin interceptor';
595
+ if (args.targetClass) query += ` ${args.targetClass}`;
596
+ if (args.targetMethod) query += ` ${args.targetMethod} before after around`;
597
+
598
+ const raw = rustSearch(query, 15);
599
+ const results = raw.map(normalizeResult).filter(r =>
600
+ r.isPlugin || r.path?.includes('/Plugin/') || r.path?.includes('di.xml')
601
+ );
602
+
603
+ return {
604
+ content: [{
605
+ type: 'text',
606
+ text: formatSearchResults(results)
607
+ }]
608
+ };
609
+ }
610
+
611
+ case 'magento_find_observer': {
612
+ const query = `event ${args.eventName} observer`;
613
+ const raw = rustSearch(query, 15);
614
+ const results = raw.map(normalizeResult).filter(r =>
615
+ r.isObserver || r.path?.includes('/Observer/') || r.path?.includes('events.xml')
616
+ );
617
+
618
+ return {
619
+ content: [{
620
+ type: 'text',
621
+ text: `## Observers for event: ${args.eventName}\n\n` + formatSearchResults(results)
622
+ }]
623
+ };
624
+ }
625
+
626
+ case 'magento_find_preference': {
627
+ const query = `preference ${args.interfaceName}`;
628
+ const raw = rustSearch(query, 15);
629
+ const results = raw.map(normalizeResult).filter(r =>
630
+ r.path?.includes('di.xml')
631
+ );
632
+
633
+ return {
634
+ content: [{
635
+ type: 'text',
636
+ text: `## Preferences for: ${args.interfaceName}\n\n` + formatSearchResults(results)
637
+ }]
638
+ };
639
+ }
640
+
641
+ case 'magento_find_api': {
642
+ let query = `webapi route ${args.query}`;
643
+ if (args.method) query += ` method="${args.method}"`;
644
+
645
+ const raw = rustSearch(query, 15);
646
+ const results = raw.map(normalizeResult);
647
+
648
+ return {
649
+ content: [{
650
+ type: 'text',
651
+ text: `## API Endpoints matching: ${args.query}\n\n` + formatSearchResults(results)
652
+ }]
653
+ };
654
+ }
655
+
656
+ case 'magento_find_controller': {
657
+ const parts = args.route.split('/');
658
+ const query = `controller ${parts.join(' ')} execute action`;
659
+
660
+ const raw = rustSearch(query, 15);
661
+ let results = raw.map(normalizeResult).filter(r =>
662
+ r.isController || r.path?.includes('/Controller/')
663
+ );
664
+
665
+ if (args.area) {
666
+ results = results.filter(r => r.area === args.area || r.path?.includes(`/${args.area}/`));
667
+ }
668
+
669
+ return {
670
+ content: [{
671
+ type: 'text',
672
+ text: `## Controllers for route: ${args.route}\n\n` + formatSearchResults(results)
673
+ }]
674
+ };
675
+ }
676
+
677
+ case 'magento_find_block': {
678
+ const query = `block ${args.query}`;
679
+ const raw = rustSearch(query, 15);
680
+ const results = raw.map(normalizeResult).filter(r =>
681
+ r.isBlock || r.path?.includes('/Block/')
682
+ );
683
+
684
+ return {
685
+ content: [{
686
+ type: 'text',
687
+ text: formatSearchResults(results)
688
+ }]
689
+ };
690
+ }
691
+
692
+ case 'magento_find_cron': {
693
+ const query = `cron job ${args.jobName}`;
694
+ const raw = rustSearch(query, 15);
695
+ const results = raw.map(normalizeResult).filter(r =>
696
+ r.path?.includes('crontab.xml') || r.path?.includes('/Cron/')
697
+ );
698
+
699
+ return {
700
+ content: [{
701
+ type: 'text',
702
+ text: `## Cron jobs matching: ${args.jobName}\n\n` + formatSearchResults(results)
703
+ }]
704
+ };
705
+ }
706
+
707
+ case 'magento_find_graphql': {
708
+ let query = `graphql ${args.query}`;
709
+ if (args.schemaType) query += ` ${args.schemaType}`;
710
+
711
+ const raw = rustSearch(query, 15);
712
+ const results = raw.map(normalizeResult).filter(r =>
713
+ r.isResolver || r.path?.includes('/Resolver/') ||
714
+ r.path?.includes('.graphqls') || r.type === 'graphql'
715
+ );
716
+
717
+ return {
718
+ content: [{
719
+ type: 'text',
720
+ text: `## GraphQL matching: ${args.query}\n\n` + formatSearchResults(results)
721
+ }]
722
+ };
723
+ }
724
+
725
+ case 'magento_find_db_schema': {
726
+ const query = `table ${args.tableName} column db_schema`;
727
+ const raw = rustSearch(query, 15);
728
+ const results = raw.map(normalizeResult).filter(r =>
729
+ r.path?.includes('db_schema.xml')
730
+ );
731
+
732
+ return {
733
+ content: [{
734
+ type: 'text',
735
+ text: `## Database schema for: ${args.tableName}\n\n` + formatSearchResults(results)
736
+ }]
737
+ };
738
+ }
739
+
740
+ case 'magento_module_structure': {
741
+ const raw = rustSearch(args.moduleName, 100);
742
+ const moduleName = args.moduleName.replace('_', '/');
743
+ const results = raw.map(normalizeResult).filter(r =>
744
+ r.path?.includes(moduleName) || r.module?.includes(args.moduleName)
745
+ );
746
+
747
+ const structure = {
748
+ controllers: results.filter(r => r.isController || r.path?.includes('/Controller/')),
749
+ models: results.filter(r => r.isModel || (r.path?.includes('/Model/') && !r.path?.includes('ResourceModel'))),
750
+ blocks: results.filter(r => r.isBlock || r.path?.includes('/Block/')),
751
+ plugins: results.filter(r => r.isPlugin || r.path?.includes('/Plugin/')),
752
+ observers: results.filter(r => r.isObserver || r.path?.includes('/Observer/')),
753
+ apis: results.filter(r => r.path?.includes('/Api/')),
754
+ configs: results.filter(r => r.type === 'xml'),
755
+ other: results.filter(r =>
756
+ !r.isController && !r.isModel && !r.isBlock && !r.isPlugin && !r.isObserver &&
757
+ !r.path?.includes('/Api/') && r.type !== 'xml' &&
758
+ !r.path?.includes('/Controller/') && !r.path?.includes('/Model/') &&
759
+ !r.path?.includes('/Block/') && !r.path?.includes('/Plugin/') &&
760
+ !r.path?.includes('/Observer/')
761
+ )
762
+ };
763
+
764
+ let text = `## Module Structure: ${args.moduleName}\n\n`;
765
+
766
+ for (const [category, items] of Object.entries(structure)) {
767
+ if (items.length > 0) {
768
+ text += `### ${category.charAt(0).toUpperCase() + category.slice(1)} (${items.length})\n`;
769
+ items.slice(0, 10).forEach(item => {
770
+ text += `- ${item.className || item.path} (${item.path})\n`;
771
+ });
772
+ if (items.length > 10) text += ` ... and ${items.length - 10} more\n`;
773
+ text += '\n';
774
+ }
775
+ }
776
+
777
+ if (results.length === 0) {
778
+ text += 'No code found for this module. Try re-indexing or check the module name.';
779
+ }
780
+
781
+ return { content: [{ type: 'text', text }] };
782
+ }
783
+
784
+ case 'magento_analyze_diff': {
785
+ const analysis = await analyzeDiff({
786
+ commitHash: args.commitHash,
787
+ staged: args.staged !== false
788
+ });
789
+
790
+ let text = '## Diff Analysis\n\n';
791
+ text += `- **Total additions:** ${analysis.totalAdditions}\n`;
792
+ text += `- **Total deletions:** ${analysis.totalDeletions}\n`;
793
+ text += `- **Risk score:** ${(analysis.riskScore || 0).toFixed(2)}\n\n`;
794
+
795
+ if (analysis.message) {
796
+ text += `_${analysis.message}_\n\n`;
797
+ }
798
+
799
+ if (analysis.files && analysis.files.length > 0) {
800
+ text += '### Per-file Analysis\n\n';
801
+ for (const f of analysis.files) {
802
+ text += `**${f.file}**\n`;
803
+ text += ` - Category: \`${f.category || 'unknown'}\`\n`;
804
+ text += ` - Risk: ${(f.riskScore || 0).toFixed(2)}`;
805
+ text += ` | +${f.totalAdditions || 0} / -${f.totalDeletions || 0}\n`;
806
+ }
807
+ }
808
+
809
+ return { content: [{ type: 'text', text }] };
810
+ }
811
+
812
+ case 'magento_complexity': {
813
+ let filePaths = [];
814
+
815
+ if (args.path) {
816
+ const pathStat = await stat(args.path).catch(() => null);
817
+ if (pathStat && pathStat.isDirectory()) {
818
+ filePaths = await glob('**/*.php', { cwd: args.path, absolute: true, nodir: true });
819
+ } else if (pathStat) {
820
+ filePaths = [args.path];
821
+ }
822
+ } else if (args.module) {
823
+ const modulePath = args.module.replace('_', '/');
824
+ const root = config.magentoRoot;
825
+ const patterns = [
826
+ `vendor/magento/module-${args.module.split('_')[1]?.toLowerCase()}/**/*.php`,
827
+ `app/code/${modulePath}/**/*.php`
828
+ ];
829
+ for (const pattern of patterns) {
830
+ const found = await glob(pattern, { cwd: root, absolute: true, nodir: true });
831
+ filePaths.push(...found);
832
+ }
833
+ }
834
+
835
+ if (filePaths.length === 0) {
836
+ return { content: [{ type: 'text', text: 'No files found to analyze. Specify a module or path.' }] };
837
+ }
838
+
839
+ const results = await analyzeComplexity(filePaths);
840
+ const threshold = args.threshold || 0;
841
+ const filtered = results
842
+ .filter(r => r.cyclomaticComplexity >= threshold)
843
+ .sort((a, b) => b.cyclomaticComplexity - a.cyclomaticComplexity);
844
+
845
+ let text = `## Complexity Analysis (${filtered.length} files)\n\n`;
846
+ text += '| File | Complexity | Rating | Functions | Lines |\n';
847
+ text += '|------|-----------|--------|-----------|-------|\n';
848
+
849
+ for (const r of filtered.slice(0, 50)) {
850
+ const shortPath = r.file.replace(config.magentoRoot + '/', '');
851
+ text += `| ${shortPath} | ${r.cyclomaticComplexity} | ${r.rating} | ${r.functions} | ${r.lines} |\n`;
852
+ }
853
+
854
+ if (filtered.length > 50) {
855
+ text += `\n_...and ${filtered.length - 50} more files_\n`;
856
+ }
857
+
858
+ return { content: [{ type: 'text', text }] };
859
+ }
860
+
861
+ default:
862
+ return {
863
+ content: [{
864
+ type: 'text',
865
+ text: `Unknown tool: ${name}`
866
+ }],
867
+ isError: true
868
+ };
869
+ }
870
+ } catch (error) {
871
+ return {
872
+ content: [{
873
+ type: 'text',
874
+ text: `Error: ${error.message}`
875
+ }],
876
+ isError: true
877
+ };
878
+ }
879
+ });
880
+
881
+ server.setRequestHandler(ListResourcesRequestSchema, async () => ({
882
+ resources: [
883
+ {
884
+ uri: 'magector://stats',
885
+ name: 'Index Statistics',
886
+ description: 'Current index statistics from Rust core',
887
+ mimeType: 'application/json'
888
+ }
889
+ ]
890
+ }));
891
+
892
+ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
893
+ const { uri } = request.params;
894
+
895
+ if (uri === 'magector://stats') {
896
+ const stats = rustStats();
897
+ return {
898
+ contents: [{
899
+ uri,
900
+ mimeType: 'application/json',
901
+ text: JSON.stringify(stats, null, 2)
902
+ }]
903
+ };
904
+ }
905
+
906
+ throw new Error(`Unknown resource: ${uri}`);
907
+ });
908
+
909
+ async function main() {
910
+ const transport = new StdioServerTransport();
911
+ await server.connect(transport);
912
+ console.error('Magector MCP server started (Rust core backend)');
913
+ }
914
+
915
+ main().catch(console.error);