lean_ctx/tools/registered/
ctx_compose.rs1use 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 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}