cf-memory-mcp 3.25.0 → 3.27.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 +229 -2
- package/package.json +1 -1
package/bin/cf-memory-mcp.js
CHANGED
|
@@ -3548,7 +3548,31 @@ class CFMemoryMCP {
|
|
|
3548
3548
|
}
|
|
3549
3549
|
}
|
|
3550
3550
|
|
|
3551
|
+
/**
|
|
3552
|
+
* Retry-aware wrapper. Tries once, then once more after a short
|
|
3553
|
+
* backoff if the first attempt got a network error, 5xx, or timeout.
|
|
3554
|
+
* Opt out with CF_MEMORY_NO_RETRY=1.
|
|
3555
|
+
*/
|
|
3551
3556
|
async makeRequest(message, extraHeaders = null) {
|
|
3557
|
+
const noRetry = process.env.CF_MEMORY_NO_RETRY === '1' || process.env.CF_MEMORY_NO_RETRY === 'true';
|
|
3558
|
+
const first = await this.makeRequestOnce(message, extraHeaders);
|
|
3559
|
+
if (noRetry) return first;
|
|
3560
|
+
// Retry only on transient failures: connection error, timeout, or 5xx.
|
|
3561
|
+
// We never retry application-level errors (4xx / -32602 / -32603 from
|
|
3562
|
+
// server logic) since those are likely to repeat.
|
|
3563
|
+
const isTransient = first?.error && (
|
|
3564
|
+
/Network error|Request timeout/.test(first.error.message || '') ||
|
|
3565
|
+
/HTTP 5\d\d/.test(first.error.message || '')
|
|
3566
|
+
);
|
|
3567
|
+
if (!isTransient) return first;
|
|
3568
|
+
// Brief backoff to let an edge blip clear.
|
|
3569
|
+
await new Promise(r => setTimeout(r, 250));
|
|
3570
|
+
_mcpTrace('RETRY', `id=${message.id} after transient error: ${first.error.message}`);
|
|
3571
|
+
const second = await this.makeRequestOnce(message, extraHeaders);
|
|
3572
|
+
return second;
|
|
3573
|
+
}
|
|
3574
|
+
|
|
3575
|
+
async makeRequestOnce(message, extraHeaders = null) {
|
|
3552
3576
|
return new Promise((resolve) => {
|
|
3553
3577
|
const serverUrl = this.useStreamableHttp ? this.streamableHttpUrl : this.legacyServerUrl;
|
|
3554
3578
|
const url = new URL(serverUrl);
|
|
@@ -3588,6 +3612,18 @@ class CFMemoryMCP {
|
|
|
3588
3612
|
res.on('end', () => {
|
|
3589
3613
|
try {
|
|
3590
3614
|
const response = JSON.parse(body);
|
|
3615
|
+
// Promote 5xx HTTP status to a transient error
|
|
3616
|
+
// envelope so the retry path can catch it. The
|
|
3617
|
+
// server may have returned a valid JSON-RPC error
|
|
3618
|
+
// wrapped in 200, OR a body-less 5xx with no
|
|
3619
|
+
// error field — handle both.
|
|
3620
|
+
if (res.statusCode && res.statusCode >= 500 && !response.error) {
|
|
3621
|
+
response.error = {
|
|
3622
|
+
code: -32603,
|
|
3623
|
+
message: `HTTP ${res.statusCode}`,
|
|
3624
|
+
data: `Server returned ${res.statusCode}`,
|
|
3625
|
+
};
|
|
3626
|
+
}
|
|
3591
3627
|
resolve(response);
|
|
3592
3628
|
} catch (error) {
|
|
3593
3629
|
// Include HTTP status + body snippet so callers can
|
|
@@ -3596,12 +3632,16 @@ class CFMemoryMCP {
|
|
|
3596
3632
|
// "Invalid JSON" alone hides whether the worker is
|
|
3597
3633
|
// even reachable.
|
|
3598
3634
|
const bodyPreview = body.slice(0, 200).replace(/\s+/g, ' ');
|
|
3635
|
+
// 5xx with non-JSON body is transient too (Cloudflare
|
|
3636
|
+
// edge HTML pages). Tag the error message so retry catches it.
|
|
3637
|
+
const transientTag = (res.statusCode && res.statusCode >= 500)
|
|
3638
|
+
? `HTTP ${res.statusCode}` : `HTTP ${res.statusCode || '?'}`;
|
|
3599
3639
|
resolve({
|
|
3600
3640
|
jsonrpc: '2.0',
|
|
3601
3641
|
id: message.id || null,
|
|
3602
3642
|
error: {
|
|
3603
3643
|
code: -32603,
|
|
3604
|
-
message: `Invalid JSON response from server (
|
|
3644
|
+
message: `Invalid JSON response from server (${transientTag})`,
|
|
3605
3645
|
data: `${error.message}; body[0..200]=${bodyPreview}`
|
|
3606
3646
|
}
|
|
3607
3647
|
});
|
|
@@ -3752,6 +3792,8 @@ Usage:
|
|
|
3752
3792
|
npx cf-memory-mcp status Show bridge state + server resume availability
|
|
3753
3793
|
npx cf-memory-mcp clean Delete local disk cache for current cwd
|
|
3754
3794
|
npx cf-memory-mcp clean --all Delete ALL local disk caches
|
|
3795
|
+
npx cf-memory-mcp export <id> Print a session's handoff as JSON bundle (backup)
|
|
3796
|
+
npx cf-memory-mcp import <file> Restore a handoff bundle (cross-machine sync); use "-" for stdin
|
|
3755
3797
|
npx cf-memory-mcp --version Show version
|
|
3756
3798
|
npx cf-memory-mcp --help Show this help
|
|
3757
3799
|
npx cf-memory-mcp --diagnose Test connectivity and report issues
|
|
@@ -3773,7 +3815,7 @@ For more information, visit: https://github.com/johnlam90/cf-memory-mcp
|
|
|
3773
3815
|
}
|
|
3774
3816
|
|
|
3775
3817
|
// Parse positional + flag args for CLI subcommands. Returns
|
|
3776
|
-
// { positional: string[], flags: { json
|
|
3818
|
+
// { positional: string[], flags: { json, limit, md_path } }.
|
|
3777
3819
|
function parseCliArgs(rest) {
|
|
3778
3820
|
const positional = [];
|
|
3779
3821
|
const flags = { json: false };
|
|
@@ -3786,6 +3828,11 @@ function parseCliArgs(rest) {
|
|
|
3786
3828
|
} else if (a.startsWith('--limit=')) {
|
|
3787
3829
|
const n = parseInt(a.slice('--limit='.length), 10);
|
|
3788
3830
|
if (Number.isFinite(n) && n > 0) flags.limit = Math.min(n, 50);
|
|
3831
|
+
} else if (a === '--md') {
|
|
3832
|
+
// Next arg is path; supports `--md=path` too.
|
|
3833
|
+
flags.md_path = rest[++i];
|
|
3834
|
+
} else if (a.startsWith('--md=')) {
|
|
3835
|
+
flags.md_path = a.slice('--md='.length);
|
|
3789
3836
|
} else positional.push(a);
|
|
3790
3837
|
}
|
|
3791
3838
|
return { positional, flags };
|
|
@@ -3830,6 +3877,27 @@ async function runResumeCli() {
|
|
|
3830
3877
|
// 0 — handoff found and printed
|
|
3831
3878
|
// 3 — no handoff found (lets scripts branch: `if ! cf-memory-mcp resume; then ...`)
|
|
3832
3879
|
const found = !!payload.resume_handoff?.handoff;
|
|
3880
|
+
|
|
3881
|
+
// Warm the disk cache. The CLI bypasses the stdio dispatch hook
|
|
3882
|
+
// that normally writes the cache on a successful resume, so do
|
|
3883
|
+
// it here too. Means `cf-memory-mcp resume` ALSO populates the
|
|
3884
|
+
// offline-fallback cache, not just MCP-stdio usage.
|
|
3885
|
+
if (found) server.saveResumeToDisk(response);
|
|
3886
|
+
|
|
3887
|
+
// --md <path>: write the rendered markdown to a file instead of
|
|
3888
|
+
// (or in addition to) stdout. Useful for piping to a markdown
|
|
3889
|
+
// viewer or saving to the project.
|
|
3890
|
+
if (found && flags.md_path && payload.resume_prompt) {
|
|
3891
|
+
try {
|
|
3892
|
+
fs.writeFileSync(flags.md_path, payload.resume_prompt + '\n');
|
|
3893
|
+
process.stdout.write(`Wrote ${payload.resume_prompt.length} chars to ${flags.md_path}\n`);
|
|
3894
|
+
process.exit(0);
|
|
3895
|
+
} catch (err) {
|
|
3896
|
+
console.error(`Failed to write ${flags.md_path}: ${err.message}`);
|
|
3897
|
+
process.exit(1);
|
|
3898
|
+
}
|
|
3899
|
+
}
|
|
3900
|
+
|
|
3833
3901
|
if (flags.json) {
|
|
3834
3902
|
process.stdout.write(JSON.stringify(payload, null, 2) + '\n');
|
|
3835
3903
|
process.exit(found ? 0 : 3);
|
|
@@ -3889,6 +3957,8 @@ async function runListCli() {
|
|
|
3889
3957
|
const payload = JSON.parse(text || '{}');
|
|
3890
3958
|
const allList = Array.isArray(payload.recent_handoffs) ? payload.recent_handoffs : [];
|
|
3891
3959
|
const list = flags.limit ? allList.slice(0, flags.limit) : allList;
|
|
3960
|
+
// Warm the disk cache when there's a primary handoff in the payload.
|
|
3961
|
+
if (payload.resume_handoff) server.saveResumeToDisk(response);
|
|
3892
3962
|
|
|
3893
3963
|
if (flags.json) {
|
|
3894
3964
|
process.stdout.write(JSON.stringify({
|
|
@@ -3930,6 +4000,153 @@ async function runListCli() {
|
|
|
3930
4000
|
}
|
|
3931
4001
|
}
|
|
3932
4002
|
|
|
4003
|
+
async function runExportCli() {
|
|
4004
|
+
if (!API_KEY) {
|
|
4005
|
+
console.error('Error: CF_MEMORY_API_KEY environment variable is required');
|
|
4006
|
+
process.exit(1);
|
|
4007
|
+
}
|
|
4008
|
+
const { positional, flags } = parseCliArgs(process.argv.slice(3));
|
|
4009
|
+
const sessionArg = positional[0];
|
|
4010
|
+
if (!sessionArg) {
|
|
4011
|
+
console.error('Usage: cf-memory-mcp export <session-id-or-prefix> [--md path]');
|
|
4012
|
+
process.exit(1);
|
|
4013
|
+
}
|
|
4014
|
+
const server = new CFMemoryMCP();
|
|
4015
|
+
server.logDebug = () => {};
|
|
4016
|
+
try {
|
|
4017
|
+
const args = { resume: true, session_id_hint: sessionArg };
|
|
4018
|
+
const response = await server.makeRequest({
|
|
4019
|
+
jsonrpc: '2.0',
|
|
4020
|
+
id: `cli-export-${Date.now()}`,
|
|
4021
|
+
method: 'tools/call',
|
|
4022
|
+
params: { name: 'get_context_bootstrap', arguments: args },
|
|
4023
|
+
});
|
|
4024
|
+
const text = response?.result?.content?.[0]?.text;
|
|
4025
|
+
const payload = JSON.parse(text || '{}');
|
|
4026
|
+
const envelope = payload.resume_handoff;
|
|
4027
|
+
if (!envelope?.handoff) {
|
|
4028
|
+
process.stderr.write((payload.empty_hint || `No handoff found for "${sessionArg}".`) + '\n');
|
|
4029
|
+
process.exit(3);
|
|
4030
|
+
}
|
|
4031
|
+
// Build an exportable bundle: enough to fully reconstruct via import.
|
|
4032
|
+
const bundle = {
|
|
4033
|
+
kind: 'cf-memory-handoff',
|
|
4034
|
+
version: 1,
|
|
4035
|
+
exported_at: new Date().toISOString(),
|
|
4036
|
+
session_id: envelope.session_id,
|
|
4037
|
+
started_at: envelope.started_at,
|
|
4038
|
+
ended_at: envelope.ended_at,
|
|
4039
|
+
handoff: envelope.handoff,
|
|
4040
|
+
};
|
|
4041
|
+
const output = JSON.stringify(bundle, null, 2);
|
|
4042
|
+
if (flags.md_path) {
|
|
4043
|
+
fs.writeFileSync(flags.md_path, output);
|
|
4044
|
+
process.stdout.write(`Wrote ${output.length} bytes to ${flags.md_path}\n`);
|
|
4045
|
+
} else {
|
|
4046
|
+
process.stdout.write(output + '\n');
|
|
4047
|
+
}
|
|
4048
|
+
process.exit(0);
|
|
4049
|
+
} catch (err) {
|
|
4050
|
+
console.error('export command failed:', err.message);
|
|
4051
|
+
process.exit(1);
|
|
4052
|
+
}
|
|
4053
|
+
}
|
|
4054
|
+
|
|
4055
|
+
async function runImportCli() {
|
|
4056
|
+
if (!API_KEY) {
|
|
4057
|
+
console.error('Error: CF_MEMORY_API_KEY environment variable is required');
|
|
4058
|
+
process.exit(1);
|
|
4059
|
+
}
|
|
4060
|
+
const { positional, flags } = parseCliArgs(process.argv.slice(3));
|
|
4061
|
+
const sourcePath = positional[0];
|
|
4062
|
+
let raw;
|
|
4063
|
+
try {
|
|
4064
|
+
if (sourcePath === '-' || sourcePath === undefined) {
|
|
4065
|
+
// Read from stdin so `cat handoff.json | cf-memory-mcp import` works.
|
|
4066
|
+
raw = fs.readFileSync(0, 'utf8'); // fd 0 = stdin
|
|
4067
|
+
} else {
|
|
4068
|
+
raw = fs.readFileSync(sourcePath, 'utf8');
|
|
4069
|
+
}
|
|
4070
|
+
} catch (err) {
|
|
4071
|
+
console.error('import: cannot read input:', err.message);
|
|
4072
|
+
process.exit(1);
|
|
4073
|
+
}
|
|
4074
|
+
let bundle;
|
|
4075
|
+
try { bundle = JSON.parse(raw); } catch (err) {
|
|
4076
|
+
console.error('import: input is not valid JSON:', err.message);
|
|
4077
|
+
process.exit(1);
|
|
4078
|
+
}
|
|
4079
|
+
if (bundle.kind !== 'cf-memory-handoff' || !bundle.handoff) {
|
|
4080
|
+
console.error('import: input is not a cf-memory-handoff bundle (expected kind="cf-memory-handoff" + handoff field).');
|
|
4081
|
+
process.exit(1);
|
|
4082
|
+
}
|
|
4083
|
+
const server = new CFMemoryMCP();
|
|
4084
|
+
server.logDebug = () => {};
|
|
4085
|
+
try {
|
|
4086
|
+
// Imports always create a fresh session and write the imported
|
|
4087
|
+
// handoff to it. The original session_id is preserved in
|
|
4088
|
+
// handoff.notes so the link to the source isn't lost.
|
|
4089
|
+
const meta = server.getRepoMetadata();
|
|
4090
|
+
const startArgs = { context: 'main' };
|
|
4091
|
+
if (meta.repo_path) startArgs.repo_path = meta.repo_path;
|
|
4092
|
+
if (meta.branch) startArgs.branch = meta.branch;
|
|
4093
|
+
const fake = { params: { name: 'retrieve_context', arguments: {} } };
|
|
4094
|
+
await server.maybeFillProjectId(fake);
|
|
4095
|
+
if (fake.params.arguments.project_id) startArgs.project_id = fake.params.arguments.project_id;
|
|
4096
|
+
|
|
4097
|
+
const startRes = await server.makeRequest({
|
|
4098
|
+
jsonrpc: '2.0', id: `cli-import-start-${Date.now()}`,
|
|
4099
|
+
method: 'tools/call', params: { name: 'start_session', arguments: startArgs },
|
|
4100
|
+
});
|
|
4101
|
+
const startText = startRes?.result?.content?.[0]?.text;
|
|
4102
|
+
const startPayload = JSON.parse(startText || '{}');
|
|
4103
|
+
const newSessionId = startPayload.session_id;
|
|
4104
|
+
if (!newSessionId) {
|
|
4105
|
+
console.error('import: failed to create new session:', startText);
|
|
4106
|
+
process.exit(1);
|
|
4107
|
+
}
|
|
4108
|
+
|
|
4109
|
+
// Build the handoff: take the imported one, append import note.
|
|
4110
|
+
const importNote = `[imported from session ${bundle.session_id} on ${new Date().toISOString()}; originally started ${bundle.started_at}]`;
|
|
4111
|
+
const handoff = { ...bundle.handoff };
|
|
4112
|
+
handoff.notes = handoff.notes ? `${handoff.notes}\n\n${importNote}` : importNote;
|
|
4113
|
+
// Fill repo metadata from current context if the imported handoff
|
|
4114
|
+
// didn't specify (cross-machine sync use case).
|
|
4115
|
+
if (!handoff.repo_path && meta.repo_path) handoff.repo_path = meta.repo_path;
|
|
4116
|
+
if (!handoff.branch && meta.branch) handoff.branch = meta.branch;
|
|
4117
|
+
if (!handoff.project_id && fake.params.arguments.project_id) handoff.project_id = fake.params.arguments.project_id;
|
|
4118
|
+
|
|
4119
|
+
const endRes = await server.makeRequest({
|
|
4120
|
+
jsonrpc: '2.0', id: `cli-import-end-${Date.now()}`,
|
|
4121
|
+
method: 'tools/call', params: { name: 'end_session', arguments: {
|
|
4122
|
+
session_id: newSessionId,
|
|
4123
|
+
keep_open: true,
|
|
4124
|
+
handoff,
|
|
4125
|
+
} },
|
|
4126
|
+
});
|
|
4127
|
+
const endText = endRes?.result?.content?.[0]?.text;
|
|
4128
|
+
const endPayload = JSON.parse(endText || '{}');
|
|
4129
|
+
if (flags.json) {
|
|
4130
|
+
process.stdout.write(JSON.stringify({
|
|
4131
|
+
imported_from: bundle.session_id,
|
|
4132
|
+
new_session_id: newSessionId,
|
|
4133
|
+
short_id: newSessionId.slice(0, 8),
|
|
4134
|
+
handoff_stored: !!endPayload.handoff_stored,
|
|
4135
|
+
resume_command: `npx cf-memory-mcp resume ${newSessionId.slice(0, 8)}`,
|
|
4136
|
+
}, null, 2) + '\n');
|
|
4137
|
+
} else {
|
|
4138
|
+
process.stdout.write(`Imported handoff into session ${newSessionId.slice(0, 8)} (kept_open).\n`);
|
|
4139
|
+
process.stdout.write(`Original: ${bundle.session_id}\n`);
|
|
4140
|
+
process.stdout.write(`Goal: ${handoff.goal}\n`);
|
|
4141
|
+
process.stdout.write(`To resume: npx cf-memory-mcp resume ${newSessionId.slice(0, 8)}\n`);
|
|
4142
|
+
}
|
|
4143
|
+
process.exit(endPayload.handoff_stored ? 0 : 2);
|
|
4144
|
+
} catch (err) {
|
|
4145
|
+
console.error('import command failed:', err.message);
|
|
4146
|
+
process.exit(1);
|
|
4147
|
+
}
|
|
4148
|
+
}
|
|
4149
|
+
|
|
3933
4150
|
async function runCleanCli() {
|
|
3934
4151
|
const { flags, positional } = parseCliArgs(process.argv.slice(3));
|
|
3935
4152
|
const all = positional.includes('--all') || process.argv.includes('--all');
|
|
@@ -4162,6 +4379,16 @@ if (process.argv[2] === 'clean') {
|
|
|
4162
4379
|
return;
|
|
4163
4380
|
}
|
|
4164
4381
|
|
|
4382
|
+
if (process.argv[2] === 'export') {
|
|
4383
|
+
runExportCli();
|
|
4384
|
+
return;
|
|
4385
|
+
}
|
|
4386
|
+
|
|
4387
|
+
if (process.argv[2] === 'import') {
|
|
4388
|
+
runImportCli();
|
|
4389
|
+
return;
|
|
4390
|
+
}
|
|
4391
|
+
|
|
4165
4392
|
if (process.argv.includes('--diagnose')) {
|
|
4166
4393
|
(async () => {
|
|
4167
4394
|
console.log(`CF Memory MCP v${PACKAGE_VERSION} - Diagnostics`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cf-memory-mcp",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.27.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": {
|