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}
14
15impl ToolOutput {
16 pub fn simple(text: String) -> Self {
17 Self {
18 text,
19 original_tokens: 0,
20 saved_tokens: 0,
21 mode: None,
22 path: None,
23 }
24 }
25
26 pub fn with_savings(text: String, original: usize, saved: usize) -> Self {
27 Self {
28 text,
29 original_tokens: original,
30 saved_tokens: saved,
31 mode: None,
32 path: None,
33 }
34 }
35}
36
37pub trait McpTool: Send + Sync {
44 fn name(&self) -> &'static str;
46
47 fn tool_def(&self) -> Tool;
50
51 fn handle(&self, args: &Map<String, Value>, ctx: &ToolContext)
54 -> Result<ToolOutput, ErrorData>;
55}
56
57pub struct ToolContext {
62 pub project_root: String,
63 pub minimal: bool,
64 pub resolved_paths: std::collections::HashMap<String, String>,
66 pub crp_mode: crate::tools::CrpMode,
68 pub cache: Option<crate::tools::SharedCache>,
70 pub session: Option<std::sync::Arc<tokio::sync::RwLock<crate::core::session::SessionState>>>,
72 pub tool_calls:
74 Option<std::sync::Arc<tokio::sync::RwLock<Vec<crate::core::protocol::ToolCallRecord>>>>,
75 pub agent_id: Option<std::sync::Arc<tokio::sync::RwLock<Option<String>>>>,
77 pub workflow:
79 Option<std::sync::Arc<tokio::sync::RwLock<Option<crate::core::workflow::WorkflowRun>>>>,
80 pub ledger:
82 Option<std::sync::Arc<tokio::sync::RwLock<crate::core::context_ledger::ContextLedger>>>,
83 pub client_name: Option<std::sync::Arc<tokio::sync::RwLock<String>>>,
85 pub pipeline_stats:
87 Option<std::sync::Arc<tokio::sync::RwLock<crate::core::pipeline::PipelineStats>>>,
88 pub call_count: Option<std::sync::Arc<std::sync::atomic::AtomicUsize>>,
90 pub autonomy: Option<std::sync::Arc<crate::tools::autonomy::AutonomyState>>,
92}
93
94impl ToolContext {
95 pub fn resolved_path(&self, arg: &str) -> Option<&str> {
96 self.resolved_paths.get(arg).map(String::as_str)
97 }
98
99 pub fn resolve_path_sync(&self, path: &str) -> Result<String, String> {
102 let normalized = crate::core::pathutil::normalize_tool_path(path);
103 if normalized.is_empty() || normalized == "." {
104 return Ok(normalized);
105 }
106 let p = std::path::Path::new(&normalized);
107 let resolved = if p.is_absolute() || p.exists() {
108 std::path::PathBuf::from(&normalized)
109 } else {
110 let joined = std::path::Path::new(&self.project_root).join(&normalized);
111 if joined.exists() {
112 joined
113 } else {
114 std::path::Path::new(&self.project_root).join(&normalized)
115 }
116 };
117 let jail_root = std::path::Path::new(&self.project_root);
118 let jailed = crate::core::pathjail::jail_path(&resolved, jail_root)?;
119 crate::core::io_boundary::check_secret_path_for_tool("resolve_path", &jailed)?;
120 Ok(crate::core::pathutil::normalize_tool_path(
121 &jailed.to_string_lossy().replace('\\', "/"),
122 ))
123 }
124}
125
126pub fn get_str(args: &Map<String, Value>, key: &str) -> Option<String> {
129 args.get(key).and_then(|v| v.as_str()).map(String::from)
130}
131
132pub fn get_int(args: &Map<String, Value>, key: &str) -> Option<i64> {
133 args.get(key).and_then(serde_json::Value::as_i64)
134}
135
136pub fn get_bool(args: &Map<String, Value>, key: &str) -> Option<bool> {
137 args.get(key).and_then(serde_json::Value::as_bool)
138}
139
140pub fn get_str_array(args: &Map<String, Value>, key: &str) -> Option<Vec<String>> {
141 args.get(key).and_then(|v| v.as_array()).map(|arr| {
142 arr.iter()
143 .filter_map(|v| v.as_str().map(String::from))
144 .collect()
145 })
146}