Skip to main content

lean_ctx/server/
tool_trait.rs

1use rmcp::model::Tool;
2use rmcp::ErrorData;
3use serde_json::{Map, Value};
4
5/// Result returned by an McpTool handler.
6pub struct ToolOutput {
7    pub text: String,
8    pub original_tokens: usize,
9    pub saved_tokens: usize,
10    pub mode: Option<String>,
11    /// Path associated with the tool call (for record_call_with_path).
12    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
37/// Trait for a self-contained MCP tool. Each tool provides its own schema
38/// definition and handler, eliminating the possibility of schema/handler drift.
39///
40/// Handlers are synchronous because all 47 existing tool handlers are sync.
41/// The async boundary (cache locks, session reads) is handled by the dispatch
42/// layer before calling `handle`.
43pub trait McpTool: Send + Sync {
44    /// Tool name as registered in the MCP protocol (e.g. "ctx_tree").
45    fn name(&self) -> &'static str;
46
47    /// MCP tool definition including JSON schema. This replaces the
48    /// corresponding entry in `granular_tool_defs()`.
49    fn tool_def(&self) -> Tool;
50
51    /// Execute the tool. Args are the raw JSON-RPC arguments.
52    /// `ctx` provides access to resolved paths and project state.
53    fn handle(&self, args: &Map<String, Value>, ctx: &ToolContext)
54        -> Result<ToolOutput, ErrorData>;
55}
56
57/// Context passed to tool handlers. Contains pre-resolved values that
58/// many tools need, avoiding repeated async lock acquisition inside
59/// handlers. Extended with shared server state for tools that need
60/// cache/session access.
61pub struct ToolContext {
62    pub project_root: String,
63    pub minimal: bool,
64    /// Pre-resolved paths keyed by argument name (e.g. "path" -> "/abs/dir").
65    pub resolved_paths: std::collections::HashMap<String, String>,
66    /// CRP mode for compression-aware tools.
67    pub crp_mode: crate::tools::CrpMode,
68    /// Shared cache handle for tools that need read/write access.
69    pub cache: Option<crate::tools::SharedCache>,
70    /// Shared session handle for tools that need session access.
71    pub session: Option<std::sync::Arc<tokio::sync::RwLock<crate::core::session::SessionState>>>,
72    /// Tool call records for session-aware tools (e.g. ctx_session status).
73    pub tool_calls:
74        Option<std::sync::Arc<tokio::sync::RwLock<Vec<crate::core::protocol::ToolCallRecord>>>>,
75    /// Current agent identity for multi-agent tools.
76    pub agent_id: Option<std::sync::Arc<tokio::sync::RwLock<Option<String>>>>,
77    /// Active workflow run state.
78    pub workflow:
79        Option<std::sync::Arc<tokio::sync::RwLock<Option<crate::core::workflow::WorkflowRun>>>>,
80    /// Context ledger for handoff operations.
81    pub ledger:
82        Option<std::sync::Arc<tokio::sync::RwLock<crate::core::context_ledger::ContextLedger>>>,
83    /// Client name (cursor, claude, etc.).
84    pub client_name: Option<std::sync::Arc<tokio::sync::RwLock<String>>>,
85    /// Pipeline stats for metrics/proof tools.
86    pub pipeline_stats:
87        Option<std::sync::Arc<tokio::sync::RwLock<crate::core::pipeline::PipelineStats>>>,
88    /// Global call counter for context tools.
89    pub call_count: Option<std::sync::Arc<std::sync::atomic::AtomicUsize>>,
90    /// Autonomy state for search repeat detection.
91    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    /// Sync path resolution using project_root. Simplified version
100    /// of LeanCtxServer::resolve_path for use in sync tool handlers.
101    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
126// ── Arg extraction helpers (mirror server/helpers.rs for standalone use) ──
127
128pub 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}