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, PromptsCapabilities, Resource, ResourceResult, ResourcesCapabilities,
59    ServerCapabilities, ServerInfo, Tool, ToolResult, ToolsCapabilities,
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(ToolsCapabilities {
221                list_changed: Some(true),
222            });
223        }
224
225        if !self.list_resources().is_empty() {
226            capabilities.resources = Some(ResourcesCapabilities {
227                subscribe: None,
228                list_changed: Some(true),
229            });
230        }
231
232        if !self.list_prompts().is_empty() {
233            capabilities.prompts = Some(PromptsCapabilities {
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    // ===== Resource subscriptions (MCP 2025-11-25) =====
399
400    /// Subscribes to update notifications for the given resource URI.
401    ///
402    /// Called in response to `resources/subscribe` requests. The default
403    /// implementation returns `capability_not_supported`. Servers that
404    /// advertise `resources.subscribe = true` MUST override this method —
405    /// the router calls it whenever a client invokes `resources/subscribe`.
406    ///
407    /// # Arguments
408    ///
409    /// * `uri` - The URI being subscribed to.
410    /// * `ctx` - Request context.
411    fn subscribe<'a>(
412        &'a self,
413        _uri: &'a str,
414        _ctx: &'a RequestContext,
415    ) -> impl Future<Output = McpResult<()>> + MaybeSend + 'a {
416        async {
417            Err(crate::error::McpError::capability_not_supported(
418                "resources/subscribe",
419            ))
420        }
421    }
422
423    /// Cancels a previously installed resource subscription.
424    ///
425    /// Default implementation returns `capability_not_supported`.
426    fn unsubscribe<'a>(
427        &'a self,
428        _uri: &'a str,
429        _ctx: &'a RequestContext,
430    ) -> impl Future<Output = McpResult<()>> + MaybeSend + 'a {
431        async {
432            Err(crate::error::McpError::capability_not_supported(
433                "resources/unsubscribe",
434            ))
435        }
436    }
437
438    // ===== Logging (MCP 2025-11-25) =====
439
440    /// Sets the minimum log level the server should emit via
441    /// `notifications/message`.
442    ///
443    /// Called in response to `logging/setLevel`. The level is the raw spec
444    /// string (`"debug" | "info" | "notice" | "warning" | "error" |
445    /// "critical" | "alert" | "emergency"`). The default returns
446    /// `capability_not_supported`; servers advertising the `logging`
447    /// capability must override and persist the level for use by their
448    /// `LoggingNotification`-emitting code.
449    fn set_log_level<'a>(
450        &'a self,
451        _level: &'a str,
452        _ctx: &'a RequestContext,
453    ) -> impl Future<Output = McpResult<()>> + MaybeSend + 'a {
454        async {
455            Err(crate::error::McpError::capability_not_supported(
456                "logging/setLevel",
457            ))
458        }
459    }
460
461    // ===== Completions (MCP 2025-11-25) =====
462
463    /// Returns argument completion suggestions.
464    ///
465    /// Called in response to `completion/complete`. `params` is the raw
466    /// JSON-RPC `params` object (i.e. the `CompleteRequestParams` shape:
467    /// `{ ref: …, argument: { name, value }, context?: { arguments } }`).
468    /// The return value is the raw `CompleteResult` shape (`{ completion:
469    /// { values, total?, hasMore? }, _meta? }`).
470    ///
471    /// We accept and return `serde_json::Value` here because the typed
472    /// `CompleteRequestParams` / `CompleteResult` live in `turbomcp-protocol`
473    /// (which depends on this crate, so we cannot depend on it here without
474    /// inverting the layer cake). Higher-level wrappers in `turbomcp` /
475    /// `#[server]` may expose typed signatures over this raw shape.
476    fn complete<'a>(
477        &'a self,
478        _params: Value,
479        _ctx: &'a RequestContext,
480    ) -> impl Future<Output = McpResult<Value>> + MaybeSend + 'a {
481        async {
482            Err(crate::error::McpError::capability_not_supported(
483                "completion/complete",
484            ))
485        }
486    }
487
488    // ===== Lifecycle Hooks =====
489
490    /// Called when the server is initialized.
491    ///
492    /// Override this to perform setup tasks like loading configuration,
493    /// establishing database connections, or warming caches.
494    ///
495    /// Default implementation does nothing.
496    fn on_initialize(&self) -> impl Future<Output = McpResult<()>> + MaybeSend {
497        async { Ok(()) }
498    }
499
500    /// Called when the server is shutting down.
501    ///
502    /// Override this to perform cleanup tasks like flushing buffers,
503    /// closing connections, or saving state.
504    ///
505    /// Default implementation does nothing.
506    fn on_shutdown(&self) -> impl Future<Output = McpResult<()>> + MaybeSend {
507        async { Ok(()) }
508    }
509}
510
511#[cfg(test)]
512mod tests {
513    use super::*;
514    use crate::error::McpError;
515
516    #[derive(Clone)]
517    struct TestHandler;
518
519    impl McpHandler for TestHandler {
520        fn server_info(&self) -> ServerInfo {
521            ServerInfo::new("test-handler", "1.0.0")
522        }
523
524        fn list_tools(&self) -> Vec<Tool> {
525            vec![Tool::new("greet", "Say hello")]
526        }
527
528        fn list_resources(&self) -> Vec<Resource> {
529            vec![]
530        }
531
532        fn list_prompts(&self) -> Vec<Prompt> {
533            vec![]
534        }
535
536        fn call_tool<'a>(
537            &'a self,
538            name: &'a str,
539            args: Value,
540            _ctx: &'a RequestContext,
541        ) -> impl Future<Output = McpResult<ToolResult>> + MaybeSend + 'a {
542            let name = name.to_string();
543            async move {
544                match name.as_str() {
545                    "greet" => {
546                        let who = args.get("name").and_then(|v| v.as_str()).unwrap_or("World");
547                        Ok(ToolResult::text(format!("Hello, {}!", who)))
548                    }
549                    _ => Err(McpError::tool_not_found(&name)),
550                }
551            }
552        }
553
554        fn read_resource<'a>(
555            &'a self,
556            uri: &'a str,
557            _ctx: &'a RequestContext,
558        ) -> impl Future<Output = McpResult<ResourceResult>> + MaybeSend + 'a {
559            let uri = uri.to_string();
560            async move { Err(McpError::resource_not_found(&uri)) }
561        }
562
563        fn get_prompt<'a>(
564            &'a self,
565            name: &'a str,
566            _args: Option<Value>,
567            _ctx: &'a RequestContext,
568        ) -> impl Future<Output = McpResult<PromptResult>> + MaybeSend + 'a {
569            let name = name.to_string();
570            async move { Err(McpError::prompt_not_found(&name)) }
571        }
572    }
573
574    #[test]
575    fn test_server_info() {
576        let handler = TestHandler;
577        let info = handler.server_info();
578        assert_eq!(info.name, "test-handler");
579        assert_eq!(info.version, "1.0.0");
580    }
581
582    #[test]
583    fn test_list_tools() {
584        let handler = TestHandler;
585        let tools = handler.list_tools();
586        assert_eq!(tools.len(), 1);
587        assert_eq!(tools[0].name, "greet");
588    }
589
590    #[tokio::test]
591    async fn test_call_tool() {
592        let handler = TestHandler;
593        let ctx = RequestContext::stdio();
594        let args = serde_json::json!({"name": "Alice"});
595
596        let result = handler.call_tool("greet", args, &ctx).await.unwrap();
597        assert_eq!(result.first_text(), Some("Hello, Alice!"));
598    }
599
600    #[tokio::test]
601    async fn test_call_tool_not_found() {
602        let handler = TestHandler;
603        let ctx = RequestContext::stdio();
604        let args = serde_json::json!({});
605
606        let result = handler.call_tool("unknown", args, &ctx).await;
607        assert!(result.is_err());
608    }
609
610    #[tokio::test]
611    async fn test_lifecycle_hooks() {
612        let handler = TestHandler;
613        assert!(handler.on_initialize().await.is_ok());
614        assert!(handler.on_shutdown().await.is_ok());
615    }
616
617    // Verify that the trait object is Send + Sync on native
618    #[cfg(not(target_arch = "wasm32"))]
619    #[test]
620    fn test_handler_is_send_sync() {
621        fn assert_send_sync<T: Send + Sync>() {}
622        assert_send_sync::<TestHandler>();
623    }
624}