#![cfg(feature = "macros")]
use schemars::JsonSchema;
use serde::Deserialize;
use tower_mcp::CallToolResult;
use tower_mcp::tool_fn;
#[derive(Debug, Deserialize, JsonSchema)]
struct AddInput {
a: i64,
b: i64,
}
#[tool_fn(description = "Add two numbers")]
async fn add(input: AddInput) -> Result<CallToolResult, tower_mcp::Error> {
Ok(CallToolResult::text(format!("{}", input.a + input.b)))
}
#[tool_fn(name = "custom-name", description = "Custom named tool")]
async fn multiply(input: AddInput) -> Result<CallToolResult, tower_mcp::Error> {
Ok(CallToolResult::text(format!("{}", input.a * input.b)))
}
#[tokio::test]
async fn test_tool_fn_macro_generates_working_tool() {
let tool = add_tool();
assert_eq!(tool.name, "add");
assert_eq!(tool.description.as_deref(), Some("Add two numbers"));
let result = tool.call(serde_json::json!({"a": 2, "b": 3})).await;
assert!(!result.is_error);
assert_eq!(result.first_text().unwrap(), "5");
}
#[tokio::test]
async fn test_tool_fn_macro_custom_name() {
let tool = multiply_tool();
assert_eq!(tool.name, "custom-name");
assert_eq!(tool.description.as_deref(), Some("Custom named tool"));
let result = tool.call(serde_json::json!({"a": 4, "b": 5})).await;
assert!(!result.is_error);
assert_eq!(result.first_text().unwrap(), "20");
}
#[test]
fn test_tool_fn_macro_tool_registers_in_router() {
use tower_mcp::McpRouter;
let _router = McpRouter::new()
.server_info("test", "1.0.0")
.tool(add_tool())
.tool(multiply_tool());
}
#[tokio::test]
async fn test_tool_fn_default_name_converts_underscores_to_hyphens() {
let tool = add_tool();
assert_eq!(tool.name, "add");
}
use std::collections::HashMap;
use tower_mcp::prompt_fn;
use tower_mcp::protocol::GetPromptResult;
#[prompt_fn(description = "Greet someone", args(name = "Name to greet"))]
async fn greet(args: HashMap<String, String>) -> Result<GetPromptResult, tower_mcp::Error> {
let name = args.get("name").cloned().unwrap_or_default();
Ok(GetPromptResult::user_message(format!("Hello, {name}!")))
}
#[prompt_fn(
name = "custom-prompt",
description = "A custom named prompt",
args(topic = "The topic", ?style = "Optional style")
)]
async fn summarize(args: HashMap<String, String>) -> Result<GetPromptResult, tower_mcp::Error> {
let topic = args.get("topic").cloned().unwrap_or_default();
let style = args.get("style").cloned().unwrap_or_else(|| "brief".into());
Ok(GetPromptResult::user_message(format!(
"Summarize {topic} in a {style} style"
)))
}
#[tokio::test]
async fn test_prompt_fn_macro_generates_working_prompt() {
let prompt = greet_prompt();
assert_eq!(prompt.name, "greet");
assert_eq!(prompt.description.as_deref(), Some("Greet someone"));
assert_eq!(prompt.arguments.len(), 1);
assert_eq!(prompt.arguments[0].name, "name");
assert!(prompt.arguments[0].required);
}
#[tokio::test]
async fn test_prompt_fn_macro_custom_name_and_optional_args() {
let prompt = summarize_prompt();
assert_eq!(prompt.name, "custom-prompt");
assert_eq!(prompt.arguments.len(), 2);
assert_eq!(prompt.arguments[0].name, "topic");
assert!(prompt.arguments[0].required);
assert_eq!(prompt.arguments[1].name, "style");
assert!(!prompt.arguments[1].required);
}
#[test]
fn test_prompt_fn_macro_registers_in_router() {
use tower_mcp::McpRouter;
let _router = McpRouter::new()
.server_info("test", "1.0.0")
.prompt(greet_prompt())
.prompt(summarize_prompt());
}
use tower_mcp::protocol::ReadResourceResult;
use tower_mcp::resource_fn;
use tower_mcp::resource_template_fn;
#[resource_fn(uri = "app://config", name = "config", description = "App config")]
async fn config() -> Result<ReadResourceResult, tower_mcp::Error> {
Ok(ReadResourceResult::text("app://config", "debug=true"))
}
#[resource_fn(uri = "app://version", mime_type = "text/plain")]
async fn version() -> Result<ReadResourceResult, tower_mcp::Error> {
Ok(ReadResourceResult::text("app://version", "1.0.0"))
}
#[test]
fn test_resource_fn_macro_generates_resource() {
let resource = config_resource();
assert_eq!(resource.uri, "app://config");
assert_eq!(resource.name, "config");
assert_eq!(resource.description.as_deref(), Some("App config"));
}
#[test]
fn test_resource_fn_macro_default_name() {
let resource = version_resource();
assert_eq!(resource.uri, "app://version");
assert_eq!(resource.name, "version");
}
#[test]
fn test_resource_fn_macro_registers_in_router() {
use tower_mcp::McpRouter;
let _router = McpRouter::new()
.server_info("test", "1.0.0")
.resource(config_resource())
.resource(version_resource());
}
#[resource_template_fn(
uri_template = "file:///{+path}",
name = "file",
description = "Read a file"
)]
async fn read_file(
uri: String,
vars: HashMap<String, String>,
) -> Result<ReadResourceResult, tower_mcp::Error> {
let path = vars.get("path").cloned().unwrap_or_default();
Ok(ReadResourceResult::text(uri, format!("contents of {path}")))
}
#[resource_template_fn(uri_template = "db://users/{id}")]
async fn get_user(
uri: String,
_vars: HashMap<String, String>,
) -> Result<ReadResourceResult, tower_mcp::Error> {
Ok(ReadResourceResult::text(uri, "user data"))
}
#[test]
fn test_resource_template_fn_macro_generates_template() {
let template = read_file_resource_template();
assert_eq!(template.uri_template, "file:///{+path}");
assert_eq!(template.name, "file");
assert_eq!(template.description.as_deref(), Some("Read a file"));
}
#[test]
fn test_resource_template_fn_macro_default_name() {
let template = get_user_resource_template();
assert_eq!(template.uri_template, "db://users/{id}");
}
#[test]
fn test_resource_template_fn_macro_registers_in_router() {
use tower_mcp::McpRouter;
let _router = McpRouter::new()
.server_info("test", "1.0.0")
.resource_template(read_file_resource_template())
.resource_template(get_user_resource_template());
}