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.
- package/bin/cf-memory-mcp.js +118 -1
- package/package.json +1 -1
package/bin/cf-memory-mcp.js
CHANGED
|
@@ -286,7 +286,7 @@ const TOOLS_LIST = [
|
|
|
286
286
|
},
|
|
287
287
|
{
|
|
288
288
|
name: 'get_file_content',
|
|
289
|
-
description: '
|
|
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.
|
|
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": {
|