lean_ctx/tools/registered/
plugin_tool.rs1use std::sync::Arc;
6
7use rmcp::ErrorData;
8use rmcp::model::Tool;
9use serde_json::{Map, Value};
10
11use crate::core::plugins::tools::PluginToolSpec;
12use crate::server::tool_trait::{McpTool, ToolContext, ToolOutput};
13
14pub struct PluginTool {
16 name: &'static str,
18 spec: PluginToolSpec,
19}
20
21impl PluginTool {
22 #[must_use]
28 pub fn from_spec(spec: PluginToolSpec) -> Self {
29 let name: &'static str = Box::leak(spec.name.clone().into_boxed_str());
30 Self { name, spec }
31 }
32}
33
34impl McpTool for PluginTool {
35 fn name(&self) -> &'static str {
36 self.name
37 }
38
39 fn tool_def(&self) -> Tool {
40 let mut schema: Map<String, Value> = if let Value::Object(map) = &self.spec.input_schema {
41 map.clone()
42 } else {
43 let mut map = Map::new();
44 map.insert("type".to_string(), Value::String("object".to_string()));
45 map
46 };
47 crate::tool_defs::normalize_for_strict_validators(&mut schema);
50 let description = if self.spec.description.is_empty() {
51 format!("Plugin tool provided by '{}'", self.spec.plugin_name)
52 } else {
53 self.spec.description.clone()
54 };
55 Tool::new(self.name, description, Arc::new(schema))
56 }
57
58 fn handle(
59 &self,
60 args: &Map<String, Value>,
61 _ctx: &ToolContext,
62 ) -> Result<ToolOutput, ErrorData> {
63 let args_json = serde_json::to_string(args).unwrap_or_else(|_| "{}".to_string());
64 match crate::core::plugins::tools::invoke(&self.spec, &args_json) {
65 Ok(text) => Ok(ToolOutput::simple(text)),
66 Err(e) => Err(ErrorData::internal_error(e, None)),
67 }
68 }
69}
70
71#[cfg(test)]
72mod tests {
73 use super::*;
74 use std::path::PathBuf;
75
76 fn spec() -> PluginToolSpec {
77 PluginToolSpec {
78 plugin_name: "weather".into(),
79 plugin_dir: PathBuf::from("/tmp"),
80 name: "weather_lookup".into(),
81 description: "Look up weather".into(),
82 command: "cat".into(),
83 timeout_ms: 2000,
84 input_schema: serde_json::json!({"type": "object"}),
85 policy: crate::core::plugins::sandbox::SandboxPolicy::strict(),
86 }
87 }
88
89 #[test]
90 fn tool_def_reflects_spec() {
91 let tool = PluginTool::from_spec(spec());
92 assert_eq!(tool.name(), "weather_lookup");
93 let def = tool.tool_def();
94 assert_eq!(def.name.as_ref(), "weather_lookup");
95 assert_eq!(def.description.as_deref(), Some("Look up weather"));
96 }
97
98 #[test]
99 fn missing_description_falls_back() {
100 let mut s = spec();
101 s.description = String::new();
102 let tool = PluginTool::from_spec(s);
103 let def = tool.tool_def();
104 assert!(def.description.as_deref().unwrap_or("").contains("weather"));
105 }
106}