lean_ctx/tools/registered/
ctx_search.rs1use 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 = ctx.resolved_path("path").unwrap_or(".").to_string();
41 let ext = get_str(args, "ext");
42 let max = (get_int(args, "max_results").unwrap_or(20) as usize).min(500);
43 let no_gitignore = get_bool(args, "ignore_gitignore").unwrap_or(false);
44
45 if no_gitignore {
46 if let Err(e) = crate::core::io_boundary::ensure_ignore_gitignore_allowed("ctx_search")
47 {
48 return Ok(ToolOutput::simple(e));
49 }
50 }
51
52 let crp = ctx.crp_mode;
53 let respect = !no_gitignore;
54 let allow_secret_paths = crate::core::roles::active_role().io.allow_secret_paths;
55
56 let pattern_clone = pattern.clone();
57 let path_clone = path.clone();
58
59 let search_result = tokio::task::block_in_place(|| {
60 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
61 crate::tools::ctx_search::handle(
62 &pattern_clone,
63 &path_clone,
64 ext.as_deref(),
65 max,
66 crp,
67 respect,
68 allow_secret_paths,
69 )
70 }));
71 match result {
72 Ok(r) => Ok(r),
73 Err(_) => Err("search task panicked"),
74 }
75 });
76
77 let (result, original) = match search_result {
78 Ok(r) => r,
79 Err(e) => {
80 return Err(ErrorData::internal_error(
81 format!("search task failed: {e}"),
82 None,
83 ));
84 }
85 };
86
87 let sent = crate::core::tokens::count_tokens(&result);
88 let saved = original.saturating_sub(sent);
89
90 let final_out = crate::core::protocol::append_savings(&result, original, sent);
91
92 Ok(ToolOutput {
93 text: final_out,
94 original_tokens: original,
95 saved_tokens: saved,
96 mode: None,
97 path: Some(path),
98 changed: false,
99 })
100 }
101}