Skip to main content

lean_ctx/tools/registered/
ctx_search.rs

1use rmcp::model::Tool;
2use rmcp::ErrorData;
3use serde_json::{json, Map, Value};
4
5use crate::server::tool_trait::{get_bool, get_int, get_str, McpTool, ToolContext, ToolOutput};
6use crate::tool_defs::tool_def;
7
8pub struct CtxSearchTool;
9
10impl McpTool for CtxSearchTool {
11    fn name(&self) -> &'static str {
12        "ctx_search"
13    }
14
15    fn tool_def(&self) -> Tool {
16        tool_def(
17            "ctx_search",
18            "Regex code search (.gitignore aware, compact results). Deterministic ordering. Secret-like files (e.g. .env, *.pem) are skipped unless role allows. ignore_gitignore requires explicit policy.",
19            json!({
20                "type": "object",
21                "properties": {
22                    "pattern": { "type": "string", "description": "Regex pattern" },
23                    "path": { "type": "string", "description": "Directory to search" },
24                    "ext": { "type": "string", "description": "File extension filter" },
25                    "max_results": { "type": "integer", "description": "Max results (default: 20)" },
26                    "ignore_gitignore": { "type": "boolean", "description": "Set true to scan ALL files including .gitignore'd paths (default: false). Requires role policy (e.g. admin)." }
27                },
28                "required": ["pattern"]
29            }),
30        )
31    }
32
33    fn handle(
34        &self,
35        args: &Map<String, Value>,
36        ctx: &ToolContext,
37    ) -> Result<ToolOutput, ErrorData> {
38        let pattern = get_str(args, "pattern")
39            .ok_or_else(|| ErrorData::invalid_params("pattern is required", None))?;
40        let path = if let Some(p) = ctx.resolved_path("path") {
41            p.to_string()
42        } else if let Some(err) = ctx.path_error("path") {
43            return Err(ErrorData::invalid_params(format!("path: {err}"), None));
44        } else {
45            ".".to_string()
46        };
47        let ext = get_str(args, "ext");
48        let max = (get_int(args, "max_results").unwrap_or(20) as usize).min(500);
49        let no_gitignore = get_bool(args, "ignore_gitignore").unwrap_or(false);
50
51        if no_gitignore {
52            if let Err(e) = crate::core::io_boundary::ensure_ignore_gitignore_allowed("ctx_search")
53            {
54                return Ok(ToolOutput::simple(e));
55            }
56        }
57
58        let crp = ctx.crp_mode;
59        let respect = !no_gitignore;
60        let allow_secret_paths = crate::core::roles::active_role().io.allow_secret_paths;
61
62        let pattern_clone = pattern.clone();
63        let path_clone = path.clone();
64
65        let search_result = tokio::task::block_in_place(|| {
66            let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
67                crate::tools::ctx_search::handle(
68                    &pattern_clone,
69                    &path_clone,
70                    ext.as_deref(),
71                    max,
72                    crp,
73                    respect,
74                    allow_secret_paths,
75                )
76            }));
77            match result {
78                Ok(r) => Ok(r),
79                Err(_) => Err("search task panicked"),
80            }
81        });
82
83        let (result, original) = match search_result {
84            Ok(r) => r,
85            Err(e) => {
86                return Err(ErrorData::internal_error(
87                    format!("search task failed: {e}"),
88                    None,
89                ));
90            }
91        };
92
93        let sent = crate::core::tokens::count_tokens(&result);
94        let saved = original.saturating_sub(sent);
95
96        let final_out = crate::core::protocol::append_savings(&result, original, sent);
97
98        Ok(ToolOutput {
99            text: final_out,
100            original_tokens: original,
101            saved_tokens: saved,
102            mode: None,
103            path: Some(path),
104            changed: false,
105        })
106    }
107}