cf-memory-mcp 3.13.0 → 3.14.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 +195 -10
- package/package.json +1 -1
package/bin/cf-memory-mcp.js
CHANGED
|
@@ -201,37 +201,74 @@ const TOOLS_LIST = [
|
|
|
201
201
|
},
|
|
202
202
|
{
|
|
203
203
|
name: 'get_context_bootstrap',
|
|
204
|
-
description: '
|
|
204
|
+
description: 'Bootstrap a new chat with relevant memories. Pass {resume:true, repo_path, branch} to recover the latest handoff for this repo — returns resume_handoff (goal/status/files/next_steps/anchors), durable_context, active_code_context (with stale markers + refresh_hint), next_actions, and recent_handoffs alternates. Bridge auto-fills repo_path/branch/project_id from cwd + git so {resume:true} alone usually works.',
|
|
205
205
|
inputSchema: {
|
|
206
206
|
type: 'object',
|
|
207
207
|
properties: {
|
|
208
|
-
max_tokens: { type: 'number', description: 'Token budget for bootstrap context (default:
|
|
209
|
-
|
|
208
|
+
max_tokens: { type: 'number', description: 'Token budget for bootstrap context (default: 4000)' },
|
|
209
|
+
priority_tags: { type: 'array', items: { type: 'string' }, description: 'Tags to prioritize when picking memories' },
|
|
210
|
+
recent_sessions: { type: 'number', description: 'Number of recent session summaries to include' },
|
|
211
|
+
current_context: { type: 'string', description: 'Current conversation context to bias relevance' },
|
|
212
|
+
resume: { type: 'boolean', description: 'When true, look up the prior matching handoff for repo_path/project_id and surface it under resume_handoff. Status-preferred (in_progress > completed) and time-decayed (older handoffs lose match_confidence).' },
|
|
213
|
+
repo_path: { type: 'string', description: 'Absolute project root path. Bridge auto-fills from cwd if omitted.' },
|
|
214
|
+
project_id: { type: 'string', description: 'Canonical project id (proj_...). Used before repo_path when set.' },
|
|
215
|
+
branch: { type: 'string', description: 'Current git branch. Mismatch downgrades match_confidence but does not hide. Bridge auto-fills from git.' },
|
|
216
|
+
session_id_hint: { type: 'string', description: 'Resume a specific session by id (overrides repo/branch matching). Useful after seeing recent_handoffs.' },
|
|
217
|
+
max_age_minutes: { type: 'number', description: 'Hard cutoff: hide handoffs older than this many minutes. Without it, older handoffs surface with reduced confidence.' }
|
|
210
218
|
}
|
|
211
219
|
}
|
|
212
220
|
},
|
|
213
221
|
{
|
|
214
222
|
name: 'start_session',
|
|
215
|
-
description: 'Begin a new tracked
|
|
223
|
+
description: 'Begin a new tracked session. Returns session_id. Optionally pass {include_prior_handoff:true} to also get the prior matching handoff inline (one-shot resume). Bridge auto-fills repo_path/branch/project_id from cwd + git.',
|
|
216
224
|
inputSchema: {
|
|
217
225
|
type: 'object',
|
|
218
226
|
properties: {
|
|
219
227
|
context: { type: 'string', enum: ['main', 'group', 'background'], description: 'Session context type' },
|
|
220
|
-
platform: { type: 'string', description: 'Client platform (e.g., "claude-code", "claude-desktop")' }
|
|
228
|
+
platform: { type: 'string', description: 'Client platform (e.g., "claude-code", "claude-desktop")' },
|
|
229
|
+
repo_path: { type: 'string', description: 'Absolute project root path. Bridge auto-fills from cwd if omitted.' },
|
|
230
|
+
project_id: { type: 'string', description: 'Canonical project id (proj_...). Bridge auto-fills if known.' },
|
|
231
|
+
branch: { type: 'string', description: 'Current git branch. Bridge auto-fills from git if omitted.' },
|
|
232
|
+
goal: { type: 'string', description: 'One-sentence high-level goal for the session.' },
|
|
233
|
+
include_prior_handoff: { type: 'boolean', description: 'When true, also fetch the most-recent matching handoff and return it under prior_handoff. One round-trip resume.' }
|
|
221
234
|
},
|
|
222
235
|
required: ['context']
|
|
223
236
|
}
|
|
224
237
|
},
|
|
225
238
|
{
|
|
226
239
|
name: 'end_session',
|
|
227
|
-
description: 'End a
|
|
240
|
+
description: 'End a session and (optionally) persist a structured handoff so the next chat can resume via get_context_bootstrap({resume:true}). Pass {keep_open:true} to checkpoint mid-session (handoff written but ended_at stays null). The bridge auto-fills session_id from the implicit per-cwd cache when omitted, and synthesizes a minimal handoff from tracked tool activity when none provided.',
|
|
228
241
|
inputSchema: {
|
|
229
242
|
type: 'object',
|
|
230
243
|
properties: {
|
|
231
|
-
session_id: { type: 'string', description: 'Session ID returned by start_session' },
|
|
232
|
-
summary: { type: 'string', description: '
|
|
233
|
-
|
|
234
|
-
|
|
244
|
+
session_id: { type: 'string', description: 'Session ID returned by start_session. Bridge auto-fills from the implicit-per-cwd cache when omitted.' },
|
|
245
|
+
summary: { type: 'string', description: 'Free-form summary of what was discussed (separate from the structured handoff).' },
|
|
246
|
+
extract_memories: { type: 'boolean', description: 'Extract summary as a session_summary memory.' },
|
|
247
|
+
keep_open: { type: 'boolean', description: 'Mid-session checkpoint: persist the handoff but keep the session active (do not set ended_at). Call multiple times to update; final end_session without keep_open finalizes.' },
|
|
248
|
+
handoff: {
|
|
249
|
+
type: 'object',
|
|
250
|
+
description: 'Structured resume packet for the next chat window. Target 600-1500 tokens. Fields: goal (required), status (required), repo_path, project_id, branch, files_touched, files_read, decisions, blockers, commands_run, next_steps, code_anchors, notes. Bridge auto-fills repo_path/branch/project_id from cwd + git, and files_read from tracked tool activity.',
|
|
251
|
+
properties: {
|
|
252
|
+
goal: { type: 'string', description: 'What the user is trying to accomplish (one sentence).' },
|
|
253
|
+
status: { type: 'string', enum: ['in_progress', 'blocked', 'completed', 'abandoned'], description: 'Current state of the work.' },
|
|
254
|
+
repo_path: { type: 'string' },
|
|
255
|
+
project_id: { type: 'string' },
|
|
256
|
+
branch: { type: 'string' },
|
|
257
|
+
files_touched: { type: 'array', description: 'Files materially edited.', items: { type: 'object', properties: { path: { type: 'string' }, why: { type: 'string' } }, required: ['path'] } },
|
|
258
|
+
files_read: { type: 'array', description: 'Files read for context but not modified. Auto-filled by bridge if omitted.', items: { type: 'object', properties: { path: { type: 'string' }, why: { type: 'string' } }, required: ['path'] } },
|
|
259
|
+
decisions: { type: 'array', items: { type: 'string' }, description: 'Decisions made and reasoning behind them.' },
|
|
260
|
+
blockers: { type: 'array', items: { type: 'string' }, description: 'Open blockers preventing progress.' },
|
|
261
|
+
commands_run: { type: 'array', items: { type: 'object', properties: { cmd: { type: 'string' }, result: { type: 'string', enum: ['pass', 'fail', 'unknown'] }, notes: { type: 'string' } }, required: ['cmd'] } },
|
|
262
|
+
next_steps: { type: 'array', items: { type: 'string' }, description: 'Recommended next steps in priority order.' },
|
|
263
|
+
code_anchors: { type: 'array', description: 'File/symbol anchors future agents should jump to first.', items: { type: 'object', properties: { file_path: { type: 'string' }, name: { type: 'string' }, lines: { type: 'string' }, why: { type: 'string' } }, required: ['file_path'] } },
|
|
264
|
+
notes: { type: 'string', description: 'Free-form notes that did not fit elsewhere.' }
|
|
265
|
+
},
|
|
266
|
+
required: ['goal', 'status']
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
// No required fields: bridge auto-fills session_id from the
|
|
270
|
+
// implicit per-cwd cache, so an agent can call end_session({})
|
|
271
|
+
// and the bridge does the right thing.
|
|
235
272
|
}
|
|
236
273
|
},
|
|
237
274
|
{
|
|
@@ -431,6 +468,15 @@ class CFMemoryMCP {
|
|
|
431
468
|
// retrieve_context arrives, the cache is hot — no extra roundtrip.
|
|
432
469
|
this.prewarmProjectIdCache();
|
|
433
470
|
|
|
471
|
+
// Background-fetch the resume handoff for this cwd. If the
|
|
472
|
+
// agent's first call is get_context_bootstrap({resume:true}),
|
|
473
|
+
// we serve from the warm cache instead of paying the round-trip.
|
|
474
|
+
// Opt out via CF_MEMORY_PREWARM_RESUME=off.
|
|
475
|
+
const prewarmResumeOpt = process.env.CF_MEMORY_PREWARM_RESUME;
|
|
476
|
+
if (!(prewarmResumeOpt === '0' || prewarmResumeOpt === 'false' || prewarmResumeOpt === 'off')) {
|
|
477
|
+
this.prewarmResumeContext();
|
|
478
|
+
}
|
|
479
|
+
|
|
434
480
|
// Start auto-watcher if CF_MEMORY_AUTO_WATCH is set
|
|
435
481
|
if (process.env.CF_MEMORY_AUTO_WATCH === '1' || process.env.CF_MEMORY_AUTO_WATCH === 'true') {
|
|
436
482
|
this.startAutoWatcher();
|
|
@@ -792,6 +838,20 @@ class CFMemoryMCP {
|
|
|
792
838
|
await this.maybeAttachResumeMetadata(message);
|
|
793
839
|
}
|
|
794
840
|
|
|
841
|
+
// Resume prewarm: when the agent's first call is
|
|
842
|
+
// get_context_bootstrap({resume:true}), try to serve from the
|
|
843
|
+
// background-fetched cache instead of paying a real round-trip.
|
|
844
|
+
if (message.method === 'tools/call' &&
|
|
845
|
+
message.params?.name === 'get_context_bootstrap' &&
|
|
846
|
+
message.params.arguments?.resume) {
|
|
847
|
+
const cached = this.maybeServePrewarmedResume(message);
|
|
848
|
+
if (cached) {
|
|
849
|
+
_mcpTrace('RESUME_PREWARM', `served get_context_bootstrap id=${message.id} from cache`);
|
|
850
|
+
process.stdout.write(JSON.stringify(cached) + '\n');
|
|
851
|
+
return;
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
|
|
795
855
|
// Record which files the agent is touching so end_session can
|
|
796
856
|
// auto-populate handoff.files_read. Cheap (in-memory Map), safe
|
|
797
857
|
// (try/catch'd), and the agent's hand-written list always wins.
|
|
@@ -2152,6 +2212,19 @@ class CFMemoryMCP {
|
|
|
2152
2212
|
_mcpTrace('IMPLICIT_SESSION', `end_session using implicit session=${implicit}`);
|
|
2153
2213
|
}
|
|
2154
2214
|
}
|
|
2215
|
+
// Auto-synthesize a minimal handoff when the agent didn't
|
|
2216
|
+
// provide one but the bridge has observed file activity.
|
|
2217
|
+
// Means even agents that don't follow the protocol leave
|
|
2218
|
+
// useful resume state behind. Opt out with
|
|
2219
|
+
// CF_MEMORY_AUTO_HANDOFF=off if the silent synthesis is
|
|
2220
|
+
// surprising in a particular environment.
|
|
2221
|
+
const autoHandoffEnv = process.env.CF_MEMORY_AUTO_HANDOFF;
|
|
2222
|
+
const autoHandoffEnabled = !(autoHandoffEnv === '0' || autoHandoffEnv === 'false' || autoHandoffEnv === 'off');
|
|
2223
|
+
if (autoHandoffEnabled && !args.handoff &&
|
|
2224
|
+
this._activityFiles && this._activityFiles.size > 0) {
|
|
2225
|
+
args.handoff = this.synthesizeMinimalHandoff();
|
|
2226
|
+
_mcpTrace('AUTO_HANDOFF', `synthesized for end_session session=${args.session_id||'?'} files=${this._activityFiles.size}`);
|
|
2227
|
+
}
|
|
2155
2228
|
if (args.handoff && typeof args.handoff === 'object') {
|
|
2156
2229
|
const meta = this.getRepoMetadata();
|
|
2157
2230
|
if (!args.handoff.repo_path && meta.repo_path) args.handoff.repo_path = meta.repo_path;
|
|
@@ -2324,6 +2397,118 @@ class CFMemoryMCP {
|
|
|
2324
2397
|
}
|
|
2325
2398
|
}
|
|
2326
2399
|
|
|
2400
|
+
/**
|
|
2401
|
+
* Build a minimal handoff packet from observed bridge activity. Used by
|
|
2402
|
+
* end_session auto-synthesis and the periodic auto-checkpoint. Returns
|
|
2403
|
+
* just the fields the bridge can credibly populate — agent-supplied
|
|
2404
|
+
* fields like decisions/blockers are intentionally omitted so a future
|
|
2405
|
+
* resume reader can tell the difference between "agent wrote this" and
|
|
2406
|
+
* "bridge synthesized this".
|
|
2407
|
+
*/
|
|
2408
|
+
synthesizeMinimalHandoff() {
|
|
2409
|
+
const handoff = {
|
|
2410
|
+
// The status is necessarily a guess. We use 'in_progress' as the
|
|
2411
|
+
// default because a synthesized handoff usually means "agent
|
|
2412
|
+
// didn't explicitly finish" — agents who finish properly will
|
|
2413
|
+
// provide their own handoff.
|
|
2414
|
+
status: 'in_progress',
|
|
2415
|
+
};
|
|
2416
|
+
if (this._lastRetrieveQuery) {
|
|
2417
|
+
handoff.goal = `Recent activity: ${this._lastRetrieveQuery.slice(0, 160)}`;
|
|
2418
|
+
} else if (this._activityFiles && this._activityFiles.size > 0) {
|
|
2419
|
+
const topFile = Array.from(this._activityFiles.entries())
|
|
2420
|
+
.sort((a, b) => b[1].count - a[1].count)[0];
|
|
2421
|
+
handoff.goal = topFile
|
|
2422
|
+
? `Bridge-synthesized: most-touched file was ${topFile[0]}`
|
|
2423
|
+
: 'Bridge-synthesized handoff (no explicit goal)';
|
|
2424
|
+
} else {
|
|
2425
|
+
handoff.goal = 'Bridge-synthesized handoff (no explicit goal)';
|
|
2426
|
+
}
|
|
2427
|
+
handoff.notes = `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.`;
|
|
2428
|
+
return handoff;
|
|
2429
|
+
}
|
|
2430
|
+
|
|
2431
|
+
/**
|
|
2432
|
+
* Pre-fetch the resume handoff for the current cwd at bridge startup.
|
|
2433
|
+
* The result is cached and surfaced when the agent's first call is
|
|
2434
|
+
* get_context_bootstrap({resume:true}). Reduces first-call latency
|
|
2435
|
+
* from one full bootstrap round-trip to nearly zero.
|
|
2436
|
+
*
|
|
2437
|
+
* Failures are silent — this is purely an optimization. If pre-warming
|
|
2438
|
+
* fails (no handoff, network error), the regular bootstrap path still
|
|
2439
|
+
* works.
|
|
2440
|
+
*/
|
|
2441
|
+
async prewarmResumeContext() {
|
|
2442
|
+
try {
|
|
2443
|
+
const meta = this.getRepoMetadata();
|
|
2444
|
+
if (!meta.repo_path) return;
|
|
2445
|
+
const args = { resume: true, repo_path: meta.repo_path };
|
|
2446
|
+
if (meta.branch) args.branch = meta.branch;
|
|
2447
|
+
const fake = { params: { name: 'retrieve_context', arguments: {} } };
|
|
2448
|
+
await this.maybeFillProjectId(fake);
|
|
2449
|
+
if (fake.params.arguments.project_id) args.project_id = fake.params.arguments.project_id;
|
|
2450
|
+
|
|
2451
|
+
const t0 = Date.now();
|
|
2452
|
+
const response = await this.makeRequest({
|
|
2453
|
+
jsonrpc: '2.0',
|
|
2454
|
+
id: `prewarm-resume-${Date.now()}`,
|
|
2455
|
+
method: 'tools/call',
|
|
2456
|
+
params: { name: 'get_context_bootstrap', arguments: args },
|
|
2457
|
+
});
|
|
2458
|
+
const elapsed = Date.now() - t0;
|
|
2459
|
+
// Cache the response keyed by the args it was made for so we
|
|
2460
|
+
// can serve a subsequent identical request directly.
|
|
2461
|
+
this._prewarmedResume = {
|
|
2462
|
+
args,
|
|
2463
|
+
response,
|
|
2464
|
+
fetched_at: Date.now(),
|
|
2465
|
+
};
|
|
2466
|
+
const text = response?.result?.content?.[0]?.text;
|
|
2467
|
+
let handoff_present = false;
|
|
2468
|
+
try {
|
|
2469
|
+
const parsed = JSON.parse(text || '{}');
|
|
2470
|
+
handoff_present = !!parsed.resume_handoff;
|
|
2471
|
+
} catch (_) { /* ignore */ }
|
|
2472
|
+
_mcpTrace('RESUME_PREWARM', `elapsed=${elapsed}ms handoff_present=${handoff_present}`);
|
|
2473
|
+
} catch (err) {
|
|
2474
|
+
this.logDebug(`prewarmResumeContext failed: ${err && err.message}`);
|
|
2475
|
+
}
|
|
2476
|
+
}
|
|
2477
|
+
|
|
2478
|
+
/**
|
|
2479
|
+
* Serve a get_context_bootstrap call from the prewarm cache when the
|
|
2480
|
+
* args match. Cache is dropped on first use so a stale prewarm never
|
|
2481
|
+
* masks a real change. Returns the cached response or null.
|
|
2482
|
+
*/
|
|
2483
|
+
maybeServePrewarmedResume(message) {
|
|
2484
|
+
try {
|
|
2485
|
+
if (!this._prewarmedResume) return null;
|
|
2486
|
+
if (message?.params?.name !== 'get_context_bootstrap') return null;
|
|
2487
|
+
const args = message.params.arguments || {};
|
|
2488
|
+
if (!args.resume) return null;
|
|
2489
|
+
// Stale cutoff: prewarm older than 30s is too stale to trust.
|
|
2490
|
+
const ageMs = Date.now() - this._prewarmedResume.fetched_at;
|
|
2491
|
+
if (ageMs > 30_000) {
|
|
2492
|
+
this._prewarmedResume = null;
|
|
2493
|
+
return null;
|
|
2494
|
+
}
|
|
2495
|
+
// Match on the resume-defining fields. Different repo_path or
|
|
2496
|
+
// session_id_hint means different question — re-fetch.
|
|
2497
|
+
const cached = this._prewarmedResume.args;
|
|
2498
|
+
if ((args.repo_path || cached.repo_path) && args.repo_path !== cached.repo_path) return null;
|
|
2499
|
+
if (args.project_id && args.project_id !== cached.project_id) return null;
|
|
2500
|
+
if (args.session_id_hint) return null; // never serve a specific-session ask from cache
|
|
2501
|
+
const cachedResp = this._prewarmedResume.response;
|
|
2502
|
+
this._prewarmedResume = null; // single-use
|
|
2503
|
+
// Rebind the response id to match the actual request id so the
|
|
2504
|
+
// client can correlate it.
|
|
2505
|
+
return { ...cachedResp, id: message.id };
|
|
2506
|
+
} catch (err) {
|
|
2507
|
+
this.logDebug(`maybeServePrewarmedResume failed: ${err && err.message}`);
|
|
2508
|
+
return null;
|
|
2509
|
+
}
|
|
2510
|
+
}
|
|
2511
|
+
|
|
2327
2512
|
/**
|
|
2328
2513
|
* Fire-and-forget auto-checkpoint. Every N tool calls, if there's an
|
|
2329
2514
|
* active implicit session AND there's been meaningful activity, send a
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cf-memory-mcp",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.14.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": {
|