turbomcp_core/handler.rs
1//! Unified MCP handler trait for cross-platform server implementations.
2//!
3//! This module provides the core `McpHandler` trait that defines the interface for
4//! all MCP server operations. The trait is designed to work identically on native
5//! and WASM targets through platform-adaptive bounds.
6//!
7//! # Design Philosophy
8//!
9//! The `McpHandler` trait follows several key design principles:
10//!
11//! 1. **Unified Definition**: Single trait definition works on both native and WASM
12//! 2. **Platform-Adaptive Bounds**: Uses `MaybeSend`/`MaybeSync` for conditional thread safety
13//! 3. **Zero-Boilerplate**: Automatically implemented by the `#[server]` macro
14//! 4. **no_std Compatible**: Core trait works in `no_std` environments with `alloc`
15//!
16//! # Platform Behavior
17//!
18//! - **Native**: Methods return `impl Future + Send`, enabling multi-threaded executors
19//! - **WASM**: Methods return `impl Future`, compatible with single-threaded runtimes
20//!
21//! # Example
22//!
23//! ```rust,ignore
24//! use turbomcp::prelude::*;
25//!
26//! #[derive(Clone)]
27//! struct MyServer;
28//!
29//! #[server(name = "my-server", version = "1.0.0")]
30//! impl MyServer {
31//! #[tool]
32//! async fn greet(&self, name: String) -> String {
33//! format!("Hello, {}!", name)
34//! }
35//! }
36//!
37//! // On native:
38//! #[tokio::main]
39//! async fn main() {
40//! MyServer.run_stdio().await.unwrap();
41//! }
42//!
43//! // On WASM (Cloudflare Workers):
44//! #[event(fetch)]
45//! async fn fetch(req: Request, _env: Env, _ctx: Context) -> Result<Response> {
46//! MyServer.handle_worker_request(req).await
47//! }
48//! ```
49
50use alloc::vec::Vec;
51use core::future::Future;
52use serde_json::Value;
53
54use crate::context::RequestContext;
55use crate::error::McpResult;
56use crate::marker::{MaybeSend, MaybeSync};
57use turbomcp_types::{
58 Prompt, PromptResult, Resource, ResourceResult, ServerInfo, Tool, ToolResult,
59};
60
61/// The unified MCP handler trait.
62///
63/// This trait defines the complete interface for an MCP server. It's designed to:
64/// - Work identically on native (std) and WASM (no_std) targets
65/// - Be automatically implemented by the `#[server]` macro
66/// - Enable zero-boilerplate server development
67///
68/// # Required Methods
69///
70/// - [`server_info`](McpHandler::server_info): Returns server metadata
71/// - [`list_tools`](McpHandler::list_tools): Returns available tools
72/// - [`list_resources`](McpHandler::list_resources): Returns available resources
73/// - [`list_prompts`](McpHandler::list_prompts): Returns available prompts
74/// - [`call_tool`](McpHandler::call_tool): Executes a tool
75/// - [`read_resource`](McpHandler::read_resource): Reads a resource
76/// - [`get_prompt`](McpHandler::get_prompt): Gets a prompt
77///
78/// # Optional Hooks
79///
80/// - [`on_initialize`](McpHandler::on_initialize): Called during server initialization
81/// - [`on_shutdown`](McpHandler::on_shutdown): Called during server shutdown
82///
83/// # Thread Safety
84///
85/// The trait requires `MaybeSend + MaybeSync` bounds, which translate to:
86/// - **Native**: `Send + Sync` required for multi-threaded execution
87/// - **WASM**: No thread safety requirements (single-threaded)
88///
89/// # Manual Implementation
90///
91/// While the `#[server]` macro is recommended, you can implement manually:
92///
93/// ```rust
94/// use core::future::Future;
95/// use serde_json::Value;
96/// use turbomcp_core::handler::McpHandler;
97/// use turbomcp_core::context::RequestContext;
98/// use turbomcp_core::error::{McpError, McpResult};
99/// use turbomcp_types::{Prompt, PromptResult, Resource, ResourceResult, ServerInfo, Tool, ToolResult};
100///
101/// #[derive(Clone)]
102/// struct MyHandler;
103///
104/// impl McpHandler for MyHandler {
105/// fn server_info(&self) -> ServerInfo {
106/// ServerInfo::new("my-handler", "1.0.0")
107/// }
108///
109/// fn list_tools(&self) -> Vec<Tool> {
110/// vec![Tool::new("hello", "Say hello")]
111/// }
112///
113/// fn list_resources(&self) -> Vec<Resource> {
114/// vec![]
115/// }
116///
117/// fn list_prompts(&self) -> Vec<Prompt> {
118/// vec![]
119/// }
120///
121/// fn call_tool<'a>(
122/// &'a self,
123/// name: &'a str,
124/// args: Value,
125/// _ctx: &'a RequestContext,
126/// ) -> impl Future<Output = McpResult<ToolResult>> + 'a {
127/// let name = name.to_string();
128/// async move {
129/// match name.as_str() {
130/// "hello" => {
131/// let who = args.get("name")
132/// .and_then(|v| v.as_str())
133/// .unwrap_or("World");
134/// Ok(ToolResult::text(format!("Hello, {}!", who)))
135/// }
136/// _ => Err(McpError::tool_not_found(&name))
137/// }
138/// }
139/// }
140///
141/// fn read_resource<'a>(
142/// &'a self,
143/// uri: &'a str,
144/// _ctx: &'a RequestContext,
145/// ) -> impl Future<Output = McpResult<ResourceResult>> + 'a {
146/// let uri = uri.to_string();
147/// async move { Err(McpError::resource_not_found(&uri)) }
148/// }
149///
150/// fn get_prompt<'a>(
151/// &'a self,
152/// name: &'a str,
153/// _args: Option<Value>,
154/// _ctx: &'a RequestContext,
155/// ) -> impl Future<Output = McpResult<PromptResult>> + 'a {
156/// let name = name.to_string();
157/// async move { Err(McpError::prompt_not_found(&name)) }
158/// }
159/// }
160/// ```
161///
162/// # Clone Bound Rationale
163///
164/// The `Clone` bound is required because MCP handlers are typically shared across multiple
165/// concurrent connections and requests. This enables:
166///
167/// - **Multi-connection support**: Each connection can hold its own handler instance
168/// - **Cheap sharing**: Handlers follow the Arc-cloning pattern (like Axum/Tower services)
169/// - **Zero-cost abstraction**: Clone typically just increments an Arc reference count
170///
171/// ## Recommended Pattern
172///
173/// Wrap your server state in `Arc` for cheap cloning:
174///
175/// ```rust,ignore
176/// use std::sync::Arc;
177/// use turbomcp::prelude::*;
178///
179/// #[derive(Clone)]
180/// struct MyServer {
181/// state: Arc<ServerState>,
182/// }
183///
184/// struct ServerState {
185/// database: Database,
186/// cache: Cache,
187/// // Heavy resources that shouldn't be cloned
188/// }
189///
190/// #[server(name = "my-server", version = "1.0.0")]
191/// impl MyServer {
192/// #[tool]
193/// async fn process(&self, input: String) -> String {
194/// // Access shared state via Arc (cheap clone on each call)
195/// self.state.database.query(&input).await
196/// }
197/// }
198/// ```
199///
200/// Cloning `MyServer` only increments the Arc reference count, not the actual state.
201pub trait McpHandler: Clone + MaybeSend + MaybeSync + 'static {
202 // ===== Server Metadata =====
203
204 /// Returns server information (name, version, description, etc.)
205 ///
206 /// This is called during the MCP `initialize` handshake to provide
207 /// server metadata to the client.
208 fn server_info(&self) -> ServerInfo;
209
210 // ===== Capability Listings =====
211
212 /// Returns all available tools.
213 ///
214 /// Called in response to `tools/list` requests. The returned tools
215 /// will be advertised to clients with their schemas.
216 fn list_tools(&self) -> Vec<Tool>;
217
218 /// Returns all available resources.
219 ///
220 /// Called in response to `resources/list` requests.
221 fn list_resources(&self) -> Vec<Resource>;
222
223 /// Returns all available prompts.
224 ///
225 /// Called in response to `prompts/list` requests.
226 fn list_prompts(&self) -> Vec<Prompt>;
227
228 // ===== Request Handlers =====
229
230 /// Calls a tool by name with the given arguments.
231 ///
232 /// Called in response to `tools/call` requests.
233 ///
234 /// # Arguments
235 ///
236 /// * `name` - The name of the tool to call
237 /// * `args` - JSON arguments for the tool
238 /// * `ctx` - Request context with metadata
239 ///
240 /// # Returns
241 ///
242 /// The tool result or an error. Use `McpError::tool_not_found()`
243 /// for unknown tools.
244 fn call_tool<'a>(
245 &'a self,
246 name: &'a str,
247 args: Value,
248 ctx: &'a RequestContext,
249 ) -> impl Future<Output = McpResult<ToolResult>> + MaybeSend + 'a;
250
251 /// Reads a resource by URI.
252 ///
253 /// Called in response to `resources/read` requests.
254 ///
255 /// # Arguments
256 ///
257 /// * `uri` - The URI of the resource to read
258 /// * `ctx` - Request context with metadata
259 ///
260 /// # Returns
261 ///
262 /// The resource content or an error. Use `McpError::resource_not_found()`
263 /// for unknown resources.
264 fn read_resource<'a>(
265 &'a self,
266 uri: &'a str,
267 ctx: &'a RequestContext,
268 ) -> impl Future<Output = McpResult<ResourceResult>> + MaybeSend + 'a;
269
270 /// Gets a prompt by name with optional arguments.
271 ///
272 /// Called in response to `prompts/get` requests.
273 ///
274 /// # Arguments
275 ///
276 /// * `name` - The name of the prompt
277 /// * `args` - Optional JSON arguments for the prompt
278 /// * `ctx` - Request context with metadata
279 ///
280 /// # Returns
281 ///
282 /// The prompt messages or an error. Use `McpError::prompt_not_found()`
283 /// for unknown prompts.
284 fn get_prompt<'a>(
285 &'a self,
286 name: &'a str,
287 args: Option<Value>,
288 ctx: &'a RequestContext,
289 ) -> impl Future<Output = McpResult<PromptResult>> + MaybeSend + 'a;
290
291 // ===== Task Management (SEP-1686) =====
292
293 /// Lists all active and recent tasks.
294 ///
295 /// # Arguments
296 ///
297 /// * `cursor` - Opaque pagination cursor
298 /// * `limit` - Maximum number of tasks to return
299 /// * `ctx` - Request context
300 fn list_tasks<'a>(
301 &'a self,
302 _cursor: Option<&'a str>,
303 _limit: Option<usize>,
304 _ctx: &'a RequestContext,
305 ) -> impl Future<Output = McpResult<turbomcp_types::ListTasksResult>> + MaybeSend + 'a {
306 async {
307 Err(crate::error::McpError::capability_not_supported(
308 "tasks/list",
309 ))
310 }
311 }
312
313 /// Gets the current state of a specific task.
314 ///
315 /// # Arguments
316 ///
317 /// * `task_id` - Unique task identifier
318 /// * `ctx` - Request context
319 fn get_task<'a>(
320 &'a self,
321 _task_id: &'a str,
322 _ctx: &'a RequestContext,
323 ) -> impl Future<Output = McpResult<turbomcp_types::Task>> + MaybeSend + 'a {
324 async {
325 Err(crate::error::McpError::capability_not_supported(
326 "tasks/get",
327 ))
328 }
329 }
330
331 /// Cancels a running task.
332 ///
333 /// # Arguments
334 ///
335 /// * `task_id` - Unique task identifier
336 /// * `ctx` - Request context
337 fn cancel_task<'a>(
338 &'a self,
339 _task_id: &'a str,
340 _ctx: &'a RequestContext,
341 ) -> impl Future<Output = McpResult<turbomcp_types::Task>> + MaybeSend + 'a {
342 async {
343 Err(crate::error::McpError::capability_not_supported(
344 "tasks/cancel",
345 ))
346 }
347 }
348
349 /// Gets the result of a completed task.
350 ///
351 /// # Arguments
352 ///
353 /// * `task_id` - Unique task identifier
354 /// * `ctx` - Request context
355 fn get_task_result<'a>(
356 &'a self,
357 _task_id: &'a str,
358 _ctx: &'a RequestContext,
359 ) -> impl Future<Output = McpResult<Value>> + MaybeSend + 'a {
360 async {
361 Err(crate::error::McpError::capability_not_supported(
362 "tasks/result",
363 ))
364 }
365 }
366
367 // ===== Lifecycle Hooks =====
368
369 /// Called when the server is initialized.
370 ///
371 /// Override this to perform setup tasks like loading configuration,
372 /// establishing database connections, or warming caches.
373 ///
374 /// Default implementation does nothing.
375 fn on_initialize(&self) -> impl Future<Output = McpResult<()>> + MaybeSend {
376 async { Ok(()) }
377 }
378
379 /// Called when the server is shutting down.
380 ///
381 /// Override this to perform cleanup tasks like flushing buffers,
382 /// closing connections, or saving state.
383 ///
384 /// Default implementation does nothing.
385 fn on_shutdown(&self) -> impl Future<Output = McpResult<()>> + MaybeSend {
386 async { Ok(()) }
387 }
388}
389
390#[cfg(test)]
391mod tests {
392 use super::*;
393 use crate::error::McpError;
394
395 #[derive(Clone)]
396 struct TestHandler;
397
398 impl McpHandler for TestHandler {
399 fn server_info(&self) -> ServerInfo {
400 ServerInfo::new("test-handler", "1.0.0")
401 }
402
403 fn list_tools(&self) -> Vec<Tool> {
404 vec![Tool::new("greet", "Say hello")]
405 }
406
407 fn list_resources(&self) -> Vec<Resource> {
408 vec![]
409 }
410
411 fn list_prompts(&self) -> Vec<Prompt> {
412 vec![]
413 }
414
415 fn call_tool<'a>(
416 &'a self,
417 name: &'a str,
418 args: Value,
419 _ctx: &'a RequestContext,
420 ) -> impl Future<Output = McpResult<ToolResult>> + MaybeSend + 'a {
421 let name = name.to_string();
422 async move {
423 match name.as_str() {
424 "greet" => {
425 let who = args.get("name").and_then(|v| v.as_str()).unwrap_or("World");
426 Ok(ToolResult::text(format!("Hello, {}!", who)))
427 }
428 _ => Err(McpError::tool_not_found(&name)),
429 }
430 }
431 }
432
433 fn read_resource<'a>(
434 &'a self,
435 uri: &'a str,
436 _ctx: &'a RequestContext,
437 ) -> impl Future<Output = McpResult<ResourceResult>> + MaybeSend + 'a {
438 let uri = uri.to_string();
439 async move { Err(McpError::resource_not_found(&uri)) }
440 }
441
442 fn get_prompt<'a>(
443 &'a self,
444 name: &'a str,
445 _args: Option<Value>,
446 _ctx: &'a RequestContext,
447 ) -> impl Future<Output = McpResult<PromptResult>> + MaybeSend + 'a {
448 let name = name.to_string();
449 async move { Err(McpError::prompt_not_found(&name)) }
450 }
451 }
452
453 #[test]
454 fn test_server_info() {
455 let handler = TestHandler;
456 let info = handler.server_info();
457 assert_eq!(info.name, "test-handler");
458 assert_eq!(info.version, "1.0.0");
459 }
460
461 #[test]
462 fn test_list_tools() {
463 let handler = TestHandler;
464 let tools = handler.list_tools();
465 assert_eq!(tools.len(), 1);
466 assert_eq!(tools[0].name, "greet");
467 }
468
469 #[tokio::test]
470 async fn test_call_tool() {
471 let handler = TestHandler;
472 let ctx = RequestContext::stdio();
473 let args = serde_json::json!({"name": "Alice"});
474
475 let result = handler.call_tool("greet", args, &ctx).await.unwrap();
476 assert_eq!(result.first_text(), Some("Hello, Alice!"));
477 }
478
479 #[tokio::test]
480 async fn test_call_tool_not_found() {
481 let handler = TestHandler;
482 let ctx = RequestContext::stdio();
483 let args = serde_json::json!({});
484
485 let result = handler.call_tool("unknown", args, &ctx).await;
486 assert!(result.is_err());
487 }
488
489 #[tokio::test]
490 async fn test_lifecycle_hooks() {
491 let handler = TestHandler;
492 assert!(handler.on_initialize().await.is_ok());
493 assert!(handler.on_shutdown().await.is_ok());
494 }
495
496 // Verify that the trait object is Send + Sync on native
497 #[cfg(not(target_arch = "wasm32"))]
498 #[test]
499 fn test_handler_is_send_sync() {
500 fn assert_send_sync<T: Send + Sync>() {}
501 assert_send_sync::<TestHandler>();
502 }
503}