@mrxkun/mcfast-mcp 4.1.10 → 4.1.12

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,491 @@
1
+ /**
2
+ * project_analyze Tool
3
+ * Analyzes project structure and generates comprehensive overview
4
+ * Calls mcfast API to use Mercury for AI-powered analysis
5
+ */
6
+
7
+ import fs from 'fs/promises';
8
+ import path from 'path';
9
+ import { MemoryEngine } from '../memory/index.js';
10
+
11
+ const API_URL = process.env.MCFAST_API_URL || 'https://mcfast.vercel.app/api/v1';
12
+
13
+ /**
14
+ * Execute project analysis
15
+ * @param {Object} args - Tool arguments
16
+ * @param {boolean} [args.force=false] - Force re-analysis
17
+ * @param {boolean} [args.updateMemory=true] - Update MEMORY.md with results
18
+ * @returns {Promise<Object>} Analysis results
19
+ */
20
+ export async function execute(args) {
21
+ const {
22
+ force = false,
23
+ updateMemory = true
24
+ } = args;
25
+
26
+ const startTime = Date.now();
27
+ const projectPath = process.cwd();
28
+
29
+ try {
30
+ // Check for API key
31
+ const apiKey = process.env.MCFAST_TOKEN;
32
+ if (!apiKey) {
33
+ return {
34
+ content: [{
35
+ type: 'text',
36
+ text: `❌ MCFAST_TOKEN not configured
37
+
38
+ To use project analysis, set MCFAST_TOKEN:
39
+ 1. Get your token from https://mcfast.vercel.app
40
+ 2. Set environment variable: MCFAST_TOKEN=your_token
41
+
42
+ Alternatively, use local-only mode with basic detection.`
43
+ }],
44
+ isError: true
45
+ };
46
+ }
47
+
48
+ console.error('[project_analyze] Starting analysis...');
49
+ console.error('[project_analyze] Project path:', projectPath);
50
+
51
+ // Gather project information
52
+ const projectInfo = await gatherProjectInfo(projectPath);
53
+
54
+ // Call mcfast API for analysis
55
+ const analysisResult = await callProjectAnalyzeAPI(projectInfo, apiKey, force);
56
+
57
+ // Update MEMORY.md if requested
58
+ let memoryUpdated = false;
59
+ if (updateMemory && analysisResult.success) {
60
+ memoryUpdated = await updateMemoryWithAnalysis(projectPath, analysisResult.analysis);
61
+ }
62
+
63
+ const latencyMs = Date.now() - startTime;
64
+
65
+ // Format output
66
+ const output = formatAnalysisResult(analysisResult.analysis, latencyMs, memoryUpdated);
67
+
68
+ return {
69
+ content: [{
70
+ type: 'text',
71
+ text: output
72
+ }],
73
+ metadata: {
74
+ analysis: analysisResult.analysis,
75
+ cached: analysisResult.cached,
76
+ latencyMs,
77
+ memoryUpdated,
78
+ projectPath
79
+ }
80
+ };
81
+
82
+ } catch (error) {
83
+ console.error('[project_analyze] Error:', error);
84
+ return {
85
+ content: [{
86
+ type: 'text',
87
+ text: `❌ Project analysis failed: ${error.message}`
88
+ }],
89
+ isError: true
90
+ };
91
+ }
92
+ }
93
+
94
+ /**
95
+ * Gather project information for analysis
96
+ */
97
+ async function gatherProjectInfo(projectPath) {
98
+ console.error('[project_analyze] Gathering project info...');
99
+
100
+ // Read package.json
101
+ let packageJson = null;
102
+ try {
103
+ const packageJsonPath = path.join(projectPath, 'package.json');
104
+ const content = await fs.readFile(packageJsonPath, 'utf-8');
105
+ packageJson = JSON.parse(content);
106
+ } catch (e) {
107
+ console.warn('[project_analyze] No package.json found');
108
+ }
109
+
110
+ // Get file structure
111
+ const fileStructure = await getFileStructure(projectPath);
112
+
113
+ // Read key files
114
+ const keyFiles = await readKeyFiles(projectPath, fileStructure);
115
+
116
+ return {
117
+ projectPath,
118
+ packageJson,
119
+ fileStructure,
120
+ keyFiles
121
+ };
122
+ }
123
+
124
+ /**
125
+ * Get file structure as tree
126
+ */
127
+ async function getFileStructure(dir, depth = 3, prefix = '') {
128
+ const lines = [];
129
+
130
+ try {
131
+ const entries = await fs.readdir(dir, { withFileTypes: true });
132
+ const ignored = ['node_modules', '.git', 'dist', 'build', '.next', '.mcfast', 'coverage'];
133
+
134
+ const filtered = entries
135
+ .filter(e => !ignored.includes(e.name) && !e.name.startsWith('.'))
136
+ .sort((a, b) => {
137
+ if (a.isDirectory() && !b.isDirectory()) return -1;
138
+ if (!a.isDirectory() && b.isDirectory()) return 1;
139
+ return a.name.localeCompare(b.name);
140
+ });
141
+
142
+ for (let i = 0; i < filtered.length; i++) {
143
+ const entry = filtered[i];
144
+ const isLast = i === filtered.length - 1;
145
+ const connector = isLast ? '└── ' : '├── ';
146
+ const line = prefix + connector + entry.name + (entry.isDirectory() ? '/' : '');
147
+ lines.push(line);
148
+
149
+ if (entry.isDirectory() && depth > 1) {
150
+ const newPrefix = prefix + (isLast ? ' ' : '│ ');
151
+ const subLines = await getFileStructure(
152
+ path.join(dir, entry.name),
153
+ depth - 1,
154
+ newPrefix
155
+ );
156
+ lines.push(...subLines);
157
+ }
158
+ }
159
+ } catch (e) {
160
+ // Ignore errors
161
+ }
162
+
163
+ return lines.join('\n');
164
+ }
165
+
166
+ /**
167
+ * Read key files for analysis
168
+ */
169
+ async function readKeyFiles(projectPath, fileStructure) {
170
+ const keyFiles = {};
171
+
172
+ // Priority files to read
173
+ const priorityFiles = [
174
+ 'README.md',
175
+ 'AGENTS.md',
176
+ 'app/page.tsx',
177
+ 'app/layout.tsx',
178
+ 'pages/index.tsx',
179
+ 'pages/index.js',
180
+ 'src/index.ts',
181
+ 'src/index.js',
182
+ 'src/main.ts',
183
+ 'src/app.ts',
184
+ 'server.ts',
185
+ 'server.js',
186
+ 'index.ts',
187
+ 'index.js'
188
+ ];
189
+
190
+ // Find route files
191
+ const routePatterns = [
192
+ /app\/api\/.*\/route\.ts$/,
193
+ /app\/api\/.*\/route\.js$/,
194
+ /pages\/api\/.*\.ts$/,
195
+ /pages\/api\/.*\.js$/,
196
+ /src\/routes\/.*\.ts$/,
197
+ /src\/routes\/.*\.js$/
198
+ ];
199
+
200
+ // Parse file structure to find routes
201
+ const lines = fileStructure.split('\n');
202
+ for (const line of lines) {
203
+ for (const pattern of routePatterns) {
204
+ const match = line.match(/[\w\/\-\.]+/);
205
+ if (match && pattern.test(match[0])) {
206
+ priorityFiles.push(match[0]);
207
+ }
208
+ }
209
+ }
210
+
211
+ // Read files
212
+ for (const filePath of priorityFiles) {
213
+ try {
214
+ const fullPath = path.join(projectPath, filePath);
215
+ const content = await fs.readFile(fullPath, 'utf-8');
216
+ keyFiles[filePath] = content.substring(0, 3000); // Limit size
217
+ } catch (e) {
218
+ // File doesn't exist, skip
219
+ }
220
+ }
221
+
222
+ return keyFiles;
223
+ }
224
+
225
+ /**
226
+ * Call mcfast API for project analysis
227
+ */
228
+ async function callProjectAnalyzeAPI(projectInfo, apiKey, force) {
229
+ const url = `${API_URL}/project/analyze`;
230
+
231
+ console.error('[project_analyze] Calling API:', url);
232
+
233
+ const response = await fetch(url, {
234
+ method: 'POST',
235
+ headers: {
236
+ 'Content-Type': 'application/json',
237
+ 'Authorization': `Bearer ${apiKey}`,
238
+ 'X-MCP-Client': 'mcfast-mcp-v4'
239
+ },
240
+ body: JSON.stringify({
241
+ projectPath: projectInfo.projectPath,
242
+ packageJson: projectInfo.packageJson,
243
+ fileStructure: projectInfo.fileStructure,
244
+ keyFiles: projectInfo.keyFiles,
245
+ forceReanalyze: force
246
+ })
247
+ });
248
+
249
+ if (!response.ok) {
250
+ const error = await response.json().catch(() => ({ error: 'Unknown error' }));
251
+ throw new Error(error.error || `API error: ${response.status}`);
252
+ }
253
+
254
+ return response.json();
255
+ }
256
+
257
+ /**
258
+ * Update MEMORY.md with analysis results
259
+ */
260
+ async function updateMemoryWithAnalysis(projectPath, analysis) {
261
+ try {
262
+ const memoryPath = path.join(projectPath, '.mcfast', 'MEMORY.md');
263
+
264
+ // Read existing memory or create new
265
+ let content = '';
266
+ try {
267
+ content = await fs.readFile(memoryPath, 'utf-8');
268
+ } catch (e) {
269
+ content = getDefaultMemoryTemplate();
270
+ }
271
+
272
+ // Build project context section
273
+ const projectContext = buildProjectContextSection(analysis);
274
+
275
+ // Update Project Context section
276
+ content = updateSection(content, 'Project Context', projectContext);
277
+
278
+ // Update timestamp
279
+ content = content.replace(
280
+ /\*Last updated:.*\*/,
281
+ `*Last updated: ${new Date().toISOString()}*`
282
+ );
283
+
284
+ // Ensure directory exists
285
+ await fs.mkdir(path.dirname(memoryPath), { recursive: true });
286
+ await fs.writeFile(memoryPath, content);
287
+
288
+ console.error('[project_analyze] Updated MEMORY.md');
289
+ return true;
290
+ } catch (error) {
291
+ console.error('[project_analyze] Failed to update MEMORY.md:', error.message);
292
+ return false;
293
+ }
294
+ }
295
+
296
+ /**
297
+ * Build Project Context section content
298
+ */
299
+ function buildProjectContextSection(analysis) {
300
+ const lines = [];
301
+ const timestamp = new Date().toISOString().split('T')[0];
302
+
303
+ // Project overview
304
+ if (analysis.projectOverview) {
305
+ const { name, description, purpose, type } = analysis.projectOverview;
306
+ lines.push(`- **${timestamp}** Project: **${name || 'Unknown'}**`);
307
+ if (type) lines.push(` - Type: ${type}`);
308
+ if (description) lines.push(` - Description: ${description}`);
309
+ if (purpose) lines.push(` - Purpose: ${purpose}`);
310
+ }
311
+
312
+ // Technologies
313
+ if (analysis.technologies) {
314
+ const { languages, frameworks, databases } = analysis.technologies;
315
+ if (languages?.length) lines.push(` - Languages: ${languages.join(', ')}`);
316
+ if (frameworks?.length) lines.push(` - Frameworks: ${frameworks.join(', ')}`);
317
+ if (databases?.length) lines.push(` - Databases: ${databases.join(', ')}`);
318
+ }
319
+
320
+ // API Endpoints
321
+ if (analysis.apiEndpoints?.length > 0) {
322
+ lines.push('');
323
+ lines.push(`- **API Endpoints** (${analysis.apiEndpoints.length} found):`);
324
+ analysis.apiEndpoints.slice(0, 10).forEach(ep => {
325
+ lines.push(` - ${ep.method || 'GET'} ${ep.path}: ${ep.description || 'N/A'}`);
326
+ });
327
+ if (analysis.apiEndpoints.length > 10) {
328
+ lines.push(` - ... and ${analysis.apiEndpoints.length - 10} more`);
329
+ }
330
+ }
331
+
332
+ // Main features
333
+ if (analysis.mainFeatures?.length > 0) {
334
+ lines.push('');
335
+ lines.push(`- **Main Features**:`);
336
+ analysis.mainFeatures.slice(0, 5).forEach(f => {
337
+ lines.push(` - ${f.name}: ${f.description || 'N/A'}`);
338
+ });
339
+ }
340
+
341
+ // Entry points
342
+ if (analysis.entryPoints?.length > 0) {
343
+ lines.push('');
344
+ lines.push(`- **Entry Points**: ${analysis.entryPoints.join(', ')}`);
345
+ }
346
+
347
+ return lines.join('\n');
348
+ }
349
+
350
+ /**
351
+ * Update a section in markdown content
352
+ */
353
+ function updateSection(content, sectionName, newContent) {
354
+ const sectionRegex = new RegExp(`(## ${sectionName}\\n[\\s\\S]*?)(?=\\n## |$)`, 's');
355
+ const match = content.match(sectionRegex);
356
+
357
+ if (match) {
358
+ // Update existing section
359
+ return content.replace(
360
+ sectionRegex,
361
+ `## ${sectionName}\n\n${newContent}\n\n`
362
+ );
363
+ } else {
364
+ // Add new section before the footer
365
+ const newSection = `\n## ${sectionName}\n\n${newContent}\n\n`;
366
+ return content.replace(
367
+ /(---\n\*Last updated:.*\*)/,
368
+ `${newSection}$1`
369
+ );
370
+ }
371
+ }
372
+
373
+ /**
374
+ * Get default memory template
375
+ */
376
+ function getDefaultMemoryTemplate() {
377
+ return `# Long-term Memory
378
+
379
+ This file contains curated, persistent knowledge.
380
+
381
+ ## User Preferences
382
+
383
+ <!-- Add user preferences here -->
384
+
385
+ ## Important Decisions
386
+
387
+ <!-- Add important decisions here -->
388
+
389
+ ## Key Contacts
390
+
391
+ <!-- Add key contacts here -->
392
+
393
+ ## Project Context
394
+
395
+ <!-- Add project context here -->
396
+
397
+ ## Lessons Learned
398
+
399
+ <!-- Add lessons learned here -->
400
+
401
+ ---
402
+ *Last updated: ${new Date().toISOString()}*
403
+ `;
404
+ }
405
+
406
+ /**
407
+ * Format analysis result for display
408
+ */
409
+ function formatAnalysisResult(analysis, latencyMs, memoryUpdated) {
410
+ let output = `🔍 Project Analysis Results\n`;
411
+ output += `─`.repeat(50) + `\n\n`;
412
+
413
+ // Project Overview
414
+ if (analysis.projectOverview) {
415
+ const { name, description, purpose, type } = analysis.projectOverview;
416
+ output += `📦 **Project Overview**\n`;
417
+ output += ` Name: ${name || 'Unknown'}\n`;
418
+ output += ` Type: ${type || 'Unknown'}\n`;
419
+ if (description) output += ` Description: ${description}\n`;
420
+ if (purpose) output += ` Purpose: ${purpose}\n`;
421
+ output += `\n`;
422
+ }
423
+
424
+ // Technologies
425
+ if (analysis.technologies) {
426
+ const { languages, frameworks, databases, dependencies } = analysis.technologies;
427
+ output += `🛠️ **Technologies**\n`;
428
+ if (languages?.length) output += ` Languages: ${languages.join(', ')}\n`;
429
+ if (frameworks?.length) output += ` Frameworks: ${frameworks.join(', ')}\n`;
430
+ if (databases?.length) output += ` Databases: ${databases.join(', ')}\n`;
431
+ if (dependencies?.main?.length) {
432
+ output += ` Main Deps: ${dependencies.main.slice(0, 10).join(', ')}\n`;
433
+ }
434
+ output += `\n`;
435
+ }
436
+
437
+ // API Endpoints
438
+ if (analysis.apiEndpoints?.length > 0) {
439
+ output += `🔌 **API Endpoints** (${analysis.apiEndpoints.length} found)\n`;
440
+ analysis.apiEndpoints.slice(0, 15).forEach(ep => {
441
+ const method = (ep.method || 'GET').padEnd(6);
442
+ output += ` ${method} ${ep.path}\n`;
443
+ if (ep.description) {
444
+ output += ` → ${ep.description}\n`;
445
+ }
446
+ });
447
+ if (analysis.apiEndpoints.length > 15) {
448
+ output += ` ... and ${analysis.apiEndpoints.length - 15} more\n`;
449
+ }
450
+ output += `\n`;
451
+ }
452
+
453
+ // Main Features
454
+ if (analysis.mainFeatures?.length > 0) {
455
+ output += `✨ **Main Features**\n`;
456
+ analysis.mainFeatures.forEach((f, i) => {
457
+ output += ` ${i + 1}. ${f.name}`;
458
+ if (f.description) output += ` - ${f.description}`;
459
+ output += `\n`;
460
+ });
461
+ output += `\n`;
462
+ }
463
+
464
+ // Entry Points
465
+ if (analysis.entryPoints?.length > 0) {
466
+ output += `🚀 **Entry Points**: ${analysis.entryPoints.join(', ')}\n\n`;
467
+ }
468
+
469
+ // Recommendations
470
+ if (analysis.recommendations?.length > 0) {
471
+ output += `💡 **Recommendations**\n`;
472
+ analysis.recommendations.forEach(r => {
473
+ output += ` • ${r}\n`;
474
+ });
475
+ output += `\n`;
476
+ }
477
+
478
+ // Footer
479
+ output += `─`.repeat(50) + `\n`;
480
+ output += `⏱️ Analysis time: ${latencyMs}ms\n`;
481
+ if (memoryUpdated) {
482
+ output += `📝 MEMORY.md updated with project context\n`;
483
+ }
484
+ if (analysis.isFallback) {
485
+ output += `⚠️ Analysis was generated locally (Mercury unavailable)\n`;
486
+ }
487
+
488
+ return output;
489
+ }
490
+
491
+ export default { execute };
@@ -16,6 +16,7 @@ export class AuditQueue {
16
16
 
17
17
  // Start flush timer
18
18
  this.flushTimer = setInterval(() => this.flush(), this.flushInterval);
19
+ this.flushTimer.unref(); // Prevent timer from keeping process alive
19
20
 
20
21
  // Flush on process exit
21
22
  process.on('beforeExit', () => this.flushSync());