Skip to main content

lean_ctx/tools/registered/
ctx_tree.rs

1use 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            "Directory tree with file counts per directory. depth=N (default 3);\n\
19             show_hidden for dotfiles; paths for multi-root.\n\
20             respect_gitignore filters ignored files (default true).\n\
21             WORKFLOW: lightweight orientation before ctx_repomap or ctx_compose.",
22            json!({
23                "type": "object",
24                "properties": {
25                    "path": { "type": "string", "description": "Dir" },
26                    "paths": {
27                        "type": "array",
28                        "items": { "type": "string" },
29                        "description": "Multi-root (alternative to path)"
30                    },
31                    "depth": { "type": "integer", "description": "Max depth" },
32                    "show_hidden": { "type": "boolean", "description": "Include dotfiles" },
33                    "respect_gitignore": { "type": "boolean", "description": "Filter ignored" }
34                }
35            }),
36        )
37    }
38
39    fn handle(
40        &self,
41        args: &Map<String, Value>,
42        ctx: &ToolContext,
43    ) -> Result<ToolOutput, ErrorData> {
44        let resolved = crate::server::multi_path::resolve_tool_paths(args, ctx)
45            .map_err(|e| ErrorData::invalid_params(format!("ERROR: {e}"), None))?;
46        let depth = (get_int(args, "depth").unwrap_or(3) as usize).min(10);
47        let show_hidden = get_bool(args, "show_hidden").unwrap_or(false);
48        let respect_gitignore = get_bool(args, "respect_gitignore").unwrap_or(true);
49
50        if !resolved.is_multi {
51            return handle_single(&resolved.roots[0], depth, show_hidden, respect_gitignore);
52        }
53
54        let mut combined = String::new();
55        let mut total_original: usize = 0;
56        let mut total_sent: usize = 0;
57
58        for root in &resolved.roots {
59            let root_clone = root.clone();
60            let Ok((result, original)) =
61                std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
62                    crate::tools::ctx_tree::handle(
63                        &root_clone,
64                        depth,
65                        show_hidden,
66                        respect_gitignore,
67                    )
68                }))
69            else {
70                combined.push_str(&format!("── {root} ──\nERROR: internal panic\n\n"));
71                continue;
72            };
73
74            if result.starts_with("ERROR:") {
75                combined.push_str(&format!("── {root} ──\n{result}\n\n"));
76                continue;
77            }
78
79            combined.push_str(&format!("── {root} ──\n{result}\n\n"));
80            total_original += original;
81            total_sent += crate::core::tokens::count_tokens(&result);
82        }
83
84        let final_out =
85            crate::core::protocol::append_savings(&combined, total_original, total_sent);
86        let saved = total_original.saturating_sub(total_sent);
87
88        Ok(ToolOutput {
89            text: final_out,
90            original_tokens: total_original,
91            saved_tokens: saved,
92            mode: None,
93            path: None,
94            changed: false,
95            shell_outcome: None,
96            content_blocks: None,
97        })
98    }
99}
100
101fn handle_single(
102    path: &str,
103    depth: usize,
104    show_hidden: bool,
105    respect_gitignore: bool,
106) -> Result<ToolOutput, ErrorData> {
107    let path_clone = path.to_string();
108    let Ok((result, original)) = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
109        crate::tools::ctx_tree::handle(&path_clone, depth, show_hidden, respect_gitignore)
110    })) else {
111        return Err(ErrorData::internal_error(
112            format!(
113                "ctx_tree panicked while processing '{path}'. This is a bug — please report it."
114            ),
115            None,
116        ));
117    };
118
119    if result.starts_with("ERROR:") {
120        return Err(ErrorData::invalid_params(result, None));
121    }
122
123    let sent = crate::core::tokens::count_tokens(&result);
124    let saved = original.saturating_sub(sent);
125    let final_out = crate::core::protocol::append_savings(&result, original, sent);
126
127    Ok(ToolOutput {
128        text: final_out,
129        original_tokens: original,
130        saved_tokens: saved,
131        mode: None,
132        path: Some(path.to_string()),
133        changed: false,
134        shell_outcome: None,
135        content_blocks: None,
136    })
137}