Skip to main content

lean_ctx/tools/registered/
ctx_compose.rs

1use rmcp::ErrorData;
2use rmcp::model::Tool;
3use serde_json::{Map, Value, json};
4
5use crate::server::tool_trait::{McpTool, ToolContext, ToolOutput, get_str};
6use crate::tool_defs::tool_def;
7
8pub struct CtxComposeTool;
9
10impl McpTool for CtxComposeTool {
11    fn name(&self) -> &'static str {
12        "ctx_compose"
13    }
14
15    fn tool_def(&self) -> Tool {
16        tool_def(
17            "ctx_compose",
18            "PRIMARY TOOL — call FIRST for understanding code (before editing/debugging/'how does X work').\n\
19             Returns ranked files with relevant symbol source inline grouped by file.\n\
20             Combines BM25 lexical+semantic+associative retrieval+submodular optimization.\n\
21             ANTIPATTERN: Do NOT chain search→read→symbol — one compose replaces the whole chain.\n\
22             ANTIPATTERN: Do NOT Read files whose source compose already returned — it IS the source.\n\
23             WORKFLOW: Fire parallel ctx_read or ctx_compose for different areas.",
24            json!({
25                "type": "object",
26                "properties": {
27                    "task": { "type": "string", "description": "Short English task/question or symbol names" },
28                    "path": { "type": "string", "description": "Project root" }
29                },
30                "required": ["task"]
31            }),
32        )
33    }
34
35    fn handle(
36        &self,
37        args: &Map<String, Value>,
38        ctx: &ToolContext,
39    ) -> Result<ToolOutput, ErrorData> {
40        let task = get_str(args, "task")
41            .ok_or_else(|| ErrorData::invalid_params("task is required", None))?;
42        let path = if let Some(p) = ctx.resolved_path("path") {
43            p.to_string()
44        } else if let Some(err) = ctx.path_error("path") {
45            return Err(ErrorData::invalid_params(format!("path: {err}"), None));
46        } else {
47            ctx.project_root.clone()
48        };
49
50        // Share the resident BM25 cache with the composed semantic search.
51        if let Some(ref cache) = ctx.bm25_cache {
52            crate::tools::ctx_semantic_search::set_thread_cache(cache.clone());
53        }
54
55        let (text, sent) = tokio::task::block_in_place(|| {
56            crate::tools::ctx_compose::handle(&task, &path, ctx.crp_mode)
57        });
58
59        if text.starts_with("ERROR") {
60            return Err(ErrorData::invalid_params(text, None));
61        }
62
63        Ok(ToolOutput {
64            text,
65            original_tokens: sent,
66            saved_tokens: 0,
67            mode: Some("compose".to_string()),
68            path: Some(path),
69            changed: false,
70            shell_outcome: None,
71        })
72    }
73}