#![allow(dead_code)]
#![allow(unused_variables)]
use serde::{Deserialize, Serialize};
use server_less::{mcp, server};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
struct Item {
id: String,
name: String,
}
#[derive(Clone)]
struct TestService {
items: Vec<Item>,
}
#[mcp(namespace = "test")]
impl TestService {
pub fn list_items(&self) -> Vec<Item> {
self.items.clone()
}
pub fn get_item(&self, item_id: String) -> Option<Item> {
self.items.iter().find(|i| i.id == item_id).cloned()
}
pub fn create_item(&self, name: String) -> Item {
Item {
id: "new".to_string(),
name,
}
}
pub fn search_items(&self, query: String, limit: Option<u32>) -> Vec<Item> {
let limit = limit.unwrap_or(10) as usize;
self.items
.iter()
.filter(|i| i.name.contains(&query))
.take(limit)
.cloned()
.collect()
}
}
#[test]
fn test_mcp_tools_generated() {
let tools = TestService::mcp_tools();
assert_eq!(tools.len(), 4);
let names: Vec<_> = tools
.iter()
.map(|t| t.get("name").unwrap().as_str().unwrap())
.collect();
assert!(names.contains(&"test_list_items"));
assert!(names.contains(&"test_get_item"));
assert!(names.contains(&"test_create_item"));
assert!(names.contains(&"test_search_items"));
}
#[test]
fn test_mcp_method_names() {
let names = TestService::mcp_method_names();
assert_eq!(names.len(), 4);
assert!(names.contains(&"test_list_items".to_string()));
}
#[test]
fn test_mcp_call_list() {
let service = TestService {
items: vec![Item {
id: "1".to_string(),
name: "Test".to_string(),
}],
};
let result = service.mcp_call("test_list_items", serde_json::json!({}));
assert!(result.is_ok());
let items: Vec<Item> = serde_json::from_value(result.unwrap()).unwrap();
assert_eq!(items.len(), 1);
assert_eq!(items[0].name, "Test");
}
#[test]
fn test_mcp_call_get_option() {
let service = TestService {
items: vec![Item {
id: "1".to_string(),
name: "Test".to_string(),
}],
};
let result = service.mcp_call("test_get_item", serde_json::json!({"item_id": "1"}));
assert!(result.is_ok());
let item: Item = serde_json::from_value(result.unwrap()).unwrap();
assert_eq!(item.id, "1");
let result = service.mcp_call("test_get_item", serde_json::json!({"item_id": "999"}));
assert!(result.is_ok());
assert!(result.unwrap().is_null());
}
#[test]
fn test_mcp_call_create() {
let service = TestService { items: vec![] };
let result = service.mcp_call("test_create_item", serde_json::json!({"name": "NewItem"}));
assert!(result.is_ok());
let item: Item = serde_json::from_value(result.unwrap()).unwrap();
assert_eq!(item.name, "NewItem");
}
#[test]
fn test_mcp_call_with_optional_param() {
let service = TestService {
items: vec![
Item {
id: "1".to_string(),
name: "Apple".to_string(),
},
Item {
id: "2".to_string(),
name: "Apricot".to_string(),
},
],
};
let result = service.mcp_call("test_search_items", serde_json::json!({"query": "Ap"}));
assert!(result.is_ok());
let items: Vec<Item> = serde_json::from_value(result.unwrap()).unwrap();
assert_eq!(items.len(), 2);
let result = service.mcp_call(
"test_search_items",
serde_json::json!({"query": "Ap", "limit": 1}),
);
assert!(result.is_ok());
let items: Vec<Item> = serde_json::from_value(result.unwrap()).unwrap();
assert_eq!(items.len(), 1);
}
#[test]
fn test_mcp_call_unknown_tool() {
let service = TestService { items: vec![] };
let result = service.mcp_call("unknown_tool", serde_json::json!({}));
assert!(result.is_err());
assert!(result.unwrap_err().contains("Unknown tool"));
}
#[test]
fn test_mcp_tool_schema() {
let tools = TestService::mcp_tools();
let create_tool = tools
.iter()
.find(|t| t.get("name").unwrap() == "test_create_item")
.unwrap();
let schema = create_tool.get("inputSchema").unwrap();
assert_eq!(schema.get("type").unwrap(), "object");
let properties = schema.get("properties").unwrap().as_object().unwrap();
assert!(properties.contains_key("name"));
let required = schema.get("required").unwrap().as_array().unwrap();
assert!(required.contains(&serde_json::json!("name")));
}
#[derive(Clone)]
struct AsyncService;
#[mcp(namespace = "async")]
impl AsyncService {
pub fn sync_add(&self, a: i64, b: i64) -> i64 {
a + b
}
pub async fn async_fetch(&self, id: String) -> String {
format!("Fetched: {}", id)
}
pub async fn async_compute(&self, n: i64) -> i64 {
n * 2
}
}
#[test]
fn test_mcp_sync_method_with_sync_call() {
let service = AsyncService;
let result = service.mcp_call("async_sync_add", serde_json::json!({"a": 5, "b": 3}));
assert!(result.is_ok());
assert_eq!(result.unwrap(), serde_json::json!(8));
}
#[test]
fn test_mcp_async_method_with_sync_call_returns_error() {
let service = AsyncService;
let result = service.mcp_call("async_async_fetch", serde_json::json!({"id": "123"}));
assert!(result.is_err());
assert!(
result
.unwrap_err()
.contains("not supported in sync context")
);
}
#[tokio::test]
async fn test_mcp_sync_method_with_async_call() {
let service = AsyncService;
let result = service
.mcp_call_async("async_sync_add", serde_json::json!({"a": 10, "b": 7}))
.await;
assert!(result.is_ok());
assert_eq!(result.unwrap(), serde_json::json!(17));
}
#[tokio::test]
async fn test_mcp_async_method_with_async_call() {
let service = AsyncService;
let result = service
.mcp_call_async("async_async_fetch", serde_json::json!({"id": "abc"}))
.await;
assert!(result.is_ok());
assert_eq!(result.unwrap(), serde_json::json!("Fetched: abc"));
}
#[tokio::test]
async fn test_mcp_async_compute() {
let service = AsyncService;
let result = service
.mcp_call_async("async_async_compute", serde_json::json!({"n": 21}))
.await;
assert!(result.is_ok());
assert_eq!(result.unwrap(), serde_json::json!(42));
}
use futures::stream::{self, Stream};
#[derive(Clone)]
struct StreamService;
#[mcp(namespace = "stream")]
impl StreamService {
fn stream_numbers(&self, count: u32) -> impl Stream<Item = u32> + use<> {
stream::iter(0..count)
}
async fn stream_items(&self, prefix: String, count: u32) -> impl Stream<Item = String> + use<> {
stream::iter((0..count).map(move |i| format!("{}{}", prefix, i)))
}
}
#[tokio::test]
async fn test_mcp_stream_numbers() {
let service = StreamService;
let result = service
.mcp_call_async("stream_stream_numbers", serde_json::json!({"count": 5}))
.await;
assert!(result.is_ok());
assert_eq!(result.unwrap(), serde_json::json!([0, 1, 2, 3, 4]));
}
#[tokio::test]
async fn test_mcp_stream_items() {
let service = StreamService;
let result = service
.mcp_call_async(
"stream_stream_items",
serde_json::json!({"prefix": "item-", "count": 3}),
)
.await;
assert!(result.is_ok());
assert_eq!(
result.unwrap(),
serde_json::json!(["item-0", "item-1", "item-2"])
);
}
#[tokio::test]
async fn test_mcp_stream_with_sync_call_fails() {
let service = StreamService;
let result = service.mcp_call("stream_stream_numbers", serde_json::json!({"count": 5}));
assert!(result.is_err());
assert!(
result
.unwrap_err()
.contains("not supported in sync context")
);
}
#[derive(Clone)]
struct UserTools;
#[mcp]
impl UserTools {
fn list(&self) -> Vec<String> {
vec!["alice".to_string(), "bob".to_string()]
}
fn get(&self, name: String) -> String {
format!("User: {}", name)
}
}
#[derive(Clone)]
struct PostTools;
#[mcp]
impl PostTools {
fn list(&self) -> Vec<String> {
vec!["post1".to_string()]
}
}
#[derive(Clone)]
struct McpApp {
user_tools: UserTools,
post_tools: PostTools,
}
#[mcp]
impl McpApp {
fn health(&self) -> String {
"ok".to_string()
}
fn users(&self) -> &UserTools {
&self.user_tools
}
fn posts(&self) -> &PostTools {
&self.post_tools
}
}
#[test]
fn test_mcp_static_mount_tools_listed() {
let tools = McpApp::mcp_tools();
let names: Vec<_> = tools
.iter()
.map(|t| t.get("name").unwrap().as_str().unwrap().to_string())
.collect();
assert!(names.contains(&"health".to_string()));
assert!(names.contains(&"users_list".to_string()));
assert!(names.contains(&"users_get".to_string()));
assert!(names.contains(&"posts_list".to_string()));
}
#[test]
fn test_mcp_static_mount_dispatch_sync() {
let app = McpApp {
user_tools: UserTools,
post_tools: PostTools,
};
let result = app.mcp_call("health", serde_json::json!({}));
assert!(result.is_ok());
assert_eq!(result.unwrap(), serde_json::json!("ok"));
let result = app.mcp_call("users_list", serde_json::json!({}));
assert!(result.is_ok());
let users: Vec<String> = serde_json::from_value(result.unwrap()).unwrap();
assert_eq!(users, vec!["alice", "bob"]);
let result = app.mcp_call("users_get", serde_json::json!({"name": "alice"}));
assert!(result.is_ok());
assert_eq!(result.unwrap(), serde_json::json!("User: alice"));
}
#[tokio::test]
async fn test_mcp_static_mount_dispatch_async() {
let app = McpApp {
user_tools: UserTools,
post_tools: PostTools,
};
let result = app
.mcp_call_async("users_get", serde_json::json!({"name": "bob"}))
.await;
assert!(result.is_ok());
assert_eq!(result.unwrap(), serde_json::json!("User: bob"));
}
#[test]
fn test_mcp_multiple_static_mounts() {
let app = McpApp {
user_tools: UserTools,
post_tools: PostTools,
};
assert!(app.mcp_call("users_list", serde_json::json!({})).is_ok());
assert!(app.mcp_call("posts_list", serde_json::json!({})).is_ok());
}
#[derive(Clone)]
struct McpSlugApp {
user_tools: UserTools,
}
#[mcp]
impl McpSlugApp {
fn user(&self, id: String) -> &UserTools {
let _ = &id;
&self.user_tools
}
}
#[test]
fn test_mcp_slug_mount_tools_have_slug_param() {
let tools = McpSlugApp::mcp_tools();
let names: Vec<_> = tools
.iter()
.map(|t| t.get("name").unwrap().as_str().unwrap().to_string())
.collect();
assert!(names.contains(&"user_list".to_string()));
assert!(names.contains(&"user_get".to_string()));
let user_list = tools
.iter()
.find(|t| t.get("name").unwrap().as_str().unwrap() == "user_list")
.unwrap();
let schema = user_list.get("inputSchema").unwrap();
let props = schema.get("properties").unwrap().as_object().unwrap();
assert!(props.contains_key("id"));
}
#[test]
fn test_mcp_slug_mount_dispatch() {
let app = McpSlugApp {
user_tools: UserTools,
};
let result = app.mcp_call("user_list", serde_json::json!({"id": "42"}));
assert!(result.is_ok());
let users: Vec<String> = serde_json::from_value(result.unwrap()).unwrap();
assert_eq!(users, vec!["alice", "bob"]);
let result = app.mcp_call("user_get", serde_json::json!({"id": "42", "name": "alice"}));
assert!(result.is_ok());
assert_eq!(result.unwrap(), serde_json::json!("User: alice"));
}
#[test]
fn test_mcp_namespace_trait_implemented() {
use server_less::McpNamespace;
let tools = <UserTools as McpNamespace>::mcp_namespace_tools();
assert_eq!(tools.len(), 2);
let names = <UserTools as McpNamespace>::mcp_namespace_tool_names();
assert!(names.contains(&"list".to_string()));
assert!(names.contains(&"get".to_string()));
let svc = UserTools;
let result =
<UserTools as McpNamespace>::mcp_namespace_call(&svc, "list", serde_json::json!({}));
assert!(result.is_ok());
}
#[derive(Clone)]
struct HiddenService;
#[mcp]
impl HiddenService {
pub fn public_tool(&self) -> String {
"public".to_string()
}
#[server(hidden)]
pub fn hidden_tool(&self) -> String {
"hidden".to_string()
}
}
#[test]
fn test_mcp_hidden_method_not_in_tool_list() {
let tools = HiddenService::mcp_tools();
let names: Vec<_> = tools
.iter()
.map(|t| t.get("name").unwrap().as_str().unwrap())
.collect();
assert!(names.contains(&"public_tool"));
assert!(!names.contains(&"hidden_tool"));
}
#[test]
fn test_mcp_hidden_method_not_in_tool_names() {
let names = HiddenService::mcp_method_names();
assert!(names.contains(&"public_tool".to_string()));
assert!(!names.contains(&"hidden_tool".to_string()));
}
#[test]
fn test_mcp_hidden_method_still_callable() {
let svc = HiddenService;
let result = svc.mcp_call("hidden_tool", serde_json::json!({}));
assert!(result.is_ok());
assert_eq!(result.unwrap(), serde_json::json!("hidden"));
}
#[tokio::test]
async fn test_mcp_hidden_method_callable_async() {
let svc = HiddenService;
let result = svc.mcp_call_async("hidden_tool", serde_json::json!({})).await;
assert!(result.is_ok());
assert_eq!(result.unwrap(), serde_json::json!("hidden"));
}
#[derive(Clone)]
struct ContextMcpService;
#[mcp(namespace = "ctx")]
impl ContextMcpService {
pub fn echo_name(&self, ctx: server_less::Context, name: String) -> String {
let _ = ctx;
format!("Hello, {}!", name)
}
pub fn ping(&self) -> String {
"pong".to_string()
}
}
#[test]
fn test_mcp_context_param_not_in_input_schema() {
let tools = ContextMcpService::mcp_tools();
let echo_tool = tools
.iter()
.find(|t| t.get("name").unwrap().as_str().unwrap() == "ctx_echo_name")
.expect("ctx_echo_name tool should exist");
let schema = echo_tool.get("inputSchema").unwrap();
let properties = schema.get("properties").unwrap().as_object().unwrap();
assert!(
!properties.contains_key("ctx"),
"Context parameter should not appear in inputSchema, got: {:?}",
properties
);
assert!(
properties.contains_key("name"),
"Regular parameter 'name' should appear in inputSchema"
);
let required = schema.get("required").unwrap().as_array().unwrap();
assert!(
!required.iter().any(|v| v.as_str() == Some("ctx")),
"Context parameter should not be in the required list"
);
assert!(
required.iter().any(|v| v.as_str() == Some("name")),
"Regular parameter 'name' should be required"
);
}
#[test]
fn test_mcp_context_param_method_callable() {
let service = ContextMcpService;
let result = service.mcp_call("ctx_echo_name", serde_json::json!({"name": "Alice"}));
assert!(result.is_ok(), "Call should succeed: {:?}", result);
assert_eq!(result.unwrap(), serde_json::json!("Hello, Alice!"));
}
#[tokio::test]
async fn test_mcp_context_param_method_callable_async() {
let service = ContextMcpService;
let result = service
.mcp_call_async("ctx_echo_name", serde_json::json!({"name": "Bob"}))
.await;
assert!(result.is_ok(), "Async call should succeed: {:?}", result);
assert_eq!(result.unwrap(), serde_json::json!("Hello, Bob!"));
}
#[derive(Clone)]
struct HelpParamService;
#[mcp]
impl HelpParamService {
pub fn greet(
&self,
#[param(help = "The name to greet")] name: String,
#[param(help = "Optional title prefix")] title: Option<String>,
) -> String {
match title {
Some(t) => format!("{} {}!", t, name),
None => format!("Hello, {}!", name),
}
}
}
#[test]
fn test_mcp_param_help_in_input_schema() {
let tools = HelpParamService::mcp_tools();
let tool = tools
.iter()
.find(|t| t.get("name").unwrap().as_str().unwrap() == "greet")
.expect("greet tool should exist");
let props = tool
.get("inputSchema")
.unwrap()
.get("properties")
.unwrap()
.as_object()
.unwrap();
let name_desc = props["name"].get("description").unwrap().as_str().unwrap();
assert_eq!(name_desc, "The name to greet");
let title_desc = props["title"]
.get("description")
.unwrap()
.as_str()
.unwrap();
assert_eq!(title_desc, "Optional title prefix");
}
#[derive(Clone)]
struct BadParamService;
#[mcp(namespace = "bp")]
impl BadParamService {
pub fn greet(&self, name: String) -> String {
format!("Hello, {}!", name)
}
pub fn add(&self, a: i64, b: i64) -> i64 {
a + b
}
pub fn search(&self, query: String, limit: Option<u32>) -> String {
format!("query={} limit={:?}", query, limit)
}
}
#[test]
fn test_mcp_missing_required_param_returns_error() {
let svc = BadParamService;
let result = svc.mcp_call("bp_greet", serde_json::json!({}));
assert!(
result.is_err(),
"calling with missing required param should return Err, got: {:?}",
result
);
let err = result.unwrap_err();
assert!(
err.contains("name") || err.contains("Missing"),
"error should mention the missing param or 'Missing', got: {}",
err
);
}
#[test]
fn test_mcp_missing_one_of_multiple_required_params_returns_error() {
let svc = BadParamService;
let result = svc.mcp_call("bp_add", serde_json::json!({"a": 1}));
assert!(
result.is_err(),
"calling with one missing required param should return Err, got: {:?}",
result
);
let err = result.unwrap_err();
assert!(
err.contains("b") || err.contains("Missing"),
"error should mention the missing param 'b' or 'Missing', got: {}",
err
);
}
#[test]
fn test_mcp_wrong_type_param_returns_error() {
let svc = BadParamService;
let result = svc.mcp_call("bp_add", serde_json::json!({"a": "not-a-number", "b": 2}));
assert!(
result.is_err(),
"calling with wrong-type param should return Err, got: {:?}",
result
);
let err = result.unwrap_err();
assert!(
err.contains("a") || err.contains("Invalid"),
"error should mention the bad param 'a' or 'Invalid', got: {}",
err
);
}
#[test]
fn test_mcp_missing_optional_param_is_ok() {
let svc = BadParamService;
let result = svc.mcp_call("bp_search", serde_json::json!({"query": "hello"}));
assert!(
result.is_ok(),
"calling with missing optional param should succeed, got: {:?}",
result
);
}
#[tokio::test]
async fn test_mcp_missing_required_param_async_returns_error() {
let svc = BadParamService;
let result = svc
.mcp_call_async("bp_greet", serde_json::json!({}))
.await;
assert!(
result.is_err(),
"async call with missing required param should return Err, got: {:?}",
result
);
let err = result.unwrap_err();
assert!(
err.contains("name") || err.contains("Missing"),
"error should mention the missing param or 'Missing', got: {}",
err
);
}
#[tokio::test]
async fn test_mcp_wrong_type_param_async_returns_error() {
let svc = BadParamService;
let result = svc
.mcp_call_async("bp_add", serde_json::json!({"a": "bad", "b": 3}))
.await;
assert!(
result.is_err(),
"async call with wrong-type param should return Err, got: {:?}",
result
);
let err = result.unwrap_err();
assert!(
err.contains("a") || err.contains("Invalid"),
"error should mention the bad param 'a' or 'Invalid', got: {}",
err
);
}
#[test]
fn test_mcp_optional_wrong_type_returns_error() {
let svc = BadParamService;
let result = svc.mcp_call(
"bp_search",
serde_json::json!({"query": "hello", "limit": "not-a-number"}),
);
assert!(
result.is_err(),
"optional param with wrong type should return Err, got: {:?}",
result
);
let err = result.unwrap_err();
assert!(
err.contains("limit") || err.contains("invalid") || err.contains("Invalid"),
"error should mention 'limit' or 'invalid type', got: {}",
err
);
}
#[test]
fn test_mcp_optional_absent_is_ok() {
let svc = BadParamService;
let result = svc.mcp_call("bp_search", serde_json::json!({"query": "hello"}));
assert!(
result.is_ok(),
"absent optional param should succeed, got: {:?}",
result
);
}
#[tokio::test]
async fn test_mcp_optional_wrong_type_async_returns_error() {
let svc = BadParamService;
let result = svc
.mcp_call_async(
"bp_search",
serde_json::json!({"query": "hello", "limit": "bad"}),
)
.await;
assert!(
result.is_err(),
"async optional param with wrong type should return Err, got: {:?}",
result
);
let err = result.unwrap_err();
assert!(
err.contains("limit") || err.contains("invalid") || err.contains("Invalid"),
"error should mention 'limit' or 'invalid type', got: {}",
err
);
}
#[test]
fn test_mcp_unknown_param_does_not_break_dispatch() {
let svc = BadParamService;
let result = svc.mcp_call(
"bp_greet",
serde_json::json!({"name": "Alice", "unexpected_key": "oops"}),
);
assert!(
result.is_ok(),
"unknown param should not cause an error, got: {:?}",
result
);
assert_eq!(
result.unwrap(),
serde_json::json!("Hello, Alice!"),
"should still return the correct result"
);
}
#[tokio::test]
async fn test_mcp_unknown_param_async_does_not_break_dispatch() {
let svc = BadParamService;
let result = svc
.mcp_call_async(
"bp_greet",
serde_json::json!({"name": "Bob", "typo_param": "ignored"}),
)
.await;
assert!(
result.is_ok(),
"unknown param in async call should not cause an error, got: {:?}",
result
);
assert_eq!(result.unwrap(), serde_json::json!("Hello, Bob!"));
}