lean_ctx/tools/registered/
plugin_tool.rs1use std::sync::Arc;
6
7use rmcp::model::Tool;
8use rmcp::ErrorData;
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(&Value::Object(args.clone()))
64 .unwrap_or_else(|_| "{}".to_string());
65 match crate::core::plugins::tools::invoke(&self.spec, &args_json) {
66 Ok(text) => Ok(ToolOutput::simple(text)),
67 Err(e) => Err(ErrorData::internal_error(e, None)),
68 }
69 }
70}
71
72#[cfg(test)]
73mod tests {
74 use super::*;
75 use std::path::PathBuf;
76
77 fn spec() -> PluginToolSpec {
78 PluginToolSpec {
79 plugin_name: "weather".into(),
80 plugin_dir: PathBuf::from("/tmp"),
81 name: "weather_lookup".into(),
82 description: "Look up weather".into(),
83 command: "cat".into(),
84 timeout_ms: 2000,
85 input_schema: serde_json::json!({"type": "object"}),
86 policy: crate::core::plugins::sandbox::SandboxPolicy::strict(),
87 }
88 }
89
90 #[test]
91 fn tool_def_reflects_spec() {
92 let tool = PluginTool::from_spec(spec());
93 assert_eq!(tool.name(), "weather_lookup");
94 let def = tool.tool_def();
95 assert_eq!(def.name.as_ref(), "weather_lookup");
96 assert_eq!(def.description.as_deref(), Some("Look up weather"));
97 }
98
99 #[test]
100 fn missing_description_falls_back() {
101 let mut s = spec();
102 s.description = String::new();
103 let tool = PluginTool::from_spec(s);
104 let def = tool.tool_def();
105 assert!(def.description.as_deref().unwrap_or("").contains("weather"));
106 }
107}