Skip to main content

lean_ctx/tools/registered/
ctx_explore.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_str, get_usize};
6use crate::tool_defs::tool_def;
7
8pub struct CtxExploreTool;
9
10impl McpTool for CtxExploreTool {
11    fn name(&self) -> &'static str {
12        "ctx_explore"
13    }
14
15    fn tool_def(&self) -> Tool {
16        tool_def(
17            "ctx_explore",
18            "Iterative, deterministic code exploration → compact file:line citations.\n\
19             Runs a bounded multi-turn loop (BM25 + static call/import graph + AST symbols)\n\
20             and returns a <final_answer> block of `path:start-end` spans instead of bodies.\n\
21             USE WHEN: locating WHERE behavior lives across many files, cheaply.\n\
22             vs ctx_compose: compose inlines bodies in one shot; explore returns citations\n\
23             over N turns (far fewer tokens). citation=true emits only the block.",
24            json!({
25                "type": "object",
26                "properties": {
27                    "query": { "type": "string", "description": "Natural-language question or symbol names" },
28                    "path": { "type": "string", "description": "Project root" },
29                    "max_turns": { "type": "integer", "description": "Exploration depth (1-8, default 3)" },
30                    "citation": { "type": "boolean", "description": "Emit only the <final_answer> citation block" }
31                },
32                "required": ["query"]
33            }),
34        )
35    }
36
37    fn handle(
38        &self,
39        args: &Map<String, Value>,
40        ctx: &ToolContext,
41    ) -> Result<ToolOutput, ErrorData> {
42        let query = get_str(args, "query")
43            .ok_or_else(|| ErrorData::invalid_params("query is required", None))?;
44        let path = if let Some(p) = ctx.resolved_path("path") {
45            p.to_string()
46        } else if let Some(err) = ctx.path_error("path") {
47            return Err(ErrorData::invalid_params(format!("path: {err}"), None));
48        } else {
49            ctx.project_root.clone()
50        };
51
52        let opts = crate::tools::ctx_explore::ExploreOptions::new(
53            get_usize(args, "max_turns"),
54            get_bool(args, "citation").unwrap_or(false),
55        );
56
57        // Share the resident BM25 cache with the explore loop (warm index reuse).
58        if let Some(ref cache) = ctx.bm25_cache {
59            crate::tools::ctx_semantic_search::set_thread_cache(cache.clone());
60        }
61
62        let outcome = tokio::task::block_in_place(|| {
63            crate::tools::ctx_explore::handle(&query, &path, ctx.crp_mode, &opts)
64        });
65
66        if outcome.text.starts_with("ERROR") {
67            return Err(ErrorData::invalid_params(outcome.text, None));
68        }
69
70        Ok(ToolOutput {
71            text: outcome.text,
72            original_tokens: outcome.tokens,
73            saved_tokens: 0,
74            mode: Some("explore".to_string()),
75            path: Some(path),
76            changed: false,
77            shell_outcome: None,
78        })
79    }
80}