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, require_resolved_path, 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(require_resolved_path(ctx, args, "path")?)
47 } else {
48 None
49 };
50
51 let cache = ctx.cache.as_ref().unwrap();
52 let Some(mut guard) = crate::server::bounded_lock::write(cache, "ctx_cache") else {
53 return Ok(ToolOutput::simple(
54 "[cache lock temporarily unavailable — retry in a moment]".to_string(),
55 ));
56 };
57
58 let result = match action.as_str() {
59 "status" => {
60 let entries = guard.get_all_entries();
61 if entries.is_empty() {
62 "Cache empty — no files tracked.".to_string()
63 } else {
64 let mut lines = vec![format!("Cache: {} file(s)", entries.len())];
65 for (path, entry) in &entries {
66 let fref = guard
67 .file_ref_map()
68 .get(*path)
69 .map_or("F?", std::string::String::as_str);
70 lines.push(format!(
71 " {fref}={} [{}L, {}t, read {}x]",
72 crate::core::protocol::shorten_path(path),
73 entry.line_count,
74 entry.original_tokens,
75 entry.read_count
76 ));
77 }
78 lines.join("\n")
79 }
80 }
81 "clear" => {
82 let count = guard.clear();
83 format!(
84 "Cache cleared — {count} file(s) removed. Next ctx_read will return full content."
85 )
86 }
87 "invalidate" => {
88 let Some(path) = invalidate_path else {
89 return Ok(ToolOutput::simple(
90 "Missing path for invalidate action.".to_string(),
91 ));
92 };
93 if guard.invalidate(&path) {
94 format!(
95 "Invalidated cache for {}. Next ctx_read will return full content.",
96 crate::core::protocol::shorten_path(&path)
97 )
98 } else {
99 format!(
100 "{} was not in cache.",
101 crate::core::protocol::shorten_path(&path)
102 )
103 }
104 }
105 _ => "Unknown action. Use: status, clear, invalidate".to_string(),
106 };
107
108 Ok(ToolOutput {
109 text: result,
110 original_tokens: 0,
111 saved_tokens: 0,
112 mode: Some(action),
113 path: None,
114 changed: false,
115 })
116 }
117}