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.
- package/bin/cf-memory-mcp.js +109 -17
- package/package.json +1 -1
package/bin/cf-memory-mcp.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
|
833
|
+
return { error: 'project_id is required' };
|
|
813
834
|
}
|
|
814
835
|
if (filePaths.length === 0) {
|
|
815
|
-
return
|
|
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
|
|
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
|
|
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.
|
|
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": {
|