#[cfg(all(not(target_arch = "wasm32"), feature = "schema-generation"))]
mod tests {
use pmcp::{SimpleTool, SimpleToolExt, SyncTool, SyncToolExt, TypedSyncTool, TypedTool};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use serde_json::json;
fn test_extra() -> pmcp::RequestHandlerExtra {
use tokio_util::sync::CancellationToken;
let cancellation_token = CancellationToken::new();
pmcp::RequestHandlerExtra::new("test-request".to_string(), cancellation_token)
}
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
struct SearchArgs {
query: String,
#[serde(default = "default_limit")]
limit: Option<u32>,
#[serde(default)]
include_archived: bool,
}
#[allow(clippy::unnecessary_wraps)]
fn default_limit() -> Option<u32> {
Some(10)
}
#[tokio::test]
async fn test_typed_tool_with_schema_generation() {
use pmcp::ToolHandler;
let tool = TypedTool::new("search", |args: SearchArgs, _extra| {
Box::pin(async move {
Ok(json!({
"query": args.query,
"limit": args.limit.unwrap_or(10),
"include_archived": args.include_archived
}))
})
})
.with_description("Search for items in the database");
let metadata = tool.metadata().expect("Tool should have metadata");
assert_eq!(metadata.name, "search");
assert_eq!(
metadata.description,
Some("Search for items in the database".to_string())
);
let schema = &metadata.input_schema;
assert!(schema.is_object());
let schema_str = serde_json::to_string(schema).unwrap();
assert!(schema_str.contains("query"));
assert!(schema_str.contains("limit"));
assert!(schema_str.contains("include_archived"));
let args = json!({
"query": "test search",
"limit": 5,
"include_archived": true
});
let extra = test_extra();
let result = tool.handle(args, extra).await.unwrap();
assert_eq!(result["query"], "test search");
assert_eq!(result["limit"], 5);
assert_eq!(result["include_archived"], true);
}
#[tokio::test]
async fn test_typed_tool_validation_error() {
use pmcp::ToolHandler;
let tool = TypedTool::new("search", |args: SearchArgs, _extra| {
Box::pin(async move { Ok(json!({ "query": args.query })) })
});
let args = json!({
"limit": 5
});
let extra = test_extra();
let result = tool.handle(args, extra).await;
assert!(result.is_err());
let error = result.unwrap_err();
assert!(error.to_string().contains("Invalid arguments"));
}
#[test]
fn test_typed_sync_tool() {
use pmcp::ToolHandler;
use tokio::runtime::Runtime;
let tool = TypedSyncTool::new("search", |args: SearchArgs, _extra| {
Ok(json!({
"query": args.query,
"limit": args.limit.unwrap_or(10)
}))
})
.with_description("Synchronous search tool");
let metadata = tool.metadata().expect("Tool should have metadata");
assert_eq!(metadata.name, "search");
assert_eq!(
metadata.description,
Some("Synchronous search tool".to_string())
);
let rt = Runtime::new().unwrap();
let args = json!({
"query": "test"
});
let extra = test_extra();
let result = rt.block_on(tool.handle(args, extra)).unwrap();
assert_eq!(result["query"], "test");
assert_eq!(result["limit"], 10);
}
#[test]
fn test_simple_tool_with_schema_from() {
use pmcp::ToolHandler;
let tool = SimpleTool::new("search", |args, _extra| {
Box::pin(async move {
let query = args["query"].as_str().unwrap_or("");
Ok(json!({ "query": query }))
})
})
.with_description("Search with generated schema")
.with_schema_from::<SearchArgs>();
let metadata = tool.metadata().expect("Tool should have metadata");
let schema = &metadata.input_schema;
let schema_str = serde_json::to_string(schema).unwrap();
assert!(schema_str.contains("query"));
assert!(schema_str.contains("limit"));
assert!(schema_str.contains("include_archived"));
}
#[test]
fn test_sync_tool_with_schema_from() {
use pmcp::ToolHandler;
let tool = SyncTool::new("search", |args| {
let query = args["query"].as_str().unwrap_or("");
Ok(json!({ "query": query }))
})
.with_description("Sync search with generated schema")
.with_schema_from::<SearchArgs>();
let metadata = tool.metadata().expect("Tool should have metadata");
let schema = &metadata.input_schema;
let schema_str = serde_json::to_string(schema).unwrap();
assert!(schema_str.contains("query"));
assert!(schema_str.contains("limit"));
}
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
struct ComplexArgs {
action: ActionType,
target: String,
#[serde(default)]
params: Vec<String>,
config: Config,
}
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
#[serde(rename_all = "lowercase")]
enum ActionType {
Create,
Update,
Delete,
}
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
struct Config {
verbose: bool,
timeout: u32,
}
#[tokio::test]
async fn test_complex_schema_generation() {
use pmcp::ToolHandler;
let tool = TypedTool::new("complex", |args: ComplexArgs, _extra| {
Box::pin(async move {
Ok(json!({
"action": format!("{:?}", args.action),
"target": args.target,
"params": args.params,
"verbose": args.config.verbose,
"timeout": args.config.timeout
}))
})
});
let metadata = tool.metadata().expect("Tool should have metadata");
let schema = &metadata.input_schema;
let schema_str = serde_json::to_string(schema).unwrap();
assert!(schema_str.contains("action"));
assert!(schema_str.contains("target"));
assert!(schema_str.contains("config"));
let args = json!({
"action": "create",
"target": "resource-1",
"params": ["param1", "param2"],
"config": {
"verbose": true,
"timeout": 30
}
});
let extra = test_extra();
let result = tool.handle(args, extra).await.unwrap();
assert_eq!(result["action"], "Create");
assert_eq!(result["target"], "resource-1");
assert_eq!(result["verbose"], true);
assert_eq!(result["timeout"], 30);
}
}