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.
- package/bin/cf-memory-mcp.js +114 -0
- package/package.json +1 -1
package/bin/cf-memory-mcp.js
CHANGED
|
@@ -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.
|
|
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": {
|