lean_ctx/tools/registered/
ctx_cache.rs1use rmcp::model::Tool;
2use rmcp::ErrorData;
3use serde_json::{json, Map, Value};
4
5use crate::server::tool_trait::{get_str, McpTool, ToolContext, ToolOutput};
6use crate::tool_defs::tool_def;
7
8pub struct CtxCacheTool;
9
10impl McpTool for CtxCacheTool {
11 fn name(&self) -> &'static str {
12 "ctx_cache"
13 }
14
15 fn tool_def(&self) -> Tool {
16 tool_def(
17 "ctx_cache",
18 "Cache ops: status|clear|invalidate.",
19 json!({
20 "type": "object",
21 "properties": {
22 "action": {
23 "type": "string",
24 "enum": ["status", "clear", "invalidate"],
25 "description": "Cache operation to perform"
26 },
27 "path": {
28 "type": "string",
29 "description": "File path (required for 'invalidate' action)"
30 }
31 },
32 "required": ["action"]
33 }),
34 )
35 }
36
37 fn handle(
38 &self,
39 args: &Map<String, Value>,
40 ctx: &ToolContext,
41 ) -> Result<ToolOutput, ErrorData> {
42 let action = get_str(args, "action")
43 .ok_or_else(|| ErrorData::invalid_params("action is required", None))?;
44
45 let invalidate_path = if action == "invalidate" {
46 Some(
47 ctx.resolved_path("path")
48 .ok_or_else(|| {
49 ErrorData::invalid_params("path is required for invalidate", None)
50 })?
51 .to_string(),
52 )
53 } else {
54 None
55 };
56
57 let cache = ctx.cache.as_ref().unwrap();
58 let mut guard = tokio::task::block_in_place(|| cache.blocking_write());
59
60 let result = match action.as_str() {
61 "status" => {
62 let entries = guard.get_all_entries();
63 if entries.is_empty() {
64 "Cache empty — no files tracked.".to_string()
65 } else {
66 let mut lines = vec![format!("Cache: {} file(s)", entries.len())];
67 for (path, entry) in &entries {
68 let fref = guard
69 .file_ref_map()
70 .get(*path)
71 .map_or("F?", std::string::String::as_str);
72 lines.push(format!(
73 " {fref}={} [{}L, {}t, read {}x]",
74 crate::core::protocol::shorten_path(path),
75 entry.line_count,
76 entry.original_tokens,
77 entry.read_count
78 ));
79 }
80 lines.join("\n")
81 }
82 }
83 "clear" => {
84 let count = guard.clear();
85 format!(
86 "Cache cleared — {count} file(s) removed. Next ctx_read will return full content."
87 )
88 }
89 "invalidate" => {
90 let Some(path) = invalidate_path else {
91 return Ok(ToolOutput::simple(
92 "Missing path for invalidate action.".to_string(),
93 ));
94 };
95 if guard.invalidate(&path) {
96 format!(
97 "Invalidated cache for {}. Next ctx_read will return full content.",
98 crate::core::protocol::shorten_path(&path)
99 )
100 } else {
101 format!(
102 "{} was not in cache.",
103 crate::core::protocol::shorten_path(&path)
104 )
105 }
106 }
107 _ => "Unknown action. Use: status, clear, invalidate".to_string(),
108 };
109
110 Ok(ToolOutput {
111 text: result,
112 original_tokens: 0,
113 saved_tokens: 0,
114 mode: Some(action),
115 path: None,
116 })
117 }
118}