use std::sync::Arc;
use rmcp::model::Tool;
use rmcp::ErrorData;
use serde_json::{Map, Value};
use crate::core::plugins::tools::PluginToolSpec;
use crate::server::tool_trait::{McpTool, ToolContext, ToolOutput};
pub struct PluginTool {
name: &'static str,
spec: PluginToolSpec,
}
impl PluginTool {
#[must_use]
pub fn from_spec(spec: PluginToolSpec) -> Self {
let name: &'static str = Box::leak(spec.name.clone().into_boxed_str());
Self { name, spec }
}
}
impl McpTool for PluginTool {
fn name(&self) -> &'static str {
self.name
}
fn tool_def(&self) -> Tool {
let mut schema: Map<String, Value> = if let Value::Object(map) = &self.spec.input_schema {
map.clone()
} else {
let mut map = Map::new();
map.insert("type".to_string(), Value::String("object".to_string()));
map
};
crate::tool_defs::normalize_for_strict_validators(&mut schema);
let description = if self.spec.description.is_empty() {
format!("Plugin tool provided by '{}'", self.spec.plugin_name)
} else {
self.spec.description.clone()
};
Tool::new(self.name, description, Arc::new(schema))
}
fn handle(
&self,
args: &Map<String, Value>,
_ctx: &ToolContext,
) -> Result<ToolOutput, ErrorData> {
let args_json = serde_json::to_string(&Value::Object(args.clone()))
.unwrap_or_else(|_| "{}".to_string());
match crate::core::plugins::tools::invoke(&self.spec, &args_json) {
Ok(text) => Ok(ToolOutput::simple(text)),
Err(e) => Err(ErrorData::internal_error(e, None)),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
fn spec() -> PluginToolSpec {
PluginToolSpec {
plugin_name: "weather".into(),
plugin_dir: PathBuf::from("/tmp"),
name: "weather_lookup".into(),
description: "Look up weather".into(),
command: "cat".into(),
timeout_ms: 2000,
input_schema: serde_json::json!({"type": "object"}),
policy: crate::core::plugins::sandbox::SandboxPolicy::strict(),
}
}
#[test]
fn tool_def_reflects_spec() {
let tool = PluginTool::from_spec(spec());
assert_eq!(tool.name(), "weather_lookup");
let def = tool.tool_def();
assert_eq!(def.name.as_ref(), "weather_lookup");
assert_eq!(def.description.as_deref(), Some("Look up weather"));
}
#[test]
fn missing_description_falls_back() {
let mut s = spec();
s.description = String::new();
let tool = PluginTool::from_spec(s);
let def = tool.tool_def();
assert!(def.description.as_deref().unwrap_or("").contains("weather"));
}
}