use alloc::vec::Vec;
use core::future::Future;
use serde_json::Value;
use crate::context::RequestContext;
use crate::error::McpResult;
use crate::marker::{MaybeSend, MaybeSync};
use turbomcp_types::{
Prompt, PromptCapabilities, PromptResult, Resource, ResourceCapabilities, ResourceResult,
ServerCapabilities, ServerInfo, Tool, ToolCapabilities, ToolResult,
};
pub trait McpHandler: Clone + MaybeSend + MaybeSync + 'static {
fn server_info(&self) -> ServerInfo;
fn server_capabilities(&self) -> ServerCapabilities {
let mut capabilities = ServerCapabilities::default();
if !self.list_tools().is_empty() {
capabilities.tools = Some(ToolCapabilities {
list_changed: Some(true),
});
}
if !self.list_resources().is_empty() {
capabilities.resources = Some(ResourceCapabilities {
subscribe: None,
list_changed: Some(true),
});
}
if !self.list_prompts().is_empty() {
capabilities.prompts = Some(PromptCapabilities {
list_changed: Some(true),
});
}
capabilities
}
fn list_tools(&self) -> Vec<Tool>;
fn list_resources(&self) -> Vec<Resource>;
fn list_prompts(&self) -> Vec<Prompt>;
fn call_tool<'a>(
&'a self,
name: &'a str,
args: Value,
ctx: &'a RequestContext,
) -> impl Future<Output = McpResult<ToolResult>> + MaybeSend + 'a;
fn read_resource<'a>(
&'a self,
uri: &'a str,
ctx: &'a RequestContext,
) -> impl Future<Output = McpResult<ResourceResult>> + MaybeSend + 'a;
fn get_prompt<'a>(
&'a self,
name: &'a str,
args: Option<Value>,
ctx: &'a RequestContext,
) -> impl Future<Output = McpResult<PromptResult>> + MaybeSend + 'a;
fn list_tasks<'a>(
&'a self,
_cursor: Option<&'a str>,
_limit: Option<usize>,
_ctx: &'a RequestContext,
) -> impl Future<Output = McpResult<turbomcp_types::ListTasksResult>> + MaybeSend + 'a {
async {
Err(crate::error::McpError::capability_not_supported(
"tasks/list",
))
}
}
fn get_task<'a>(
&'a self,
_task_id: &'a str,
_ctx: &'a RequestContext,
) -> impl Future<Output = McpResult<turbomcp_types::Task>> + MaybeSend + 'a {
async {
Err(crate::error::McpError::capability_not_supported(
"tasks/get",
))
}
}
fn cancel_task<'a>(
&'a self,
_task_id: &'a str,
_ctx: &'a RequestContext,
) -> impl Future<Output = McpResult<turbomcp_types::Task>> + MaybeSend + 'a {
async {
Err(crate::error::McpError::capability_not_supported(
"tasks/cancel",
))
}
}
fn get_task_result<'a>(
&'a self,
_task_id: &'a str,
_ctx: &'a RequestContext,
) -> impl Future<Output = McpResult<Value>> + MaybeSend + 'a {
async {
Err(crate::error::McpError::capability_not_supported(
"tasks/result",
))
}
}
fn on_initialize(&self) -> impl Future<Output = McpResult<()>> + MaybeSend {
async { Ok(()) }
}
fn on_shutdown(&self) -> impl Future<Output = McpResult<()>> + MaybeSend {
async { Ok(()) }
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::error::McpError;
#[derive(Clone)]
struct TestHandler;
impl McpHandler for TestHandler {
fn server_info(&self) -> ServerInfo {
ServerInfo::new("test-handler", "1.0.0")
}
fn list_tools(&self) -> Vec<Tool> {
vec![Tool::new("greet", "Say hello")]
}
fn list_resources(&self) -> Vec<Resource> {
vec![]
}
fn list_prompts(&self) -> Vec<Prompt> {
vec![]
}
fn call_tool<'a>(
&'a self,
name: &'a str,
args: Value,
_ctx: &'a RequestContext,
) -> impl Future<Output = McpResult<ToolResult>> + MaybeSend + 'a {
let name = name.to_string();
async move {
match name.as_str() {
"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<'a>(
&'a self,
uri: &'a str,
_ctx: &'a RequestContext,
) -> impl Future<Output = McpResult<ResourceResult>> + MaybeSend + 'a {
let uri = uri.to_string();
async move { Err(McpError::resource_not_found(&uri)) }
}
fn get_prompt<'a>(
&'a self,
name: &'a str,
_args: Option<Value>,
_ctx: &'a RequestContext,
) -> impl Future<Output = McpResult<PromptResult>> + MaybeSend + 'a {
let name = name.to_string();
async move { Err(McpError::prompt_not_found(&name)) }
}
}
#[test]
fn test_server_info() {
let handler = TestHandler;
let info = handler.server_info();
assert_eq!(info.name, "test-handler");
assert_eq!(info.version, "1.0.0");
}
#[test]
fn test_list_tools() {
let handler = TestHandler;
let tools = handler.list_tools();
assert_eq!(tools.len(), 1);
assert_eq!(tools[0].name, "greet");
}
#[tokio::test]
async fn test_call_tool() {
let handler = TestHandler;
let ctx = RequestContext::stdio();
let args = serde_json::json!({"name": "Alice"});
let result = handler.call_tool("greet", args, &ctx).await.unwrap();
assert_eq!(result.first_text(), Some("Hello, Alice!"));
}
#[tokio::test]
async fn test_call_tool_not_found() {
let handler = TestHandler;
let ctx = RequestContext::stdio();
let args = serde_json::json!({});
let result = handler.call_tool("unknown", args, &ctx).await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_lifecycle_hooks() {
let handler = TestHandler;
assert!(handler.on_initialize().await.is_ok());
assert!(handler.on_shutdown().await.is_ok());
}
#[cfg(not(target_arch = "wasm32"))]
#[test]
fn test_handler_is_send_sync() {
fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<TestHandler>();
}
}