Skip to main content

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}