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