@mrxkun/mcfast-mcp 4.0.0 → 4.0.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.
@@ -0,0 +1,61 @@
1
+ import chokidar from 'chokidar';
2
+
3
+ // Simple debounce implementation to avoid lodash dependency
4
+ function debounce(func, wait) {
5
+ let timeout;
6
+ return function executedFunction(...args) {
7
+ const later = () => {
8
+ clearTimeout(timeout);
9
+ func(...args);
10
+ };
11
+ clearTimeout(timeout);
12
+ timeout = setTimeout(later, wait);
13
+ };
14
+ }
15
+
16
+ export class FileWatcher {
17
+ constructor(projectPath, memoryEngine) {
18
+ this.projectPath = projectPath;
19
+ this.memory = memoryEngine;
20
+ this.watcher = null;
21
+ this.pendingUpdates = new Map();
22
+ }
23
+
24
+ async start() {
25
+ this.watcher = chokidar.watch(this.projectPath, {
26
+ ignored: ['**/node_modules/**', '**/.git/**', '**/dist/**'],
27
+ persistent: true
28
+ });
29
+
30
+ this.watcher.on('add', path => this.queueUpdate(path, 'add'));
31
+ this.watcher.on('change', path => this.queueUpdate(path, 'change'));
32
+ this.watcher.on('unlink', path => this.handleDelete(path));
33
+
34
+ this.processQueue = debounce(() => this.flushQueue(), 1500);
35
+ }
36
+
37
+ queueUpdate(filePath, type) {
38
+ this.pendingUpdates.set(filePath, { path: filePath, type, timestamp: Date.now() });
39
+ this.processQueue();
40
+ }
41
+
42
+ async flushQueue() {
43
+ const updates = Array.from(this.pendingUpdates.values());
44
+ this.pendingUpdates.clear();
45
+ await Promise.all(updates.map(u => this.processUpdate(u)));
46
+ }
47
+
48
+ async processUpdate(update) {
49
+ console.log(`[Watcher] ${update.type}: ${update.path}`);
50
+ }
51
+
52
+ async handleDelete(filePath) {
53
+ console.log(`[Watcher] Deleted: ${filePath}`);
54
+ }
55
+
56
+ async stop() {
57
+ if (this.watcher) await this.watcher.close();
58
+ }
59
+ }
60
+
61
+ export default FileWatcher;
@@ -0,0 +1,235 @@
1
+ /**
2
+ * memory_get Tool
3
+ * Read content from memory storage
4
+ * Version: 1.0.0
5
+ */
6
+
7
+ const fs = require('fs');
8
+ const path = require('path');
9
+ const os = require('os');
10
+
11
+ /**
12
+ * Execute memory get
13
+ * @param {Object} args - Tool arguments
14
+ * @param {string} args.path - Path to memory file (relative to ~/.mcfast/memory/)
15
+ * @param {number} [args.from=1] - Start line
16
+ * @param {number} [args.lines=50] - Number of lines to read
17
+ * @param {Object} context - Runtime context
18
+ * @returns {Promise<Object>} File content
19
+ */
20
+ async function execute(args, context = {}) {
21
+ const {
22
+ path: relativePath,
23
+ from = 1,
24
+ lines = 50
25
+ } = args;
26
+
27
+ if (!relativePath) {
28
+ return {
29
+ content: [{
30
+ type: "text",
31
+ text: "❌ Error: 'path' parameter is required"
32
+ }],
33
+ isError: true
34
+ };
35
+ }
36
+
37
+ try {
38
+ // Security: Only allow reading from memory directory
39
+ if (!relativePath.startsWith('memory/') && !relativePath.startsWith('/')) {
40
+ return {
41
+ content: [{
42
+ type: "text",
43
+ text: "❌ Error: Invalid path. Must be in memory/ directory"
44
+ }],
45
+ isError: true
46
+ };
47
+ }
48
+
49
+ // Construct full path
50
+ const memoryDir = path.join(os.homedir(), '.mcfast', 'memory');
51
+ const fullPath = path.join(memoryDir, relativePath.replace(/^\//, ''));
52
+
53
+ // Security: Ensure path is within memory directory
54
+ if (!fullPath.startsWith(memoryDir)) {
55
+ return {
56
+ content: [{
57
+ type: "text",
58
+ text: "❌ Error: Path traversal not allowed"
59
+ }],
60
+ isError: true
61
+ };
62
+ }
63
+
64
+ // Check if file exists
65
+ if (!fs.existsSync(fullPath)) {
66
+ // Check if it's a special memory path
67
+ if (relativePath === 'memory/logs' || relativePath === 'logs') {
68
+ return await getMemoryLogs(memoryDir, from, lines);
69
+ }
70
+
71
+ if (relativePath === 'memory/stats' || relativePath === 'stats') {
72
+ return await getMemoryStats(context.store);
73
+ }
74
+
75
+ return {
76
+ content: [{
77
+ type: "text",
78
+ text: `❌ File not found: ${relativePath}`
79
+ }],
80
+ isError: true
81
+ };
82
+ }
83
+
84
+ const stats = fs.statSync(fullPath);
85
+ if (!stats.isFile()) {
86
+ return {
87
+ content: [{
88
+ type: "text",
89
+ text: "❌ Path is not a file"
90
+ }],
91
+ isError: true
92
+ };
93
+ }
94
+
95
+ // Read file content
96
+ const content = fs.readFileSync(fullPath, 'utf8');
97
+ const allLines = content.split('\n');
98
+
99
+ // Extract specific lines
100
+ const startLine = Math.max(1, from);
101
+ const endLine = Math.min(allLines.length, from - 1 + lines);
102
+ const extractedLines = allLines.slice(startLine - 1, endLine);
103
+ const extractedContent = extractedLines.join('\n');
104
+
105
+ // Format output
106
+ const output = `📄 Memory: ${relativePath}\n`;
107
+ output += `Lines: ${startLine}-${endLine} of ${allLines.length}\n`;
108
+ output += '─'.repeat(40) + '\n';
109
+ output += extractedContent;
110
+
111
+ return {
112
+ content: [{
113
+ type: "text",
114
+ text: output
115
+ }],
116
+ metadata: {
117
+ path: relativePath,
118
+ lines: extractedLines.length,
119
+ totalLines: allLines.length
120
+ }
121
+ };
122
+ } catch (error) {
123
+ console.error('[memory_get] Error:', error);
124
+ return {
125
+ content: [{
126
+ type: "text",
127
+ text: `❌ Error reading memory: ${error.message}`
128
+ }],
129
+ isError: true
130
+ };
131
+ }
132
+ }
133
+
134
+ /**
135
+ * Get memory logs (daily logs)
136
+ */
137
+ async function getMemoryLogs(memoryDir, from, lines) {
138
+ const logsDir = path.join(memoryDir, 'logs');
139
+
140
+ if (!fs.existsSync(logsDir)) {
141
+ return {
142
+ content: [{
143
+ type: "text",
144
+ text: "📝 No memory logs found"
145
+ }],
146
+ metadata: { empty: true }
147
+ };
148
+ }
149
+
150
+ // Get all log files
151
+ const logFiles = fs.readdirSync(logsDir)
152
+ .filter(f => f.endsWith('.md'))
153
+ .sort()
154
+ .reverse();
155
+
156
+ if (logFiles.length === 0) {
157
+ return {
158
+ content: [{
159
+ type: "text",
160
+ text: "📝 No memory logs found"
161
+ }],
162
+ metadata: { empty: true }
163
+ };
164
+ }
165
+
166
+ // Read latest logs
167
+ let output = `📝 Memory Logs\n`;
168
+ output += `Found: ${logFiles.length} log file(s)\n`;
169
+ output += '─'.repeat(40) + '\n\n';
170
+
171
+ const filesToRead = logFiles.slice(0, 5);
172
+ for (const file of filesToRead) {
173
+ const filePath = path.join(logsDir, file);
174
+ const content = fs.readFileSync(filePath, 'utf8');
175
+ const lines = content.split('\n').slice(0, 20);
176
+
177
+ output += `📅 ${file.replace('.md', '')}\n`;
178
+ output += lines.join('\n');
179
+ output += '\n\n---\n\n';
180
+ }
181
+
182
+ return {
183
+ content: [{
184
+ type: "text",
185
+ text: output
186
+ }]
187
+ };
188
+ }
189
+
190
+ /**
191
+ * Get memory statistics
192
+ */
193
+ async function getMemoryStats(store) {
194
+ if (!store) {
195
+ return {
196
+ content: [{
197
+ type: "text",
198
+ text: "❌ Memory store not initialized"
199
+ }],
200
+ isError: true
201
+ };
202
+ }
203
+
204
+ try {
205
+ const stats = store.getStats();
206
+ const syncState = store.getSyncState();
207
+
208
+ let output = `📊 Memory Statistics\n`;
209
+ output += '─'.repeat(40) + '\n';
210
+ output += `Indexed Files: ${stats.fileCount}\n`;
211
+ output += `Facts Stored: ${stats.factCount}\n`;
212
+ output += `Code Chunks: ${stats.chunkCount}\n`;
213
+ output += `Edit History: ${stats.editCount}\n\n`;
214
+ output += `Last Sync: ${syncState?.last_sync_time ? new Date(syncState.last_sync_time).toISOString() : 'Never'}\n`;
215
+ output += `Sync Status: ${syncState?.last_sync_status || 'unknown'}\n`;
216
+
217
+ return {
218
+ content: [{
219
+ type: "text",
220
+ text: output
221
+ }],
222
+ metadata: stats
223
+ };
224
+ } catch (error) {
225
+ return {
226
+ content: [{
227
+ type: "text",
228
+ text: `❌ Error getting stats: ${error.message}`
229
+ }],
230
+ isError: true
231
+ };
232
+ }
233
+ }
234
+
235
+ module.exports = { execute };
@@ -0,0 +1,296 @@
1
+ /**
2
+ * memory_search Tool
3
+ * Hybrid search tool combining vector and keyword search
4
+ * Version: 1.0.0
5
+ */
6
+
7
+ const { LocalMemoryStore } = require('./local-store');
8
+
9
+ /**
10
+ * Execute memory search
11
+ * @param {Object} args - Tool arguments
12
+ * @param {string} args.query - Search query
13
+ * @param {string} [args.type='all'] - Filter by type: 'all', 'facts', 'history', 'code'
14
+ * @param {number} [args.maxResults=6] - Maximum results to return
15
+ * @param {number} [args.minScore=0.35] - Minimum score threshold
16
+ * @param {Object} [args.context] - Context information
17
+ * @param {string} [args.context.currentFile] - Current file being edited
18
+ * @param {number} [args.context.currentLine] - Current line number
19
+ * @param {Object} context - Runtime context
20
+ * @returns {Promise<Object>} Search results
21
+ */
22
+ async function execute(args, context = {}) {
23
+ const {
24
+ query,
25
+ type = 'all',
26
+ maxResults = 6,
27
+ minScore = 0.35,
28
+ context: contextInfo = {}
29
+ } = args;
30
+
31
+ const startTime = Date.now();
32
+
33
+ try {
34
+ // Initialize store if not provided
35
+ let store = context.store;
36
+ if (!store) {
37
+ store = new LocalMemoryStore();
38
+ await store.initialize();
39
+ }
40
+
41
+ // Get embedding for query if possible
42
+ let embedding = null;
43
+ try {
44
+ embedding = await getEmbedding(query);
45
+ } catch (e) {
46
+ console.log('[memory_search] Embedding not available, using keyword search only');
47
+ }
48
+
49
+ // Perform hybrid search
50
+ let results;
51
+
52
+ if (type === 'facts') {
53
+ // Search facts table only
54
+ results = await searchFacts(store, query, { maxResults });
55
+ } else if (type === 'history') {
56
+ // Search edit history
57
+ results = await searchHistory(store, query, { maxResults });
58
+ } else if (type === 'code') {
59
+ // Search code chunks only
60
+ results = store.hybridSearch(query, embedding, {
61
+ vectorWeight: 0.7,
62
+ keywordWeight: 0.3,
63
+ maxResults,
64
+ minScore
65
+ });
66
+ } else {
67
+ // Search all (hybrid)
68
+ results = await searchAll(store, query, embedding, { maxResults, minScore });
69
+ }
70
+
71
+ // Format output
72
+ const formattedResults = formatResults(results, type);
73
+
74
+ const latency = Date.now() - startTime;
75
+
76
+ return {
77
+ content: [{
78
+ type: "text",
79
+ text: formattedResults
80
+ }],
81
+ metadata: {
82
+ query,
83
+ type,
84
+ resultCount: results.length,
85
+ latency,
86
+ sources: [...new Set(results.map(r => r.source || 'unknown'))]
87
+ }
88
+ };
89
+ } catch (error) {
90
+ console.error('[memory_search] Error:', error);
91
+ return {
92
+ content: [{
93
+ type: "text",
94
+ text: `❌ Memory search error: ${error.message}`
95
+ }],
96
+ isError: true
97
+ };
98
+ }
99
+ }
100
+
101
+ /**
102
+ * Search facts only
103
+ */
104
+ async function searchFacts(store, query, options) {
105
+ const { maxResults = 10 } = options;
106
+
107
+ const facts = store.searchFacts(query, { maxResults });
108
+
109
+ return facts.map(fact => ({
110
+ type: 'fact',
111
+ content: `${fact.type}: ${fact.name}${fact.signature ? ` (${fact.signature})' : ''}`,
112
+ file: fact.file_path,
113
+ lines: [fact.line_start, fact.line_end],
114
+ score: fact.confidence || 0.8,
115
+ source: 'facts'
116
+ }));
117
+ }
118
+
119
+ /**
120
+ * Search edit history
121
+ */
122
+ async function searchHistory(store, query, options) {
123
+ const { maxResults = 10 } = options;
124
+
125
+ const history = store.getEditHistory({ limit: maxResults * 2 });
126
+
127
+ // Filter by query relevance
128
+ const filtered = history.filter(edit =>
129
+ edit.instruction.toLowerCase().includes(query.toLowerCase()) ||
130
+ edit.files.some(f => f.toLowerCase().includes(query.toLowerCase()))
131
+ );
132
+
133
+ return filtered.slice(0, maxResults).map(edit => ({
134
+ type: 'history',
135
+ content: edit.instruction,
136
+ files: edit.files,
137
+ strategy: edit.strategy,
138
+ success: edit.success,
139
+ timestamp: edit.timestamp,
140
+ score: edit.success ? 0.8 : 0.5,
141
+ source: 'history'
142
+ }));
143
+ }
144
+
145
+ /**
146
+ * Search all sources (hybrid)
147
+ */
148
+ async function searchAll(store, query, embedding, options) {
149
+ const { maxResults = 10, minScore = 0.35 } = options;
150
+
151
+ const results = [];
152
+
153
+ // Get facts
154
+ const facts = await searchFacts(store, query, { maxResults: maxResults * 2 });
155
+ results.push(...facts);
156
+
157
+ // Get code chunks via hybrid search
158
+ if (embedding) {
159
+ const chunks = store.hybridSearch(query, embedding, {
160
+ vectorWeight: 0.7,
161
+ keywordWeight: 0.3,
162
+ maxResults: maxResults * 2,
163
+ minScore: minScore * 0.5 // Lower threshold for chunks
164
+ });
165
+
166
+ results.push(...chunks.map(chunk => ({
167
+ type: 'chunk',
168
+ content: chunk.content,
169
+ file: chunk.filePath,
170
+ lines: [chunk.startLine, chunk.endLine],
171
+ score: chunk.weightedScore || chunk.score,
172
+ source: chunk.source
173
+ })));
174
+ } else {
175
+ // Fallback to keyword search only
176
+ const keywordResults = store.ftsSearch(query, { maxResults: maxResults * 2 });
177
+ results.push(...keywordResults.map(r => ({
178
+ type: 'chunk',
179
+ content: r.content,
180
+ file: r.file_path,
181
+ lines: [r.start_line, r.end_line],
182
+ score: 0.6,
183
+ source: 'keyword'
184
+ })));
185
+ }
186
+
187
+ // Get recent history
188
+ const history = await searchHistory(store, query, { maxResults: Math.floor(maxResults / 2) });
189
+ results.push(...history);
190
+
191
+ // Deduplicate and sort by score
192
+ const uniqueMap = new Map();
193
+ for (const result of results) {
194
+ const key = `${result.type}:${result.file || ''}:${result.content?.substring(0, 50)}`;
195
+ if (!uniqueMap.has(key) || uniqueMap.get(key).score < result.score) {
196
+ uniqueMap.set(key, result);
197
+ }
198
+ }
199
+
200
+ return Array.from(uniqueMap.values())
201
+ .filter(r => r.score >= minScore)
202
+ .sort((a, b) => b.score - a.score)
203
+ .slice(0, maxResults);
204
+ }
205
+
206
+ /**
207
+ * Format results for display
208
+ */
209
+ function formatResults(results, type) {
210
+ if (!results || results.length === 0) {
211
+ return `🔍 Memory Search: No results found`;
212
+ }
213
+
214
+ let output = `🔍 Memory Search Results\n`;
215
+ output += `Found: ${results.length} results\n\n`;
216
+
217
+ // Group by type
218
+ const byType = {};
219
+ for (const result of results) {
220
+ if (!byType[result.type]) byType[result.type] = [];
221
+ byType[result.type].push(result);
222
+ }
223
+
224
+ for (const [resultType, items] of Object.entries(byType)) {
225
+ output += `${getTypeIcon(resultType)} ${resultType.toUpperCase()} (${items.length})\n`;
226
+ output += '─'.repeat(40) + '\n';
227
+
228
+ for (const item of items.slice(0, 5)) {
229
+ output += ` ${formatItem(item)}\n\n`;
230
+ }
231
+ }
232
+
233
+ return output;
234
+ }
235
+
236
+ function getTypeIcon(type) {
237
+ const icons = {
238
+ fact: '📌',
239
+ chunk: '📝',
240
+ history: '📜'
241
+ };
242
+ return icons[type] || '•';
243
+ }
244
+
245
+ function formatItem(item) {
246
+ let text = '';
247
+
248
+ if (item.file) {
249
+ text += `${item.file}`;
250
+ if (item.lines && item.lines[0]) {
251
+ text += `:${item.lines[0]}`;
252
+ if (item.lines[1] && item.lines[1] !== item.lines[0]) {
253
+ text += `-${item.lines[1]}`;
254
+ }
255
+ }
256
+ text += '\n';
257
+ }
258
+
259
+ // Truncate content
260
+ const content = item.content?.substring(0, 150) || '';
261
+ text += ` ${content}${content.length >= 150 ? '...' : ''}`;
262
+
263
+ text += `\n Score: ${(item.score * 100).toFixed(0)}%`;
264
+
265
+ if (item.source) {
266
+ text += ` • ${item.source}`;
267
+ }
268
+
269
+ return text;
270
+ }
271
+
272
+ /**
273
+ * Get embedding for query
274
+ * Placeholder - implement with actual embedding API
275
+ */
276
+ async function getEmbedding(text) {
277
+ // Check if OpenAI is available
278
+ try {
279
+ const { OpenAI } = require('openai');
280
+ if (process.env.OPENAI_API_KEY) {
281
+ const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
282
+ const response = await openai.embeddings.create({
283
+ model: 'text-embedding-3-small',
284
+ input: text
285
+ });
286
+ return new Float32Array(response.data[0].embedding);
287
+ }
288
+ } catch (e) {
289
+ // OpenAI not available
290
+ }
291
+
292
+ // Return null to fallback to keyword search
293
+ return null;
294
+ }
295
+
296
+ module.exports = { execute };