cf-memory-mcp 3.10.1 → 3.11.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.
@@ -792,6 +792,11 @@ class CFMemoryMCP {
792
792
  await this.maybeAttachResumeMetadata(message);
793
793
  }
794
794
 
795
+ // Record which files the agent is touching so end_session can
796
+ // auto-populate handoff.files_read. Cheap (in-memory Map), safe
797
+ // (try/catch'd), and the agent's hand-written list always wins.
798
+ this.trackToolActivity(message);
799
+
795
800
  let response = await this.makeRequest(message);
796
801
  _mcpTrace('DISPATCH_DONE', `id=${message.id} method=${message.method} elapsed=${Date.now()-_t0}ms`);
797
802
 
@@ -801,6 +806,9 @@ class CFMemoryMCP {
801
806
  // acting on stale indexed content as if it were current.
802
807
  if (message.method === 'tools/call' &&
803
808
  message.params && message.params.name === 'retrieve_context') {
809
+ // Track which files the response surfaced — these are what
810
+ // the agent will read next, and they belong in handoff.files_read.
811
+ this.trackRetrieveContextResponse(response);
804
812
  this.maybeAnnotateStaleness(response, message.params.arguments);
805
813
 
806
814
  // Auto-refresh path: when CF_MEMORY_AUTO_REFRESH=true OR the
@@ -2122,6 +2130,11 @@ class CFMemoryMCP {
2122
2130
  args.handoff.project_id = fake.params.arguments.project_id;
2123
2131
  }
2124
2132
  }
2133
+ // Auto-fill files_read from observed retrieve_context /
2134
+ // get_file_content / get_file_outline calls during this
2135
+ // bridge session, if the agent didn't supply them. The
2136
+ // agent's hand-written files_touched stays authoritative.
2137
+ this.autoFillHandoffFilesRead(args.handoff);
2125
2138
  return;
2126
2139
  }
2127
2140
  } catch (err) {
@@ -2129,6 +2142,107 @@ class CFMemoryMCP {
2129
2142
  }
2130
2143
  }
2131
2144
 
2145
+ /**
2146
+ * Record file paths the agent touched via retrieve_context, get_file_content,
2147
+ * get_file_outline, and refresh_files. Used to seed end_session.handoff.files_read
2148
+ * when the agent didn't bother listing them. The set is bounded so a chatty
2149
+ * session can't blow up memory.
2150
+ */
2151
+ trackToolActivity(message) {
2152
+ try {
2153
+ if (message?.method !== 'tools/call' || !message.params) return;
2154
+ const toolName = message.params.name;
2155
+ const args = message.params.arguments || {};
2156
+ if (!this._activityFiles) this._activityFiles = new Map();
2157
+
2158
+ const noteFile = (file_path, source) => {
2159
+ if (typeof file_path !== 'string' || !file_path) return;
2160
+ // Bound to 50 entries — past that we drop oldest.
2161
+ if (this._activityFiles.size >= 50 && !this._activityFiles.has(file_path)) {
2162
+ const firstKey = this._activityFiles.keys().next().value;
2163
+ this._activityFiles.delete(firstKey);
2164
+ }
2165
+ const existing = this._activityFiles.get(file_path) || { sources: new Set(), count: 0 };
2166
+ existing.sources.add(source);
2167
+ existing.count += 1;
2168
+ existing.last_seen = Date.now();
2169
+ this._activityFiles.set(file_path, existing);
2170
+ };
2171
+
2172
+ if (toolName === 'get_file_content' || toolName === 'get_file_outline') {
2173
+ noteFile(args.file_path, toolName);
2174
+ } else if (toolName === 'refresh_files' && Array.isArray(args.file_paths)) {
2175
+ for (const p of args.file_paths) noteFile(p, 'refresh_files');
2176
+ }
2177
+ } catch (err) {
2178
+ this.logDebug(`trackToolActivity failed: ${err && err.message}`);
2179
+ }
2180
+ }
2181
+
2182
+ /**
2183
+ * Record file paths from a retrieve_context response. Called after the
2184
+ * server responds so we capture what was actually returned (not just
2185
+ * what was queried for).
2186
+ */
2187
+ trackRetrieveContextResponse(response) {
2188
+ try {
2189
+ if (!this._activityFiles) this._activityFiles = new Map();
2190
+ const text = response?.result?.content?.[0]?.text;
2191
+ if (!text) return;
2192
+ let parsed;
2193
+ try { parsed = JSON.parse(text); } catch (_) { return; }
2194
+ const results = Array.isArray(parsed?.results) ? parsed.results : [];
2195
+ for (const r of results.slice(0, 20)) {
2196
+ if (typeof r?.file_path !== 'string') continue;
2197
+ if (this._activityFiles.size >= 50 && !this._activityFiles.has(r.file_path)) {
2198
+ const firstKey = this._activityFiles.keys().next().value;
2199
+ this._activityFiles.delete(firstKey);
2200
+ }
2201
+ const existing = this._activityFiles.get(r.file_path) || { sources: new Set(), count: 0 };
2202
+ existing.sources.add('retrieve_context');
2203
+ existing.count += 1;
2204
+ existing.last_seen = Date.now();
2205
+ this._activityFiles.set(r.file_path, existing);
2206
+ }
2207
+ } catch (err) {
2208
+ this.logDebug(`trackRetrieveContextResponse failed: ${err && err.message}`);
2209
+ }
2210
+ }
2211
+
2212
+ /**
2213
+ * Populate handoff.files_read from observed tool activity, but only
2214
+ * when the agent did not already list any. Hand-written entries always
2215
+ * win — the bridge is a fallback, not a replacement.
2216
+ */
2217
+ autoFillHandoffFilesRead(handoff) {
2218
+ try {
2219
+ if (!handoff || typeof handoff !== 'object') return;
2220
+ if (Array.isArray(handoff.files_read) && handoff.files_read.length > 0) return;
2221
+ if (!this._activityFiles || this._activityFiles.size === 0) return;
2222
+
2223
+ // Exclude files the agent already listed as touched — those
2224
+ // outrank "read". Then take the top 20 by access count.
2225
+ const touchedPaths = new Set(
2226
+ Array.isArray(handoff.files_touched)
2227
+ ? handoff.files_touched.map(f => f.path).filter(Boolean)
2228
+ : []
2229
+ );
2230
+ const entries = Array.from(this._activityFiles.entries())
2231
+ .filter(([p]) => !touchedPaths.has(p))
2232
+ .sort((a, b) => b[1].count - a[1].count)
2233
+ .slice(0, 20);
2234
+ if (entries.length === 0) return;
2235
+
2236
+ handoff.files_read = entries.map(([path, info]) => ({
2237
+ path,
2238
+ why: `Observed via ${Array.from(info.sources).join(', ')} (${info.count}x)`,
2239
+ }));
2240
+ _mcpTrace('HANDOFF_AUTOFILL', `files_read=${entries.length}`);
2241
+ } catch (err) {
2242
+ this.logDebug(`autoFillHandoffFilesRead failed: ${err && err.message}`);
2243
+ }
2244
+ }
2245
+
2132
2246
  /**
2133
2247
  * If the just-returned retrieve_context response had stale files,
2134
2248
  * refresh them via refreshFilesCore, re-run the original query, and
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cf-memory-mcp",
3
- "version": "3.10.1",
3
+ "version": "3.11.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": {