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.
- package/bin/cf-memory-mcp.js +135 -2
- 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
|
|
@@ -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
|
|
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.
|
|
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": {
|