lean_ctx/server/
tool_trait.rs1use rmcp::model::Tool;
2use rmcp::ErrorData;
3use serde_json::{Map, Value};
4
5pub struct ToolOutput {
7 pub text: String,
8 pub original_tokens: usize,
9 pub saved_tokens: usize,
10 pub mode: Option<String>,
11 pub path: Option<String>,
13 pub changed: bool,
16}
17
18impl ToolOutput {
19 pub fn simple(text: String) -> Self {
20 Self {
21 text,
22 original_tokens: 0,
23 saved_tokens: 0,
24 mode: None,
25 path: None,
26 changed: false,
27 }
28 }
29
30 pub fn to_header_line(&self, tool_name: &str) -> String {
32 let path_str = self.path.as_deref().unwrap_or("—");
33 let mode_str = self.mode.as_deref().unwrap_or("—");
34 let sent = self.original_tokens.saturating_sub(self.saved_tokens);
35 let pct = if self.original_tokens > 0 {
36 (self.saved_tokens as f64 / self.original_tokens as f64 * 100.0) as u32
37 } else {
38 0
39 };
40 format!("[{tool_name}: {path_str}, mode={mode_str}, {sent} tok sent, -{pct}%]")
41 }
42
43 pub fn with_savings(text: String, original: usize, saved: usize) -> Self {
44 Self {
45 text,
46 original_tokens: original,
47 saved_tokens: saved,
48 mode: None,
49 path: None,
50 changed: false,
51 }
52 }
53}
54
55pub trait McpTool: Send + Sync {
62 fn name(&self) -> &'static str;
64
65 fn tool_def(&self) -> Tool;
68
69 fn handle(&self, args: &Map<String, Value>, ctx: &ToolContext)
72 -> Result<ToolOutput, ErrorData>;
73}
74
75pub struct ToolContext {
80 pub project_root: String,
81 pub minimal: bool,
82 pub resolved_paths: std::collections::HashMap<String, String>,
84 pub crp_mode: crate::tools::CrpMode,
86 pub cache: Option<crate::tools::SharedCache>,
88 pub session: Option<std::sync::Arc<tokio::sync::RwLock<crate::core::session::SessionState>>>,
90 pub tool_calls:
92 Option<std::sync::Arc<tokio::sync::RwLock<Vec<crate::core::protocol::ToolCallRecord>>>>,
93 pub agent_id: Option<std::sync::Arc<tokio::sync::RwLock<Option<String>>>>,
95 pub workflow:
97 Option<std::sync::Arc<tokio::sync::RwLock<Option<crate::core::workflow::WorkflowRun>>>>,
98 pub ledger:
100 Option<std::sync::Arc<tokio::sync::RwLock<crate::core::context_ledger::ContextLedger>>>,
101 pub client_name: Option<std::sync::Arc<tokio::sync::RwLock<String>>>,
103 pub pipeline_stats:
105 Option<std::sync::Arc<tokio::sync::RwLock<crate::core::pipeline::PipelineStats>>>,
106 pub call_count: Option<std::sync::Arc<std::sync::atomic::AtomicUsize>>,
108 pub autonomy: Option<std::sync::Arc<crate::tools::autonomy::AutonomyState>>,
110 pub pressure_snapshot: Option<crate::core::context_ledger::ContextPressure>,
112}
113
114impl ToolContext {
115 pub fn resolved_path(&self, arg: &str) -> Option<&str> {
116 self.resolved_paths.get(arg).map(String::as_str)
117 }
118
119 pub fn resolve_path_sync(&self, path: &str) -> Result<String, String> {
122 let normalized = crate::core::pathutil::normalize_tool_path(path);
123 if normalized.is_empty() || normalized == "." {
124 return Ok(normalized);
125 }
126 let p = std::path::Path::new(&normalized);
127 let resolved = if p.is_absolute() || p.exists() {
128 std::path::PathBuf::from(&normalized)
129 } else {
130 let joined = std::path::Path::new(&self.project_root).join(&normalized);
131 if joined.exists() {
132 joined
133 } else {
134 std::path::Path::new(&self.project_root).join(&normalized)
135 }
136 };
137 let jail_root = std::path::Path::new(&self.project_root);
138 let jailed = crate::core::pathjail::jail_path(&resolved, jail_root)?;
139 crate::core::io_boundary::check_secret_path_for_tool("resolve_path", &jailed)?;
140 Ok(crate::core::pathutil::normalize_tool_path(
141 &jailed.to_string_lossy().replace('\\', "/"),
142 ))
143 }
144}
145
146pub fn get_str(args: &Map<String, Value>, key: &str) -> Option<String> {
149 args.get(key).and_then(|v| v.as_str()).map(String::from)
150}
151
152pub fn get_int(args: &Map<String, Value>, key: &str) -> Option<i64> {
153 args.get(key).and_then(serde_json::Value::as_i64)
154}
155
156pub fn get_bool(args: &Map<String, Value>, key: &str) -> Option<bool> {
157 args.get(key).and_then(serde_json::Value::as_bool)
158}
159
160pub fn get_str_array(args: &Map<String, Value>, key: &str) -> Option<Vec<String>> {
161 args.get(key).and_then(|v| v.as_array()).map(|arr| {
162 arr.iter()
163 .filter_map(|v| v.as_str().map(String::from))
164 .collect()
165 })
166}