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