cf-memory-mcp 3.3.1 → 3.4.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.
Files changed (2) hide show
  1. package/bin/cf-memory-mcp.js +145 -104
  2. package/package.json +1 -1
@@ -229,81 +229,89 @@ class CFMemoryMCP {
229
229
  }
230
230
 
231
231
  /**
232
- * Handle local project indexing
232
+ * Handle local project indexing - scans local files and sends them via MCP
233
233
  */
234
234
  async handleIndexProject(message) {
235
- const { project_path, project_name } = message.params.arguments;
235
+ const { project_path, project_name, include_patterns, exclude_patterns } = message.params.arguments;
236
236
  const resolvedPath = path.resolve(project_path);
237
237
  const name = project_name || path.basename(resolvedPath);
238
238
 
239
239
  this.logDebug(`Intercepted index_project for: ${resolvedPath} (${name})`);
240
240
 
241
241
  try {
242
- // 1. Check/Create Project remotely
243
- const projectResult = await this.makeRequest({
244
- jsonrpc: '2.0',
245
- id: `create-proj-${Date.now()}`,
246
- method: 'tools/call',
247
- params: {
248
- name: 'index_project',
249
- arguments: {
250
- project_path: resolvedPath,
251
- project_name: name
252
- }
253
- }
254
- });
255
-
256
- if (projectResult.error) {
257
- throw new Error(`Remote project creation failed: ${projectResult.error.message}`);
258
- }
259
-
260
- // Parse the result to get project ID
261
- let projectId;
262
- try {
263
- const content = JSON.parse(projectResult.result.content[0].text);
264
- projectId = content.project_id;
265
- } catch (e) {
266
- // Fallback: try to find existing project if creation didn't return generic ID
267
- this.logDebug('Could not parse create result, listing projects...');
268
- const listResp = await this.makeAuthenticatedRequest('/api/projects', 'GET');
269
- const existing = listResp.projects?.find(p => p.name === name);
270
- if (existing) {
271
- projectId = existing.id;
272
- } else {
273
- throw new Error('Could not determine project ID');
274
- }
275
- }
276
-
277
- // 2. Scan Local Files
242
+ // 1. Scan Local Files
278
243
  this.logDebug(`Scanning files in ${resolvedPath}...`);
279
- const files = this.scanDirectory(resolvedPath);
244
+ const files = this.scanDirectory(resolvedPath, '', include_patterns, exclude_patterns);
280
245
  this.logDebug(`Found ${files.length} files to index.`);
281
246
 
282
- // 3. Batch Upload
283
- let indexed = 0;
284
- const BATCH_SIZE = 10;
247
+ if (files.length === 0) {
248
+ const response = {
249
+ jsonrpc: '2.0',
250
+ id: message.id,
251
+ result: {
252
+ content: [{
253
+ type: 'text',
254
+ text: JSON.stringify({
255
+ project_id: null,
256
+ project_name: name,
257
+ files_found: 0,
258
+ files_indexed: 0,
259
+ chunks_created: 0,
260
+ status: 'complete',
261
+ message: 'No matching files found in directory'
262
+ }, null, 2)
263
+ }]
264
+ }
265
+ };
266
+ process.stdout.write(JSON.stringify(response) + '\n');
267
+ return;
268
+ }
269
+
270
+ // 2. Send files directly via MCP tool call (batched to avoid timeouts)
271
+ const BATCH_SIZE = 20; // Process 20 files at a time
272
+ let totalIndexed = 0;
273
+ let totalChunks = 0;
274
+ let projectId = null;
285
275
 
286
276
  for (let i = 0; i < files.length; i += BATCH_SIZE) {
287
277
  const batch = files.slice(i, i + BATCH_SIZE);
278
+ this.logDebug(`Processing batch ${Math.floor(i/BATCH_SIZE) + 1}/${Math.ceil(files.length/BATCH_SIZE)} (${batch.length} files)`);
279
+
280
+ const projectResult = await this.makeRequest({
281
+ jsonrpc: '2.0',
282
+ id: `index-batch-${Date.now()}-${i}`,
283
+ method: 'tools/call',
284
+ params: {
285
+ name: 'index_project',
286
+ arguments: {
287
+ project_path: resolvedPath,
288
+ project_name: name,
289
+ files: batch.map(f => ({
290
+ path: f.relativePath,
291
+ content: f.content
292
+ })),
293
+ include_patterns,
294
+ exclude_patterns
295
+ }
296
+ }
297
+ });
298
+
299
+ if (projectResult.error) {
300
+ this.logError(`Batch ${i} failed:`, projectResult.error);
301
+ continue;
302
+ }
303
+
288
304
  try {
289
- const payload = {
290
- files: batch.map(f => ({
291
- path: f.relativePath,
292
- content: f.content
293
- }))
294
- };
295
-
296
- await this.makeAuthenticatedRequest(`/api/projects/${projectId}/files/batch`, 'POST', payload);
297
- indexed += batch.length;
298
-
299
- // Optional: Send progress notification to client (if supported)
300
- // this.logDebug(`Indexed ${indexed}/${files.length}`);
301
- } catch (err) {
302
- this.logError(`Batch upload failed at index ${i}`, err);
305
+ const content = JSON.parse(projectResult.result.content[0].text);
306
+ projectId = content.project_id;
307
+ totalIndexed += content.files_indexed || 0;
308
+ totalChunks += content.chunks_created || 0;
309
+ } catch (e) {
310
+ this.logError('Could not parse batch result:', e);
303
311
  }
304
312
  }
305
313
 
306
- // 4. Return success
314
+ // 3. Return aggregated success
307
315
  const response = {
308
316
  jsonrpc: '2.0',
309
317
  id: message.id,
@@ -312,9 +320,11 @@ class CFMemoryMCP {
312
320
  type: 'text',
313
321
  text: JSON.stringify({
314
322
  project_id: projectId,
323
+ project_name: name,
315
324
  files_found: files.length,
316
- files_indexed: indexed,
317
- status: 'completed'
325
+ files_indexed: totalIndexed,
326
+ chunks_created: totalChunks,
327
+ status: 'complete'
318
328
  }, null, 2)
319
329
  }]
320
330
  }
@@ -336,12 +346,35 @@ class CFMemoryMCP {
336
346
  }
337
347
 
338
348
  /**
339
- * Recursive scan
349
+ * Recursive directory scan with filtering
340
350
  */
341
- scanDirectory(dir, basePath = '') {
351
+ scanDirectory(dir, basePath = '', includePatterns = null, excludePatterns = null) {
342
352
  let files = [];
343
- const EXCLUDE_DIRS = ['node_modules', '.git', 'dist', 'build', '.wrangler', 'coverage'];
344
- const INCLUDE_EXTS = ['.ts', '.tsx', '.js', '.jsx', '.py', '.md', '.sql', '.css', '.html'];
353
+
354
+ // Default exclusions (always excluded)
355
+ const DEFAULT_EXCLUDE_DIRS = [
356
+ 'node_modules', '.git', '.svn', '.hg',
357
+ 'dist', 'build', 'out', '.next', '.nuxt',
358
+ '__pycache__', '.pytest_cache', 'venv', '.venv',
359
+ 'coverage', '.coverage', '.nyc_output',
360
+ '.wrangler', '.turbo', '.cache'
361
+ ];
362
+
363
+ // Default file extensions to include
364
+ const DEFAULT_INCLUDE_EXTS = [
365
+ '.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs',
366
+ '.py', '.rb', '.go', '.rs', '.java', '.kt', '.scala',
367
+ '.cs', '.cpp', '.c', '.h', '.hpp', '.swift', '.php',
368
+ '.sql', '.sh', '.bash',
369
+ '.json', '.yaml', '.yml', '.toml',
370
+ '.html', '.css', '.scss', '.vue', '.svelte',
371
+ '.md', '.mdx'
372
+ ];
373
+
374
+ // Merge custom exclude patterns
375
+ const effectiveExcludes = excludePatterns
376
+ ? [...DEFAULT_EXCLUDE_DIRS, ...excludePatterns]
377
+ : DEFAULT_EXCLUDE_DIRS;
345
378
 
346
379
  try {
347
380
  const entries = fs.readdirSync(dir, { withFileTypes: true });
@@ -351,21 +384,61 @@ class CFMemoryMCP {
351
384
  const relPath = path.join(basePath, entry.name);
352
385
 
353
386
  if (entry.isDirectory()) {
354
- if (!EXCLUDE_DIRS.includes(entry.name)) {
355
- files = files.concat(this.scanDirectory(fullPath, relPath));
387
+ // Check if directory should be excluded
388
+ const shouldExclude = effectiveExcludes.some(pattern => {
389
+ if (pattern.includes('*')) {
390
+ // Simple glob matching
391
+ const regex = new RegExp('^' + pattern.replace(/\*/g, '.*') + '$');
392
+ return regex.test(entry.name);
393
+ }
394
+ return entry.name === pattern;
395
+ });
396
+
397
+ if (!shouldExclude) {
398
+ files = files.concat(this.scanDirectory(fullPath, relPath, includePatterns, excludePatterns));
356
399
  }
357
400
  } else if (entry.isFile()) {
358
- if (INCLUDE_EXTS.includes(path.extname(entry.name).toLowerCase())) {
401
+ const ext = path.extname(entry.name).toLowerCase();
402
+
403
+ // Check if file should be excluded
404
+ const fileExcluded = effectiveExcludes.some(pattern => {
405
+ if (pattern.startsWith('*.')) {
406
+ return ext === pattern.substring(1);
407
+ }
408
+ return entry.name === pattern || relPath.includes(pattern);
409
+ });
410
+
411
+ if (fileExcluded) continue;
412
+
413
+ // Check if file matches include patterns
414
+ let shouldInclude = false;
415
+ if (includePatterns && includePatterns.length > 0) {
416
+ shouldInclude = includePatterns.some(pattern => {
417
+ if (pattern.startsWith('*.')) {
418
+ return ext === pattern.substring(1);
419
+ }
420
+ return entry.name.includes(pattern) || relPath.includes(pattern);
421
+ });
422
+ } else {
423
+ // Use default extensions
424
+ shouldInclude = DEFAULT_INCLUDE_EXTS.includes(ext);
425
+ }
426
+
427
+ if (shouldInclude) {
359
428
  try {
360
429
  const content = fs.readFileSync(fullPath, 'utf8');
361
- // Skip huge files > 1MB
362
- if (content.length < 1024 * 1024) {
430
+ // Skip huge files > 500KB
431
+ if (content.length < 512 * 1024) {
363
432
  files.push({
364
433
  relativePath: relPath,
365
434
  content: content
366
435
  });
436
+ } else {
437
+ this.logDebug(`Skipping large file (${Math.round(content.length/1024)}KB): ${relPath}`);
367
438
  }
368
- } catch (e) { /* ignore read errors */ }
439
+ } catch (e) {
440
+ this.logDebug(`Could not read file: ${relPath}`);
441
+ }
369
442
  }
370
443
  }
371
444
  }
@@ -376,40 +449,7 @@ class CFMemoryMCP {
376
449
  }
377
450
 
378
451
  /**
379
- * Helper for Direct API calls (non-JSON-RPC)
380
- */
381
- async makeAuthenticatedRequest(endpoint, method, body) {
382
- return new Promise((resolve, reject) => {
383
- const serverUrl = 'https://cf-memory-mcp-simplified.johnlam90.workers.dev' + endpoint; // Hardcoded base for simplicity in this helper
384
- const url = new URL(serverUrl);
385
- const postData = body ? JSON.stringify(body) : '';
386
-
387
- const options = {
388
- hostname: url.hostname,
389
- port: 443,
390
- path: url.pathname,
391
- method: method,
392
- headers: {
393
- 'Content-Type': 'application/json',
394
- 'X-API-Key': process.env.CF_MEMORY_API_KEY || API_KEY
395
- }
396
- };
397
-
398
- const req = https.request(options, (res) => {
399
- let data = '';
400
- res.on('data', c => data += c);
401
- res.on('end', () => {
402
- try { resolve(JSON.parse(data)); } catch (e) { resolve({}); }
403
- });
404
- });
405
- req.on('error', reject);
406
- if (postData) req.write(postData);
407
- req.end();
408
- });
409
- }
410
-
411
- /**
412
- * Make HTTP request to the Cloudflare Worker (Standard MCP)
452
+ * Make HTTP request to the Cloudflare Worker (MCP JSON-RPC)
413
453
  */
414
454
  async makeRequest(message) {
415
455
  return new Promise((resolve) => {
@@ -427,7 +467,8 @@ class CFMemoryMCP {
427
467
  'Accept': 'application/json',
428
468
  'Accept-Encoding': 'identity',
429
469
  'User-Agent': this.userAgent,
430
- 'X-API-Key': API_KEY,
470
+ 'Authorization': `Bearer ${API_KEY}`,
471
+ 'X-API-Key': API_KEY, // Also include for backwards compatibility
431
472
  'Content-Length': Buffer.byteLength(postData)
432
473
  },
433
474
  timeout: TIMEOUT_MS
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cf-memory-mcp",
3
- "version": "3.3.1",
3
+ "version": "3.4.0",
4
4
  "description": "Best-in-class MCP (Model Context Protocol) server for AI memory storage with MIRIX-Inspired Specialized Memory Types (Core, Episodic, Semantic, Procedural, Resource, Knowledge Vault), Progressive Disclosure, AI-Powered Summaries, Context Window Optimization, Summary Memory Feature (TL;DR), Enhanced JSON Processing, Temporal Relationship Tracking, AI-Enhanced Context-Aware + Temporal Intelligence, Smart Tool Recommendation Engine, Memory Intelligence Engine, autonomous optimization, A/B testing, self-improving algorithms, intelligent search, smart auto-features, memory collections, project onboarding workflows, advanced lifecycle management, Project Intelligence for seamless agent handoff, and Unified Project Intelligence Tool with 57% performance improvement",
5
5
  "main": "bin/cf-memory-mcp.js",
6
6
  "bin": {