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}