cf-memory-mcp 3.15.0 → 3.17.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/bin/cf-memory-mcp.js +318 -11
- package/package.json +1 -1
package/bin/cf-memory-mcp.js
CHANGED
|
@@ -688,7 +688,12 @@ class CFMemoryMCP {
|
|
|
688
688
|
result: {
|
|
689
689
|
protocolVersion: respondVersion,
|
|
690
690
|
capabilities: {
|
|
691
|
-
tools: {}
|
|
691
|
+
tools: {},
|
|
692
|
+
// Resources capability exposes the resume handoff at
|
|
693
|
+
// cfm://resume/current and recent handoffs at
|
|
694
|
+
// cfm://resume/recent for clients that prefer the
|
|
695
|
+
// resources protocol over tool calls.
|
|
696
|
+
resources: {}
|
|
692
697
|
},
|
|
693
698
|
serverInfo: {
|
|
694
699
|
name: 'cf-memory-mcp-simplified',
|
|
@@ -726,12 +731,93 @@ class CFMemoryMCP {
|
|
|
726
731
|
return;
|
|
727
732
|
}
|
|
728
733
|
|
|
729
|
-
// Handle resources/list locally
|
|
734
|
+
// Handle resources/list locally — expose the resume handoff
|
|
735
|
+
// and recent handoffs as MCP resources for clients that prefer
|
|
736
|
+
// the resources capability over tool calls.
|
|
730
737
|
if (message.method === 'resources/list') {
|
|
738
|
+
const meta = this.getRepoMetadata();
|
|
739
|
+
const repoPath = meta.repo_path || 'unknown';
|
|
740
|
+
const branch = meta.branch || 'unknown';
|
|
741
|
+
const resources = [
|
|
742
|
+
{
|
|
743
|
+
uri: 'cfm://resume/current',
|
|
744
|
+
name: 'Current resume handoff',
|
|
745
|
+
description: `The most-recent matching handoff for ${repoPath} (branch=${branch}). Returns the same payload as get_context_bootstrap({resume:true}).`,
|
|
746
|
+
mimeType: 'application/json',
|
|
747
|
+
},
|
|
748
|
+
{
|
|
749
|
+
uri: 'cfm://resume/recent',
|
|
750
|
+
name: 'Recent handoffs (top 5)',
|
|
751
|
+
description: `The 5 most-recent handoffs for ${repoPath}, status-ranked (in_progress > completed). Useful for picking a specific session via session_id_hint.`,
|
|
752
|
+
mimeType: 'application/json',
|
|
753
|
+
},
|
|
754
|
+
];
|
|
755
|
+
const response = {
|
|
756
|
+
jsonrpc: '2.0',
|
|
757
|
+
id: message.id,
|
|
758
|
+
result: { resources },
|
|
759
|
+
};
|
|
760
|
+
process.stdout.write(JSON.stringify(response) + '\n');
|
|
761
|
+
return;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
// Handle resources/read — serve the resume packet as if the
|
|
765
|
+
// agent had called get_context_bootstrap({resume:true}). Lets
|
|
766
|
+
// MCP clients that use the resources capability (instead of
|
|
767
|
+
// tools) recover prior chat state.
|
|
768
|
+
if (message.method === 'resources/read') {
|
|
769
|
+
const uri = message.params?.uri;
|
|
770
|
+
if (uri === 'cfm://resume/current' || uri === 'cfm://resume/recent') {
|
|
771
|
+
try {
|
|
772
|
+
const meta = this.getRepoMetadata();
|
|
773
|
+
const args = { resume: true };
|
|
774
|
+
if (meta.repo_path) args.repo_path = meta.repo_path;
|
|
775
|
+
if (meta.branch) args.branch = meta.branch;
|
|
776
|
+
const fake = { params: { name: 'retrieve_context', arguments: {} } };
|
|
777
|
+
await this.maybeFillProjectId(fake);
|
|
778
|
+
if (fake.params.arguments.project_id) args.project_id = fake.params.arguments.project_id;
|
|
779
|
+
const bootstrap = await this.makeRequest({
|
|
780
|
+
jsonrpc: '2.0',
|
|
781
|
+
id: `resource-read-${Date.now()}`,
|
|
782
|
+
method: 'tools/call',
|
|
783
|
+
params: { name: 'get_context_bootstrap', arguments: args },
|
|
784
|
+
});
|
|
785
|
+
const text = bootstrap?.result?.content?.[0]?.text || '{}';
|
|
786
|
+
// For cfm://resume/recent, surface only the recent_handoffs summaries.
|
|
787
|
+
let payload = text;
|
|
788
|
+
if (uri === 'cfm://resume/recent') {
|
|
789
|
+
try {
|
|
790
|
+
const parsed = JSON.parse(text);
|
|
791
|
+
payload = JSON.stringify({
|
|
792
|
+
recent_handoffs: parsed.recent_handoffs || [],
|
|
793
|
+
}, null, 2);
|
|
794
|
+
} catch (_) { /* fall through with raw text */ }
|
|
795
|
+
}
|
|
796
|
+
const response = {
|
|
797
|
+
jsonrpc: '2.0',
|
|
798
|
+
id: message.id,
|
|
799
|
+
result: { contents: [{ uri, mimeType: 'application/json', text: payload }] },
|
|
800
|
+
};
|
|
801
|
+
process.stdout.write(JSON.stringify(response) + '\n');
|
|
802
|
+
return;
|
|
803
|
+
} catch (err) {
|
|
804
|
+
const response = {
|
|
805
|
+
jsonrpc: '2.0',
|
|
806
|
+
id: message.id,
|
|
807
|
+
error: { code: -32603, message: `Failed to read ${uri}: ${err && err.message}` },
|
|
808
|
+
};
|
|
809
|
+
process.stdout.write(JSON.stringify(response) + '\n');
|
|
810
|
+
return;
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
// Unknown resource URI
|
|
731
814
|
const response = {
|
|
732
815
|
jsonrpc: '2.0',
|
|
733
816
|
id: message.id,
|
|
734
|
-
|
|
817
|
+
error: {
|
|
818
|
+
code: -32602,
|
|
819
|
+
message: `Unknown resource URI: ${uri}. Supported: cfm://resume/current, cfm://resume/recent`,
|
|
820
|
+
},
|
|
735
821
|
};
|
|
736
822
|
process.stdout.write(JSON.stringify(response) + '\n');
|
|
737
823
|
return;
|
|
@@ -846,6 +932,7 @@ class CFMemoryMCP {
|
|
|
846
932
|
message.params.arguments?.resume) {
|
|
847
933
|
const cached = this.maybeServePrewarmedResume(message);
|
|
848
934
|
if (cached) {
|
|
935
|
+
this.injectRepoDiffIntoResume(cached);
|
|
849
936
|
_mcpTrace('RESUME_PREWARM', `served get_context_bootstrap id=${message.id} from cache`);
|
|
850
937
|
process.stdout.write(JSON.stringify(cached) + '\n');
|
|
851
938
|
return;
|
|
@@ -900,6 +987,16 @@ class CFMemoryMCP {
|
|
|
900
987
|
this.cacheImplicitSessionFromResponse(response);
|
|
901
988
|
}
|
|
902
989
|
|
|
990
|
+
// Inject bridge-side git diff into resume responses. Lets the
|
|
991
|
+
// agent see what's changed in the repo since the handoff was
|
|
992
|
+
// written — files modified, commits added, branch moves —
|
|
993
|
+
// without having to call git themselves.
|
|
994
|
+
if (message.method === 'tools/call' &&
|
|
995
|
+
message.params?.name === 'get_context_bootstrap' &&
|
|
996
|
+
message.params.arguments?.resume) {
|
|
997
|
+
this.injectRepoDiffIntoResume(response);
|
|
998
|
+
}
|
|
999
|
+
|
|
903
1000
|
// Send response to stdout
|
|
904
1001
|
process.stdout.write(JSON.stringify(response) + '\n');
|
|
905
1002
|
|
|
@@ -2392,18 +2489,128 @@ class CFMemoryMCP {
|
|
|
2392
2489
|
// Tool-call counter for auto-checkpoint safety net. Counts all
|
|
2393
2490
|
// tool/call messages; auto-checkpoint fires every N calls.
|
|
2394
2491
|
this._toolCallCount = (this._toolCallCount || 0) + 1;
|
|
2492
|
+
|
|
2493
|
+
// Activity timeline: keep the last 15 user-meaningful tool calls
|
|
2494
|
+
// in a ring buffer so synthesized handoffs include a "what was
|
|
2495
|
+
// I doing" trail. Excludes housekeeping (health_check, list_*)
|
|
2496
|
+
// and the internal auto-* calls themselves.
|
|
2497
|
+
const TIMELINE_TOOLS = new Set([
|
|
2498
|
+
'retrieve_context', 'get_file_content', 'get_file_outline',
|
|
2499
|
+
'get_related_code', 'refresh_files', 'refresh_stale',
|
|
2500
|
+
'index_project', 'index_github', 'store_memory',
|
|
2501
|
+
'find_stale_files',
|
|
2502
|
+
]);
|
|
2503
|
+
if (TIMELINE_TOOLS.has(toolName)) {
|
|
2504
|
+
if (!this._activityTimeline) this._activityTimeline = [];
|
|
2505
|
+
const summary = this.summarizeToolCall(toolName, args);
|
|
2506
|
+
this._activityTimeline.push({ tool: toolName, summary, at: Date.now() });
|
|
2507
|
+
if (this._activityTimeline.length > 15) {
|
|
2508
|
+
this._activityTimeline.shift();
|
|
2509
|
+
}
|
|
2510
|
+
}
|
|
2395
2511
|
} catch (err) {
|
|
2396
2512
|
this.logDebug(`trackToolActivity failed: ${err && err.message}`);
|
|
2397
2513
|
}
|
|
2398
2514
|
}
|
|
2399
2515
|
|
|
2516
|
+
/**
|
|
2517
|
+
* Mutates a get_context_bootstrap response to attach a `repo_diff`
|
|
2518
|
+
* field describing what's changed in the local repo since the
|
|
2519
|
+
* handoff. Bridge-only — server has no git access. Best-effort, silent
|
|
2520
|
+
* on failure.
|
|
2521
|
+
*
|
|
2522
|
+
* Opt out via CF_MEMORY_RESUME_DIFF=off.
|
|
2523
|
+
*/
|
|
2524
|
+
injectRepoDiffIntoResume(response) {
|
|
2525
|
+
try {
|
|
2526
|
+
const opt = process.env.CF_MEMORY_RESUME_DIFF;
|
|
2527
|
+
if (opt === '0' || opt === 'false' || opt === 'off') return;
|
|
2528
|
+
const text = response?.result?.content?.[0]?.text;
|
|
2529
|
+
if (!text) return;
|
|
2530
|
+
let parsed;
|
|
2531
|
+
try { parsed = JSON.parse(text); } catch (_) { return; }
|
|
2532
|
+
const envelope = parsed?.resume_handoff;
|
|
2533
|
+
if (!envelope) return;
|
|
2534
|
+
|
|
2535
|
+
const meta = this.getRepoMetadata();
|
|
2536
|
+
const repoRoot = meta.repo_path;
|
|
2537
|
+
if (!repoRoot) return;
|
|
2538
|
+
// Reference point: prefer ended_at, fall back to started_at.
|
|
2539
|
+
const since = envelope.ended_at || envelope.started_at;
|
|
2540
|
+
if (!since) return;
|
|
2541
|
+
|
|
2542
|
+
const diff = this.computeRepoDiff(repoRoot, since, envelope.handoff?.branch || meta.branch);
|
|
2543
|
+
if (diff) {
|
|
2544
|
+
envelope.repo_diff = diff;
|
|
2545
|
+
// Re-serialize the modified envelope back into the response.
|
|
2546
|
+
response.result.content[0].text = JSON.stringify(parsed);
|
|
2547
|
+
}
|
|
2548
|
+
} catch (err) {
|
|
2549
|
+
this.logDebug(`injectRepoDiffIntoResume failed: ${err && err.message}`);
|
|
2550
|
+
}
|
|
2551
|
+
}
|
|
2552
|
+
|
|
2553
|
+
/**
|
|
2554
|
+
* Run a bounded git query in the repo root to summarize changes since
|
|
2555
|
+
* the handoff was written. Returns { commits, files_changed,
|
|
2556
|
+
* branch_now, branch_at_handoff } or null if git isn't available.
|
|
2557
|
+
*/
|
|
2558
|
+
computeRepoDiff(repoRoot, sinceIso, handoffBranch) {
|
|
2559
|
+
try {
|
|
2560
|
+
const { execSync } = require('child_process');
|
|
2561
|
+
const run = (cmd) => execSync(cmd, {
|
|
2562
|
+
cwd: repoRoot,
|
|
2563
|
+
encoding: 'utf8',
|
|
2564
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
2565
|
+
timeout: 1000,
|
|
2566
|
+
});
|
|
2567
|
+
// Git accepts ISO timestamps via --since
|
|
2568
|
+
const commitsOut = run(`git log --since='${sinceIso}' --pretty=format:'%h %s' -n 25`);
|
|
2569
|
+
const commits = commitsOut.split('\n').filter(Boolean).map(line => {
|
|
2570
|
+
const [hash, ...rest] = line.split(' ');
|
|
2571
|
+
return { hash, subject: rest.join(' ').slice(0, 120) };
|
|
2572
|
+
});
|
|
2573
|
+
|
|
2574
|
+
// Files changed since the handoff. Compare HEAD against the
|
|
2575
|
+
// commit closest to the handoff time. If no commits since,
|
|
2576
|
+
// diff vs. HEAD~0 returns nothing.
|
|
2577
|
+
let filesChanged = [];
|
|
2578
|
+
try {
|
|
2579
|
+
const namesOut = run(`git log --since='${sinceIso}' --name-only --pretty=format:''`);
|
|
2580
|
+
const names = new Set(namesOut.split('\n').map(s => s.trim()).filter(Boolean));
|
|
2581
|
+
filesChanged = Array.from(names).slice(0, 50);
|
|
2582
|
+
} catch (_) { /* empty diff is fine */ }
|
|
2583
|
+
|
|
2584
|
+
// Branch information.
|
|
2585
|
+
let branchNow = null;
|
|
2586
|
+
try {
|
|
2587
|
+
branchNow = run('git symbolic-ref --short HEAD').trim();
|
|
2588
|
+
} catch (_) { /* detached HEAD */ }
|
|
2589
|
+
|
|
2590
|
+
const diff = {
|
|
2591
|
+
commits_since_handoff: commits,
|
|
2592
|
+
files_changed_count: filesChanged.length,
|
|
2593
|
+
files_changed: filesChanged,
|
|
2594
|
+
};
|
|
2595
|
+
if (branchNow) diff.branch_now = branchNow;
|
|
2596
|
+
if (handoffBranch) diff.branch_at_handoff = handoffBranch;
|
|
2597
|
+
if (handoffBranch && branchNow && handoffBranch !== branchNow) {
|
|
2598
|
+
diff.branch_changed = true;
|
|
2599
|
+
}
|
|
2600
|
+
return diff;
|
|
2601
|
+
} catch (err) {
|
|
2602
|
+
this.logDebug(`computeRepoDiff failed: ${err && err.message}`);
|
|
2603
|
+
return null;
|
|
2604
|
+
}
|
|
2605
|
+
}
|
|
2606
|
+
|
|
2400
2607
|
/**
|
|
2401
2608
|
* Build a minimal handoff packet from observed bridge activity. Used by
|
|
2402
|
-
* end_session auto-synthesis
|
|
2403
|
-
* just the fields the bridge can
|
|
2404
|
-
* fields like decisions/blockers
|
|
2405
|
-
*
|
|
2406
|
-
* "bridge synthesized this".
|
|
2609
|
+
* end_session auto-synthesis, the periodic auto-checkpoint, and the
|
|
2610
|
+
* SIGINT/SIGTERM safety net. Returns just the fields the bridge can
|
|
2611
|
+
* credibly populate — agent-supplied fields like decisions/blockers
|
|
2612
|
+
* are intentionally omitted so a future resume reader can tell the
|
|
2613
|
+
* difference between "agent wrote this" and "bridge synthesized this".
|
|
2407
2614
|
*/
|
|
2408
2615
|
synthesizeMinimalHandoff() {
|
|
2409
2616
|
const handoff = {
|
|
@@ -2424,10 +2631,65 @@ class CFMemoryMCP {
|
|
|
2424
2631
|
} else {
|
|
2425
2632
|
handoff.goal = 'Bridge-synthesized handoff (no explicit goal)';
|
|
2426
2633
|
}
|
|
2427
|
-
|
|
2634
|
+
|
|
2635
|
+
// Activity timeline: last 15 meaningful tool calls, condensed to
|
|
2636
|
+
// a single line each. Goes in handoff.notes so the next chat
|
|
2637
|
+
// sees the recent trajectory ("what was I doing").
|
|
2638
|
+
const timelineNote = this.formatActivityTimeline();
|
|
2639
|
+
const baseNote = `Auto-synthesized by cf-memory-mcp bridge. Agent did not provide a handoff. Tool calls observed: ${this._toolCallCount || 0}. Set CF_MEMORY_AUTO_HANDOFF=off to disable.`;
|
|
2640
|
+
handoff.notes = timelineNote ? `${baseNote}\n\nRecent activity:\n${timelineNote}` : baseNote;
|
|
2428
2641
|
return handoff;
|
|
2429
2642
|
}
|
|
2430
2643
|
|
|
2644
|
+
/**
|
|
2645
|
+
* Condense an MCP tool call into a single-line summary. Used for the
|
|
2646
|
+
* activity timeline so the synthesized handoff includes "what was I
|
|
2647
|
+
* doing" context. Keeps each line under 120 chars.
|
|
2648
|
+
*/
|
|
2649
|
+
summarizeToolCall(toolName, args) {
|
|
2650
|
+
try {
|
|
2651
|
+
const trim = (s, n) => typeof s === 'string' ? s.slice(0, n) : '';
|
|
2652
|
+
switch (toolName) {
|
|
2653
|
+
case 'retrieve_context':
|
|
2654
|
+
return `retrieve_context: "${trim(args.query, 80)}"`;
|
|
2655
|
+
case 'get_file_content':
|
|
2656
|
+
return `get_file_content: ${trim(args.file_path, 80)}`;
|
|
2657
|
+
case 'get_file_outline':
|
|
2658
|
+
return `get_file_outline: ${trim(args.file_path, 80)}`;
|
|
2659
|
+
case 'get_related_code':
|
|
2660
|
+
return `get_related_code: ${trim(args.chunk_name || args.chunk_id, 80)}`;
|
|
2661
|
+
case 'refresh_files':
|
|
2662
|
+
return `refresh_files: ${(args.file_paths || []).slice(0, 3).join(', ')}${(args.file_paths || []).length > 3 ? '…' : ''}`;
|
|
2663
|
+
case 'refresh_stale':
|
|
2664
|
+
return `refresh_stale${args.project_id ? ' '+args.project_id : ''}`;
|
|
2665
|
+
case 'index_project':
|
|
2666
|
+
return `index_project: ${trim(args.project_path, 80)}`;
|
|
2667
|
+
case 'index_github':
|
|
2668
|
+
return `index_github: ${trim(args.repo_url, 80)}`;
|
|
2669
|
+
case 'store_memory':
|
|
2670
|
+
return `store_memory: "${trim(args.content, 80)}"`;
|
|
2671
|
+
case 'find_stale_files':
|
|
2672
|
+
return `find_stale_files`;
|
|
2673
|
+
default:
|
|
2674
|
+
return toolName;
|
|
2675
|
+
}
|
|
2676
|
+
} catch (_) {
|
|
2677
|
+
return toolName;
|
|
2678
|
+
}
|
|
2679
|
+
}
|
|
2680
|
+
|
|
2681
|
+
/**
|
|
2682
|
+
* Format the activity timeline as a compact bullet list. Returns ""
|
|
2683
|
+
* when no timeline exists.
|
|
2684
|
+
*/
|
|
2685
|
+
formatActivityTimeline() {
|
|
2686
|
+
if (!this._activityTimeline || this._activityTimeline.length === 0) return '';
|
|
2687
|
+
return this._activityTimeline
|
|
2688
|
+
.slice(-10) // last 10 of the up-to-15 buffer
|
|
2689
|
+
.map(e => ` - ${e.summary}`)
|
|
2690
|
+
.join('\n');
|
|
2691
|
+
}
|
|
2692
|
+
|
|
2431
2693
|
/**
|
|
2432
2694
|
* Pre-fetch the resume handoff for the current cwd at bridge startup.
|
|
2433
2695
|
* The result is cached and surfaced when the agent's first call is
|
|
@@ -3061,11 +3323,56 @@ class CFMemoryMCP {
|
|
|
3061
3323
|
}
|
|
3062
3324
|
|
|
3063
3325
|
/**
|
|
3064
|
-
* Graceful shutdown
|
|
3326
|
+
* Graceful shutdown. Best-effort: tries to flush a synthesized handoff
|
|
3327
|
+
* to the active implicit session so the resume state survives the
|
|
3328
|
+
* shutdown. Uses keep_open so the session can still be properly closed
|
|
3329
|
+
* later if the agent reconnects. Bounded by a 2s timeout so we never
|
|
3330
|
+
* hang the exit on a slow network round-trip.
|
|
3331
|
+
*
|
|
3332
|
+
* Opt out via CF_MEMORY_SHUTDOWN_HANDOFF=off.
|
|
3065
3333
|
*/
|
|
3066
3334
|
shutdown(reason = 'UNKNOWN') {
|
|
3067
3335
|
this.logDebug(`Shutting down (reason: ${reason})`);
|
|
3068
|
-
process.
|
|
3336
|
+
const optOut = process.env.CF_MEMORY_SHUTDOWN_HANDOFF;
|
|
3337
|
+
const enabled = !(optOut === '0' || optOut === 'false' || optOut === 'off');
|
|
3338
|
+
if (!enabled) {
|
|
3339
|
+
process.exit(0);
|
|
3340
|
+
return;
|
|
3341
|
+
}
|
|
3342
|
+
// Resolve the implicit session WITHOUT creating a new one — we
|
|
3343
|
+
// don't want to leave an empty session row behind if the agent
|
|
3344
|
+
// never did anything. Only flush when there's tracked activity.
|
|
3345
|
+
const cwd = process.env.CF_MEMORY_WATCH_PATH || process.cwd();
|
|
3346
|
+
const sessionId = this._implicitSessionByCwd?.get(cwd);
|
|
3347
|
+
const hasActivity = this._activityFiles && this._activityFiles.size > 0;
|
|
3348
|
+
if (!sessionId || !hasActivity) {
|
|
3349
|
+
this.logDebug(`shutdown handoff skipped: session=${sessionId||'(none)'} activity=${this._activityFiles?.size||0}`);
|
|
3350
|
+
process.exit(0);
|
|
3351
|
+
return;
|
|
3352
|
+
}
|
|
3353
|
+
|
|
3354
|
+
const handoff = this.synthesizeMinimalHandoff();
|
|
3355
|
+
const meta = this.getRepoMetadata();
|
|
3356
|
+
if (meta.repo_path) handoff.repo_path = meta.repo_path;
|
|
3357
|
+
if (meta.branch) handoff.branch = meta.branch;
|
|
3358
|
+
|
|
3359
|
+
const args = { session_id: sessionId, keep_open: true, handoff };
|
|
3360
|
+
const message = {
|
|
3361
|
+
jsonrpc: '2.0',
|
|
3362
|
+
id: `shutdown-${Date.now()}`,
|
|
3363
|
+
method: 'tools/call',
|
|
3364
|
+
params: { name: 'end_session', arguments: args },
|
|
3365
|
+
};
|
|
3366
|
+
_mcpTrace('SHUTDOWN_HANDOFF', `session=${sessionId} reason=${reason}`);
|
|
3367
|
+
|
|
3368
|
+
// Race the request against a 2s deadline so we never hang exit.
|
|
3369
|
+
const deadline = new Promise(resolve => setTimeout(resolve, 2000));
|
|
3370
|
+
Promise.race([
|
|
3371
|
+
this.makeRequest(message).catch(err => {
|
|
3372
|
+
this.logDebug(`shutdown handoff request failed: ${err && err.message}`);
|
|
3373
|
+
}),
|
|
3374
|
+
deadline,
|
|
3375
|
+
]).finally(() => process.exit(0));
|
|
3069
3376
|
}
|
|
3070
3377
|
}
|
|
3071
3378
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cf-memory-mcp",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.17.0",
|
|
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": {
|