Skip to main content

lean_ctx/tools/registered/
ctx_tree.rs

1use rmcp::model::Tool;
2use rmcp::ErrorData;
3use serde_json::{json, Map, Value};
4
5use crate::server::tool_trait::{get_bool, get_int, McpTool, ToolContext, ToolOutput};
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            "Directory listing with file counts. Supports multi-root via `paths` array.",
19            json!({
20                "type": "object",
21                "properties": {
22                    "path": { "type": "string", "description": "Directory path (default: .)" },
23                    "paths": {
24                        "type": "array",
25                        "items": { "type": "string" },
26                        "description": "Multiple directories to list (alternative to path)"
27                    },
28                    "depth": { "type": "integer", "description": "Max depth (default: 3)" },
29                    "show_hidden": { "type": "boolean", "description": "Show hidden files" }
30                }
31            }),
32        )
33    }
34
35    fn handle(
36        &self,
37        args: &Map<String, Value>,
38        ctx: &ToolContext,
39    ) -> Result<ToolOutput, ErrorData> {
40        let resolved = crate::server::multi_path::resolve_tool_paths(args, ctx);
41        let depth = (get_int(args, "depth").unwrap_or(3) as usize).min(10);
42        let show_hidden = get_bool(args, "show_hidden").unwrap_or(false);
43
44        if !resolved.is_multi {
45            return handle_single(&resolved.roots[0], depth, show_hidden);
46        }
47
48        let mut combined = String::new();
49        let mut total_original: usize = 0;
50        let mut total_sent: usize = 0;
51
52        for root in &resolved.roots {
53            let root_clone = root.clone();
54            let Ok((result, original)) =
55                std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
56                    crate::tools::ctx_tree::handle(&root_clone, depth, show_hidden)
57                }))
58            else {
59                combined.push_str(&format!("── {root} ──\nERROR: internal panic\n\n"));
60                continue;
61            };
62
63            if result.starts_with("ERROR:") {
64                combined.push_str(&format!("── {root} ──\n{result}\n\n"));
65                continue;
66            }
67
68            combined.push_str(&format!("── {root} ──\n{result}\n\n"));
69            total_original += original;
70            total_sent += crate::core::tokens::count_tokens(&result);
71        }
72
73        let final_out =
74            crate::core::protocol::append_savings(&combined, total_original, total_sent);
75        let saved = total_original.saturating_sub(total_sent);
76
77        Ok(ToolOutput {
78            text: final_out,
79            original_tokens: total_original,
80            saved_tokens: saved,
81            mode: None,
82            path: None,
83            changed: false,
84        })
85    }
86}
87
88fn handle_single(path: &str, depth: usize, show_hidden: bool) -> Result<ToolOutput, ErrorData> {
89    let path_clone = path.to_string();
90    let Ok((result, original)) = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
91        crate::tools::ctx_tree::handle(&path_clone, depth, show_hidden)
92    })) else {
93        return Err(ErrorData::internal_error(
94            format!(
95                "ctx_tree panicked while processing '{path}'. This is a bug — please report it."
96            ),
97            None,
98        ));
99    };
100
101    if result.starts_with("ERROR:") {
102        return Err(ErrorData::invalid_params(result, None));
103    }
104
105    let sent = crate::core::tokens::count_tokens(&result);
106    let saved = original.saturating_sub(sent);
107    let final_out = crate::core::protocol::append_savings(&result, original, sent);
108
109    Ok(ToolOutput {
110        text: final_out,
111        original_tokens: original,
112        saved_tokens: saved,
113        mode: None,
114        path: Some(path.to_string()),
115        changed: false,
116    })
117}