use serde_json::json;
use std::future::Future;
use turbomcp_core::context::RequestContext as CoreRequestContext;
use turbomcp_core::error::{McpError, McpResult};
use turbomcp_server::{McpHandler, McpHandlerExt, RequestContext};
use turbomcp_types::{
Prompt, PromptResult, Resource, ResourceResult, ServerInfo, Tool, ToolResult,
};
#[derive(Clone)]
struct Calculator;
impl Calculator {
fn new() -> Self {
Self
}
}
impl McpHandler for Calculator {
fn server_info(&self) -> ServerInfo {
ServerInfo::new("calculator", "1.0.0").with_description("A simple calculator server")
}
fn list_tools(&self) -> Vec<Tool> {
vec![
Tool::new("add", "Add two numbers"),
Tool::new("multiply", "Multiply two numbers"),
Tool::new("greet", "Greet someone by name"),
]
}
fn list_resources(&self) -> Vec<Resource> {
vec![Resource::new("config://calculator", "config")]
}
fn list_prompts(&self) -> Vec<Prompt> {
vec![Prompt::new("math_help", "Get help with math")]
}
fn call_tool(
&self,
name: &str,
args: serde_json::Value,
_ctx: &CoreRequestContext,
) -> impl Future<Output = McpResult<ToolResult>> + Send {
let name = name.to_string();
async move {
match name.as_str() {
"add" => {
let a = args.get("a").and_then(|v| v.as_i64()).unwrap_or(0);
let b = args.get("b").and_then(|v| v.as_i64()).unwrap_or(0);
Ok(ToolResult::text(format!("{}", a + b)))
}
"multiply" => {
let a = args.get("a").and_then(|v| v.as_i64()).unwrap_or(0);
let b = args.get("b").and_then(|v| v.as_i64()).unwrap_or(0);
Ok(ToolResult::text(format!("{}", a * b)))
}
"greet" => {
let who = args.get("name").and_then(|v| v.as_str()).unwrap_or("World");
Ok(ToolResult::text(format!("Hello, {}!", who)))
}
_ => Err(McpError::tool_not_found(&name)),
}
}
}
fn read_resource(
&self,
uri: &str,
_ctx: &CoreRequestContext,
) -> impl Future<Output = McpResult<ResourceResult>> + Send {
let uri = uri.to_string();
async move {
if uri == "config://calculator" {
Ok(ResourceResult::json(
&uri,
&json!({
"precision": 10,
"mode": "standard"
}),
)?)
} else {
Err(McpError::resource_not_found(&uri))
}
}
}
fn get_prompt(
&self,
name: &str,
_args: Option<serde_json::Value>,
_ctx: &CoreRequestContext,
) -> impl Future<Output = McpResult<PromptResult>> + Send {
let name = name.to_string();
async move {
if name == "math_help" {
Ok(PromptResult::user("I need help with math calculations."))
} else {
Err(McpError::prompt_not_found(&name))
}
}
}
}
#[tokio::test]
async fn test_calculator_server_info() {
let calc = Calculator::new();
let info = calc.server_info();
assert_eq!(info.name, "calculator");
assert_eq!(info.version, "1.0.0");
assert_eq!(
info.description.as_deref(),
Some("A simple calculator server")
);
}
#[tokio::test]
async fn test_calculator_list_tools() {
let calc = Calculator::new();
let tools = calc.list_tools();
assert_eq!(tools.len(), 3);
assert!(tools.iter().any(|t| t.name == "add"));
assert!(tools.iter().any(|t| t.name == "multiply"));
assert!(tools.iter().any(|t| t.name == "greet"));
}
#[tokio::test]
async fn test_calculator_add() {
let calc = Calculator::new();
let ctx = CoreRequestContext::stdio();
let result = calc
.call_tool("add", json!({"a": 5, "b": 3}), &ctx)
.await
.unwrap();
assert_eq!(result.first_text(), Some("8"));
}
#[tokio::test]
async fn test_calculator_multiply() {
let calc = Calculator::new();
let ctx = CoreRequestContext::stdio();
let result = calc
.call_tool("multiply", json!({"a": 4, "b": 7}), &ctx)
.await
.unwrap();
assert_eq!(result.first_text(), Some("28"));
}
#[tokio::test]
async fn test_calculator_greet() {
let calc = Calculator::new();
let ctx = CoreRequestContext::stdio();
let result = calc
.call_tool("greet", json!({"name": "Alice"}), &ctx)
.await
.unwrap();
assert_eq!(result.first_text(), Some("Hello, Alice!"));
}
#[tokio::test]
async fn test_handle_request_initialize() {
let calc = Calculator::new();
let ctx = RequestContext::new();
let request = json!({
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2025-11-25",
"clientInfo": {
"name": "test-client",
"version": "1.0.0"
},
"capabilities": {}
}
});
let response = calc.handle_request(request, ctx).await.unwrap();
assert_eq!(response["jsonrpc"], "2.0");
assert_eq!(response["id"], 1);
assert_eq!(response["result"]["serverInfo"]["name"], "calculator");
assert_eq!(
response["result"]["serverInfo"]["description"],
"A simple calculator server"
);
assert!(response["result"]["capabilities"]["tools"].is_object());
}
#[tokio::test]
async fn test_handle_request_tools_list() {
let calc = Calculator::new();
let ctx = RequestContext::new();
let request = json!({
"jsonrpc": "2.0",
"id": 2,
"method": "tools/list"
});
let response = calc.handle_request(request, ctx).await.unwrap();
assert_eq!(response["jsonrpc"], "2.0");
let tools = response["result"]["tools"].as_array().unwrap();
assert_eq!(tools.len(), 3);
}
#[tokio::test]
async fn test_handle_request_tools_call() {
let calc = Calculator::new();
let ctx = RequestContext::new();
let request = json!({
"jsonrpc": "2.0",
"id": 3,
"method": "tools/call",
"params": {
"name": "add",
"arguments": {"a": 100, "b": 50}
}
});
let response = calc.handle_request(request, ctx).await.unwrap();
assert_eq!(response["jsonrpc"], "2.0");
assert!(response.get("error").is_none());
let content = response["result"]["content"].as_array().unwrap();
assert!(!content.is_empty());
}
#[tokio::test]
async fn test_handle_request_resources_list() {
let calc = Calculator::new();
let ctx = RequestContext::new();
let request = json!({
"jsonrpc": "2.0",
"id": 4,
"method": "resources/list"
});
let response = calc.handle_request(request, ctx).await.unwrap();
let resources = response["result"]["resources"].as_array().unwrap();
assert_eq!(resources.len(), 1);
assert_eq!(resources[0]["uri"], "config://calculator");
}
#[tokio::test]
async fn test_handle_request_prompts_list() {
let calc = Calculator::new();
let ctx = RequestContext::new();
let request = json!({
"jsonrpc": "2.0",
"id": 5,
"method": "prompts/list"
});
let response = calc.handle_request(request, ctx).await.unwrap();
let prompts = response["result"]["prompts"].as_array().unwrap();
assert_eq!(prompts.len(), 1);
assert_eq!(prompts[0]["name"], "math_help");
}
#[tokio::test]
async fn test_handle_request_error_unknown_method() {
let calc = Calculator::new();
let ctx = RequestContext::new();
let request = json!({
"jsonrpc": "2.0",
"id": 99,
"method": "unknown/method"
});
let response = calc.handle_request(request, ctx).await.unwrap();
assert!(response.get("error").is_some());
assert_eq!(response["error"]["code"], -32601); }