cf-memory-mcp 3.9.1 → 3.9.3

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.
@@ -286,7 +286,7 @@ const TOOLS_LIST = [
286
286
  },
287
287
  {
288
288
  name: 'get_file_content',
289
- description: 'Reassemble and return the full content of an indexed file from its stored chunks. Use when retrieve_context returned a fragment and you need the whole file context, or when working remotely without local filesystem access. Returns indexed_at + file_hash for staleness checks.',
289
+ description: 'Return the full content of a file. When the bridge can resolve the file under the project root locally, it reads byte-exact source from disk and returns source:"local" suitable for code review and patch planning. When local read fails (remote/CI environments, missing project root), falls back to server-side reassembly from indexed chunks and returns reconstructed_from_chunks:true + reconstruction_warning + missing_line_ranges (approximate, suitable for orientation only). Always returns indexed_at + file_hash for staleness checks.',
290
290
  inputSchema: {
291
291
  type: 'object',
292
292
  properties: {
@@ -731,6 +731,17 @@ class CFMemoryMCP {
731
731
  return;
732
732
  }
733
733
 
734
+ // Intercept get_file_content: prefer byte-exact local file read
735
+ // over server-side chunk reconstruction when the file is
736
+ // resolvable locally. Codex review flagged the reconstruction
737
+ // path as "useful for orientation only, not exact review or
738
+ // patch planning". Local read closes that gap.
739
+ if (message.method === 'tools/call' && message.params && message.params.name === 'get_file_content') {
740
+ const handled = await this.maybeServeLocalFileContent(message);
741
+ if (handled) return;
742
+ // Fall through to server-side reconstruction if local read failed.
743
+ }
744
+
734
745
  _mcpTrace('DISPATCH', `id=${message.id} method=${message.method} -> network`);
735
746
  // If this is a retrieve_context call with no project_id and no
736
747
  // all_projects, try to auto-fill project_id from the current
@@ -1694,6 +1705,21 @@ class CFMemoryMCP {
1694
1705
  });
1695
1706
  _mcpTrace('AUTO_REFRESH_DONE', `refreshed=${refreshResult.files_refreshed || 0}`);
1696
1707
 
1708
+ // If refresh itself errored or refreshed 0 files, return the
1709
+ // original response with a clear note rather than re-querying
1710
+ // and pretending the refresh succeeded.
1711
+ if (refreshResult.error || (refreshResult.files_refreshed || 0) === 0) {
1712
+ try {
1713
+ parsed.auto_refresh_failed = {
1714
+ attempted: stalePaths.length,
1715
+ error: refreshResult.error || 'refresh ran but indexed 0 files (paths may be unreadable or project_root mismatched)',
1716
+ skipped: refreshResult.skipped,
1717
+ };
1718
+ response.result.content[0].text = JSON.stringify(parsed);
1719
+ } catch (_) { /* best-effort tag */ }
1720
+ return response;
1721
+ }
1722
+
1697
1723
  // Re-run the original retrieve_context (without auto_refresh
1698
1724
  // to prevent loops). Server cache invalidates on write so the
1699
1725
  // re-query sees fresh data.
@@ -1712,7 +1738,8 @@ class CFMemoryMCP {
1712
1738
  if (freshText) {
1713
1739
  const freshParsed = JSON.parse(freshText);
1714
1740
  freshParsed.auto_refreshed = {
1715
- files_refreshed: refreshResult.files_refreshed || stalePaths.length,
1741
+ files_refreshed: refreshResult.files_refreshed,
1742
+ chunks_created: refreshResult.chunks_created || 0,
1716
1743
  file_paths: stalePaths,
1717
1744
  };
1718
1745
  fresh.result.content[0].text = JSON.stringify(freshParsed);
@@ -1731,6 +1758,112 @@ class CFMemoryMCP {
1731
1758
  }
1732
1759
  }
1733
1760
 
1761
+ /**
1762
+ * Read the requested file from the local filesystem if we can resolve
1763
+ * it via the project's root_path or the bridge's cwd. Returns the
1764
+ * byte-exact source plus a `source: "local"` flag so callers know it's
1765
+ * not a chunk reconstruction. Falls back (returns false) when:
1766
+ * - We can't resolve a project root locally
1767
+ * - The file doesn't exist at the resolved path
1768
+ * - The local read errors for any reason
1769
+ *
1770
+ * When fallthrough happens, the message flows on to the server which
1771
+ * returns the chunk-reconstructed approximation with reconstruction_warning.
1772
+ */
1773
+ async maybeServeLocalFileContent(message) {
1774
+ try {
1775
+ const args = message.params?.arguments || {};
1776
+ const filePath = args.file_path;
1777
+ const projectIdOrName = args.project_id;
1778
+ const maxChars = Math.min(100000, Math.max(1000, Number(args.max_chars) || 50000));
1779
+ if (!filePath) return false;
1780
+
1781
+ // Resolve project root: prefer explicit project_root arg, then
1782
+ // look up the project's stored root_path on the server, then
1783
+ // fall back to CF_MEMORY_WATCH_PATH / cwd.
1784
+ let projectRoot = args.project_root ? path.resolve(args.project_root) : null;
1785
+ if (!projectRoot && projectIdOrName) {
1786
+ try {
1787
+ const list = await this.makeRequest({
1788
+ jsonrpc: '2.0',
1789
+ id: `gfc-list-${Date.now()}`,
1790
+ method: 'tools/call',
1791
+ params: { name: 'list_projects', arguments: {} },
1792
+ });
1793
+ const projects = JSON.parse(list.result.content[0].text);
1794
+ const match = Array.isArray(projects)
1795
+ ? projects.find(p => p.id === projectIdOrName || p.name === projectIdOrName)
1796
+ : null;
1797
+ if (match?.root_path) projectRoot = match.root_path;
1798
+ } catch (_) { /* ignore lookup failure, fall through */ }
1799
+ }
1800
+ if (!projectRoot) {
1801
+ projectRoot = process.env.CF_MEMORY_WATCH_PATH || process.cwd();
1802
+ }
1803
+
1804
+ // Try exact path first, then walk up substring matches under root
1805
+ const candidates = [
1806
+ path.resolve(projectRoot, filePath),
1807
+ path.resolve(filePath),
1808
+ ];
1809
+ let resolvedFull = null;
1810
+ for (const c of candidates) {
1811
+ try {
1812
+ const stat = fs.statSync(c);
1813
+ if (stat.isFile()) {
1814
+ resolvedFull = c;
1815
+ break;
1816
+ }
1817
+ } catch (_) { /* try next */ }
1818
+ }
1819
+ if (!resolvedFull) return false;
1820
+
1821
+ const fullContent = fs.readFileSync(resolvedFull, 'utf8');
1822
+ const stat = fs.statSync(resolvedFull);
1823
+ const truncated = fullContent.length > maxChars;
1824
+ const content = truncated ? fullContent.slice(0, maxChars) + '\n... [truncated]' : fullContent;
1825
+ const totalLines = fullContent.split('\n').length;
1826
+ const localHash = crypto.createHash('sha256').update(fullContent, 'utf8').digest('hex');
1827
+
1828
+ // Detect language from extension (mirror the server's mapping)
1829
+ const ext = path.extname(resolvedFull).toLowerCase();
1830
+ const extToLang = {
1831
+ '.ts': 'typescript', '.tsx': 'typescript', '.js': 'javascript', '.jsx': 'javascript',
1832
+ '.mjs': 'javascript', '.cjs': 'javascript', '.py': 'python', '.go': 'go',
1833
+ '.rs': 'rust', '.java': 'java', '.kt': 'kotlin', '.scala': 'scala',
1834
+ '.cs': 'csharp', '.cpp': 'cpp', '.c': 'c', '.h': 'c', '.hpp': 'cpp',
1835
+ '.rb': 'ruby', '.php': 'php', '.swift': 'swift', '.md': 'markdown',
1836
+ '.sql': 'sql', '.sh': 'bash', '.json': 'json', '.yaml': 'yaml', '.yml': 'yaml',
1837
+ '.toml': 'toml',
1838
+ };
1839
+ const language = extToLang[ext] || null;
1840
+
1841
+ const payload = {
1842
+ file_path: path.relative(projectRoot, resolvedFull) || path.basename(resolvedFull),
1843
+ language,
1844
+ total_lines: totalLines,
1845
+ size_bytes: stat.size,
1846
+ indexed_at: stat.mtime.toISOString(),
1847
+ file_hash: localHash,
1848
+ content,
1849
+ truncated,
1850
+ source: 'local',
1851
+ local_path: resolvedFull,
1852
+ };
1853
+
1854
+ process.stdout.write(JSON.stringify({
1855
+ jsonrpc: '2.0',
1856
+ id: message.id,
1857
+ result: { content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }] },
1858
+ }) + '\n');
1859
+ _mcpTrace('GFC_LOCAL', `served ${resolvedFull} (${stat.size}b, lang=${language})`);
1860
+ return true;
1861
+ } catch (err) {
1862
+ this.logDebug(`maybeServeLocalFileContent failed: ${err && err.message}`);
1863
+ return false;
1864
+ }
1865
+ }
1866
+
1734
1867
  maybeAnnotateStaleness(response, args) {
1735
1868
  try {
1736
1869
  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.9.1",
3
+ "version": "3.9.3",
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": {