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