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