cf-memory-mcp 3.9.10 → 3.9.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.
- package/README.md +3 -3
- package/bin/cf-memory-mcp.js +325 -73
- package/package.json +1 -1
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** -
|
|
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
|
-
- **
|
|
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`)
|
package/bin/cf-memory-mcp.js
CHANGED
|
@@ -103,11 +103,23 @@ const TOOLS_LIST = [
|
|
|
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
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.' }
|
|
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.' }
|
|
107
108
|
},
|
|
108
109
|
required: ['project_path']
|
|
109
110
|
}
|
|
110
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
|
+
},
|
|
111
123
|
{
|
|
112
124
|
name: 'index_github',
|
|
113
125
|
description: 'Index a public GitHub repository server-side without cloning locally. Use when the user wants to search a remote codebase.',
|
|
@@ -370,6 +382,7 @@ class CFMemoryMCP {
|
|
|
370
382
|
this.userAgent = `cf-memory-mcp/${PACKAGE_VERSION} (${os.platform()} ${os.arch()}; Node.js ${process.version})`;
|
|
371
383
|
this.useStreamableHttp = true; // Try Streamable HTTP first
|
|
372
384
|
this.autoWatcher = null; // Background file watcher
|
|
385
|
+
this.indexJobs = new Map(); // bridge_job_id -> local background indexing status
|
|
373
386
|
|
|
374
387
|
// Set up stdio encoding
|
|
375
388
|
process.stdin.setEncoding('utf8');
|
|
@@ -731,6 +744,14 @@ class CFMemoryMCP {
|
|
|
731
744
|
return;
|
|
732
745
|
}
|
|
733
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
|
+
|
|
734
755
|
// Intercept index_project tool call to perform local scanning
|
|
735
756
|
if (message.method === 'tools/call' && message.params && message.params.name === 'index_project') {
|
|
736
757
|
await this.handleIndexProject(message);
|
|
@@ -1175,7 +1196,7 @@ class CFMemoryMCP {
|
|
|
1175
1196
|
|
|
1176
1197
|
async handleIndexProject(message) {
|
|
1177
1198
|
const args = message.params?.arguments || {};
|
|
1178
|
-
const { project_path, project_name, include_patterns, exclude_patterns, force_reindex, allow_system_path } = args;
|
|
1199
|
+
const { project_path, project_name, include_patterns, exclude_patterns, force_reindex, allow_system_path, wait_for_completion } = args;
|
|
1179
1200
|
|
|
1180
1201
|
// Boundary validation: bad inputs were producing unhelpful Node errors
|
|
1181
1202
|
// ("path must be string"). Return a clean MCP error with the hint
|
|
@@ -1276,8 +1297,6 @@ class CFMemoryMCP {
|
|
|
1276
1297
|
|
|
1277
1298
|
// 2. Create project via MCP (no inline files), then upload files via batch endpoint.
|
|
1278
1299
|
// This avoids repeating full index_project for every file batch.
|
|
1279
|
-
let totalIndexed = 0;
|
|
1280
|
-
let totalChunks = 0;
|
|
1281
1300
|
let projectId = null;
|
|
1282
1301
|
|
|
1283
1302
|
const init = await this.makeRequest({
|
|
@@ -1312,84 +1331,54 @@ class CFMemoryMCP {
|
|
|
1312
1331
|
// Adaptive batching: limit by file count and payload bytes to reduce timeouts.
|
|
1313
1332
|
const batches = this.createAdaptiveBatches(files);
|
|
1314
1333
|
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
// request limits, so we cap conservatively.
|
|
1325
|
-
const CONCURRENCY = Math.min(3, batches.length);
|
|
1326
|
-
const aggregateBatchResult = (uploadResult, b, batch) => {
|
|
1327
|
-
if (!uploadResult) {
|
|
1328
|
-
failedBatches.push({ batch: b + 1, files: batch.length, reason: 'no response (timeout/network)' });
|
|
1329
|
-
this.logError(`Batch ${b + 1}/${batches.length} returned no response (timeout or network error). ${batch.length} files unaccounted for.`);
|
|
1330
|
-
return;
|
|
1331
|
-
}
|
|
1332
|
-
if (typeof uploadResult.files_indexed === 'number') {
|
|
1333
|
-
totalIndexed += uploadResult.files_indexed;
|
|
1334
|
-
}
|
|
1335
|
-
if (typeof uploadResult.chunks_created === 'number') {
|
|
1336
|
-
totalChunks += uploadResult.chunks_created;
|
|
1337
|
-
}
|
|
1338
|
-
if (typeof uploadResult.files_skipped === 'number') {
|
|
1339
|
-
totalSkipped += uploadResult.files_skipped;
|
|
1340
|
-
}
|
|
1341
|
-
if (typeof uploadResult.files_unchanged === 'number') {
|
|
1342
|
-
totalUnchanged += uploadResult.files_unchanged;
|
|
1343
|
-
}
|
|
1344
|
-
if (Array.isArray(uploadResult.errors) && uploadResult.errors.length > 0) {
|
|
1345
|
-
for (const err of uploadResult.errors) {
|
|
1346
|
-
batchErrors.push(err);
|
|
1347
|
-
}
|
|
1348
|
-
}
|
|
1349
|
-
};
|
|
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
|
+
});
|
|
1350
1343
|
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
}
|
|
1361
|
-
await Promise.all(window);
|
|
1362
|
-
}
|
|
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;
|
|
1363
1353
|
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
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;
|
|
1372
1369
|
}
|
|
1373
1370
|
|
|
1374
|
-
// 4. Return aggregated success (with skipped/error visibility)
|
|
1375
|
-
const status = failedBatches.length > 0 ? 'partial' : 'complete';
|
|
1376
1371
|
const responsePayload = {
|
|
1377
1372
|
project_id: projectId,
|
|
1378
1373
|
project_name: name,
|
|
1374
|
+
bridge_job_id: bridgeJobId,
|
|
1379
1375
|
files_found: files.length,
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
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.`
|
|
1385
1381
|
};
|
|
1386
|
-
if (batchErrors.length > 0) {
|
|
1387
|
-
responsePayload.errors = batchErrors.slice(0, 50);
|
|
1388
|
-
responsePayload.errors_truncated = batchErrors.length > 50;
|
|
1389
|
-
}
|
|
1390
|
-
if (failedBatches.length > 0) {
|
|
1391
|
-
responsePayload.failed_batches = failedBatches;
|
|
1392
|
-
}
|
|
1393
1382
|
const response = {
|
|
1394
1383
|
jsonrpc: '2.0',
|
|
1395
1384
|
id: message.id,
|
|
@@ -1421,6 +1410,177 @@ class CFMemoryMCP {
|
|
|
1421
1410
|
}
|
|
1422
1411
|
}
|
|
1423
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
|
+
|
|
1424
1584
|
/**
|
|
1425
1585
|
* Recursive directory scan with filtering
|
|
1426
1586
|
*/
|
|
@@ -1713,6 +1873,98 @@ class CFMemoryMCP {
|
|
|
1713
1873
|
});
|
|
1714
1874
|
}
|
|
1715
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
|
+
|
|
1716
1968
|
/**
|
|
1717
1969
|
* Compare each result's indexed_file_hash against the local file's
|
|
1718
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.
|
|
3
|
+
"version": "3.9.12",
|
|
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": {
|