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}