cf-memory-mcp 3.22.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 +152 -13
  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 = {
@@ -3657,13 +3792,15 @@ async function runResumeCli() {
3657
3792
  process.exit(2);
3658
3793
  }
3659
3794
  const payload = JSON.parse(text);
3660
- // JSON output mode: emit the full payload as a single JSON object.
3661
- // Scripts can pipe through jq to extract specific fields.
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;
3662
3799
  if (flags.json) {
3663
3800
  process.stdout.write(JSON.stringify(payload, null, 2) + '\n');
3664
- process.exit(payload.resume_handoff ? 0 : 0);
3801
+ process.exit(found ? 0 : 3);
3665
3802
  }
3666
- if (payload.resume_handoff?.handoff) {
3803
+ if (found) {
3667
3804
  if (payload.resume_prompt) {
3668
3805
  process.stdout.write(payload.resume_prompt + '\n');
3669
3806
  } else {
@@ -3679,8 +3816,10 @@ async function runResumeCli() {
3679
3816
  }
3680
3817
  process.exit(0);
3681
3818
  } else {
3682
- process.stdout.write((payload.empty_hint || 'No resume handoff available.') + '\n');
3683
- 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);
3684
3823
  }
3685
3824
  } catch (err) {
3686
3825
  console.error('resume command failed:', err.message);
@@ -3725,11 +3864,11 @@ async function runListCli() {
3725
3864
  handoffs: list,
3726
3865
  empty_hint: list.length === 0 ? payload.empty_hint : undefined,
3727
3866
  }, null, 2) + '\n');
3728
- process.exit(0);
3867
+ process.exit(list.length === 0 ? 3 : 0);
3729
3868
  }
3730
3869
  if (list.length === 0) {
3731
- process.stdout.write((payload.empty_hint || 'No handoffs for this context.') + '\n');
3732
- process.exit(0);
3870
+ process.stderr.write((payload.empty_hint || 'No handoffs for this context.') + '\n');
3871
+ process.exit(3);
3733
3872
  }
3734
3873
  // Render a compact table to terminal.
3735
3874
  process.stdout.write(`Recent handoffs for ${meta.repo_path || 'this cwd'}:\n\n`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cf-memory-mcp",
3
- "version": "3.22.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": {