lean_ctx/tools/registered/
ctx_multi_read.rs1use rmcp::model::Tool;
2use rmcp::ErrorData;
3use serde_json::{json, Map, Value};
4
5use crate::server::tool_trait::{get_str, get_str_array, McpTool, ToolContext, ToolOutput};
6use crate::tool_defs::tool_def;
7
8pub struct CtxMultiReadTool;
9
10impl McpTool for CtxMultiReadTool {
11 fn name(&self) -> &'static str {
12 "ctx_multi_read"
13 }
14
15 fn tool_def(&self) -> Tool {
16 tool_def(
17 "ctx_multi_read",
18 "Batch read files in one call. Same modes as ctx_read.",
19 json!({
20 "type": "object",
21 "properties": {
22 "paths": {
23 "type": "array",
24 "items": { "type": "string" },
25 "description": "Absolute file paths to read, in order"
26 },
27 "mode": {
28 "type": "string",
29 "description": "Compression mode (default: full). Same modes as ctx_read (auto, full, map, signatures, diff, aggressive, entropy, task, reference, lines:N-M)."
30 }
31 },
32 "required": ["paths"]
33 }),
34 )
35 }
36
37 fn handle(
38 &self,
39 args: &Map<String, Value>,
40 ctx: &ToolContext,
41 ) -> Result<ToolOutput, ErrorData> {
42 let raw_paths = get_str_array(args, "paths")
43 .ok_or_else(|| ErrorData::invalid_params("paths array is required", None))?;
44
45 tokio::task::block_in_place(|| {
46 let session_lock = ctx
47 .session
48 .as_ref()
49 .ok_or_else(|| ErrorData::internal_error("session not available", None))?;
50 let cache_lock = ctx
51 .cache
52 .as_ref()
53 .ok_or_else(|| ErrorData::internal_error("cache not available", None))?;
54
55 let cap = crate::core::limits::max_read_bytes() as u64;
56 let mut paths = Vec::with_capacity(raw_paths.len());
57 {
58 let session = session_lock.blocking_read();
59 for p in &raw_paths {
60 let resolved = super::resolve_path_sync(&session, p)
61 .map_err(|e| ErrorData::invalid_params(e, None))?;
62 if crate::core::binary_detect::is_binary_file(&resolved) {
63 continue;
64 }
65 if let Ok(meta) = std::fs::metadata(&resolved) {
66 if meta.len() > cap {
67 continue;
68 }
69 }
70 paths.push(resolved);
71 }
72 }
73 if paths.is_empty() {
74 return Err(ErrorData::invalid_params(
75 "all paths are binary or exceed the size limit",
76 None,
77 ));
78 }
79
80 let mode = get_str(args, "mode").unwrap_or_else(|| {
81 let p = crate::core::profiles::active_profile();
82 let dm = p.read.default_mode_effective();
83 if dm == "auto" {
84 "full".to_string()
85 } else {
86 dm.to_string()
87 }
88 });
89 let current_task = {
90 let session = session_lock.blocking_read();
91 session.task.as_ref().map(|t| t.description.clone())
92 };
93
94 let mut cache = cache_lock.blocking_write();
95 let output = crate::tools::ctx_multi_read::handle_with_task(
96 &mut cache,
97 &paths,
98 &mode,
99 ctx.crp_mode,
100 current_task.as_deref(),
101 );
102 let mut total_original: usize = 0;
103 for path in &paths {
104 total_original =
105 total_original.saturating_add(cache.get(path).map_or(0, |e| e.original_tokens));
106 }
107 let tokens = crate::core::tokens::count_tokens(&output);
108 drop(cache);
109
110 Ok(ToolOutput {
111 text: output,
112 original_tokens: total_original,
113 saved_tokens: total_original.saturating_sub(tokens),
114 mode: Some(mode),
115 path: None,
116 })
117 })
118 }
119}