cf-memory-mcp 3.8.12 → 3.9.1

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 +109 -17
  2. package/package.json +1 -1
@@ -155,7 +155,8 @@ const TOOLS_LIST = [
155
155
  file_filter: { type: 'array', items: { type: 'string' }, description: 'Limit to files matching path substrings' },
156
156
  all_projects: { type: 'boolean', description: 'Search across ALL your indexed projects (overrides project_id). Useful for "find X in any of my repos".' },
157
157
  expand_context: { type: 'boolean', description: 'Include file_imports (the file\'s module/imports chunk) with each result. Default: true.' },
158
- exclude_docs: { type: 'boolean', description: 'Filter out markdown/docs from code queries. Default: true (auto-disabled if query mentions docs/readme/tutorial). Response includes docs_filtered_count when chunks were dropped — flip to false to surface them.' }
158
+ exclude_docs: { type: 'boolean', description: 'Filter out markdown/docs from code queries. Default: true (auto-disabled if query mentions docs/readme/tutorial). Response includes docs_filtered_count when chunks were dropped — flip to false to surface them.' },
159
+ auto_refresh: { type: 'boolean', description: 'When true, if results contain stale chunks the bridge transparently calls refresh_files + re-runs the query before returning. Removes the manual find_stale_files → refresh → re-query loop. Adds ~5-10s to the request when stale files exist. Set globally via CF_MEMORY_AUTO_REFRESH=true.' }
159
160
  },
160
161
  required: ['query']
161
162
  }
@@ -740,7 +741,7 @@ class CFMemoryMCP {
740
741
  await this.maybeFillProjectId(message);
741
742
  }
742
743
 
743
- const response = await this.makeRequest(message);
744
+ let response = await this.makeRequest(message);
744
745
  _mcpTrace('DISPATCH_DONE', `id=${message.id} method=${message.method} elapsed=${Date.now()-_t0}ms`);
745
746
 
746
747
  // Annotate retrieve_context results with local staleness when
@@ -750,6 +751,20 @@ class CFMemoryMCP {
750
751
  if (message.method === 'tools/call' &&
751
752
  message.params && message.params.name === 'retrieve_context') {
752
753
  this.maybeAnnotateStaleness(response, message.params.arguments);
754
+
755
+ // Auto-refresh path: when CF_MEMORY_AUTO_REFRESH=true OR the
756
+ // caller passes auto_refresh:true in arguments, and the
757
+ // response had stale chunks, transparently refresh those
758
+ // files and re-run the query. Removes the find-stale →
759
+ // refresh → re-query loop the codex review flagged as
760
+ // operationally brittle. Capped to avoid pathological cases.
761
+ const args = message.params.arguments || {};
762
+ const autoRefreshOptIn = args.auto_refresh === true ||
763
+ process.env.CF_MEMORY_AUTO_REFRESH === '1' ||
764
+ process.env.CF_MEMORY_AUTO_REFRESH === 'true';
765
+ if (autoRefreshOptIn) {
766
+ response = await this.maybeAutoRefreshAndRequery(response, message);
767
+ }
753
768
  }
754
769
 
755
770
  // Send response to stdout
@@ -794,25 +809,31 @@ class CFMemoryMCP {
794
809
  * a full re-index, just refresh the affected files.
795
810
  */
796
811
  async handleRefreshFiles(message) {
797
- const args = (message.params && message.params.arguments) || {};
812
+ const payload = await this.refreshFilesCore(message.params?.arguments || {});
813
+ process.stdout.write(JSON.stringify({
814
+ jsonrpc: '2.0',
815
+ id: message.id,
816
+ result: { content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }] },
817
+ }) + '\n');
818
+ }
819
+
820
+ /**
821
+ * Core refresh-files logic: reads local files, uploads them, returns a
822
+ * payload. Extracted so the auto-refresh path can call it without
823
+ * needing to capture stdout (the previous approach of swapping
824
+ * process.stdout.write was racy against other concurrent messages).
825
+ */
826
+ async refreshFilesCore(args) {
798
827
  const projectIdOrName = args.project_id;
799
828
  const filePaths = Array.isArray(args.file_paths) ? args.file_paths : [];
800
829
  const projectRoot = args.project_root ? path.resolve(args.project_root)
801
830
  : (process.env.CF_MEMORY_WATCH_PATH || process.cwd());
802
831
 
803
- const respond = (payload) => {
804
- process.stdout.write(JSON.stringify({
805
- jsonrpc: '2.0',
806
- id: message.id,
807
- result: { content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }] },
808
- }) + '\n');
809
- };
810
-
811
832
  if (!projectIdOrName) {
812
- return respond({ error: 'project_id is required' });
833
+ return { error: 'project_id is required' };
813
834
  }
814
835
  if (filePaths.length === 0) {
815
- return respond({ error: 'file_paths (string[]) is required' });
836
+ return { error: 'file_paths (string[]) is required' };
816
837
  }
817
838
 
818
839
  // Resolve to project ID via list_projects if a name was given.
@@ -857,26 +878,26 @@ class CFMemoryMCP {
857
878
  }
858
879
 
859
880
  if (files.length === 0) {
860
- return respond({
881
+ return {
861
882
  project_id: projectId,
862
883
  files_refreshed: 0,
863
884
  skipped,
864
885
  hint: `All ${filePaths.length} file path(s) failed to read. Check that paths are relative to project_root (${projectRoot}). If the project was indexed from a different directory, pass project_root=<that directory>.`,
865
- });
886
+ };
866
887
  }
867
888
 
868
889
  const uploadResult = await this.uploadFileBatch(projectId, files);
869
890
  const refreshed = (uploadResult && typeof uploadResult.files_indexed === 'number') ? uploadResult.files_indexed : 0;
870
891
  const chunks = (uploadResult && typeof uploadResult.chunks_created === 'number') ? uploadResult.chunks_created : 0;
871
892
 
872
- return respond({
893
+ return {
873
894
  project_id: projectId,
874
895
  files_attempted: files.length,
875
896
  files_refreshed: refreshed,
876
897
  chunks_created: chunks,
877
898
  skipped,
878
899
  errors: uploadResult && Array.isArray(uploadResult.errors) ? uploadResult.errors : undefined,
879
- });
900
+ };
880
901
  }
881
902
 
882
903
  /**
@@ -1639,6 +1660,77 @@ class CFMemoryMCP {
1639
1660
  }
1640
1661
  }
1641
1662
 
1663
+ /**
1664
+ * If the just-returned retrieve_context response had stale files,
1665
+ * refresh them via refreshFilesCore, re-run the original query, and
1666
+ * return the fresh response. Opt-in via CF_MEMORY_AUTO_REFRESH or
1667
+ * per-call `auto_refresh:true`. Guards against infinite loops by
1668
+ * stripping auto_refresh from the re-query.
1669
+ *
1670
+ * Uses refreshFilesCore (returns data directly) rather than
1671
+ * handleRefreshFiles (writes to stdout). Avoids globally overriding
1672
+ * process.stdout.write, which was racy against any other MCP message
1673
+ * being handled concurrently in the same isolate.
1674
+ */
1675
+ async maybeAutoRefreshAndRequery(response, originalMessage) {
1676
+ try {
1677
+ const text = response?.result?.content?.[0]?.text;
1678
+ if (!text) return response;
1679
+ let parsed;
1680
+ try { parsed = JSON.parse(text); } catch (_) { return response; }
1681
+ const hint = parsed?.stale_refresh_hint;
1682
+ const stalePaths = hint?.arguments?.file_paths;
1683
+ const projectId = hint?.arguments?.project_id;
1684
+ if (!stalePaths || stalePaths.length === 0 || !projectId) {
1685
+ return response; // Nothing stale, return as-is
1686
+ }
1687
+
1688
+ _mcpTrace('AUTO_REFRESH', `${stalePaths.length} stale files; refreshing then re-querying`);
1689
+
1690
+ // Call core directly — no stdout override needed.
1691
+ const refreshResult = await this.refreshFilesCore({
1692
+ project_id: projectId,
1693
+ file_paths: stalePaths,
1694
+ });
1695
+ _mcpTrace('AUTO_REFRESH_DONE', `refreshed=${refreshResult.files_refreshed || 0}`);
1696
+
1697
+ // Re-run the original retrieve_context (without auto_refresh
1698
+ // to prevent loops). Server cache invalidates on write so the
1699
+ // re-query sees fresh data.
1700
+ const requeryArgs = { ...(originalMessage.params.arguments || {}) };
1701
+ delete requeryArgs.auto_refresh;
1702
+ const requery = {
1703
+ ...originalMessage,
1704
+ id: `${originalMessage.id}-refreshed`,
1705
+ params: { ...originalMessage.params, arguments: requeryArgs },
1706
+ };
1707
+ const fresh = await this.makeRequest(requery);
1708
+
1709
+ // Tag the fresh response so callers can see auto-refresh fired
1710
+ try {
1711
+ const freshText = fresh?.result?.content?.[0]?.text;
1712
+ if (freshText) {
1713
+ const freshParsed = JSON.parse(freshText);
1714
+ freshParsed.auto_refreshed = {
1715
+ files_refreshed: refreshResult.files_refreshed || stalePaths.length,
1716
+ file_paths: stalePaths,
1717
+ };
1718
+ fresh.result.content[0].text = JSON.stringify(freshParsed);
1719
+ }
1720
+ } catch (_) { /* best-effort tag only */ }
1721
+
1722
+ // Re-annotate so the fresh response carries the staleness check
1723
+ // result (should now be empty)
1724
+ this.maybeAnnotateStaleness(fresh, requeryArgs);
1725
+ // Restore the original message id so the MCP client correlates correctly
1726
+ fresh.id = originalMessage.id;
1727
+ return fresh;
1728
+ } catch (err) {
1729
+ this.logDebug(`maybeAutoRefreshAndRequery failed: ${err && err.message}`);
1730
+ return response;
1731
+ }
1732
+ }
1733
+
1642
1734
  maybeAnnotateStaleness(response, args) {
1643
1735
  try {
1644
1736
  const text = response?.result?.content?.[0]?.text;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cf-memory-mcp",
3
- "version": "3.8.12",
3
+ "version": "3.9.1",
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": {