magector 1.2.10 → 1.2.11

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.
Files changed (2) hide show
  1. package/package.json +8 -6
  2. package/src/mcp-server.js +251 -48
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "magector",
3
- "version": "1.2.10",
3
+ "version": "1.2.11",
4
4
  "description": "Semantic code search for Magento 2 — index, search, MCP server",
5
5
  "type": "module",
6
6
  "main": "src/mcp-server.js",
@@ -21,7 +21,9 @@
21
21
  "validate:keep": "node src/cli.js validate --verbose --keep",
22
22
  "benchmark": "node src/cli.js benchmark",
23
23
  "test": "node tests/mcp-server.test.js",
24
- "test:no-index": "node tests/mcp-server.test.js --no-index"
24
+ "test:no-index": "node tests/mcp-server.test.js --no-index",
25
+ "test:accuracy": "node tests/mcp-accuracy.test.js",
26
+ "test:accuracy:verbose": "node tests/mcp-accuracy.test.js --verbose"
25
27
  },
26
28
  "dependencies": {
27
29
  "@modelcontextprotocol/sdk": "^1.0.0",
@@ -31,10 +33,10 @@
31
33
  "ruvector": "^0.1.96"
32
34
  },
33
35
  "optionalDependencies": {
34
- "@magector/cli-darwin-arm64": "1.2.10",
35
- "@magector/cli-linux-x64": "1.2.10",
36
- "@magector/cli-linux-arm64": "1.2.10",
37
- "@magector/cli-win32-x64": "1.2.10"
36
+ "@magector/cli-darwin-arm64": "1.2.11",
37
+ "@magector/cli-linux-x64": "1.2.11",
38
+ "@magector/cli-linux-arm64": "1.2.11",
39
+ "@magector/cli-win32-x64": "1.2.11"
38
40
  },
39
41
  "keywords": [
40
42
  "magento",
package/src/mcp-server.js CHANGED
@@ -14,7 +14,8 @@ import {
14
14
  ListResourcesRequestSchema,
15
15
  ReadResourceRequestSchema
16
16
  } from '@modelcontextprotocol/sdk/types.js';
17
- import { execFileSync } from 'child_process';
17
+ import { execFileSync, spawn } from 'child_process';
18
+ import { createInterface } from 'readline';
18
19
  import { existsSync } from 'fs';
19
20
  import { stat } from 'fs/promises';
20
21
  import { glob } from 'glob';
@@ -47,7 +48,112 @@ const rustEnv = {
47
48
  RUST_LOG: 'error',
48
49
  };
49
50
 
50
- function rustSearch(query, limit = 10) {
51
+ /**
52
+ * Query cache: avoid re-embedding identical queries.
53
+ * Keyed by "query|limit", capped at 200 entries (LRU eviction).
54
+ */
55
+ const searchCache = new Map();
56
+ const CACHE_MAX = 200;
57
+
58
+ function cacheSet(key, value) {
59
+ if (searchCache.size >= CACHE_MAX) {
60
+ const oldest = searchCache.keys().next().value;
61
+ searchCache.delete(oldest);
62
+ }
63
+ searchCache.set(key, value);
64
+ }
65
+
66
+ // ─── Persistent Rust Serve Process ──────────────────────────────
67
+ // Keeps ONNX model + HNSW index loaded; eliminates ~2.6s cold start per query.
68
+ // Falls back to execFileSync if serve mode unavailable.
69
+
70
+ let serveProcess = null;
71
+ let serveReady = false;
72
+ let servePending = new Map();
73
+ let serveNextId = 1;
74
+ let serveReadline = null;
75
+
76
+ function startServeProcess() {
77
+ try {
78
+ const proc = spawn(config.rustBinary, [
79
+ 'serve',
80
+ '-d', config.dbPath,
81
+ '-c', config.modelCache
82
+ ], { stdio: ['pipe', 'pipe', 'pipe'], env: rustEnv });
83
+
84
+ proc.on('error', () => { serveProcess = null; serveReady = false; });
85
+ proc.on('exit', () => { serveProcess = null; serveReady = false; });
86
+ proc.stderr.on('data', () => {}); // drain stderr
87
+
88
+ serveReadline = createInterface({ input: proc.stdout });
89
+ serveReadline.on('line', (line) => {
90
+ let parsed;
91
+ try { parsed = JSON.parse(line); } catch { return; }
92
+
93
+ // First line is ready signal
94
+ if (parsed.ready) {
95
+ serveReady = true;
96
+ return;
97
+ }
98
+
99
+ // Route response to pending request by order (FIFO)
100
+ if (servePending.size > 0) {
101
+ const [id, resolver] = servePending.entries().next().value;
102
+ servePending.delete(id);
103
+ resolver.resolve(parsed);
104
+ }
105
+ });
106
+
107
+ serveProcess = proc;
108
+ } catch {
109
+ serveProcess = null;
110
+ serveReady = false;
111
+ }
112
+ }
113
+
114
+ function serveQuery(command, params = {}, timeoutMs = 30000) {
115
+ return new Promise((resolve, reject) => {
116
+ const id = serveNextId++;
117
+ const timer = setTimeout(() => {
118
+ servePending.delete(id);
119
+ reject(new Error('Serve query timeout'));
120
+ }, timeoutMs);
121
+ servePending.set(id, {
122
+ resolve: (v) => { clearTimeout(timer); resolve(v); }
123
+ });
124
+ const msg = JSON.stringify({ command, ...params });
125
+ serveProcess.stdin.write(msg + '\n');
126
+ });
127
+ }
128
+
129
+ async function rustSearchAsync(query, limit = 10) {
130
+ const cacheKey = `${query}|${limit}`;
131
+ if (searchCache.has(cacheKey)) {
132
+ return searchCache.get(cacheKey);
133
+ }
134
+
135
+ // Try persistent serve process first
136
+ if (serveProcess && serveReady) {
137
+ try {
138
+ const resp = await serveQuery('search', { query, limit });
139
+ if (resp.ok && resp.data) {
140
+ cacheSet(cacheKey, resp.data);
141
+ return resp.data;
142
+ }
143
+ } catch {
144
+ // Fall through to execFileSync
145
+ }
146
+ }
147
+
148
+ // Fallback: cold-start execFileSync
149
+ return rustSearchSync(query, limit);
150
+ }
151
+
152
+ function rustSearchSync(query, limit = 10) {
153
+ const cacheKey = `${query}|${limit}`;
154
+ if (searchCache.has(cacheKey)) {
155
+ return searchCache.get(cacheKey);
156
+ }
51
157
  const result = execFileSync(config.rustBinary, [
52
158
  'search', query,
53
159
  '-d', config.dbPath,
@@ -55,10 +161,18 @@ function rustSearch(query, limit = 10) {
55
161
  '-l', String(limit),
56
162
  '-f', 'json'
57
163
  ], { encoding: 'utf-8', timeout: 30000, stdio: ['pipe', 'pipe', 'pipe'], env: rustEnv });
58
- return JSON.parse(result);
164
+ const parsed = JSON.parse(result);
165
+ cacheSet(cacheKey, parsed);
166
+ return parsed;
167
+ }
168
+
169
+ // Keep backward compat: synchronous wrapper (used by tools)
170
+ function rustSearch(query, limit = 10) {
171
+ return rustSearchSync(query, limit);
59
172
  }
60
173
 
61
174
  function rustIndex(magentoRoot) {
175
+ searchCache.clear(); // invalidate cache on reindex
62
176
  const result = execFileSync(config.rustBinary, [
63
177
  'index',
64
178
  '-m', magentoRoot,
@@ -72,7 +186,7 @@ function rustStats() {
72
186
  const result = execFileSync(config.rustBinary, [
73
187
  'stats',
74
188
  '-d', config.dbPath
75
- ], { encoding: 'utf-8', timeout: 10000, stdio: ['pipe', 'pipe', 'pipe'], env: rustEnv });
189
+ ], { encoding: 'utf-8', timeout: 30000, stdio: ['pipe', 'pipe', 'pipe'], env: rustEnv });
76
190
  // Parse text output: "Total vectors: N" and "Embedding dim: N"
77
191
  const vectors = result.match(/Total vectors:\s*(\d+)/)?.[1] || '0';
78
192
  const dim = result.match(/Embedding dim:\s*(\d+)/)?.[1] || '384';
@@ -127,6 +241,7 @@ function normalizeResult(r) {
127
241
  magentoType: meta.magento_type || meta.magentoType,
128
242
  className: meta.class_name || meta.className,
129
243
  methodName: meta.method_name || meta.methodName,
244
+ methods: meta.methods || [],
130
245
  namespace: meta.namespace,
131
246
  isPlugin: meta.is_plugin || meta.isPlugin,
132
247
  isController: meta.is_controller || meta.isController,
@@ -140,6 +255,36 @@ function normalizeResult(r) {
140
255
  };
141
256
  }
142
257
 
258
+ /**
259
+ * Re-rank results by boosting scores for metadata matches.
260
+ * @param {Array} results - normalized results
261
+ * @param {Object} boosts - e.g. { fileType: 'xml', pathContains: 'di.xml', isPlugin: true }
262
+ * @param {number} weight - boost multiplier (default 0.3 = 30% score bump per match)
263
+ */
264
+ function rerank(results, boosts = {}, weight = 0.3) {
265
+ if (!boosts || Object.keys(boosts).length === 0) return results;
266
+
267
+ return results.map(r => {
268
+ let bonus = 0;
269
+ if (boosts.fileType && r.type === boosts.fileType) bonus += weight;
270
+ if (boosts.pathContains) {
271
+ const patterns = Array.isArray(boosts.pathContains) ? boosts.pathContains : [boosts.pathContains];
272
+ for (const p of patterns) {
273
+ if (r.path?.toLowerCase().includes(p.toLowerCase())) bonus += weight;
274
+ }
275
+ }
276
+ if (boosts.isPlugin && r.isPlugin) bonus += weight;
277
+ if (boosts.isController && r.isController) bonus += weight;
278
+ if (boosts.isObserver && r.isObserver) bonus += weight;
279
+ if (boosts.isRepository && r.isRepository) bonus += weight;
280
+ if (boosts.isResolver && r.isResolver) bonus += weight;
281
+ if (boosts.isModel && r.isModel) bonus += weight;
282
+ if (boosts.isBlock && r.isBlock) bonus += weight;
283
+ if (boosts.magentoType && r.magentoType === boosts.magentoType) bonus += weight;
284
+ return { ...r, score: (r.score || 0) + bonus };
285
+ }).sort((a, b) => b.score - a.score);
286
+ }
287
+
143
288
  function formatSearchResults(results) {
144
289
  if (!results || results.length === 0) {
145
290
  return 'No results found.';
@@ -508,7 +653,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
508
653
  try {
509
654
  switch (name) {
510
655
  case 'magento_search': {
511
- const raw = rustSearch(args.query, args.limit || 10);
656
+ const raw = await rustSearchAsync(args.query, args.limit || 10);
512
657
  const results = raw.map(normalizeResult);
513
658
  return {
514
659
  content: [{
@@ -519,10 +664,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
519
664
  }
520
665
 
521
666
  case 'magento_find_class': {
522
- const query = `class ${args.className} ${args.namespace || ''}`.trim();
523
- const raw = rustSearch(query, 10);
667
+ const ns = args.namespace || '';
668
+ const query = `${args.className} ${ns}`.trim();
669
+ const raw = await rustSearchAsync(query, 30);
670
+ const classLower = args.className.toLowerCase();
524
671
  const results = raw.map(normalizeResult).filter(r =>
525
- r.className?.toLowerCase().includes(args.className.toLowerCase())
672
+ r.className?.toLowerCase().includes(classLower) ||
673
+ r.path?.toLowerCase().includes(classLower.replace(/\\/g, '/'))
526
674
  );
527
675
  return {
528
676
  content: [{
@@ -533,12 +681,21 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
533
681
  }
534
682
 
535
683
  case 'magento_find_method': {
536
- const query = `function ${args.methodName} ${args.className || ''}`.trim();
537
- const raw = rustSearch(query, 20);
538
- const results = raw.map(normalizeResult).filter(r =>
539
- r.methodName?.toLowerCase() === args.methodName.toLowerCase() ||
540
- r.path?.toLowerCase().includes(args.methodName.toLowerCase())
684
+ const query = `method ${args.methodName} function ${args.className || ''}`.trim();
685
+ const raw = await rustSearchAsync(query, 30);
686
+ const methodLower = args.methodName.toLowerCase();
687
+ let results = raw.map(normalizeResult).filter(r =>
688
+ r.methodName?.toLowerCase() === methodLower ||
689
+ r.methodName?.toLowerCase().includes(methodLower) ||
690
+ r.methods?.some(m => m.toLowerCase() === methodLower || m.toLowerCase().includes(methodLower)) ||
691
+ r.path?.toLowerCase().includes(methodLower)
541
692
  );
693
+ // Boost exact method matches to top
694
+ results = results.map(r => {
695
+ const exact = r.methodName?.toLowerCase() === methodLower ||
696
+ r.methods?.some(m => m.toLowerCase() === methodLower);
697
+ return { ...r, score: (r.score || 0) + (exact ? 0.5 : 0) };
698
+ }).sort((a, b) => b.score - a.score);
542
699
  return {
543
700
  content: [{
544
701
  type: 'text',
@@ -552,12 +709,21 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
552
709
  if (args.configType && args.configType !== 'other') {
553
710
  query = `${args.configType}.xml ${args.query}`;
554
711
  }
555
- const raw = rustSearch(query, 10);
556
- const results = raw.map(normalizeResult);
712
+ const raw = await rustSearchAsync(query, 30);
713
+ const pathBoost = args.configType ? [`${args.configType}.xml`] : ['.xml'];
714
+ let normalized = raw.map(normalizeResult);
715
+ // Prefer XML results when configType is specified, but don't hard-exclude
716
+ if (args.configType) {
717
+ const xmlOnly = normalized.filter(r =>
718
+ r.type === 'xml' || r.path?.endsWith('.xml') || r.path?.includes('.xml')
719
+ );
720
+ if (xmlOnly.length > 0) normalized = xmlOnly;
721
+ }
722
+ const results = rerank(normalized, { fileType: 'xml', pathContains: pathBoost });
557
723
  return {
558
724
  content: [{
559
725
  type: 'text',
560
- text: formatSearchResults(results)
726
+ text: formatSearchResults(results.slice(0, 10))
561
727
  }]
562
728
  };
563
729
  }
@@ -566,12 +732,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
566
732
  let query = args.query;
567
733
  if (args.area) query = `${args.area} ${query}`;
568
734
  query += ' template phtml';
569
- const raw = rustSearch(query, 10);
570
- const results = raw.map(normalizeResult);
735
+ const raw = await rustSearchAsync(query, 15);
736
+ const results = rerank(raw.map(normalizeResult), { pathContains: ['.phtml'] });
571
737
  return {
572
738
  content: [{
573
739
  type: 'text',
574
- text: formatSearchResults(results)
740
+ text: formatSearchResults(results.slice(0, 10))
575
741
  }]
576
742
  };
577
743
  }
@@ -602,45 +768,48 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
602
768
  if (args.targetClass) query += ` ${args.targetClass}`;
603
769
  if (args.targetMethod) query += ` ${args.targetMethod} before after around`;
604
770
 
605
- const raw = rustSearch(query, 15);
606
- const results = raw.map(normalizeResult).filter(r =>
771
+ const raw = await rustSearchAsync(query, 30);
772
+ let results = raw.map(normalizeResult).filter(r =>
607
773
  r.isPlugin || r.path?.includes('/Plugin/') || r.path?.includes('di.xml')
608
774
  );
775
+ results = rerank(results, { isPlugin: true, pathContains: ['/Plugin/', 'di.xml'] });
609
776
 
610
777
  return {
611
778
  content: [{
612
779
  type: 'text',
613
- text: formatSearchResults(results)
780
+ text: formatSearchResults(results.slice(0, 15))
614
781
  }]
615
782
  };
616
783
  }
617
784
 
618
785
  case 'magento_find_observer': {
619
786
  const query = `event ${args.eventName} observer`;
620
- const raw = rustSearch(query, 15);
621
- const results = raw.map(normalizeResult).filter(r =>
787
+ const raw = await rustSearchAsync(query, 30);
788
+ let results = raw.map(normalizeResult).filter(r =>
622
789
  r.isObserver || r.path?.includes('/Observer/') || r.path?.includes('events.xml')
623
790
  );
791
+ results = rerank(results, { isObserver: true, pathContains: ['events.xml', '/Observer/'] });
624
792
 
625
793
  return {
626
794
  content: [{
627
795
  type: 'text',
628
- text: `## Observers for event: ${args.eventName}\n\n` + formatSearchResults(results)
796
+ text: `## Observers for event: ${args.eventName}\n\n` + formatSearchResults(results.slice(0, 15))
629
797
  }]
630
798
  };
631
799
  }
632
800
 
633
801
  case 'magento_find_preference': {
634
- const query = `preference ${args.interfaceName}`;
635
- const raw = rustSearch(query, 15);
636
- const results = raw.map(normalizeResult).filter(r =>
802
+ const query = `preference ${args.interfaceName} di.xml type`;
803
+ const raw = await rustSearchAsync(query, 30);
804
+ let results = raw.map(normalizeResult).filter(r =>
637
805
  r.path?.includes('di.xml')
638
806
  );
807
+ results = rerank(results, { fileType: 'xml', pathContains: ['di.xml'] });
639
808
 
640
809
  return {
641
810
  content: [{
642
811
  type: 'text',
643
- text: `## Preferences for: ${args.interfaceName}\n\n` + formatSearchResults(results)
812
+ text: `## Preferences for: ${args.interfaceName}\n\n` + formatSearchResults(results.slice(0, 15))
644
813
  }]
645
814
  };
646
815
  }
@@ -649,26 +818,40 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
649
818
  let query = `webapi route ${args.query}`;
650
819
  if (args.method) query += ` method="${args.method}"`;
651
820
 
652
- const raw = rustSearch(query, 15);
653
- const results = raw.map(normalizeResult);
821
+ const raw = await rustSearchAsync(query, 30);
822
+ let results = rerank(raw.map(normalizeResult), { pathContains: ['webapi.xml'] });
654
823
 
655
824
  return {
656
825
  content: [{
657
826
  type: 'text',
658
- text: `## API Endpoints matching: ${args.query}\n\n` + formatSearchResults(results)
827
+ text: `## API Endpoints matching: ${args.query}\n\n` + formatSearchResults(results.slice(0, 15))
659
828
  }]
660
829
  };
661
830
  }
662
831
 
663
832
  case 'magento_find_controller': {
664
833
  const parts = args.route.split('/');
665
- const query = `controller ${parts.join(' ')} execute action`;
834
+ // Map route to Magento namespace: catalog/product/view → Catalog Controller Product View
835
+ const namespaceParts = parts.map(p => p.charAt(0).toUpperCase() + p.slice(1));
836
+ const query = `${namespaceParts.join(' ')} controller execute action`;
666
837
 
667
- const raw = rustSearch(query, 15);
838
+ const raw = await rustSearchAsync(query, 30);
668
839
  let results = raw.map(normalizeResult).filter(r =>
669
840
  r.isController || r.path?.includes('/Controller/')
670
841
  );
671
842
 
843
+ // Boost results whose path matches the route segments
844
+ if (parts.length >= 2) {
845
+ const pathPattern = parts.map(p => p.charAt(0).toUpperCase() + p.slice(1));
846
+ results.sort((a, b) => {
847
+ const aPath = a.path || '';
848
+ const bPath = b.path || '';
849
+ const aMatches = pathPattern.filter(p => aPath.includes(p)).length;
850
+ const bMatches = pathPattern.filter(p => bPath.includes(p)).length;
851
+ return bMatches - aMatches;
852
+ });
853
+ }
854
+
672
855
  if (args.area) {
673
856
  results = results.filter(r => r.area === args.area || r.path?.includes(`/${args.area}/`));
674
857
  }
@@ -683,30 +866,32 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
683
866
 
684
867
  case 'magento_find_block': {
685
868
  const query = `block ${args.query}`;
686
- const raw = rustSearch(query, 15);
687
- const results = raw.map(normalizeResult).filter(r =>
869
+ const raw = await rustSearchAsync(query, 30);
870
+ let results = raw.map(normalizeResult).filter(r =>
688
871
  r.isBlock || r.path?.includes('/Block/')
689
872
  );
873
+ results = rerank(results, { isBlock: true, pathContains: ['/Block/'] });
690
874
 
691
875
  return {
692
876
  content: [{
693
877
  type: 'text',
694
- text: formatSearchResults(results)
878
+ text: formatSearchResults(results.slice(0, 15))
695
879
  }]
696
880
  };
697
881
  }
698
882
 
699
883
  case 'magento_find_cron': {
700
884
  const query = `cron job ${args.jobName}`;
701
- const raw = rustSearch(query, 15);
702
- const results = raw.map(normalizeResult).filter(r =>
885
+ const raw = await rustSearchAsync(query, 30);
886
+ let results = raw.map(normalizeResult).filter(r =>
703
887
  r.path?.includes('crontab.xml') || r.path?.includes('/Cron/')
704
888
  );
889
+ results = rerank(results, { pathContains: ['crontab.xml', '/Cron/'] });
705
890
 
706
891
  return {
707
892
  content: [{
708
893
  type: 'text',
709
- text: `## Cron jobs matching: ${args.jobName}\n\n` + formatSearchResults(results)
894
+ text: `## Cron jobs matching: ${args.jobName}\n\n` + formatSearchResults(results.slice(0, 15))
710
895
  }]
711
896
  };
712
897
  }
@@ -715,37 +900,39 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
715
900
  let query = `graphql ${args.query}`;
716
901
  if (args.schemaType) query += ` ${args.schemaType}`;
717
902
 
718
- const raw = rustSearch(query, 15);
719
- const results = raw.map(normalizeResult).filter(r =>
903
+ const raw = await rustSearchAsync(query, 40);
904
+ let results = raw.map(normalizeResult).filter(r =>
720
905
  r.isResolver || r.path?.includes('/Resolver/') ||
721
906
  r.path?.includes('.graphqls') || r.type === 'graphql'
722
907
  );
908
+ results = rerank(results, { isResolver: true, pathContains: ['.graphqls', '/Resolver/'] });
723
909
 
724
910
  return {
725
911
  content: [{
726
912
  type: 'text',
727
- text: `## GraphQL matching: ${args.query}\n\n` + formatSearchResults(results)
913
+ text: `## GraphQL matching: ${args.query}\n\n` + formatSearchResults(results.slice(0, 15))
728
914
  }]
729
915
  };
730
916
  }
731
917
 
732
918
  case 'magento_find_db_schema': {
733
- const query = `table ${args.tableName} column db_schema`;
734
- const raw = rustSearch(query, 15);
735
- const results = raw.map(normalizeResult).filter(r =>
919
+ const query = `db_schema.xml table ${args.tableName} column declarative schema`;
920
+ const raw = await rustSearchAsync(query, 40);
921
+ let results = raw.map(normalizeResult).filter(r =>
736
922
  r.path?.includes('db_schema.xml')
737
923
  );
924
+ results = rerank(results, { fileType: 'xml', pathContains: ['db_schema.xml'] });
738
925
 
739
926
  return {
740
927
  content: [{
741
928
  type: 'text',
742
- text: `## Database schema for: ${args.tableName}\n\n` + formatSearchResults(results)
929
+ text: `## Database schema for: ${args.tableName}\n\n` + formatSearchResults(results.slice(0, 15))
743
930
  }]
744
931
  };
745
932
  }
746
933
 
747
934
  case 'magento_module_structure': {
748
- const raw = rustSearch(args.moduleName, 100);
935
+ const raw = await rustSearchAsync(args.moduleName, 100);
749
936
  const moduleName = args.moduleName.replace('_', '/');
750
937
  const results = raw.map(normalizeResult).filter(r =>
751
938
  r.path?.includes(moduleName) || r.module?.includes(args.moduleName)
@@ -914,9 +1101,25 @@ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
914
1101
  });
915
1102
 
916
1103
  async function main() {
1104
+ // Try to start persistent Rust serve process for fast queries
1105
+ try {
1106
+ startServeProcess();
1107
+ // Give it a moment to load model+index
1108
+ await new Promise(r => setTimeout(r, 100));
1109
+ } catch {
1110
+ // Non-fatal: falls back to execFileSync per query
1111
+ }
1112
+
917
1113
  const transport = new StdioServerTransport();
918
1114
  await server.connect(transport);
919
1115
  console.error('Magector MCP server started (Rust core backend)');
920
1116
  }
921
1117
 
1118
+ // Cleanup on exit
1119
+ process.on('exit', () => {
1120
+ if (serveProcess) {
1121
+ serveProcess.kill();
1122
+ }
1123
+ });
1124
+
922
1125
  main().catch(console.error);