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}