cf-memory-mcp 3.11.0 → 3.13.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 +223 -14
- package/package.json +1 -1
package/bin/cf-memory-mcp.js
CHANGED
|
@@ -826,9 +826,30 @@ class CFMemoryMCP {
|
|
|
826
826
|
}
|
|
827
827
|
}
|
|
828
828
|
|
|
829
|
+
// After end_session without keep_open, drop the implicit-session
|
|
830
|
+
// cache so a subsequent end_session creates a fresh session.
|
|
831
|
+
if (message.method === 'tools/call' && message.params?.name === 'end_session') {
|
|
832
|
+
this.clearImplicitSessionIfFinalized(message, response);
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
// After explicit start_session, cache the returned session_id as
|
|
836
|
+
// the implicit session for this cwd. Lets a subsequent
|
|
837
|
+
// end_session({handoff:{...}}) without session_id resolve to
|
|
838
|
+
// the just-created one.
|
|
839
|
+
if (message.method === 'tools/call' && message.params?.name === 'start_session') {
|
|
840
|
+
this.cacheImplicitSessionFromResponse(response);
|
|
841
|
+
}
|
|
842
|
+
|
|
829
843
|
// Send response to stdout
|
|
830
844
|
process.stdout.write(JSON.stringify(response) + '\n');
|
|
831
845
|
|
|
846
|
+
// Safety net: every N tool calls, if there's an active implicit
|
|
847
|
+
// session AND tracked activity, fire a background auto-checkpoint
|
|
848
|
+
// with a synthesized handoff. Catches agents that forget to call
|
|
849
|
+
// end_session before a crash or timeout. Fire-and-forget so it
|
|
850
|
+
// doesn't slow down the real response.
|
|
851
|
+
this.maybeAutoCheckpoint();
|
|
852
|
+
|
|
832
853
|
} catch (error) {
|
|
833
854
|
this.logError('Error handling message:', error);
|
|
834
855
|
_mcpTrace('ERROR', `${error.message} elapsed=${Date.now()-_t0}ms`);
|
|
@@ -2119,22 +2140,35 @@ class CFMemoryMCP {
|
|
|
2119
2140
|
return;
|
|
2120
2141
|
}
|
|
2121
2142
|
|
|
2122
|
-
if (toolName === 'end_session'
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
await this.
|
|
2129
|
-
if (
|
|
2130
|
-
args.
|
|
2143
|
+
if (toolName === 'end_session') {
|
|
2144
|
+
// session_id auto-fill: if the caller didn't pass one, use
|
|
2145
|
+
// the implicit session for this cwd. Lets agents call
|
|
2146
|
+
// end_session({handoff:{...}}) without threading session_id
|
|
2147
|
+
// through every checkpoint.
|
|
2148
|
+
if (!args.session_id) {
|
|
2149
|
+
const implicit = await this.getOrCreateImplicitSession();
|
|
2150
|
+
if (implicit) {
|
|
2151
|
+
args.session_id = implicit;
|
|
2152
|
+
_mcpTrace('IMPLICIT_SESSION', `end_session using implicit session=${implicit}`);
|
|
2153
|
+
}
|
|
2154
|
+
}
|
|
2155
|
+
if (args.handoff && typeof args.handoff === 'object') {
|
|
2156
|
+
const meta = this.getRepoMetadata();
|
|
2157
|
+
if (!args.handoff.repo_path && meta.repo_path) args.handoff.repo_path = meta.repo_path;
|
|
2158
|
+
if (!args.handoff.branch && meta.branch) args.handoff.branch = meta.branch;
|
|
2159
|
+
if (!args.handoff.project_id) {
|
|
2160
|
+
const fake = { params: { name: 'retrieve_context', arguments: {} } };
|
|
2161
|
+
await this.maybeFillProjectId(fake);
|
|
2162
|
+
if (fake.params.arguments.project_id) {
|
|
2163
|
+
args.handoff.project_id = fake.params.arguments.project_id;
|
|
2164
|
+
}
|
|
2131
2165
|
}
|
|
2166
|
+
// Auto-fill files_read from observed retrieve_context /
|
|
2167
|
+
// get_file_content / get_file_outline calls during this
|
|
2168
|
+
// bridge session, if the agent didn't supply them. The
|
|
2169
|
+
// agent's hand-written files_touched stays authoritative.
|
|
2170
|
+
this.autoFillHandoffFilesRead(args.handoff);
|
|
2132
2171
|
}
|
|
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);
|
|
2138
2172
|
return;
|
|
2139
2173
|
}
|
|
2140
2174
|
} catch (err) {
|
|
@@ -2142,6 +2176,107 @@ class CFMemoryMCP {
|
|
|
2142
2176
|
}
|
|
2143
2177
|
}
|
|
2144
2178
|
|
|
2179
|
+
/**
|
|
2180
|
+
* Return the implicit session_id for the current cwd, creating one via
|
|
2181
|
+
* start_session when no session is active. Lets agents skip explicit
|
|
2182
|
+
* session management — they can go straight to end_session({handoff})
|
|
2183
|
+
* for checkpoint or finalization.
|
|
2184
|
+
*
|
|
2185
|
+
* The cache is keyed by cwd so multiple bridge processes for different
|
|
2186
|
+
* repos don't share state. Sessions finalized via end_session WITHOUT
|
|
2187
|
+
* keep_open clear the implicit cache so the next call starts fresh.
|
|
2188
|
+
*/
|
|
2189
|
+
async getOrCreateImplicitSession() {
|
|
2190
|
+
try {
|
|
2191
|
+
const cwd = process.env.CF_MEMORY_WATCH_PATH || process.cwd();
|
|
2192
|
+
if (!this._implicitSessionByCwd) this._implicitSessionByCwd = new Map();
|
|
2193
|
+
const cached = this._implicitSessionByCwd.get(cwd);
|
|
2194
|
+
if (cached) return cached;
|
|
2195
|
+
|
|
2196
|
+
// Lazily start a session, tagged with the same repo metadata as
|
|
2197
|
+
// start_session would auto-attach if called explicitly.
|
|
2198
|
+
const meta = this.getRepoMetadata();
|
|
2199
|
+
const startArgs = { context: 'main' };
|
|
2200
|
+
if (meta.repo_path) startArgs.repo_path = meta.repo_path;
|
|
2201
|
+
if (meta.branch) startArgs.branch = meta.branch;
|
|
2202
|
+
const fake = { params: { name: 'retrieve_context', arguments: {} } };
|
|
2203
|
+
await this.maybeFillProjectId(fake);
|
|
2204
|
+
if (fake.params.arguments.project_id) startArgs.project_id = fake.params.arguments.project_id;
|
|
2205
|
+
|
|
2206
|
+
const response = await this.makeRequest({
|
|
2207
|
+
jsonrpc: '2.0',
|
|
2208
|
+
id: `auto-session-${Date.now()}`,
|
|
2209
|
+
method: 'tools/call',
|
|
2210
|
+
params: { name: 'start_session', arguments: startArgs },
|
|
2211
|
+
});
|
|
2212
|
+
let sessionId = null;
|
|
2213
|
+
try {
|
|
2214
|
+
const payload = JSON.parse(response.result.content[0].text);
|
|
2215
|
+
sessionId = payload.session_id;
|
|
2216
|
+
} catch (_) { /* malformed response */ }
|
|
2217
|
+
if (sessionId) {
|
|
2218
|
+
this._implicitSessionByCwd.set(cwd, sessionId);
|
|
2219
|
+
_mcpTrace('IMPLICIT_SESSION', `created implicit session=${sessionId} for cwd=${cwd}`);
|
|
2220
|
+
}
|
|
2221
|
+
return sessionId;
|
|
2222
|
+
} catch (err) {
|
|
2223
|
+
this.logDebug(`getOrCreateImplicitSession failed: ${err && err.message}`);
|
|
2224
|
+
return null;
|
|
2225
|
+
}
|
|
2226
|
+
}
|
|
2227
|
+
|
|
2228
|
+
/**
|
|
2229
|
+
* After an explicit start_session response, cache the returned session_id
|
|
2230
|
+
* as the implicit session for the current cwd. This way the next
|
|
2231
|
+
* end_session call without a session_id picks up the right one
|
|
2232
|
+
* automatically.
|
|
2233
|
+
*/
|
|
2234
|
+
cacheImplicitSessionFromResponse(response) {
|
|
2235
|
+
try {
|
|
2236
|
+
const text = response?.result?.content?.[0]?.text;
|
|
2237
|
+
if (!text) return;
|
|
2238
|
+
let parsed;
|
|
2239
|
+
try { parsed = JSON.parse(text); } catch (_) { return; }
|
|
2240
|
+
const sessionId = parsed?.session_id;
|
|
2241
|
+
if (!sessionId) return;
|
|
2242
|
+
const cwd = process.env.CF_MEMORY_WATCH_PATH || process.cwd();
|
|
2243
|
+
if (!this._implicitSessionByCwd) this._implicitSessionByCwd = new Map();
|
|
2244
|
+
this._implicitSessionByCwd.set(cwd, sessionId);
|
|
2245
|
+
_mcpTrace('IMPLICIT_SESSION', `cached implicit session=${sessionId} for cwd=${cwd}`);
|
|
2246
|
+
} catch (err) {
|
|
2247
|
+
this.logDebug(`cacheImplicitSessionFromResponse failed: ${err && err.message}`);
|
|
2248
|
+
}
|
|
2249
|
+
}
|
|
2250
|
+
|
|
2251
|
+
/**
|
|
2252
|
+
* Clear the implicit session for the current cwd after a non-keep_open
|
|
2253
|
+
* end_session call. Lets a subsequent checkpoint create a fresh
|
|
2254
|
+
* session instead of reusing a finalized one.
|
|
2255
|
+
*/
|
|
2256
|
+
clearImplicitSessionIfFinalized(originalMessage, response) {
|
|
2257
|
+
try {
|
|
2258
|
+
if (originalMessage?.params?.name !== 'end_session') return;
|
|
2259
|
+
const args = originalMessage.params.arguments || {};
|
|
2260
|
+
if (args.keep_open) return; // checkpoint — keep cache
|
|
2261
|
+
// Parse the response to confirm the session was actually ended
|
|
2262
|
+
// (vs. an error). Don't clear if the server errored.
|
|
2263
|
+
const text = response?.result?.content?.[0]?.text;
|
|
2264
|
+
if (!text) return;
|
|
2265
|
+
try {
|
|
2266
|
+
const parsed = JSON.parse(text);
|
|
2267
|
+
if (parsed?.error || parsed?.kept_open) return;
|
|
2268
|
+
} catch (_) { return; }
|
|
2269
|
+
|
|
2270
|
+
const cwd = process.env.CF_MEMORY_WATCH_PATH || process.cwd();
|
|
2271
|
+
if (this._implicitSessionByCwd?.has(cwd)) {
|
|
2272
|
+
this._implicitSessionByCwd.delete(cwd);
|
|
2273
|
+
_mcpTrace('IMPLICIT_SESSION', `cleared implicit session for cwd=${cwd}`);
|
|
2274
|
+
}
|
|
2275
|
+
} catch (err) {
|
|
2276
|
+
this.logDebug(`clearImplicitSessionIfFinalized failed: ${err && err.message}`);
|
|
2277
|
+
}
|
|
2278
|
+
}
|
|
2279
|
+
|
|
2145
2280
|
/**
|
|
2146
2281
|
* Record file paths the agent touched via retrieve_context, get_file_content,
|
|
2147
2282
|
* get_file_outline, and refresh_files. Used to seed end_session.handoff.files_read
|
|
@@ -2174,11 +2309,85 @@ class CFMemoryMCP {
|
|
|
2174
2309
|
} else if (toolName === 'refresh_files' && Array.isArray(args.file_paths)) {
|
|
2175
2310
|
for (const p of args.file_paths) noteFile(p, 'refresh_files');
|
|
2176
2311
|
}
|
|
2312
|
+
|
|
2313
|
+
// Remember the most recent retrieve_context query so auto-checkpoints
|
|
2314
|
+
// have something concrete to put in the synthesized handoff.goal.
|
|
2315
|
+
if (toolName === 'retrieve_context' && typeof args.query === 'string' && args.query.trim()) {
|
|
2316
|
+
this._lastRetrieveQuery = args.query.trim();
|
|
2317
|
+
}
|
|
2318
|
+
|
|
2319
|
+
// Tool-call counter for auto-checkpoint safety net. Counts all
|
|
2320
|
+
// tool/call messages; auto-checkpoint fires every N calls.
|
|
2321
|
+
this._toolCallCount = (this._toolCallCount || 0) + 1;
|
|
2177
2322
|
} catch (err) {
|
|
2178
2323
|
this.logDebug(`trackToolActivity failed: ${err && err.message}`);
|
|
2179
2324
|
}
|
|
2180
2325
|
}
|
|
2181
2326
|
|
|
2327
|
+
/**
|
|
2328
|
+
* Fire-and-forget auto-checkpoint. Every N tool calls, if there's an
|
|
2329
|
+
* active implicit session AND there's been meaningful activity, send a
|
|
2330
|
+
* background end_session({keep_open:true}) with a synthesized handoff.
|
|
2331
|
+
* This is the safety net for agents that forget (or crash) before
|
|
2332
|
+
* calling end_session manually.
|
|
2333
|
+
*
|
|
2334
|
+
* Behavior:
|
|
2335
|
+
* - Only fires when an implicit session exists (no surprise creation)
|
|
2336
|
+
* - Synthesizes handoff.goal from the last retrieve_context query
|
|
2337
|
+
* - Pulls files_read from activity tracker
|
|
2338
|
+
* - keep_open=true so the user can still finalize properly
|
|
2339
|
+
* - Failures are silent — never blocks the real tool call
|
|
2340
|
+
*/
|
|
2341
|
+
async maybeAutoCheckpoint() {
|
|
2342
|
+
try {
|
|
2343
|
+
const intervalEnv = parseInt(process.env.CF_MEMORY_CHECKPOINT_EVERY || '25', 10);
|
|
2344
|
+
const interval = Number.isFinite(intervalEnv) && intervalEnv > 0 ? intervalEnv : 25;
|
|
2345
|
+
const opt = process.env.CF_MEMORY_AUTO_CHECKPOINT;
|
|
2346
|
+
// Opt-out by env. Default is on. Set to 0/false/off to disable.
|
|
2347
|
+
const enabled = !(opt === '0' || opt === 'false' || opt === 'off');
|
|
2348
|
+
if (!enabled) return;
|
|
2349
|
+
const count = this._toolCallCount || 0;
|
|
2350
|
+
if (count === 0 || count % interval !== 0) return;
|
|
2351
|
+
const cwd = process.env.CF_MEMORY_WATCH_PATH || process.cwd();
|
|
2352
|
+
const sessionId = this._implicitSessionByCwd?.get(cwd);
|
|
2353
|
+
if (!sessionId) return; // no implicit session, nothing to checkpoint
|
|
2354
|
+
// Require at least one tracked file so we don't spam empty handoffs.
|
|
2355
|
+
if (!this._activityFiles || this._activityFiles.size === 0) return;
|
|
2356
|
+
|
|
2357
|
+
// Synthesize handoff.
|
|
2358
|
+
const meta = this.getRepoMetadata();
|
|
2359
|
+
const fake = { params: { name: 'retrieve_context', arguments: {} } };
|
|
2360
|
+
await this.maybeFillProjectId(fake);
|
|
2361
|
+
const handoff = {
|
|
2362
|
+
goal: this._lastRetrieveQuery
|
|
2363
|
+
? `Working on: ${this._lastRetrieveQuery.slice(0, 120)}`
|
|
2364
|
+
: `Auto-checkpoint at ${count} tool calls`,
|
|
2365
|
+
status: 'in_progress',
|
|
2366
|
+
};
|
|
2367
|
+
if (meta.repo_path) handoff.repo_path = meta.repo_path;
|
|
2368
|
+
if (meta.branch) handoff.branch = meta.branch;
|
|
2369
|
+
if (fake.params.arguments.project_id) handoff.project_id = fake.params.arguments.project_id;
|
|
2370
|
+
this.autoFillHandoffFilesRead(handoff);
|
|
2371
|
+
|
|
2372
|
+
// Fire-and-forget — don't block the real response on this.
|
|
2373
|
+
// Awaiting would serialise auto-checkpoint behind the main
|
|
2374
|
+
// request path. Failures are logged at debug only.
|
|
2375
|
+
this.makeRequest({
|
|
2376
|
+
jsonrpc: '2.0',
|
|
2377
|
+
id: `auto-checkpoint-${Date.now()}`,
|
|
2378
|
+
method: 'tools/call',
|
|
2379
|
+
params: { name: 'end_session', arguments: {
|
|
2380
|
+
session_id: sessionId,
|
|
2381
|
+
keep_open: true,
|
|
2382
|
+
handoff,
|
|
2383
|
+
} },
|
|
2384
|
+
}).catch(err => this.logDebug(`auto-checkpoint failed: ${err && err.message}`));
|
|
2385
|
+
_mcpTrace('AUTO_CHECKPOINT', `session=${sessionId} after ${count} calls files=${this._activityFiles.size}`);
|
|
2386
|
+
} catch (err) {
|
|
2387
|
+
this.logDebug(`maybeAutoCheckpoint failed: ${err && err.message}`);
|
|
2388
|
+
}
|
|
2389
|
+
}
|
|
2390
|
+
|
|
2182
2391
|
/**
|
|
2183
2392
|
* Record file paths from a retrieve_context response. Called after the
|
|
2184
2393
|
* server responds so we capture what was actually returned (not just
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cf-memory-mcp",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.13.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": {
|