cf-memory-mcp 3.9.2 → 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
@@ -1747,6 +1758,112 @@ class CFMemoryMCP {
1747
1758
  }
1748
1759
  }
1749
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
+
1750
1867
  maybeAnnotateStaleness(response, args) {
1751
1868
  try {
1752
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.2",
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": {