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, ResourceTemplate,
59    ResourcesCapabilities, 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() || !self.list_resource_templates().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 resource URI templates.
255    ///
256    /// Called in response to `resources/templates/list` requests. Servers with
257    /// dynamic resources should return URI templates here rather than exposing
258    /// templated strings as concrete `resources/list` entries.
259    fn list_resource_templates(&self) -> Vec<ResourceTemplate> {
260        Vec::new()
261    }
262
263    /// Returns all available prompts.
264    ///
265    /// Called in response to `prompts/list` requests.
266    fn list_prompts(&self) -> Vec<Prompt>;
267
268    // ===== Request Handlers =====
269
270    /// Calls a tool by name with the given arguments.
271    ///
272    /// Called in response to `tools/call` requests.
273    ///
274    /// # Arguments
275    ///
276    /// * `name` - The name of the tool to call
277    /// * `args` - JSON arguments for the tool
278    /// * `ctx` - Request context with metadata
279    ///
280    /// # Returns
281    ///
282    /// The tool result or an error. Use `McpError::tool_not_found()`
283    /// for unknown tools.
284    fn call_tool<'a>(
285        &'a self,
286        name: &'a str,
287        args: Value,
288        ctx: &'a RequestContext,
289    ) -> impl Future<Output = McpResult<ToolResult>> + MaybeSend + 'a;
290
291    /// Reads a resource by URI.
292    ///
293    /// Called in response to `resources/read` requests.
294    ///
295    /// # Arguments
296    ///
297    /// * `uri` - The URI of the resource to read
298    /// * `ctx` - Request context with metadata
299    ///
300    /// # Returns
301    ///
302    /// The resource content or an error. Use `McpError::resource_not_found()`
303    /// for unknown resources.
304    fn read_resource<'a>(
305        &'a self,
306        uri: &'a str,
307        ctx: &'a RequestContext,
308    ) -> impl Future<Output = McpResult<ResourceResult>> + MaybeSend + 'a;
309
310    /// Gets a prompt by name with optional arguments.
311    ///
312    /// Called in response to `prompts/get` requests.
313    ///
314    /// # Arguments
315    ///
316    /// * `name` - The name of the prompt
317    /// * `args` - Optional JSON arguments for the prompt
318    /// * `ctx` - Request context with metadata
319    ///
320    /// # Returns
321    ///
322    /// The prompt messages or an error. Use `McpError::prompt_not_found()`
323    /// for unknown prompts.
324    fn get_prompt<'a>(
325        &'a self,
326        name: &'a str,
327        args: Option<Value>,
328        ctx: &'a RequestContext,
329    ) -> impl Future<Output = McpResult<PromptResult>> + MaybeSend + 'a;
330
331    // ===== Task Management (SEP-1686) =====
332
333    /// Lists all active and recent tasks.
334    ///
335    /// # Arguments
336    ///
337    /// * `cursor` - Opaque pagination cursor
338    /// * `limit` - Maximum number of tasks to return
339    /// * `ctx` - Request context
340    fn list_tasks<'a>(
341        &'a self,
342        _cursor: Option<&'a str>,
343        _limit: Option<usize>,
344        _ctx: &'a RequestContext,
345    ) -> impl Future<Output = McpResult<turbomcp_types::ListTasksResult>> + MaybeSend + 'a {
346        async {
347            Err(crate::error::McpError::capability_not_supported(
348                "tasks/list",
349            ))
350        }
351    }
352
353    /// Gets the current state of a specific task.
354    ///
355    /// # Arguments
356    ///
357    /// * `task_id` - Unique task identifier
358    /// * `ctx` - Request context
359    fn get_task<'a>(
360        &'a self,
361        _task_id: &'a str,
362        _ctx: &'a RequestContext,
363    ) -> impl Future<Output = McpResult<turbomcp_types::Task>> + MaybeSend + 'a {
364        async {
365            Err(crate::error::McpError::capability_not_supported(
366                "tasks/get",
367            ))
368        }
369    }
370
371    /// Cancels a running task.
372    ///
373    /// # Arguments
374    ///
375    /// * `task_id` - Unique task identifier
376    /// * `ctx` - Request context
377    fn cancel_task<'a>(
378        &'a self,
379        _task_id: &'a str,
380        _ctx: &'a RequestContext,
381    ) -> impl Future<Output = McpResult<turbomcp_types::Task>> + MaybeSend + 'a {
382        async {
383            Err(crate::error::McpError::capability_not_supported(
384                "tasks/cancel",
385            ))
386        }
387    }
388
389    /// Gets the result of a completed task.
390    ///
391    /// # Arguments
392    ///
393    /// * `task_id` - Unique task identifier
394    /// * `ctx` - Request context
395    fn get_task_result<'a>(
396        &'a self,
397        _task_id: &'a str,
398        _ctx: &'a RequestContext,
399    ) -> impl Future<Output = McpResult<Value>> + MaybeSend + 'a {
400        async {
401            Err(crate::error::McpError::capability_not_supported(
402                "tasks/result",
403            ))
404        }
405    }
406
407    // ===== Resource subscriptions (MCP 2025-11-25) =====
408
409    /// Subscribes to update notifications for the given resource URI.
410    ///
411    /// Called in response to `resources/subscribe` requests. The default
412    /// implementation returns `capability_not_supported`. Servers that
413    /// advertise `resources.subscribe = true` MUST override this method —
414    /// the router calls it whenever a client invokes `resources/subscribe`.
415    ///
416    /// # Arguments
417    ///
418    /// * `uri` - The URI being subscribed to.
419    /// * `ctx` - Request context.
420    fn subscribe<'a>(
421        &'a self,
422        _uri: &'a str,
423        _ctx: &'a RequestContext,
424    ) -> impl Future<Output = McpResult<()>> + MaybeSend + 'a {
425        async {
426            Err(crate::error::McpError::capability_not_supported(
427                "resources/subscribe",
428            ))
429        }
430    }
431
432    /// Cancels a previously installed resource subscription.
433    ///
434    /// Default implementation returns `capability_not_supported`.
435    fn unsubscribe<'a>(
436        &'a self,
437        _uri: &'a str,
438        _ctx: &'a RequestContext,
439    ) -> impl Future<Output = McpResult<()>> + MaybeSend + 'a {
440        async {
441            Err(crate::error::McpError::capability_not_supported(
442                "resources/unsubscribe",
443            ))
444        }
445    }
446
447    // ===== Logging (MCP 2025-11-25) =====
448
449    /// Sets the minimum log level the server should emit via
450    /// `notifications/message`.
451    ///
452    /// Called in response to `logging/setLevel`. The level is the raw spec
453    /// string (`"debug" | "info" | "notice" | "warning" | "error" |
454    /// "critical" | "alert" | "emergency"`). The default returns
455    /// `capability_not_supported`; servers advertising the `logging`
456    /// capability must override and persist the level for use by their
457    /// `LoggingNotification`-emitting code.
458    fn set_log_level<'a>(
459        &'a self,
460        _level: &'a str,
461        _ctx: &'a RequestContext,
462    ) -> impl Future<Output = McpResult<()>> + MaybeSend + 'a {
463        async {
464            Err(crate::error::McpError::capability_not_supported(
465                "logging/setLevel",
466            ))
467        }
468    }
469
470    // ===== Completions (MCP 2025-11-25) =====
471
472    /// Returns argument completion suggestions.
473    ///
474    /// Called in response to `completion/complete`. `params` is the raw
475    /// JSON-RPC `params` object (i.e. the `CompleteRequestParams` shape:
476    /// `{ ref: …, argument: { name, value }, context?: { arguments } }`).
477    /// The return value is the raw `CompleteResult` shape (`{ completion:
478    /// { values, total?, hasMore? }, _meta? }`).
479    ///
480    /// We accept and return `serde_json::Value` here because the typed
481    /// `CompleteRequestParams` / `CompleteResult` live in `turbomcp-protocol`
482    /// (which depends on this crate, so we cannot depend on it here without
483    /// inverting the layer cake). Higher-level wrappers in `turbomcp` /
484    /// `#[server]` may expose typed signatures over this raw shape.
485    fn complete<'a>(
486        &'a self,
487        _params: Value,
488        _ctx: &'a RequestContext,
489    ) -> impl Future<Output = McpResult<Value>> + MaybeSend + 'a {
490        async {
491            Err(crate::error::McpError::capability_not_supported(
492                "completion/complete",
493            ))
494        }
495    }
496
497    // ===== Lifecycle Hooks =====
498
499    /// Called when the server is initialized.
500    ///
501    /// Override this to perform setup tasks like loading configuration,
502    /// establishing database connections, or warming caches.
503    ///
504    /// Default implementation does nothing.
505    fn on_initialize(&self) -> impl Future<Output = McpResult<()>> + MaybeSend {
506        async { Ok(()) }
507    }
508
509    /// Called when the server is shutting down.
510    ///
511    /// Override this to perform cleanup tasks like flushing buffers,
512    /// closing connections, or saving state.
513    ///
514    /// Default implementation does nothing.
515    fn on_shutdown(&self) -> impl Future<Output = McpResult<()>> + MaybeSend {
516        async { Ok(()) }
517    }
518}
519
520#[cfg(test)]
521mod tests {
522    use super::*;
523    use crate::error::McpError;
524
525    #[derive(Clone)]
526    struct TestHandler;
527
528    impl McpHandler for TestHandler {
529        fn server_info(&self) -> ServerInfo {
530            ServerInfo::new("test-handler", "1.0.0")
531        }
532
533        fn list_tools(&self) -> Vec<Tool> {
534            vec![Tool::new("greet", "Say hello")]
535        }
536
537        fn list_resources(&self) -> Vec<Resource> {
538            vec![]
539        }
540
541        fn list_prompts(&self) -> Vec<Prompt> {
542            vec![]
543        }
544
545        fn call_tool<'a>(
546            &'a self,
547            name: &'a str,
548            args: Value,
549            _ctx: &'a RequestContext,
550        ) -> impl Future<Output = McpResult<ToolResult>> + MaybeSend + 'a {
551            let name = name.to_string();
552            async move {
553                match name.as_str() {
554                    "greet" => {
555                        let who = args.get("name").and_then(|v| v.as_str()).unwrap_or("World");
556                        Ok(ToolResult::text(format!("Hello, {}!", who)))
557                    }
558                    _ => Err(McpError::tool_not_found(&name)),
559                }
560            }
561        }
562
563        fn read_resource<'a>(
564            &'a self,
565            uri: &'a str,
566            _ctx: &'a RequestContext,
567        ) -> impl Future<Output = McpResult<ResourceResult>> + MaybeSend + 'a {
568            let uri = uri.to_string();
569            async move { Err(McpError::resource_not_found(&uri)) }
570        }
571
572        fn get_prompt<'a>(
573            &'a self,
574            name: &'a str,
575            _args: Option<Value>,
576            _ctx: &'a RequestContext,
577        ) -> impl Future<Output = McpResult<PromptResult>> + MaybeSend + 'a {
578            let name = name.to_string();
579            async move { Err(McpError::prompt_not_found(&name)) }
580        }
581    }
582
583    #[test]
584    fn test_server_info() {
585        let handler = TestHandler;
586        let info = handler.server_info();
587        assert_eq!(info.name, "test-handler");
588        assert_eq!(info.version, "1.0.0");
589    }
590
591    #[test]
592    fn test_list_tools() {
593        let handler = TestHandler;
594        let tools = handler.list_tools();
595        assert_eq!(tools.len(), 1);
596        assert_eq!(tools[0].name, "greet");
597    }
598
599    #[tokio::test]
600    async fn test_call_tool() {
601        let handler = TestHandler;
602        let ctx = RequestContext::stdio();
603        let args = serde_json::json!({"name": "Alice"});
604
605        let result = handler.call_tool("greet", args, &ctx).await.unwrap();
606        assert_eq!(result.first_text(), Some("Hello, Alice!"));
607    }
608
609    #[tokio::test]
610    async fn test_call_tool_not_found() {
611        let handler = TestHandler;
612        let ctx = RequestContext::stdio();
613        let args = serde_json::json!({});
614
615        let result = handler.call_tool("unknown", args, &ctx).await;
616        assert!(result.is_err());
617    }
618
619    #[tokio::test]
620    async fn test_lifecycle_hooks() {
621        let handler = TestHandler;
622        assert!(handler.on_initialize().await.is_ok());
623        assert!(handler.on_shutdown().await.is_ok());
624    }
625
626    // Verify that the trait object is Send + Sync on native
627    #[cfg(not(target_arch = "wasm32"))]
628    #[test]
629    fn test_handler_is_send_sync() {
630        fn assert_send_sync<T: Send + Sync>() {}
631        assert_send_sync::<TestHandler>();
632    }
633}