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.
Files changed (2) hide show
  1. package/bin/cf-memory-mcp.js +195 -10
  2. package/package.json +1 -1
@@ -201,37 +201,74 @@ const TOOLS_LIST = [
201
201
  },
202
202
  {
203
203
  name: 'get_context_bootstrap',
204
- description: 'Get the most important memories to load at session start. Returns user preferences, ongoing tasks, and relevant facts within the token budget. Includes an empty_hint pointing at store_memory when nothing is stored yet.',
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: 1000)' },
209
- current_context: { type: 'string', description: 'Current conversation context to bias relevance' }
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 conversation session. Returns a session_id used by end_session for summarization.',
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 tracked session and optionally extract memories from the summary.',
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: 'Optional summary of what was discussed' }
233
- },
234
- required: ['session_id']
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.13.0",
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": {