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            "List a directory. Prefer over native ls/find (counts, compact tree).\n\
19             Supports multi-root via `paths` array. depth controls recursion (default 3).",
20            json!({
21                "type": "object",
22                "properties": {
23                    "path": { "type": "string", "description": "Directory path (default: .)" },
24                    "paths": {
25                        "type": "array",
26                        "items": { "type": "string" },
27                        "description": "Multiple directories to list (alternative to path)"
28                    },
29                    "depth": { "type": "integer", "description": "Max depth (default: 3)" },
30                    "show_hidden": { "type": "boolean", "description": "Show hidden files" },
31                    "respect_gitignore": { "type": "boolean", "description": "Filter out .gitignore'd files (default: true). Set false to show all files." }
32                }
33            }),
34        )
35    }
36
37    fn handle(
38        &self,
39        args: &Map<String, Value>,
40        ctx: &ToolContext,
41    ) -> Result<ToolOutput, ErrorData> {
42        let resolved = crate::server::multi_path::resolve_tool_paths(args, ctx);
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        })
93    }
94}
95
96fn handle_single(
97    path: &str,
98    depth: usize,
99    show_hidden: bool,
100    respect_gitignore: bool,
101) -> Result<ToolOutput, ErrorData> {
102    let path_clone = path.to_string();
103    let Ok((result, original)) = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
104        crate::tools::ctx_tree::handle(&path_clone, depth, show_hidden, respect_gitignore)
105    })) else {
106        return Err(ErrorData::internal_error(
107            format!(
108                "ctx_tree panicked while processing '{path}'. This is a bug — please report it."
109            ),
110            None,
111        ));
112    };
113
114    if result.starts_with("ERROR:") {
115        return Err(ErrorData::invalid_params(result, None));
116    }
117
118    let sent = crate::core::tokens::count_tokens(&result);
119    let saved = original.saturating_sub(sent);
120    let final_out = crate::core::protocol::append_savings(&result, original, sent);
121
122    Ok(ToolOutput {
123        text: final_out,
124        original_tokens: original,
125        saved_tokens: saved,
126        mode: None,
127        path: Some(path.to_string()),
128        changed: false,
129    })
130}