lean_ctx/tools/registered/
ctx_tree.rs1use rmcp::ErrorData;
2use rmcp::model::Tool;
3use serde_json::{Map, Value, json};
4
5use crate::server::tool_trait::{McpTool, ToolContext, ToolOutput, get_bool, get_int};
6use crate::tool_defs::tool_def;
7
8pub struct CtxTreeTool;
9
10impl McpTool for CtxTreeTool {
11 fn name(&self) -> &'static str {
12 "ctx_tree"
13 }
14
15 fn tool_def(&self) -> Tool {
16 tool_def(
17 "ctx_tree",
18 "List a directory. Prefer over native ls/find (counts, compact tree).",
19 json!({
20 "type": "object",
21 "properties": {
22 "path": { "type": "string", "description": "Directory (default: .)" },
23 "paths": {
24 "type": "array",
25 "items": { "type": "string" },
26 "description": "Multiple roots (alternative to path)"
27 },
28 "depth": { "type": "integer", "description": "Max depth (default 3)" },
29 "show_hidden": { "type": "boolean" },
30 "respect_gitignore": { "type": "boolean", "description": "default true" }
31 }
32 }),
33 )
34 }
35
36 fn handle(
37 &self,
38 args: &Map<String, Value>,
39 ctx: &ToolContext,
40 ) -> Result<ToolOutput, ErrorData> {
41 let resolved = crate::server::multi_path::resolve_tool_paths(args, ctx)
42 .map_err(|e| ErrorData::invalid_params(format!("ERROR: {e}"), None))?;
43 let depth = (get_int(args, "depth").unwrap_or(3) as usize).min(10);
44 let show_hidden = get_bool(args, "show_hidden").unwrap_or(false);
45 let respect_gitignore = get_bool(args, "respect_gitignore").unwrap_or(true);
46
47 if !resolved.is_multi {
48 return handle_single(&resolved.roots[0], depth, show_hidden, respect_gitignore);
49 }
50
51 let mut combined = String::new();
52 let mut total_original: usize = 0;
53 let mut total_sent: usize = 0;
54
55 for root in &resolved.roots {
56 let root_clone = root.clone();
57 let Ok((result, original)) =
58 std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
59 crate::tools::ctx_tree::handle(
60 &root_clone,
61 depth,
62 show_hidden,
63 respect_gitignore,
64 )
65 }))
66 else {
67 combined.push_str(&format!("── {root} ──\nERROR: internal panic\n\n"));
68 continue;
69 };
70
71 if result.starts_with("ERROR:") {
72 combined.push_str(&format!("── {root} ──\n{result}\n\n"));
73 continue;
74 }
75
76 combined.push_str(&format!("── {root} ──\n{result}\n\n"));
77 total_original += original;
78 total_sent += crate::core::tokens::count_tokens(&result);
79 }
80
81 let final_out =
82 crate::core::protocol::append_savings(&combined, total_original, total_sent);
83 let saved = total_original.saturating_sub(total_sent);
84
85 Ok(ToolOutput {
86 text: final_out,
87 original_tokens: total_original,
88 saved_tokens: saved,
89 mode: None,
90 path: None,
91 changed: false,
92 shell_outcome: None,
93 })
94 }
95}
96
97fn handle_single(
98 path: &str,
99 depth: usize,
100 show_hidden: bool,
101 respect_gitignore: bool,
102) -> Result<ToolOutput, ErrorData> {
103 let path_clone = path.to_string();
104 let Ok((result, original)) = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
105 crate::tools::ctx_tree::handle(&path_clone, depth, show_hidden, respect_gitignore)
106 })) else {
107 return Err(ErrorData::internal_error(
108 format!(
109 "ctx_tree panicked while processing '{path}'. This is a bug — please report it."
110 ),
111 None,
112 ));
113 };
114
115 if result.starts_with("ERROR:") {
116 return Err(ErrorData::invalid_params(result, None));
117 }
118
119 let sent = crate::core::tokens::count_tokens(&result);
120 let saved = original.saturating_sub(sent);
121 let final_out = crate::core::protocol::append_savings(&result, original, sent);
122
123 Ok(ToolOutput {
124 text: final_out,
125 original_tokens: original,
126 saved_tokens: saved,
127 mode: None,
128 path: Some(path.to_string()),
129 changed: false,
130 shell_outcome: None,
131 })
132}