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/// Read-only context passed to tool handlers. Contains pre-resolved
58/// values that many tools need, avoiding repeated async lock acquisition
59/// inside handlers.
60pub struct ToolContext {
61    pub project_root: String,
62    pub minimal: bool,
63    /// Pre-resolved paths keyed by argument name (e.g. "path" -> "/abs/dir").
64    /// Set by the dispatch layer before calling the handler so tools don't
65    /// need async access to the session/pathJail.
66    pub resolved_paths: std::collections::HashMap<String, String>,
67}
68
69impl ToolContext {
70    pub fn resolved_path(&self, arg: &str) -> Option<&str> {
71        self.resolved_paths.get(arg).map(String::as_str)
72    }
73}
74
75// ── Arg extraction helpers (mirror server/helpers.rs for standalone use) ──
76
77pub fn get_str(args: &Map<String, Value>, key: &str) -> Option<String> {
78    args.get(key).and_then(|v| v.as_str()).map(String::from)
79}
80
81pub fn get_int(args: &Map<String, Value>, key: &str) -> Option<i64> {
82    args.get(key).and_then(serde_json::Value::as_i64)
83}
84
85pub fn get_bool(args: &Map<String, Value>, key: &str) -> Option<bool> {
86    args.get(key).and_then(serde_json::Value::as_bool)
87}
88
89pub fn get_str_array(args: &Map<String, Value>, key: &str) -> Option<Vec<String>> {
90    args.get(key).and_then(|v| v.as_array()).map(|arr| {
91        arr.iter()
92            .filter_map(|v| v.as_str().map(String::from))
93            .collect()
94    })
95}