@mrxkun/mcfast-mcp 4.0.15 → 4.1.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.
@@ -1,35 +1,39 @@
1
1
  /**
2
- * memory_search Tool v2.0
3
- * Hybrid search tool using MemoryEngine
4
- * Version: 2.0.0
2
+ * memory_search Tool v3.0
3
+ * Hybrid search using MemoryEngine v4.1.0
4
+ * Supports: Vector 70% + BM25 30% hybrid search
5
5
  */
6
6
 
7
7
  import { MemoryEngine } from '../memory/index.js';
8
8
 
9
9
  // Memory engine instance (initialized lazily)
10
10
  let memoryEngine = null;
11
+ let enginePromise = null;
11
12
 
12
13
  async function getMemoryEngine() {
13
- if (!memoryEngine) {
14
+ if (memoryEngine) return memoryEngine;
15
+ if (enginePromise) return enginePromise;
16
+
17
+ enginePromise = (async () => {
14
18
  memoryEngine = new MemoryEngine({
15
19
  apiKey: process.env.MCFAST_TOKEN,
16
20
  enableSync: true
17
21
  });
18
22
  await memoryEngine.initialize(process.cwd());
19
- }
20
- return memoryEngine;
23
+ return memoryEngine;
24
+ })();
25
+
26
+ return enginePromise;
21
27
  }
22
28
 
23
29
  /**
24
- * Execute memory search using intelligent hybrid search
30
+ * Execute memory search using hybrid search (Vector 70% + BM25 30%)
25
31
  * @param {Object} args - Tool arguments
26
32
  * @param {string} args.query - Search query
27
- * @param {string} [args.type='all'] - Filter by type: 'all', 'facts', 'history', 'code'
33
+ * @param {string} [args.type='all'] - Filter by type: 'all', 'memory', 'codebase', 'facts'
28
34
  * @param {number} [args.maxResults=6] - Maximum results to return
29
35
  * @param {number} [args.minScore=0.35] - Minimum score threshold
30
- * @param {Object} [args.context] - Context information
31
- * @param {string} [args.context.currentFile] - Current file being edited
32
- * @param {number} [args.context.currentLine] - Current line number
36
+ * @param {boolean} [args.useHybrid=true] - Use hybrid search (Vector + BM25)
33
37
  * @returns {Promise<Object>} Search results
34
38
  */
35
39
  export async function execute(args) {
@@ -38,74 +42,72 @@ export async function execute(args) {
38
42
  type = 'all',
39
43
  maxResults = 6,
40
44
  minScore = 0.35,
41
- context: contextInfo = {}
45
+ useHybrid = true
42
46
  } = args;
43
47
 
48
+ if (!query || query.trim().length === 0) {
49
+ return {
50
+ content: [{
51
+ type: "text",
52
+ text: "❌ Error: 'query' parameter is required"
53
+ }],
54
+ isError: true
55
+ };
56
+ }
57
+
44
58
  const startTime = Date.now();
45
59
 
46
60
  try {
47
61
  const engine = await getMemoryEngine();
48
62
  let results = [];
63
+ let searchMetadata = {};
64
+
65
+ if (type === 'memory' || type === 'all') {
66
+ // Search memory notes (daily logs + curated)
67
+ const memoryResult = await engine.searchMemory(query, {
68
+ limit: maxResults * 2,
69
+ useHybrid,
70
+ minScore
71
+ });
72
+
73
+ results.push(...memoryResult.results.map(r => ({
74
+ type: 'memory',
75
+ content: r.content,
76
+ file: r.file_path || r.filePath,
77
+ lines: [r.start_line || r.startLine, r.end_line || r.endLine],
78
+ score: r.finalScore || r.score || r.similarity,
79
+ vectorScore: r.vectorScore,
80
+ textScore: r.textScore,
81
+ source: r.sources?.join('+') || 'vector'
82
+ })));
83
+
84
+ searchMetadata.memory = memoryResult.metadata;
85
+ }
86
+
87
+ if (type === 'codebase' || type === 'all') {
88
+ // Search codebase
89
+ const codebaseResult = await engine.searchCodebase(query, maxResults * 2);
90
+
91
+ results.push(...codebaseResult.results.map(r => ({
92
+ type: 'code',
93
+ content: r.content || r.code,
94
+ file: r.file_path || r.filePath || r.path,
95
+ lines: [r.start_line || r.startLine, r.end_line || r.endLine],
96
+ score: r.similarity || r.score,
97
+ source: r.source || 'vector'
98
+ })));
99
+
100
+ searchMetadata.codebase = codebaseResult.metadata;
101
+ }
49
102
 
50
103
  if (type === 'facts') {
51
104
  // Search facts only
52
105
  const facts = await engine.searchFacts(query, maxResults);
53
- results = facts.map(fact => ({
54
- type: 'fact',
55
- content: `${fact.type}: ${fact.name}${fact.signature ? ` (${fact.signature})` : ''}`,
56
- file: fact.file_id,
57
- score: fact.confidence || 0.8,
58
- source: 'facts'
59
- }));
60
- } else if (type === 'history') {
61
- // Search edit history via database
62
- const history = engine.db.getRecentEdits(maxResults * 2);
63
- const filtered = history.filter(edit =>
64
- edit.instruction.toLowerCase().includes(query.toLowerCase()) ||
65
- (edit.files && edit.files.some(f => f.toLowerCase().includes(query.toLowerCase())))
66
- );
67
- results = filtered.slice(0, maxResults).map(edit => ({
68
- type: 'history',
69
- content: edit.instruction,
70
- files: edit.files,
71
- strategy: edit.strategy,
72
- success: edit.success,
73
- timestamp: edit.timestamp,
74
- score: edit.success ? 0.8 : 0.5,
75
- source: 'history'
76
- }));
77
- } else if (type === 'code') {
78
- // Use ultra search for code
79
- const searchResult = await engine.ultraSearch(query, { limit: maxResults });
80
- results = searchResult.results.map(result => ({
81
- type: 'chunk',
82
- content: result.code || result.content,
83
- file: result.filePath || result.path,
84
- lines: [result.startLine, result.endLine],
85
- score: result.finalScore || result.score,
86
- source: 'ultra-hybrid'
87
- }));
88
- } else {
89
- // Use intelligent search (all types)
90
- const searchResult = await engine.intelligentSearch(query, {
91
- limit: maxResults,
92
- currentFile: contextInfo.currentFile
93
- });
94
-
95
- results = searchResult.results.map(result => ({
96
- type: 'chunk',
97
- content: result.code || result.content,
98
- file: result.filePath || result.path,
99
- score: result.finalScore || result.score,
100
- source: searchResult.metadata?.method || 'intelligent'
101
- }));
102
-
103
- // Add facts if available
104
- const facts = await engine.searchFacts(query, Math.floor(maxResults / 2));
105
106
  results.push(...facts.map(fact => ({
106
107
  type: 'fact',
107
- content: `${fact.type}: ${fact.name}`,
108
- file: fact.file_id,
108
+ content: `${fact.type}: ${fact.name}${fact.signature ? ` (${fact.signature})` : ''}`,
109
+ file: fact.file_path || fact.file_id,
110
+ lines: [fact.line_start, fact.line_end],
109
111
  score: fact.confidence || 0.8,
110
112
  source: 'facts'
111
113
  })));
@@ -118,7 +120,7 @@ export async function execute(args) {
118
120
  .slice(0, maxResults);
119
121
 
120
122
  // Format output
121
- const formattedResults = formatResults(results, type);
123
+ const formattedResults = formatResults(results, query);
122
124
  const latency = Date.now() - startTime;
123
125
 
124
126
  return {
@@ -131,6 +133,8 @@ export async function execute(args) {
131
133
  type,
132
134
  resultCount: results.length,
133
135
  latency,
136
+ minScore,
137
+ searchMetadata,
134
138
  sources: [...new Set(results.map(r => r.source || 'unknown'))]
135
139
  }
136
140
  };
@@ -149,13 +153,15 @@ export async function execute(args) {
149
153
  /**
150
154
  * Format results for display
151
155
  */
152
- function formatResults(results, type) {
156
+ function formatResults(results, query) {
153
157
  if (!results || results.length === 0) {
154
- return `🔍 Memory Search: No results found for query`;
158
+ return `🔍 Memory Search: No results found for "${query}"\n\nTip: Try a different search term or check if memory is indexed.`;
155
159
  }
156
160
 
157
161
  let output = `🔍 Memory Search Results\n`;
158
- output += `Found: ${results.length} results\n\n`;
162
+ output += `Query: "${query}"\n`;
163
+ output += `Found: ${results.length} result(s)\n`;
164
+ output += '─'.repeat(50) + '\n\n';
159
165
 
160
166
  // Group by type
161
167
  const byType = {};
@@ -164,13 +170,21 @@ function formatResults(results, type) {
164
170
  byType[result.type].push(result);
165
171
  }
166
172
 
167
- for (const [resultType, items] of Object.entries(byType)) {
168
- output += `${getTypeIcon(resultType)} ${resultType.toUpperCase()} (${items.length})\n`;
169
- output += '─'.repeat(40) + '\n';
170
-
171
- for (const item of items.slice(0, 5)) {
172
- output += ` ${formatItem(item)}\n\n`;
173
+ // Display by type
174
+ const typeOrder = ['memory', 'code', 'fact'];
175
+ for (const resultType of typeOrder) {
176
+ if (!byType[resultType]) continue;
177
+
178
+ const items = byType[resultType];
179
+ output += `${getTypeIcon(resultType)} ${getTypeLabel(resultType)} (${items.length})\n`;
180
+ output += '─'.repeat(50) + '\n';
181
+
182
+ for (let i = 0; i < items.length; i++) {
183
+ const item = items[i];
184
+ output += `[${i + 1}] ${formatItem(item)}\n\n`;
173
185
  }
186
+
187
+ output += '\n';
174
188
  }
175
189
 
176
190
  return output;
@@ -178,18 +192,28 @@ function formatResults(results, type) {
178
192
 
179
193
  function getTypeIcon(type) {
180
194
  const icons = {
181
- fact: '📌',
182
- chunk: '📝',
183
- history: '📜'
195
+ memory: '📝',
196
+ code: '💻',
197
+ fact: '📌'
184
198
  };
185
199
  return icons[type] || '•';
186
200
  }
187
201
 
202
+ function getTypeLabel(type) {
203
+ const labels = {
204
+ memory: 'Memory Notes',
205
+ code: 'Code',
206
+ fact: 'Facts'
207
+ };
208
+ return labels[type] || type;
209
+ }
210
+
188
211
  function formatItem(item) {
189
212
  let text = '';
190
213
 
214
+ // File path with line numbers
191
215
  if (item.file) {
192
- text += `${item.file}`;
216
+ text += `📄 ${item.file}`;
193
217
  if (item.lines && item.lines[0]) {
194
218
  text += `:${item.lines[0]}`;
195
219
  if (item.lines[1] && item.lines[1] !== item.lines[0]) {
@@ -199,12 +223,20 @@ function formatItem(item) {
199
223
  text += '\n';
200
224
  }
201
225
 
202
- // Truncate content
203
- const content = item.content?.substring(0, 150) || '';
204
- text += ` ${content}${content.length >= 150 ? '...' : ''}`;
205
-
206
- text += `\n Score: ${(item.score * 100).toFixed(0)}%`;
207
-
226
+ // Content preview (truncate)
227
+ const content = item.content?.substring(0, 200)?.replace(/\n/g, ' ') || '';
228
+ text += ` ${content}${content.length >= 200 ? '...' : ''}\n`;
229
+
230
+ // Score with breakdown
231
+ const scorePct = (item.score * 100).toFixed(1);
232
+ text += ` Score: ${scorePct}%`;
233
+
234
+ if (item.vectorScore !== undefined && item.textScore !== undefined) {
235
+ const vPct = (item.vectorScore * 100).toFixed(0);
236
+ const tPct = (item.textScore * 100).toFixed(0);
237
+ text += ` (vector: ${vPct}%, text: ${tPct}%)`;
238
+ }
239
+
208
240
  if (item.source) {
209
241
  text += ` • ${item.source}`;
210
242
  }