cf-memory-mcp 3.21.0 → 3.23.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 +210 -16
  2. package/package.json +1 -1
@@ -216,7 +216,8 @@ const TOOLS_LIST = [
216
216
  session_id_hint: { type: 'string', description: 'Resume a specific session by id (overrides repo/branch matching). Full UUID or short prefix (>=8 chars). Useful after seeing recent_handoffs.' },
217
217
  max_age_minutes: { type: 'number', description: 'Hard cutoff: hide handoffs older than this many minutes. Without it, older handoffs surface with reduced confidence.' },
218
218
  goal_contains: { type: 'string', description: 'Substring filter on handoff content (goal/notes/decisions). Case-insensitive. Lets agents find a specific thread by what it was about.' },
219
- since: { type: 'string', description: 'Lower bound on handoff timestamp (ISO 8601). Scope resume to a time window.' }
219
+ since: { type: 'string', description: 'Lower bound on handoff timestamp (ISO 8601). Scope resume to a time window.' },
220
+ status_filter: { description: 'Restrict matches to handoffs with this status. Single string or array of strings (e.g., ["in_progress","blocked"]).' }
220
221
  }
221
222
  }
222
223
  },
@@ -695,7 +696,11 @@ class CFMemoryMCP {
695
696
  // cfm://resume/current and recent handoffs at
696
697
  // cfm://resume/recent for clients that prefer the
697
698
  // resources protocol over tool calls.
698
- resources: {}
699
+ resources: {},
700
+ // Prompts capability exposes /resume and /list_threads
701
+ // for clients (Claude Desktop, etc.) that surface
702
+ // prompts as slash-commands in their UI.
703
+ prompts: {}
699
704
  },
700
705
  serverInfo: {
701
706
  name: 'cf-memory-mcp-simplified',
@@ -856,17 +861,147 @@ class CFMemoryMCP {
856
861
  return;
857
862
  }
858
863
 
859
- // Handle prompts/list locally (we have none)
864
+ // Handle prompts/list advertise the resume + threads prompts.
865
+ // These let MCP clients (Claude Desktop, etc.) show a slash-
866
+ // command UI like /resume that injects the prior handoff into
867
+ // the next message without an explicit tool call.
860
868
  if (message.method === 'prompts/list') {
861
869
  const response = {
862
870
  jsonrpc: '2.0',
863
871
  id: message.id,
864
- result: { prompts: [] }
872
+ result: {
873
+ prompts: [
874
+ {
875
+ name: 'resume',
876
+ description: 'Insert the prior resume handoff for this repo into the chat so the agent can pick up where the last session left off.',
877
+ arguments: [
878
+ { name: 'session_id', description: 'Optional: resume a specific session (full UUID or short prefix >=8 chars).', required: false },
879
+ ],
880
+ },
881
+ {
882
+ name: 'list_threads',
883
+ description: 'Insert a list of recent handoffs for this repo so the user can pick a specific thread to resume.',
884
+ arguments: [
885
+ { name: 'status', description: 'Optional: filter by status (in_progress, blocked, completed, abandoned).', required: false },
886
+ ],
887
+ },
888
+ ],
889
+ },
865
890
  };
866
891
  process.stdout.write(JSON.stringify(response) + '\n');
867
892
  return;
868
893
  }
869
894
 
895
+ // Handle prompts/get — render the requested prompt with current
896
+ // resume state baked in.
897
+ if (message.method === 'prompts/get') {
898
+ const promptName = message.params?.name;
899
+ const promptArgs = message.params?.arguments || {};
900
+ try {
901
+ if (promptName === 'resume') {
902
+ const meta = this.getRepoMetadata();
903
+ const args = { resume: true };
904
+ if (meta.repo_path) args.repo_path = meta.repo_path;
905
+ if (meta.branch) args.branch = meta.branch;
906
+ if (promptArgs.session_id) args.session_id_hint = promptArgs.session_id;
907
+ const fake = { params: { name: 'retrieve_context', arguments: {} } };
908
+ await this.maybeFillProjectId(fake);
909
+ if (fake.params.arguments.project_id) args.project_id = fake.params.arguments.project_id;
910
+ const bootstrap = await this.makeRequest({
911
+ jsonrpc: '2.0',
912
+ id: `prompt-resume-${Date.now()}`,
913
+ method: 'tools/call',
914
+ params: { name: 'get_context_bootstrap', arguments: args },
915
+ });
916
+ const text = bootstrap?.result?.content?.[0]?.text || '{}';
917
+ const payload = JSON.parse(text);
918
+ const body = payload.resume_prompt
919
+ || (payload.empty_hint || 'No prior resume handoff for this context.');
920
+ const response = {
921
+ jsonrpc: '2.0',
922
+ id: message.id,
923
+ result: {
924
+ description: `Resume context for ${meta.repo_path || 'this cwd'}`,
925
+ messages: [
926
+ { role: 'user', content: { type: 'text', text: body } },
927
+ ],
928
+ },
929
+ };
930
+ process.stdout.write(JSON.stringify(response) + '\n');
931
+ return;
932
+ }
933
+ if (promptName === 'list_threads') {
934
+ const meta = this.getRepoMetadata();
935
+ const args = { resume: true };
936
+ if (meta.repo_path) args.repo_path = meta.repo_path;
937
+ if (meta.branch) args.branch = meta.branch;
938
+ if (promptArgs.status) args.status_filter = promptArgs.status;
939
+ const fake = { params: { name: 'retrieve_context', arguments: {} } };
940
+ await this.maybeFillProjectId(fake);
941
+ if (fake.params.arguments.project_id) args.project_id = fake.params.arguments.project_id;
942
+ const bootstrap = await this.makeRequest({
943
+ jsonrpc: '2.0',
944
+ id: `prompt-list-${Date.now()}`,
945
+ method: 'tools/call',
946
+ params: { name: 'get_context_bootstrap', arguments: args },
947
+ });
948
+ const text = bootstrap?.result?.content?.[0]?.text || '{}';
949
+ const payload = JSON.parse(text);
950
+ const list = Array.isArray(payload.recent_handoffs) ? payload.recent_handoffs : [];
951
+ const fmtAge = (iso) => {
952
+ if (!iso) return '?';
953
+ const min = Math.round((Date.now() - new Date(iso).getTime()) / 60000);
954
+ if (min < 60) return `${min}m`;
955
+ if (min < 1440) return `${Math.round(min/60)}h`;
956
+ return `${Math.round(min/1440)}d`;
957
+ };
958
+ const lines = ['## Recent handoffs for this repo', ''];
959
+ if (list.length === 0) {
960
+ lines.push(payload.empty_hint || '(no handoffs)');
961
+ } else {
962
+ for (const h of list) {
963
+ const shortId = (h.session_id || '').slice(0, 8);
964
+ const age = fmtAge(h.ended_at || h.started_at);
965
+ lines.push(`- \`${shortId}\` [${h.status || '?'}] ${age} ago — ${h.goal || '(no goal)'}`);
966
+ if (h.next_step) lines.push(` next: ${h.next_step}`);
967
+ }
968
+ lines.push('', 'Pick one via `/resume <id>` or call `get_context_bootstrap({resume:true, session_id_hint:"<id>"})`.');
969
+ }
970
+ const response = {
971
+ jsonrpc: '2.0',
972
+ id: message.id,
973
+ result: {
974
+ description: `Recent handoffs for ${meta.repo_path || 'this cwd'}`,
975
+ messages: [
976
+ { role: 'user', content: { type: 'text', text: lines.join('\n') } },
977
+ ],
978
+ },
979
+ };
980
+ process.stdout.write(JSON.stringify(response) + '\n');
981
+ return;
982
+ }
983
+ // Unknown prompt name
984
+ const response = {
985
+ jsonrpc: '2.0',
986
+ id: message.id,
987
+ error: {
988
+ code: -32602,
989
+ message: `Unknown prompt: ${promptName}. Supported: resume, list_threads`,
990
+ },
991
+ };
992
+ process.stdout.write(JSON.stringify(response) + '\n');
993
+ return;
994
+ } catch (err) {
995
+ const response = {
996
+ jsonrpc: '2.0',
997
+ id: message.id,
998
+ error: { code: -32603, message: `Failed to render prompt: ${err && err.message}` },
999
+ };
1000
+ process.stdout.write(JSON.stringify(response) + '\n');
1001
+ return;
1002
+ }
1003
+ }
1004
+
870
1005
  // Handle ping locally
871
1006
  if (message.method === 'ping') {
872
1007
  const response = {
@@ -3580,15 +3715,17 @@ A portable MCP (Model Context Protocol) server for AI memory storage using Cloud
3580
3715
 
3581
3716
  Usage:
3582
3717
  npx cf-memory-mcp Start the MCP server
3583
- npx cf-memory-mcp resume Print the prior resume handoff for cwd (markdown)
3584
- npx cf-memory-mcp resume <id> Print a specific handoff by session_id prefix
3718
+ npx cf-memory-mcp resume [id] Print the prior resume handoff (markdown)
3585
3719
  npx cf-memory-mcp list List recent handoffs for cwd
3586
- npx cf-memory-mcp checkpoint Snapshot current state to a fresh handoff (keep session open)
3587
- npx cf-memory-mcp checkpoint "<goal>" Same, with an explicit goal string
3720
+ npx cf-memory-mcp checkpoint ["<goal>"] Snapshot current state (keep_open)
3588
3721
  npx cf-memory-mcp --version Show version
3589
3722
  npx cf-memory-mcp --help Show this help
3590
3723
  npx cf-memory-mcp --diagnose Test connectivity and report issues
3591
3724
 
3725
+ Subcommand flags:
3726
+ --json, -j Emit JSON instead of formatted text (resume/list/checkpoint)
3727
+ --limit N, -n N For 'list': max number of handoffs (default 5, max 50)
3728
+
3592
3729
  Environment Variables:
3593
3730
  CF_MEMORY_API_KEY=<key> Your CF Memory API key (required)
3594
3731
  CF_MEMORY_BASE_URL=<url> Override the default deployed worker
@@ -3601,6 +3738,25 @@ For more information, visit: https://github.com/johnlam90/cf-memory-mcp
3601
3738
  process.exit(0);
3602
3739
  }
3603
3740
 
3741
+ // Parse positional + flag args for CLI subcommands. Returns
3742
+ // { positional: string[], flags: { json: boolean, limit?: number } }.
3743
+ function parseCliArgs(rest) {
3744
+ const positional = [];
3745
+ const flags = { json: false };
3746
+ for (let i = 0; i < rest.length; i++) {
3747
+ const a = rest[i];
3748
+ if (a === '--json' || a === '-j') flags.json = true;
3749
+ else if (a === '--limit' || a === '-n') {
3750
+ const n = parseInt(rest[++i], 10);
3751
+ if (Number.isFinite(n) && n > 0) flags.limit = Math.min(n, 50);
3752
+ } else if (a.startsWith('--limit=')) {
3753
+ const n = parseInt(a.slice('--limit='.length), 10);
3754
+ if (Number.isFinite(n) && n > 0) flags.limit = Math.min(n, 50);
3755
+ } else positional.push(a);
3756
+ }
3757
+ return { positional, flags };
3758
+ }
3759
+
3604
3760
  // Read-only inspection command: print the prior resume handoff to stdout.
3605
3761
  // Lets a human see what state is captured without involving an MCP client.
3606
3762
  async function runResumeCli() {
@@ -3608,6 +3764,7 @@ async function runResumeCli() {
3608
3764
  console.error('Error: CF_MEMORY_API_KEY environment variable is required');
3609
3765
  process.exit(1);
3610
3766
  }
3767
+ const { positional, flags } = parseCliArgs(process.argv.slice(3));
3611
3768
  const server = new CFMemoryMCP();
3612
3769
  server.logDebug = () => {};
3613
3770
  server.logError = (...a) => process.stderr.write(a.join(' ') + '\n');
@@ -3617,7 +3774,7 @@ async function runResumeCli() {
3617
3774
  if (meta.repo_path) args.repo_path = meta.repo_path;
3618
3775
  if (meta.branch) args.branch = meta.branch;
3619
3776
  // Optional positional session_id argument.
3620
- const sessionArg = process.argv[3];
3777
+ const sessionArg = positional[0];
3621
3778
  if (sessionArg) args.session_id_hint = sessionArg;
3622
3779
  const fake = { params: { name: 'retrieve_context', arguments: {} } };
3623
3780
  await server.maybeFillProjectId(fake);
@@ -3635,7 +3792,15 @@ async function runResumeCli() {
3635
3792
  process.exit(2);
3636
3793
  }
3637
3794
  const payload = JSON.parse(text);
3638
- if (payload.resume_handoff?.handoff) {
3795
+ // Exit codes:
3796
+ // 0 — handoff found and printed
3797
+ // 3 — no handoff found (lets scripts branch: `if ! cf-memory-mcp resume; then ...`)
3798
+ const found = !!payload.resume_handoff?.handoff;
3799
+ if (flags.json) {
3800
+ process.stdout.write(JSON.stringify(payload, null, 2) + '\n');
3801
+ process.exit(found ? 0 : 3);
3802
+ }
3803
+ if (found) {
3639
3804
  if (payload.resume_prompt) {
3640
3805
  process.stdout.write(payload.resume_prompt + '\n');
3641
3806
  } else {
@@ -3651,8 +3816,10 @@ async function runResumeCli() {
3651
3816
  }
3652
3817
  process.exit(0);
3653
3818
  } else {
3654
- process.stdout.write((payload.empty_hint || 'No resume handoff available.') + '\n');
3655
- process.exit(0);
3819
+ // No handoff: print empty_hint to stderr so it doesn't pollute
3820
+ // stdout for scripts that pipe the output.
3821
+ process.stderr.write((payload.empty_hint || 'No resume handoff available.') + '\n');
3822
+ process.exit(3);
3656
3823
  }
3657
3824
  } catch (err) {
3658
3825
  console.error('resume command failed:', err.message);
@@ -3665,6 +3832,7 @@ async function runListCli() {
3665
3832
  console.error('Error: CF_MEMORY_API_KEY environment variable is required');
3666
3833
  process.exit(1);
3667
3834
  }
3835
+ const { flags } = parseCliArgs(process.argv.slice(3));
3668
3836
  const server = new CFMemoryMCP();
3669
3837
  server.logDebug = () => {};
3670
3838
  server.logError = (...a) => process.stderr.write(a.join(' ') + '\n');
@@ -3685,10 +3853,22 @@ async function runListCli() {
3685
3853
  });
3686
3854
  const text = response?.result?.content?.[0]?.text;
3687
3855
  const payload = JSON.parse(text || '{}');
3688
- const list = Array.isArray(payload.recent_handoffs) ? payload.recent_handoffs : [];
3856
+ const allList = Array.isArray(payload.recent_handoffs) ? payload.recent_handoffs : [];
3857
+ const list = flags.limit ? allList.slice(0, flags.limit) : allList;
3858
+
3859
+ if (flags.json) {
3860
+ process.stdout.write(JSON.stringify({
3861
+ repo_path: meta.repo_path,
3862
+ branch: meta.branch,
3863
+ count: list.length,
3864
+ handoffs: list,
3865
+ empty_hint: list.length === 0 ? payload.empty_hint : undefined,
3866
+ }, null, 2) + '\n');
3867
+ process.exit(list.length === 0 ? 3 : 0);
3868
+ }
3689
3869
  if (list.length === 0) {
3690
- process.stdout.write((payload.empty_hint || 'No handoffs for this context.') + '\n');
3691
- process.exit(0);
3870
+ process.stderr.write((payload.empty_hint || 'No handoffs for this context.') + '\n');
3871
+ process.exit(3);
3692
3872
  }
3693
3873
  // Render a compact table to terminal.
3694
3874
  process.stdout.write(`Recent handoffs for ${meta.repo_path || 'this cwd'}:\n\n`);
@@ -3721,12 +3901,13 @@ async function runCheckpointCli() {
3721
3901
  console.error('Error: CF_MEMORY_API_KEY environment variable is required');
3722
3902
  process.exit(1);
3723
3903
  }
3904
+ const { positional, flags } = parseCliArgs(process.argv.slice(3));
3724
3905
  const server = new CFMemoryMCP();
3725
3906
  server.logDebug = () => {};
3726
3907
  server.logError = (...a) => process.stderr.write(a.join(' ') + '\n');
3727
3908
  try {
3728
3909
  // Optional positional goal argument: `cf-memory-mcp checkpoint "<goal text>"`
3729
- const goalArg = process.argv.slice(3).join(' ').trim();
3910
+ const goalArg = positional.join(' ').trim();
3730
3911
  const meta = server.getRepoMetadata();
3731
3912
  // Need a session to attach the handoff to. Use or create the
3732
3913
  // implicit session for this cwd (creates a new one if no implicit).
@@ -3758,6 +3939,19 @@ async function runCheckpointCli() {
3758
3939
  });
3759
3940
  const text = response?.result?.content?.[0]?.text;
3760
3941
  const payload = JSON.parse(text || '{}');
3942
+ if (flags.json) {
3943
+ process.stdout.write(JSON.stringify({
3944
+ session_id: sessionId,
3945
+ short_id: sessionId.slice(0, 8),
3946
+ handoff_stored: !!payload.handoff_stored,
3947
+ kept_open: !!payload.kept_open,
3948
+ goal: handoff.goal,
3949
+ repo_path: handoff.repo_path,
3950
+ branch: handoff.branch,
3951
+ resume_command: `npx cf-memory-mcp resume ${sessionId.slice(0, 8)}`,
3952
+ }, null, 2) + '\n');
3953
+ process.exit(payload.handoff_stored ? 0 : 2);
3954
+ }
3761
3955
  if (payload.handoff_stored) {
3762
3956
  const shortId = sessionId.slice(0, 8);
3763
3957
  process.stdout.write(`Checkpoint saved (session ${shortId}).\n`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cf-memory-mcp",
3
- "version": "3.21.0",
3
+ "version": "3.23.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": {