cf-memory-mcp 3.9.8 → 3.9.11

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
@@ -8,9 +8,9 @@ Cloudflare-hosted MCP server for semantic code indexing, retrieval, and assistan
8
8
 
9
9
  - **110+ language support** - TypeScript, Python, Go, Rust, Java, C/C++, Ruby, PHP, Swift, Kotlin, Scala, and 100+ more including shaders (GLSL, HLSL, WGSL, Metal, CUDA)
10
10
  - **Hybrid retrieval** - 3-lane RRF (semantic + keyword + name matching) with cross-encoder reranking
11
- - **100% benchmark accuracy** - 32 queries across behavioral, conceptual, identifier, and negation categories
11
+ - **100% top-3 benchmark accuracy** - 37 queries across behavioral, conceptual, identifier, and negation categories
12
12
  - **Code graph navigation** - Track calls, imports, inheritance across 9 language families (TS/JS, Python, Go, Rust, Java/Kotlin/Scala, C/C++/C#, Ruby, PHP, Swift)
13
- - **21 MCP tools** - Indexing, retrieval, file exploration, code graph, freshness management, and persistent memory
13
+ - **22 MCP tools** - Indexing, retrieval, file exploration, code graph, freshness management, indexing status, and persistent memory
14
14
  - **Self-healing freshness** - Results auto-flag stale chunks with a copy-pasteable `refresh_files` call; `refresh_stale` rebuilds the changed files in seconds; cache invalidates on write
15
15
  - **Self-debugging errors** - `retrieve_context` includes `empty_hint` on 0-result queries; `get_file_content` / `get_file_outline` return `{error, hint}` pointing to the next call instead of bare null; calling bridge-only tools server-side returns `bridge_required` install instructions
16
16
  - **Smart defaults** - Bridge auto-detects project_id from cwd, returns chunks pre-enriched with file imports + citation + source classification
@@ -22,7 +22,7 @@ The active runtime is at [src-simplified/index.ts](src-simplified/index.ts). It
22
22
 
23
23
  - Direct remote MCP on `/mcp`
24
24
  - Legacy compatibility on `/mcp/message`
25
- - Code indexing with `index_project` and `index_github`
25
+ - Code indexing with `index_project` and `index_github`; local bridge indexing returns immediately with a `bridge_job_id`, then `get_indexing_status` reports progress
26
26
  - Retrieval with `retrieve_context` (3-lane hybrid + cross-encoder rerank, returns `file_imports` + `source_kind` + `citation`)
27
27
  - Code relationship graph with `get_related_code` (calls, imports, extends, implements)
28
28
  - Project exploration with `list_files`, `list_projects`, `get_stats`, `get_file_outline`, `get_file_content` (bridge reads local file byte-exact when resolvable; falls back to chunk-reconstruction with `reconstruction_warning` + `missing_line_ranges`)
@@ -102,11 +102,24 @@ const TOOLS_LIST = [
102
102
  properties: {
103
103
  project_path: { type: 'string', description: 'Absolute path to the project root directory' },
104
104
  project_name: { type: 'string', description: 'Display name for the project (defaults to directory basename)' },
105
- force_reindex: { type: 'boolean', description: 'If true, wipes existing chunks and rebuilds from scratch. Use only when needed; incremental is much faster.' }
105
+ force_reindex: { type: 'boolean', description: 'If true, wipes existing chunks and rebuilds from scratch. Use only when needed; incremental is much faster.' },
106
+ allow_system_path: { type: 'boolean', description: 'Allow indexing a path outside the current workspace root. Defaults false for safety.' },
107
+ wait_for_completion: { type: 'boolean', description: 'If true, block until indexing finishes. Default false: return immediately with bridge_job_id and poll get_indexing_status.' }
106
108
  },
107
109
  required: ['project_path']
108
110
  }
109
111
  },
112
+ {
113
+ name: 'get_indexing_status',
114
+ description: 'Check status for an index_project background job. Use bridge_job_id returned by index_project, or project_id as a fallback. Do not poll list_projects for indexing progress.',
115
+ inputSchema: {
116
+ type: 'object',
117
+ properties: {
118
+ bridge_job_id: { type: 'string', description: 'Bridge-local job id returned by index_project' },
119
+ project_id: { type: 'string', description: 'Project ID or name to inspect when bridge_job_id is unavailable' }
120
+ }
121
+ }
122
+ },
110
123
  {
111
124
  name: 'index_github',
112
125
  description: 'Index a public GitHub repository server-side without cloning locally. Use when the user wants to search a remote codebase.',
@@ -369,14 +382,7 @@ class CFMemoryMCP {
369
382
  this.userAgent = `cf-memory-mcp/${PACKAGE_VERSION} (${os.platform()} ${os.arch()}; Node.js ${process.version})`;
370
383
  this.useStreamableHttp = true; // Try Streamable HTTP first
371
384
  this.autoWatcher = null; // Background file watcher
372
-
373
- // Handle process termination gracefully
374
- process.on('SIGINT', () => this.shutdown('SIGINT'));
375
- process.on('SIGTERM', () => this.shutdown('SIGTERM'));
376
- process.on('uncaughtException', (error) => {
377
- this.logError('Uncaught exception:', error);
378
- this.shutdown('ERROR');
379
- });
385
+ this.indexJobs = new Map(); // bridge_job_id -> local background indexing status
380
386
 
381
387
  // Set up stdio encoding
382
388
  process.stdin.setEncoding('utf8');
@@ -414,6 +420,7 @@ class CFMemoryMCP {
414
420
  async start() {
415
421
  try {
416
422
  this.logDebug('Starting MCP message processing...');
423
+ this.installProcessHandlers();
417
424
 
418
425
  // Pre-warm the HTTPS connection in the background so the first
419
426
  // real tool call doesn't pay the TLS handshake cost.
@@ -436,6 +443,18 @@ class CFMemoryMCP {
436
443
  }
437
444
  }
438
445
 
446
+ installProcessHandlers() {
447
+ if (CFMemoryMCP._processHandlersInstalled) return;
448
+ CFMemoryMCP._processHandlersInstalled = true;
449
+
450
+ process.on('SIGINT', () => this.shutdown('SIGINT'));
451
+ process.on('SIGTERM', () => this.shutdown('SIGTERM'));
452
+ process.on('uncaughtException', (error) => {
453
+ this.logError('Uncaught exception:', error);
454
+ this.shutdown('ERROR');
455
+ });
456
+ }
457
+
439
458
  /**
440
459
  * Resolve the current cwd to a project_id in the background and
441
460
  * cache the result, so the first retrieve_context query that
@@ -725,6 +744,14 @@ class CFMemoryMCP {
725
744
  return;
726
745
  }
727
746
 
747
+ // Intercept get_indexing_status: bridge-local background index
748
+ // jobs are not visible to the remote worker until batches land,
749
+ // so status must be served by this bridge process when possible.
750
+ if (message.method === 'tools/call' && message.params && message.params.name === 'get_indexing_status') {
751
+ await this.handleGetIndexingStatus(message);
752
+ return;
753
+ }
754
+
728
755
  // Intercept index_project tool call to perform local scanning
729
756
  if (message.method === 'tools/call' && message.params && message.params.name === 'index_project') {
730
757
  await this.handleIndexProject(message);
@@ -1169,7 +1196,7 @@ class CFMemoryMCP {
1169
1196
 
1170
1197
  async handleIndexProject(message) {
1171
1198
  const args = message.params?.arguments || {};
1172
- const { project_path, project_name, include_patterns, exclude_patterns, force_reindex } = args;
1199
+ const { project_path, project_name, include_patterns, exclude_patterns, force_reindex, allow_system_path, wait_for_completion } = args;
1173
1200
 
1174
1201
  // Boundary validation: bad inputs were producing unhelpful Node errors
1175
1202
  // ("path must be string"). Return a clean MCP error with the hint
@@ -1188,6 +1215,32 @@ class CFMemoryMCP {
1188
1215
 
1189
1216
  const resolvedPath = path.resolve(project_path);
1190
1217
  const name = project_name || path.basename(resolvedPath);
1218
+ const workspaceRoot = (() => {
1219
+ const root = process.env.CF_MEMORY_WATCH_PATH || process.cwd();
1220
+ try { return fs.realpathSync(path.resolve(root)); }
1221
+ catch (_) { return path.resolve(root); }
1222
+ })();
1223
+ const resolvedRealPath = (() => {
1224
+ try { return fs.realpathSync(resolvedPath); }
1225
+ catch (_) { return resolvedPath; }
1226
+ })();
1227
+ const relativeToWorkspace = path.relative(workspaceRoot, resolvedRealPath);
1228
+ const insideWorkspace = relativeToWorkspace && !relativeToWorkspace.startsWith('..') && !path.isAbsolute(relativeToWorkspace);
1229
+ const sameAsWorkspace = relativeToWorkspace === '';
1230
+
1231
+ if (!allow_system_path && !insideWorkspace && !sameAsWorkspace) {
1232
+ process.stdout.write(JSON.stringify({
1233
+ jsonrpc: '2.0',
1234
+ id: message.id,
1235
+ result: { content: [{ type: 'text', text: JSON.stringify({
1236
+ error: 'project_path_outside_workspace',
1237
+ project_path: resolvedPath,
1238
+ workspace_root: workspaceRoot,
1239
+ hint: 'index_project is confined to the current workspace root by default. Run the bridge from that project directory, set CF_MEMORY_WATCH_PATH to that root, or pass allow_system_path:true if you intentionally want to index another readable local directory.',
1240
+ }, null, 2) }] },
1241
+ }) + '\n');
1242
+ return;
1243
+ }
1191
1244
 
1192
1245
  this.logDebug(`Intercepted index_project for: ${resolvedPath} (${name})`);
1193
1246
 
@@ -1244,8 +1297,6 @@ class CFMemoryMCP {
1244
1297
 
1245
1298
  // 2. Create project via MCP (no inline files), then upload files via batch endpoint.
1246
1299
  // This avoids repeating full index_project for every file batch.
1247
- let totalIndexed = 0;
1248
- let totalChunks = 0;
1249
1300
  let projectId = null;
1250
1301
 
1251
1302
  const init = await this.makeRequest({
@@ -1280,84 +1331,54 @@ class CFMemoryMCP {
1280
1331
  // Adaptive batching: limit by file count and payload bytes to reduce timeouts.
1281
1332
  const batches = this.createAdaptiveBatches(files);
1282
1333
 
1283
- let totalSkipped = 0;
1284
- let totalUnchanged = 0;
1285
- const batchErrors = [];
1286
- const failedBatches = [];
1287
-
1288
- // Process batches in parallel (concurrency = 3). Each batch is
1289
- // independent on the server side, so overlapping them gives a
1290
- // ~3x speedup for large projects. Higher concurrency risks
1291
- // overwhelming the Cloudflare Worker / hitting per-account
1292
- // request limits, so we cap conservatively.
1293
- const CONCURRENCY = Math.min(3, batches.length);
1294
- const aggregateBatchResult = (uploadResult, b, batch) => {
1295
- if (!uploadResult) {
1296
- failedBatches.push({ batch: b + 1, files: batch.length, reason: 'no response (timeout/network)' });
1297
- this.logError(`Batch ${b + 1}/${batches.length} returned no response (timeout or network error). ${batch.length} files unaccounted for.`);
1298
- return;
1299
- }
1300
- if (typeof uploadResult.files_indexed === 'number') {
1301
- totalIndexed += uploadResult.files_indexed;
1302
- }
1303
- if (typeof uploadResult.chunks_created === 'number') {
1304
- totalChunks += uploadResult.chunks_created;
1305
- }
1306
- if (typeof uploadResult.files_skipped === 'number') {
1307
- totalSkipped += uploadResult.files_skipped;
1308
- }
1309
- if (typeof uploadResult.files_unchanged === 'number') {
1310
- totalUnchanged += uploadResult.files_unchanged;
1311
- }
1312
- if (Array.isArray(uploadResult.errors) && uploadResult.errors.length > 0) {
1313
- for (const err of uploadResult.errors) {
1314
- batchErrors.push(err);
1315
- }
1316
- }
1317
- };
1334
+ const bridgeJobId = `bridge_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
1335
+ const job = this.createLocalIndexJob({
1336
+ bridgeJobId,
1337
+ projectId,
1338
+ projectName: name,
1339
+ projectPath: resolvedPath,
1340
+ filesFound: files.length,
1341
+ batchesTotal: batches.length,
1342
+ });
1318
1343
 
1319
- for (let i = 0; i < batches.length; i += CONCURRENCY) {
1320
- const window = [];
1321
- for (let j = i; j < Math.min(i + CONCURRENCY, batches.length); j++) {
1322
- const batch = batches[j];
1323
- const approxBytes = batch.reduce((sum, f) => sum + Buffer.byteLength(f.content || '', 'utf8'), 0);
1324
- this.logDebug(`Uploading batch ${j + 1}/${batches.length} (${batch.length} files, ~${Math.round(approxBytes / 1024)}KB)`);
1325
- window.push(
1326
- this.uploadFileBatch(projectId, batch).then(res => aggregateBatchResult(res, j, batch))
1327
- );
1328
- }
1329
- await Promise.all(window);
1330
- }
1344
+ const runPromise = this.runLocalIndexJob(bridgeJobId, {
1345
+ projectId,
1346
+ files,
1347
+ batches,
1348
+ progressStream,
1349
+ });
1350
+ // Ownership transferred to runLocalIndexJob; do not close it in
1351
+ // this request's finally block when the default async mode returns.
1352
+ progressStream = null;
1331
1353
 
1332
- // 3. Cleanup stale files (accuracy): remove server-side files not present locally.
1333
- // Best-effort: failure shouldn't fail indexing.
1334
- if (projectId) {
1335
- try {
1336
- await this.cleanupStaleFiles(projectId, files.map(f => f.relativePath));
1337
- } catch (e) {
1338
- this.logDebug(`Cleanup stale files failed: ${e && e.message ? e.message : 'unknown error'}`);
1339
- }
1354
+ if (wait_for_completion === true) {
1355
+ await runPromise;
1356
+ const responsePayload = this.serializeIndexJob(job);
1357
+ const response = {
1358
+ jsonrpc: '2.0',
1359
+ id: message.id,
1360
+ result: {
1361
+ content: [{
1362
+ type: 'text',
1363
+ text: JSON.stringify(responsePayload, null, 2)
1364
+ }]
1365
+ }
1366
+ };
1367
+ process.stdout.write(JSON.stringify(response) + '\n');
1368
+ return;
1340
1369
  }
1341
1370
 
1342
- // 4. Return aggregated success (with skipped/error visibility)
1343
- const status = failedBatches.length > 0 ? 'partial' : 'complete';
1344
1371
  const responsePayload = {
1345
1372
  project_id: projectId,
1346
1373
  project_name: name,
1374
+ bridge_job_id: bridgeJobId,
1347
1375
  files_found: files.length,
1348
- files_indexed: totalIndexed,
1349
- files_unchanged: totalUnchanged,
1350
- files_skipped: totalSkipped,
1351
- chunks_created: totalChunks,
1352
- status
1376
+ batches_queued: batches.length,
1377
+ status: 'indexing',
1378
+ async: true,
1379
+ message: 'Indexing started in the bridge background. Do not poll list_projects; call get_indexing_status with bridge_job_id.',
1380
+ progress_hint: `Call get_indexing_status with bridge_job_id="${bridgeJobId}" until status is "complete", "partial", or "failed". Retrieval before completion may be partial.`
1353
1381
  };
1354
- if (batchErrors.length > 0) {
1355
- responsePayload.errors = batchErrors.slice(0, 50);
1356
- responsePayload.errors_truncated = batchErrors.length > 50;
1357
- }
1358
- if (failedBatches.length > 0) {
1359
- responsePayload.failed_batches = failedBatches;
1360
- }
1361
1382
  const response = {
1362
1383
  jsonrpc: '2.0',
1363
1384
  id: message.id,
@@ -1389,6 +1410,177 @@ class CFMemoryMCP {
1389
1410
  }
1390
1411
  }
1391
1412
 
1413
+ createLocalIndexJob({ bridgeJobId, projectId, projectName, projectPath, filesFound, batchesTotal }) {
1414
+ const now = new Date().toISOString();
1415
+ const job = {
1416
+ bridge_job_id: bridgeJobId,
1417
+ project_id: projectId,
1418
+ project_name: projectName,
1419
+ project_path: projectPath,
1420
+ status: 'indexing',
1421
+ async: true,
1422
+ files_found: filesFound,
1423
+ files_indexed: 0,
1424
+ files_unchanged: 0,
1425
+ files_skipped: 0,
1426
+ chunks_created: 0,
1427
+ batches_total: batchesTotal,
1428
+ batches_completed: 0,
1429
+ failed_batches: [],
1430
+ errors: [],
1431
+ started_at: now,
1432
+ updated_at: now,
1433
+ completed_at: null,
1434
+ finalized: false,
1435
+ };
1436
+ this.indexJobs.set(bridgeJobId, job);
1437
+ return job;
1438
+ }
1439
+
1440
+ serializeIndexJob(job) {
1441
+ const errors = Array.isArray(job.errors) ? job.errors : [];
1442
+ const failedBatches = Array.isArray(job.failed_batches) ? job.failed_batches : [];
1443
+ return {
1444
+ bridge_job_id: job.bridge_job_id,
1445
+ project_id: job.project_id,
1446
+ project_name: job.project_name,
1447
+ project_path: job.project_path,
1448
+ status: job.status,
1449
+ async: true,
1450
+ progress: {
1451
+ batches_total: job.batches_total,
1452
+ batches_completed: job.batches_completed,
1453
+ percentage: job.batches_total > 0
1454
+ ? Math.round((job.batches_completed / job.batches_total) * 100)
1455
+ : 100,
1456
+ },
1457
+ files_found: job.files_found,
1458
+ files_indexed: job.files_indexed,
1459
+ files_unchanged: job.files_unchanged,
1460
+ files_skipped: job.files_skipped,
1461
+ chunks_created: job.chunks_created,
1462
+ finalized: job.finalized,
1463
+ started_at: job.started_at,
1464
+ updated_at: job.updated_at,
1465
+ completed_at: job.completed_at,
1466
+ errors: errors.length > 0 ? errors.slice(0, 50) : undefined,
1467
+ errors_truncated: errors.length > 50 ? true : undefined,
1468
+ failed_batches: failedBatches.length > 0 ? failedBatches : undefined,
1469
+ hint: job.status === 'indexing'
1470
+ ? `Still indexing. Poll get_indexing_status with bridge_job_id="${job.bridge_job_id}" later; do not poll list_projects.`
1471
+ : 'Indexing finished. retrieve_context can now use the completed index.',
1472
+ };
1473
+ }
1474
+
1475
+ async runLocalIndexJob(bridgeJobId, { projectId, files, batches, progressStream }) {
1476
+ const job = this.indexJobs.get(bridgeJobId);
1477
+ if (!job) return null;
1478
+
1479
+ const touch = () => { job.updated_at = new Date().toISOString(); };
1480
+ const aggregateBatchResult = (uploadResult, b, batch) => {
1481
+ if (!uploadResult) {
1482
+ job.failed_batches.push({ batch: b + 1, files: batch.length, reason: 'no response (timeout/network)' });
1483
+ this.logError(`Batch ${b + 1}/${batches.length} returned no response (timeout or network error). ${batch.length} files unaccounted for.`);
1484
+ } else {
1485
+ if (typeof uploadResult.files_indexed === 'number') {
1486
+ job.files_indexed += uploadResult.files_indexed;
1487
+ }
1488
+ if (typeof uploadResult.chunks_created === 'number') {
1489
+ job.chunks_created += uploadResult.chunks_created;
1490
+ }
1491
+ if (typeof uploadResult.files_skipped === 'number') {
1492
+ job.files_skipped += uploadResult.files_skipped;
1493
+ }
1494
+ if (typeof uploadResult.files_unchanged === 'number') {
1495
+ job.files_unchanged += uploadResult.files_unchanged;
1496
+ }
1497
+ if (Array.isArray(uploadResult.errors) && uploadResult.errors.length > 0) {
1498
+ for (const err of uploadResult.errors) {
1499
+ job.errors.push(err);
1500
+ }
1501
+ }
1502
+ }
1503
+ job.batches_completed += 1;
1504
+ touch();
1505
+ };
1506
+
1507
+ try {
1508
+ const CONCURRENCY = Math.min(3, batches.length);
1509
+ for (let i = 0; i < batches.length; i += CONCURRENCY) {
1510
+ const window = [];
1511
+ for (let j = i; j < Math.min(i + CONCURRENCY, batches.length); j++) {
1512
+ const batch = batches[j];
1513
+ const approxBytes = batch.reduce((sum, f) => sum + Buffer.byteLength(f.content || '', 'utf8'), 0);
1514
+ this.logDebug(`Uploading batch ${j + 1}/${batches.length} (${batch.length} files, ~${Math.round(approxBytes / 1024)}KB)`);
1515
+ window.push(
1516
+ this.uploadFileBatch(projectId, batch).then(res => aggregateBatchResult(res, j, batch))
1517
+ );
1518
+ }
1519
+ await Promise.all(window);
1520
+ }
1521
+
1522
+ try {
1523
+ await this.cleanupStaleFiles(projectId, files.map(f => f.relativePath));
1524
+ } catch (e) {
1525
+ this.logDebug(`Cleanup stale files failed: ${e && e.message ? e.message : 'unknown error'}`);
1526
+ }
1527
+
1528
+ const finalizeResult = await this.finalizeProject(projectId);
1529
+ if (finalizeResult && finalizeResult.success) {
1530
+ job.finalized = true;
1531
+ } else {
1532
+ job.errors.push(`finalize failed: ${finalizeResult && finalizeResult.error ? finalizeResult.error : 'no response'}`);
1533
+ }
1534
+
1535
+ job.status = job.failed_batches.length > 0 || job.errors.length > 0 ? 'partial' : 'complete';
1536
+ job.completed_at = new Date().toISOString();
1537
+ touch();
1538
+ } catch (error) {
1539
+ job.status = 'failed';
1540
+ job.errors.push(error && error.message ? error.message : String(error));
1541
+ job.completed_at = new Date().toISOString();
1542
+ touch();
1543
+ this.logError('Background index job failed:', error);
1544
+ } finally {
1545
+ if (progressStream && typeof progressStream.stop === 'function') {
1546
+ try { progressStream.stop(); } catch (_) {}
1547
+ }
1548
+ }
1549
+
1550
+ return job;
1551
+ }
1552
+
1553
+ async handleGetIndexingStatus(message) {
1554
+ const args = message.params?.arguments || {};
1555
+ const { bridge_job_id, project_id } = args;
1556
+ let payload = null;
1557
+
1558
+ if (bridge_job_id && this.indexJobs.has(bridge_job_id)) {
1559
+ payload = this.serializeIndexJob(this.indexJobs.get(bridge_job_id));
1560
+ } else if (project_id) {
1561
+ const localJob = Array.from(this.indexJobs.values())
1562
+ .reverse()
1563
+ .find((job) => job.project_id === project_id || job.project_name === project_id);
1564
+
1565
+ if (localJob) {
1566
+ payload = this.serializeIndexJob(localJob);
1567
+ } else {
1568
+ payload = await this.getRemoteProjectStatus(project_id);
1569
+ }
1570
+ } else {
1571
+ payload = {
1572
+ error: 'bridge_job_id or project_id is required',
1573
+ hint: 'Pass bridge_job_id returned by index_project. If unavailable, pass project_id.',
1574
+ };
1575
+ }
1576
+
1577
+ process.stdout.write(JSON.stringify({
1578
+ jsonrpc: '2.0',
1579
+ id: message.id,
1580
+ result: { content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }] },
1581
+ }) + '\n');
1582
+ }
1583
+
1392
1584
  /**
1393
1585
  * Recursive directory scan with filtering
1394
1586
  */
@@ -1681,6 +1873,98 @@ class CFMemoryMCP {
1681
1873
  });
1682
1874
  }
1683
1875
 
1876
+ async finalizeProject(projectId) {
1877
+ const url = new URL(this.legacyServerUrl);
1878
+ const finalizePath = `/api/projects/${encodeURIComponent(projectId)}/finalize`;
1879
+ const postData = JSON.stringify({});
1880
+
1881
+ return new Promise((resolve) => {
1882
+ const headers = {
1883
+ 'Content-Type': 'application/json',
1884
+ 'Accept': 'application/json',
1885
+ 'Accept-Encoding': 'identity',
1886
+ 'User-Agent': this.userAgent,
1887
+ 'Authorization': `Bearer ${API_KEY}`,
1888
+ 'X-API-Key': API_KEY,
1889
+ 'Content-Length': Buffer.byteLength(postData)
1890
+ };
1891
+
1892
+ const options = {
1893
+ hostname: url.hostname,
1894
+ port: url.port || 443,
1895
+ path: finalizePath,
1896
+ method: 'POST',
1897
+ headers,
1898
+ timeout: BATCH_TIMEOUT_MS,
1899
+ agent: httpsAgent
1900
+ };
1901
+
1902
+ const req = https.request(options, (res) => {
1903
+ let body = '';
1904
+ res.on('data', (chunk) => { body += chunk; });
1905
+ res.on('end', () => {
1906
+ try {
1907
+ const parsed = JSON.parse(body);
1908
+ if (res.statusCode && res.statusCode >= 400) {
1909
+ resolve({ error: parsed.error || `HTTP ${res.statusCode}` });
1910
+ } else {
1911
+ resolve(parsed);
1912
+ }
1913
+ } catch (_) {
1914
+ resolve({ error: `Invalid JSON response (HTTP ${res.statusCode})` });
1915
+ }
1916
+ });
1917
+ });
1918
+
1919
+ req.on('error', (err) => resolve({ error: err.message }));
1920
+ req.on('timeout', () => { try { req.destroy(); } catch (_) {} resolve({ error: 'timeout' }); });
1921
+ req.write(postData);
1922
+ req.end();
1923
+ });
1924
+ }
1925
+
1926
+ async getRemoteProjectStatus(projectIdOrName) {
1927
+ const response = await this.makeRequest({
1928
+ jsonrpc: '2.0',
1929
+ id: `status-${Date.now()}`,
1930
+ method: 'tools/call',
1931
+ params: { name: 'list_projects', arguments: {} },
1932
+ });
1933
+
1934
+ try {
1935
+ const parsed = JSON.parse(response.result.content[0].text);
1936
+ const projects = parsed.projects || parsed.results || [];
1937
+ const project = projects.find((p) => p.id === projectIdOrName || p.project_id === projectIdOrName || p.name === projectIdOrName);
1938
+ if (!project) {
1939
+ return {
1940
+ error: 'project_not_found',
1941
+ project_id: projectIdOrName,
1942
+ hint: 'No bridge-local job or remote project matched. Call list_projects to inspect available projects.',
1943
+ };
1944
+ }
1945
+ const status = project.status || (project.chunk_count > 0 ? 'ready' : 'pending');
1946
+ return {
1947
+ project_id: project.id || project.project_id,
1948
+ project_name: project.name,
1949
+ status,
1950
+ files_indexed: project.file_count,
1951
+ chunks_created: project.chunk_count,
1952
+ languages: project.languages,
1953
+ last_indexed_at: project.last_indexed_at,
1954
+ updated_at: project.updated_at,
1955
+ hint: status === 'indexing'
1956
+ ? 'Project is still indexing. If index_project returned bridge_job_id, poll get_indexing_status with that id for precise bridge progress.'
1957
+ : 'Project status loaded from list_projects.',
1958
+ };
1959
+ } catch (error) {
1960
+ return {
1961
+ error: 'status_lookup_failed',
1962
+ project_id: projectIdOrName,
1963
+ hint: error && error.message ? error.message : 'Could not parse list_projects response.',
1964
+ };
1965
+ }
1966
+ }
1967
+
1684
1968
  /**
1685
1969
  * Compare each result's indexed_file_hash against the local file's
1686
1970
  * current SHA-256 and set a `stale` field if they diverge. Works for
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cf-memory-mcp",
3
- "version": "3.9.8",
3
+ "version": "3.9.11",
4
4
  "description": "Cloudflare-hosted MCP server for code indexing, retrieval, and assistant memory with a direct remote MCP endpoint and local stdio bridge.",
5
5
  "main": "bin/cf-memory-mcp.js",
6
6
  "bin": {