@mrxkun/mcfast-mcp 4.1.14 → 4.2.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.
package/README.md CHANGED
@@ -29,7 +29,18 @@
29
29
 
30
30
  ---
31
31
 
32
- ## 📦 Current Version: v4.1.10
32
+ ## 📦 Current Version: v4.2.0
33
+
34
+ ### What's New in v4.2.0 🎉
35
+ - **Minor Version Release**: UI & system update for Project Bootstrapping.
36
+ - **Configurable Bootstrap Mode**: Choose between `Local`, `Cloud`, and `Hybrid` analysis modes for new projects via Dashboard or Environment.
37
+ - **Schema Update**: Added `bootstrap_mode` column to `user_settings`.
38
+
39
+ ### What's New in v4.1.18 🚀
40
+ - **Local Project Scanner**: Zero-config bootstrap! Automatically understands project name, type, tech stack, and API routes without internet/API.
41
+ - **Memory Compaction**: Automatic self-maintenance. Compacts old logs into `MEMORY.md` and trims archives when storage exceeds limits.
42
+ - **100% MCP Compliance**: Fully audited stdio communication and lifecycle management (no more JSON-RPC corruption or EOF errors).
43
+ - **Tool Enhancements**: `memory_get` now supports `type='intelligence'` and `type='compact'`.
33
44
 
34
45
  ### What's New in v4.1.10 🐛
35
46
  - **Bug Fixes**: Fixed `getCuratedMemories()` and `getIntelligenceStats()` missing methods
@@ -47,6 +58,7 @@
47
58
  - **Two-Tier Memory**: Daily logs + Curated memory
48
59
  - **Hybrid Search**: Vector 70% + BM25 30% for 90%+ accuracy
49
60
  - **File Watcher**: Auto-indexes files with debounced 1.5s delay
61
+ - **Local Bootstrapping**: Zero-config initial project scan logic.
50
62
 
51
63
  ---
52
64
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrxkun/mcfast-mcp",
3
- "version": "4.1.14",
3
+ "version": "4.2.0",
4
4
  "description": "Ultra-fast code editing with WASM acceleration, fuzzy patching, multi-layer caching, and 8 unified tools. v4.1.12: Implement proper MCP stdio transport lifecycle and cleanup to prevent zombie processes.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/index.js CHANGED
@@ -8,7 +8,10 @@
8
8
  // CRITICAL: Suppress ALL console output in MCP mode to prevent JSON parsing errors
9
9
  // MCP protocol requires stdout to contain ONLY JSON-RPC messages
10
10
 
11
- // ANSI Color Codes for Terminal Output (MUST be defined before any usage)
11
+ import os from 'os';
12
+ import crypto from 'crypto';
13
+
14
+ // ANSI Color Codes for Terminal Output
12
15
  const colors = {
13
16
  reset: '\x1b[0m',
14
17
  bold: '\x1b[1m',
@@ -23,17 +26,11 @@ const colors = {
23
26
  bgBlack: '\x1b[40m',
24
27
  };
25
28
 
26
- // Override console.log - suppress in MCP mode
27
- const originalConsoleLog = console.log;
28
- console.log = function (...args) {
29
- // Suppressed in MCP mode
30
- };
31
-
32
- // Keep console.error enabled for debugging and MCP logging
33
- // const originalConsoleError = console.error;
34
- // console.error = function(...args) {
35
- // // Suppressed in MCP mode
36
- // };
29
+ // MCP SPEC: stdout must contain ONLY JSON-RPC messages.
30
+ // Redirect console.log to stderr so native module warnings (prebuild-install, etc.)
31
+ // don't corrupt the JSON-RPC stream.
32
+ const _origConsoleLog = console.log;
33
+ console.log = (...args) => process.stderr.write(args.join(' ') + '\n');
37
34
 
38
35
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
39
36
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
@@ -81,8 +78,11 @@ const execAsync = promisify(exec);
81
78
 
82
79
  // ============================================================================
83
80
  // LOCK FILE MECHANISM - Prevent multiple instances running simultaneously
81
+ // Use home directory to avoid permission issues in project root or systems where CWD is /
84
82
  // ============================================================================
85
- const LOCK_FILE_PATH = path.join(process.cwd(), '.mcfast', '.mcp-instance.lock');
83
+ const LOCK_DIR = path.join(os.homedir(), '.mcfast', 'locks');
84
+ const projectHash = crypto.createHash('md5').update(process.cwd()).digest('hex');
85
+ const LOCK_FILE_PATH = path.join(LOCK_DIR, `${projectHash}.lock`);
86
86
  let lockFileHandle = null;
87
87
 
88
88
  async function acquireLock() {
@@ -568,13 +568,13 @@ function prettyLogResult(toolName, success, latencyMs, summary) {
568
568
 
569
569
  // Token validation moved to request handlers for better UX (prevents server crash)
570
570
  if (!TOKEN) {
571
- console.error("⚠️ Warning: MCFAST_TOKEN is missing. Tools will fail until configured.");
571
+ console.error("⚠️ Warning: MCFAST_TOKEN is missing. Tools that call the cloud API will fail.");
572
572
  }
573
573
 
574
574
  const server = new Server(
575
575
  {
576
576
  name: "mcfast",
577
- version: "1.3.0",
577
+ version: "4.1.15",
578
578
  },
579
579
  {
580
580
  capabilities: {
@@ -3448,86 +3448,91 @@ async function handleHealthCheck() {
3448
3448
  }
3449
3449
 
3450
3450
  /**
3451
- * Start Server
3451
+ * MCP Server Startup
3452
+ * Following MCP spec: https://spec.modelcontextprotocol.io/specification/
3453
+ *
3454
+ * Lifecycle order per MCP spec:
3455
+ * 1. Acquire lock (prevent duplicate instances)
3456
+ * 2. Create transport
3457
+ * 3. Register signal/error handlers
3458
+ * 4. Start background tasks (memory engine)
3459
+ * 5. Call server.connect(transport) — this MUST be last
3460
+ * 6. Process remains alive via stdio loop managed by SDK
3452
3461
  */
3453
3462
 
3454
- // Acquire lock before starting - prevents multiple instances
3455
- const lockAcquired = await acquireLock();
3456
- if (!lockAcquired) {
3457
- console.error(`${colors.red}[ERROR]${colors.reset} Failed to acquire lock. Another mcfast-mcp instance may be running.`);
3458
- console.error(`${colors.yellow}[HINT]${colors.reset} Delete ${LOCK_FILE_PATH} if you're sure no other instance is running.`);
3459
- process.exit(1);
3460
- }
3461
-
3462
- const transport = new StdioServerTransport();
3463
-
3464
- // NOTE: Do NOT add process.stdin.on('end') handler here.
3465
- // VSCode and some MCP clients send ephemeral EOF / half-close signals during
3466
- // the initialize handshake, which would cause premature gracefulShutdown
3467
- // before the server has a chance to respond, resulting in the
3468
- // "Error: calling 'initialize': EOF" error.
3469
- //
3470
- // The MCP SDK's StdioServerTransport already manages transport lifecycle.
3471
- // Only handle unrecoverable stdout errors.
3472
- process.stdout.on('error', (err) => {
3473
- if (err.code === 'EPIPE' || err.code === 'ERR_STREAM_DESTROYED') {
3474
- console.error('[MCP] stdout closed, shutting down:', err.code);
3475
- gracefulShutdown('stdout.error');
3476
- }
3477
- });
3478
-
3479
- // Pre-initialize memory engine in background
3480
- backgroundInitializeMemoryEngine().catch(err => {
3481
- console.error(`${colors.yellow}[Memory]${colors.reset} Background init error: ${err.message}`);
3482
- // Don't exit - continue without memory
3483
- });
3484
-
3485
- // Graceful shutdown handler
3463
+ // Graceful shutdown - defined BEFORE any signal handlers reference it
3486
3464
  async function gracefulShutdown(signal) {
3487
- console.error(`${colors.yellow}[Shutdown]${colors.reset} Received ${signal}, cleaning up...`);
3465
+ // Guard: avoid re-entrant shutdown calls
3466
+ if (gracefulShutdown._running) return;
3467
+ gracefulShutdown._running = true;
3488
3468
 
3489
- const SHUTDOWN_TIMEOUT = 5000; // 5 seconds per MCP best practices
3469
+ console.error(`[mcfast] Shutting down (${signal})...`);
3470
+
3471
+ const SHUTDOWN_TIMEOUT_MS = 5000; // MCP recommendation
3490
3472
 
3491
3473
  try {
3492
3474
  await Promise.race([
3493
3475
  (async () => {
3494
- // Stop memory engine
3495
3476
  if (memoryEngine && memoryEngineReady) {
3496
- await memoryEngine.cleanup();
3477
+ await memoryEngine.cleanup().catch(() => { });
3497
3478
  }
3498
-
3499
- // Flush audit queue
3500
3479
  const auditQ = getAuditQueue();
3501
- await auditQ.destroy();
3502
-
3503
- // Release lock
3480
+ await auditQ.destroy().catch(() => { });
3504
3481
  await releaseLock();
3505
3482
  })(),
3506
- new Promise(resolve => setTimeout(resolve, SHUTDOWN_TIMEOUT))
3483
+ new Promise(resolve => setTimeout(resolve, SHUTDOWN_TIMEOUT_MS))
3507
3484
  ]);
3508
-
3509
- console.error(`${colors.green}[Shutdown]${colors.reset} Cleanup complete`);
3510
- } catch (error) {
3511
- console.error(`${colors.red}[Shutdown]${colors.reset} Error during cleanup: ${error.message}`);
3485
+ console.error('[mcfast] Shutdown complete.');
3486
+ } catch (err) {
3487
+ console.error('[mcfast] Error during shutdown:', err.message);
3512
3488
  }
3513
3489
 
3514
- // Always exit (even if cleanup fails) to prevent zombie processes
3515
3490
  process.exit(0);
3516
3491
  }
3492
+ gracefulShutdown._running = false;
3517
3493
 
3494
+ // Signal handlers (registered before server.connect)
3518
3495
  process.on('SIGINT', () => gracefulShutdown('SIGINT'));
3519
3496
  process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
3520
3497
 
3521
- // Protect against zombie processes - handle unexpected errors
3498
+ // MCP SPEC: stdout must never receive non-JSON-RPC data.
3499
+ // Only shut down on actual unrecoverable stdout errors, NOT on stdin 'end'.
3500
+ // stdin 'end' can be a transient event during handshake in some MCP clients.
3501
+ process.stdout.on('error', (err) => {
3502
+ if (err.code === 'EPIPE' || err.code === 'ERR_STREAM_DESTROYED') {
3503
+ gracefulShutdown('stdout.EPIPE');
3504
+ }
3505
+ });
3506
+
3507
+ // Catch unhandled errors - log but DO NOT exit on every unhandled rejection
3508
+ // (memory scan errors etc. shouldn't kill the whole server)
3522
3509
  process.on('uncaughtException', (err) => {
3523
- console.error('[MCP] Uncaught exception:', err);
3524
- gracefulShutdown('uncaughtException');
3510
+ console.error('[mcfast] Uncaught exception:', err.message);
3511
+ // Only exit for truly fatal errors
3512
+ if (err.code === 'ERR_USE_AFTER_CLOSE' || err.code === 'ERR_SERVER_DESTROYED') {
3513
+ gracefulShutdown('uncaughtException');
3514
+ }
3525
3515
  });
3526
3516
 
3527
- process.on('unhandledRejection', (reason, promise) => {
3528
- console.error('[MCP] Unhandled rejection:', reason);
3529
- gracefulShutdown('unhandledRejection');
3517
+ process.on('unhandledRejection', (reason) => {
3518
+ // Log but don't exit — background tasks (memory, scan) may reject harmlessly
3519
+ console.error('[mcfast] Unhandled rejection:', reason?.message || reason);
3530
3520
  });
3531
3521
 
3522
+ // Acquire instance lock
3523
+ const lockAcquired = await acquireLock();
3524
+ if (!lockAcquired) {
3525
+ console.error(`[mcfast] FATAL: Could not acquire lock. Delete ${LOCK_FILE_PATH} if no other instance is running.`);
3526
+ process.exit(1);
3527
+ }
3528
+
3529
+ // Start background memory initialization (non-blocking)
3530
+ backgroundInitializeMemoryEngine().catch(err => {
3531
+ console.error('[mcfast] Memory init error (non-fatal):', err.message);
3532
+ });
3533
+
3534
+ // Connect to MCP transport — MUST be the last startup step per MCP spec
3535
+ const transport = new StdioServerTransport();
3532
3536
  await server.connect(transport);
3533
- console.error("mcfast MCP v1.0.0 running on stdio");
3537
+
3538
+ console.error(`[mcfast] MCP server v4.1.15 ready (pid=${process.pid})`);
@@ -0,0 +1,328 @@
1
+ /**
2
+ * Local Project Scanner (Bootstrap)
3
+ * Tự động scan dự án mới không cần API/internet.
4
+ * Chạy lần đầu khi chưa có "Project Context" trong MEMORY.md
5
+ *
6
+ * Phân tích:
7
+ * - package.json / pyproject.toml / Cargo.toml / go.mod / pom.xml
8
+ * - README.md (nếu có)
9
+ * - Cấu trúc thư mục
10
+ * - Tech stack detection từ dependencies
11
+ * - Entry points
12
+ * - API routes (Next.js / Express / FastAPI patterns)
13
+ */
14
+
15
+ import fs from 'fs/promises';
16
+ import path from 'path';
17
+
18
+ const IGNORED_DIRS = new Set([
19
+ 'node_modules', '.git', 'dist', 'build', '.next', '.nuxt',
20
+ '.turbo', 'coverage', '.mcfast', '__pycache__', 'target',
21
+ '.gradle', 'vendor', '.venv', 'venv', '.cache'
22
+ ]);
23
+
24
+ export class ProjectScanner {
25
+ constructor(projectPath) {
26
+ this.projectPath = projectPath;
27
+ }
28
+
29
+ /**
30
+ * Full scan — returns a structured project overview object
31
+ */
32
+ async scan() {
33
+ const [
34
+ packageInfo,
35
+ readme,
36
+ structure,
37
+ entryPoints,
38
+ routes,
39
+ techStack
40
+ ] = await Promise.all([
41
+ this.readPackageFile(),
42
+ this.readReadme(),
43
+ this.buildStructure(3),
44
+ this.detectEntryPoints(),
45
+ this.detectRoutes(),
46
+ null // will compute after packageInfo
47
+ ]);
48
+
49
+ const stack = this.detectTechStack(packageInfo, structure);
50
+
51
+ return {
52
+ name: packageInfo?.name || path.basename(this.projectPath),
53
+ description: packageInfo?.description || null,
54
+ version: packageInfo?.version || null,
55
+ license: packageInfo?.license || null,
56
+ type: this.detectProjectType(packageInfo, structure),
57
+ technologies: stack,
58
+ entryPoints,
59
+ apiRoutes: routes,
60
+ structure,
61
+ readme: readme ? readme.substring(0, 1500) : null, // cap size
62
+ generatedAt: new Date().toISOString()
63
+ };
64
+ }
65
+
66
+ /**
67
+ * Read main package descriptor (package.json, pyproject.toml, etc.)
68
+ */
69
+ async readPackageFile() {
70
+ // Node.js
71
+ try {
72
+ const content = await fs.readFile(
73
+ path.join(this.projectPath, 'package.json'), 'utf-8'
74
+ );
75
+ return JSON.parse(content);
76
+ } catch { }
77
+
78
+ // Try workspace root if monorepo
79
+ try {
80
+ const entries = await fs.readdir(this.projectPath, { withFileTypes: true });
81
+ const pkgDirs = entries.filter(e => e.isDirectory() && !IGNORED_DIRS.has(e.name));
82
+ for (const dir of pkgDirs.slice(0, 5)) {
83
+ try {
84
+ const content = await fs.readFile(
85
+ path.join(this.projectPath, dir.name, 'package.json'), 'utf-8'
86
+ );
87
+ return JSON.parse(content); // return first found
88
+ } catch { }
89
+ }
90
+ } catch { }
91
+
92
+ return null;
93
+ }
94
+
95
+ /**
96
+ * Read README file
97
+ */
98
+ async readReadme() {
99
+ const candidates = ['README.md', 'Readme.md', 'readme.md', 'README.txt'];
100
+ for (const name of candidates) {
101
+ try {
102
+ return await fs.readFile(path.join(this.projectPath, name), 'utf-8');
103
+ } catch { }
104
+ }
105
+ return null;
106
+ }
107
+
108
+ /**
109
+ * Build directory structure tree
110
+ */
111
+ async buildStructure(maxDepth = 3, dir = this.projectPath, depth = 0, prefix = '') {
112
+ if (depth >= maxDepth) return '';
113
+ const lines = [];
114
+ try {
115
+ const entries = await fs.readdir(dir, { withFileTypes: true });
116
+ const filtered = entries
117
+ .filter(e => !IGNORED_DIRS.has(e.name) && !e.name.startsWith('.'))
118
+ .sort((a, b) => {
119
+ if (a.isDirectory() && !b.isDirectory()) return -1;
120
+ if (!a.isDirectory() && b.isDirectory()) return 1;
121
+ return a.name.localeCompare(b.name);
122
+ })
123
+ .slice(0, 20); // limit entries per dir
124
+
125
+ for (let i = 0; i < filtered.length; i++) {
126
+ const e = filtered[i];
127
+ const isLast = i === filtered.length - 1;
128
+ lines.push(prefix + (isLast ? '└── ' : '├── ') + e.name + (e.isDirectory() ? '/' : ''));
129
+ if (e.isDirectory()) {
130
+ const sub = await this.buildStructure(
131
+ maxDepth,
132
+ path.join(dir, e.name),
133
+ depth + 1,
134
+ prefix + (isLast ? ' ' : '│ ')
135
+ );
136
+ if (sub) lines.push(sub);
137
+ }
138
+ }
139
+ } catch { }
140
+ return lines.join('\n');
141
+ }
142
+
143
+ /**
144
+ * Detect entry points
145
+ */
146
+ async detectEntryPoints() {
147
+ const candidates = [
148
+ 'src/index.ts', 'src/index.js', 'src/main.ts', 'src/main.js',
149
+ 'src/app.ts', 'src/app.js', 'index.ts', 'index.js',
150
+ 'server.ts', 'server.js', 'main.py', 'app.py', 'main.go',
151
+ 'app/layout.tsx', 'app/page.tsx', 'pages/index.tsx', 'pages/index.js'
152
+ ];
153
+ const found = [];
154
+ for (const f of candidates) {
155
+ try {
156
+ await fs.access(path.join(this.projectPath, f));
157
+ found.push(f);
158
+ } catch { }
159
+ }
160
+ return found;
161
+ }
162
+
163
+ /**
164
+ * Detect API routes (Next.js App Router, Pages Router, Express)
165
+ */
166
+ async detectRoutes() {
167
+ const routes = [];
168
+
169
+ // Next.js App Router: app/api/**/route.ts
170
+ const appApiDir = path.join(this.projectPath, 'app', 'api');
171
+ const appRoutes = await this.scanRouteDir(appApiDir, 'app/api');
172
+ routes.push(...appRoutes);
173
+
174
+ // Next.js Pages Router: pages/api/**
175
+ const pagesApiDir = path.join(this.projectPath, 'pages', 'api');
176
+ const pagesRoutes = await this.scanRouteDir(pagesApiDir, 'pages/api');
177
+ routes.push(...pagesRoutes);
178
+
179
+ // src/routes (Express-style)
180
+ const srcRoutesDir = path.join(this.projectPath, 'src', 'routes');
181
+ const srcRoutes = await this.scanRouteDir(srcRoutesDir, 'src/routes');
182
+ routes.push(...srcRoutes);
183
+
184
+ return routes.slice(0, 30);
185
+ }
186
+
187
+ async scanRouteDir(dir, prefix) {
188
+ const routes = [];
189
+ try {
190
+ const walk = async (d, rel) => {
191
+ const entries = await fs.readdir(d, { withFileTypes: true }).catch(() => []);
192
+ for (const e of entries) {
193
+ if (e.isDirectory()) {
194
+ await walk(path.join(d, e.name), `${rel}/${e.name}`);
195
+ } else if (/\.(ts|js)$/.test(e.name)) {
196
+ routes.push(`${rel}/${e.name}`);
197
+ }
198
+ }
199
+ };
200
+ await walk(dir, prefix);
201
+ } catch { }
202
+ return routes;
203
+ }
204
+
205
+ /**
206
+ * Detect tech stack from package.json dependencies
207
+ */
208
+ detectTechStack(packageJson, structure) {
209
+ const stack = {
210
+ languages: [],
211
+ frameworks: [],
212
+ databases: [],
213
+ testing: [],
214
+ tools: []
215
+ };
216
+
217
+ const deps = {
218
+ ...(packageJson?.dependencies || {}),
219
+ ...(packageJson?.devDependencies || {})
220
+ };
221
+ const depNames = Object.keys(deps);
222
+
223
+ // Languages
224
+ if (depNames.some(d => d === 'typescript' || d === 'ts-node')) stack.languages.push('TypeScript');
225
+ if (packageJson) stack.languages.push('JavaScript');
226
+ if (structure?.includes('main.py') || structure?.includes('app.py')) stack.languages.push('Python');
227
+ if (structure?.includes('main.go')) stack.languages.push('Go');
228
+ if (structure?.includes('Cargo.toml')) stack.languages.push('Rust');
229
+
230
+ // Frameworks
231
+ if (depNames.includes('next')) stack.frameworks.push('Next.js');
232
+ if (depNames.includes('react')) stack.frameworks.push('React');
233
+ if (depNames.includes('vue')) stack.frameworks.push('Vue.js');
234
+ if (depNames.includes('express')) stack.frameworks.push('Express');
235
+ if (depNames.includes('fastify')) stack.frameworks.push('Fastify');
236
+ if (depNames.includes('hono')) stack.frameworks.push('Hono');
237
+ if (depNames.includes('nuxt')) stack.frameworks.push('Nuxt');
238
+ if (depNames.includes('svelte')) stack.frameworks.push('Svelte');
239
+ if (depNames.includes('electron')) stack.frameworks.push('Electron');
240
+
241
+ // Databases
242
+ if (depNames.some(d => /pg|postgres|postgresql/.test(d))) stack.databases.push('PostgreSQL');
243
+ if (depNames.some(d => /mysql|mysql2/.test(d))) stack.databases.push('MySQL');
244
+ if (depNames.includes('better-sqlite3') || depNames.includes('sqlite3')) stack.databases.push('SQLite');
245
+ if (depNames.some(d => /mongoose|mongodb/.test(d))) stack.databases.push('MongoDB');
246
+ if (depNames.some(d => /redis|ioredis/.test(d))) stack.databases.push('Redis');
247
+ if (depNames.includes('prisma') || depNames.includes('@prisma/client')) stack.databases.push('Prisma ORM');
248
+ if (depNames.includes('drizzle-orm')) stack.databases.push('Drizzle ORM');
249
+
250
+ // Testing
251
+ if (depNames.includes('jest') || depNames.includes('@jest/core')) stack.testing.push('Jest');
252
+ if (depNames.includes('vitest')) stack.testing.push('Vitest');
253
+ if (depNames.includes('playwright') || depNames.includes('@playwright/test')) stack.testing.push('Playwright');
254
+ if (depNames.includes('cypress')) stack.testing.push('Cypress');
255
+
256
+ // Tools
257
+ if (depNames.includes('turbo') || structure?.includes('turbo.json')) stack.tools.push('Turborepo');
258
+ if (depNames.includes('vite')) stack.tools.push('Vite');
259
+ if (depNames.includes('webpack')) stack.tools.push('Webpack');
260
+ if (structure?.includes('docker-compose')) stack.tools.push('Docker');
261
+
262
+ return stack;
263
+ }
264
+
265
+ /**
266
+ * Detect project type
267
+ */
268
+ detectProjectType(packageJson, structure) {
269
+ if (structure?.includes('app/page.tsx') || structure?.includes('app/layout.tsx')) return 'Next.js App';
270
+ if (packageJson?.dependencies?.next) return 'Next.js';
271
+ if (packageJson?.dependencies?.electron) return 'Electron App';
272
+ if (packageJson?.dependencies?.react) return 'React App';
273
+ if (packageJson?.dependencies?.express || packageJson?.dependencies?.fastify) return 'Node.js API';
274
+ if (structure?.includes('main.py')) return 'Python App';
275
+ if (structure?.includes('Cargo.toml')) return 'Rust Project';
276
+ if (structure?.includes('go.mod')) return 'Go Project';
277
+ const name = packageJson?.name || '';
278
+ if (name.includes('mcp') || name.includes('mcfast')) return 'MCP Server';
279
+ return 'Node.js Project';
280
+ }
281
+
282
+ /**
283
+ * Generate MEMORY.md Project Context section from scan result
284
+ */
285
+ buildMemorySection(scan) {
286
+ const date = new Date().toISOString().split('T')[0];
287
+ const lines = [
288
+ `<!-- Auto-generated by mcfast local scanner on ${date} -->`,
289
+ `- **${date}** Project: **${scan.name}**`,
290
+ ` - Type: ${scan.type}`,
291
+ ];
292
+
293
+ if (scan.description) lines.push(` - Description: ${scan.description}`);
294
+ if (scan.version) lines.push(` - Version: ${scan.version}`);
295
+
296
+ const { languages, frameworks, databases, testing, tools } = scan.technologies;
297
+ if (languages.length) lines.push(` - Languages: ${[...new Set(languages)].join(', ')}`);
298
+ if (frameworks.length) lines.push(` - Frameworks: ${frameworks.join(', ')}`);
299
+ if (databases.length) lines.push(` - Databases: ${databases.join(', ')}`);
300
+ if (testing.length) lines.push(` - Testing: ${testing.join(', ')}`);
301
+ if (tools.length) lines.push(` - Tools: ${tools.join(', ')}`);
302
+
303
+ if (scan.entryPoints.length) {
304
+ lines.push(` - Entry Points: ${scan.entryPoints.join(', ')}`);
305
+ }
306
+
307
+ if (scan.apiRoutes.length) {
308
+ lines.push(` - API Routes (${scan.apiRoutes.length}): ${scan.apiRoutes.slice(0, 5).join(', ')}${scan.apiRoutes.length > 5 ? ` ...+${scan.apiRoutes.length - 5} more` : ''}`);
309
+ }
310
+
311
+ if (scan.readme) {
312
+ // Extract first paragraph of README as summary
313
+ const firstPara = scan.readme
314
+ .replace(/^#+.*\n/gm, '')
315
+ .replace(/!\[.*?\]\(.*?\)/g, '')
316
+ .replace(/\[([^\]]+)\]\([^)]+\)/g, '$1')
317
+ .split('\n\n')[0]
318
+ .trim();
319
+ if (firstPara && firstPara.length > 20) {
320
+ lines.push(` - README summary: ${firstPara.substring(0, 300)}`);
321
+ }
322
+ }
323
+
324
+ return lines.join('\n');
325
+ }
326
+ }
327
+
328
+ export default ProjectScanner;