use agentic_tools_core::fmt::TextOptions;
use agentic_tools_core::{TextFormat, Tool, ToolContext, ToolError, ToolRegistry};
use agentic_tools_mcp::{OutputMode, RegistryServer};
use futures::future::BoxFuture;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
#[derive(Clone)]
struct GreetTool;
#[derive(Debug, Clone, Deserialize, JsonSchema)]
struct GreetInput {
name: String,
#[serde(default)]
excited: bool,
}
#[derive(Debug, Clone, Serialize, JsonSchema)]
struct GreetOutput {
greeting: String,
}
impl TextFormat for GreetOutput {}
impl Tool for GreetTool {
type Input = GreetInput;
type Output = GreetOutput;
const NAME: &'static str = "greet";
const DESCRIPTION: &'static str = "Greet someone by name";
fn call(
&self,
input: Self::Input,
_ctx: &ToolContext,
) -> BoxFuture<'static, Result<Self::Output, ToolError>> {
Box::pin(async move {
let greeting = if input.excited {
format!("Hello, {}!", input.name)
} else {
format!("Hello, {}", input.name)
};
Ok(GreetOutput { greeting })
})
}
}
#[derive(Clone)]
struct CalculateTool;
#[derive(Debug, Clone, Deserialize, JsonSchema)]
struct CalculateInput {
a: i32,
b: i32,
operation: String,
}
#[derive(Debug, Clone, Serialize, JsonSchema)]
struct CalculateOutput {
result: i32,
}
impl TextFormat for CalculateOutput {}
impl Tool for CalculateTool {
type Input = CalculateInput;
type Output = CalculateOutput;
const NAME: &'static str = "calculate";
const DESCRIPTION: &'static str = "Perform arithmetic calculation";
fn call(
&self,
input: Self::Input,
_ctx: &ToolContext,
) -> BoxFuture<'static, Result<Self::Output, ToolError>> {
Box::pin(async move {
let result = match input.operation.as_str() {
"add" => input.a + input.b,
"sub" => input.a - input.b,
"mul" => input.a * input.b,
_ => return Err(ToolError::invalid_input("Unknown operation")),
};
Ok(CalculateOutput { result })
})
}
}
fn create_test_registry() -> Arc<ToolRegistry> {
Arc::new(
ToolRegistry::builder()
.register::<GreetTool, ()>(GreetTool)
.register::<CalculateTool, ()>(CalculateTool)
.finish(),
)
}
#[test]
fn test_server_creation() {
let registry = create_test_registry();
let server = RegistryServer::new(registry).with_info("test-server", "1.0.0");
assert_eq!(server.name(), "test-server");
assert_eq!(server.version(), "1.0.0");
}
#[test]
fn test_server_with_allowlist() {
let registry = create_test_registry();
let server = RegistryServer::new(registry).with_allowlist(["greet".to_string()]);
let names = server.effective_tool_names();
assert_eq!(names, vec!["greet".to_string()]);
}
#[test]
fn test_server_output_modes() {
let registry = create_test_registry();
let server_text = RegistryServer::new(registry.clone()).with_output_mode(OutputMode::Text);
assert!(matches!(server_text.output_mode(), OutputMode::Text));
let server_structured = RegistryServer::new(registry).with_output_mode(OutputMode::Structured);
assert!(matches!(
server_structured.output_mode(),
OutputMode::Structured
));
}
#[test]
fn test_registry_has_tools() {
let registry = create_test_registry();
assert_eq!(registry.len(), 2);
assert!(registry.contains("greet"));
assert!(registry.contains("calculate"));
}
#[tokio::test]
async fn test_registry_dispatch_greet() {
let registry = create_test_registry();
let ctx = ToolContext::default();
let args = serde_json::json!({
"name": "World",
"excited": true
});
let result = registry.dispatch_json("greet", args, &ctx).await;
assert!(result.is_ok());
let output = result.unwrap();
assert_eq!(output["greeting"], "Hello, World!");
}
#[tokio::test]
async fn test_registry_dispatch_calculate() {
let registry = create_test_registry();
let ctx = ToolContext::default();
let args = serde_json::json!({
"a": 10,
"b": 5,
"operation": "mul"
});
let result = registry.dispatch_json("calculate", args, &ctx).await;
assert!(result.is_ok());
let output = result.unwrap();
assert_eq!(output["result"], 50);
}
#[tokio::test]
async fn test_registry_dispatch_unknown_tool() {
let registry = create_test_registry();
let ctx = ToolContext::default();
let args = serde_json::json!({});
let result = registry.dispatch_json("nonexistent", args, &ctx).await;
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("Unknown tool"));
}
#[tokio::test]
async fn test_registry_dispatch_invalid_args() {
let registry = create_test_registry();
let ctx = ToolContext::default();
let args = serde_json::json!({
"excited": true
});
let result = registry.dispatch_json("greet", args, &ctx).await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_registry_dispatch_error_in_tool() {
let registry = create_test_registry();
let ctx = ToolContext::default();
let args = serde_json::json!({
"a": 1,
"b": 2,
"operation": "invalid"
});
let result = registry.dispatch_json("calculate", args, &ctx).await;
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("Unknown operation")
);
}
#[test]
fn test_registry_subset() {
let registry = create_test_registry();
let subset = registry.subset(["greet"]);
assert_eq!(subset.len(), 1);
assert!(subset.contains("greet"));
assert!(!subset.contains("calculate"));
}
#[tokio::test]
async fn test_registry_subset_dispatch() {
let registry = create_test_registry();
let subset = registry.subset(["greet"]);
let ctx = ToolContext::default();
let args = serde_json::json!({"name": "Test"});
let result = subset.dispatch_json("greet", args, &ctx).await;
assert!(result.is_ok());
let args = serde_json::json!({"a": 1, "b": 2, "operation": "add"});
let result = subset.dispatch_json("calculate", args, &ctx).await;
assert!(result.is_err());
}
#[test]
fn test_empty_registry() {
let registry = Arc::new(ToolRegistry::builder().finish());
let server = RegistryServer::new(registry.clone());
assert!(registry.is_empty());
assert_eq!(server.name(), "agentic-tools");
}
#[test]
fn test_tool_schemas() {
let registry = create_test_registry();
let greet = registry.get("greet").unwrap();
let schema = greet.input_schema();
let schema_json = serde_json::to_value(&schema).unwrap();
assert!(schema_json["properties"]["name"].is_object());
}
#[test]
fn test_tool_descriptions() {
let registry = create_test_registry();
let greet = registry.get("greet").unwrap();
assert_eq!(greet.description(), "Greet someone by name");
let calc = registry.get("calculate").unwrap();
assert_eq!(calc.description(), "Perform arithmetic calculation");
}
#[test]
fn test_tool_output_schema() {
let registry = create_test_registry();
let greet = registry.get("greet").unwrap();
let output_schema = greet.output_schema();
assert!(output_schema.is_some());
let schema_json = serde_json::to_value(output_schema.unwrap()).unwrap();
assert!(schema_json["properties"]["greeting"].is_object());
}
#[tokio::test]
async fn test_dispatch_json_formatted_returns_text_and_data() {
let registry = create_test_registry();
let ctx = ToolContext::default();
let text_opts = TextOptions::default();
let args = serde_json::json!({
"name": "World",
"excited": true
});
let result = registry
.dispatch_json_formatted("greet", args, &ctx, &text_opts)
.await;
assert!(result.is_ok());
let formatted = result.unwrap();
assert_eq!(formatted.data["greeting"], "Hello, World!");
assert!(formatted.text.is_some());
let text = formatted.text.unwrap();
assert!(text.contains("Hello, World!"));
}
#[tokio::test]
async fn test_dispatch_json_formatted_fallback_for_unknown_tool() {
let registry = create_test_registry();
let ctx = ToolContext::default();
let text_opts = TextOptions::default();
let args = serde_json::json!({});
let result = registry
.dispatch_json_formatted("nonexistent", args, &ctx, &text_opts)
.await;
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("Unknown tool"));
}