Skip to main content

turbomcp_server/
handler.rs

1//! Core handler trait for MCP servers.
2//!
3//! This module re-exports the unified `McpHandler` trait from `turbomcp-core`
4//! and provides the `McpHandlerExt` extension trait for native transport runners.
5//!
6//! # Unified Architecture
7//!
8//! The `McpHandler` trait is defined in `turbomcp-core` and works on both native
9//! and WASM targets. This module extends it with native-only transport methods.
10//!
11//! # Portable Code Pattern
12//!
13//! The TurboMCP architecture enables writing portable servers that work on both native
14//! and WASM without platform-specific code in your business logic:
15//!
16//! ```rust,ignore
17//! use turbomcp::prelude::*;
18//!
19//! #[derive(Clone)]
20//! struct Calculator;
21//!
22//! #[server(name = "calculator", version = "1.0.0")]
23//! impl Calculator {
24//!     /// Add two numbers together
25//!     #[tool]
26//!     async fn add(
27//!         &self,
28//!         #[description("First number")] a: i64,
29//!         #[description("Second number")] b: i64,
30//!     ) -> i64 {
31//!         a + b
32//!     }
33//! }
34//!
35//! // Native entry point (STDIO by default)
36//! #[cfg(not(target_arch = "wasm32"))]
37//! #[tokio::main]
38//! async fn main() {
39//!     Calculator.run().await.unwrap();
40//! }
41//!
42//! // WASM entry point (Cloudflare Workers)
43//! #[cfg(target_arch = "wasm32")]
44//! #[event(fetch)]
45//! async fn fetch(req: Request, _env: Env, _ctx: Context) -> Result<Response> {
46//!     Calculator.handle_worker_request(req).await
47//! }
48//! ```
49//!
50//! Note: The server implementation (Calculator) is identical - only the entry
51//! point differs per platform.
52//!
53//! # Transport Architecture
54//!
55//! Transport implementations are in the `transport` module:
56//! - `transport::stdio` - STDIO (line-based JSON-RPC)
57//! - `transport::tcp` - TCP sockets (line-based JSON-RPC)
58//! - `transport::unix` - Unix domain sockets (line-based JSON-RPC)
59//! - `transport::http` - HTTP POST (Axum-based JSON-RPC)
60//! - `transport::websocket` - WebSocket (Axum-based bidirectional)
61//!
62//! All line-based transports share the `LineTransportRunner` abstraction.
63//!
64//! # Default Entry Point
65//!
66//! For the simplest possible server, just use `.run()`:
67//!
68//! ```rust,ignore
69//! #[tokio::main]
70//! async fn main() {
71//!     MyServer.run().await.unwrap();
72//! }
73//! ```
74//!
75//! This uses STDIO transport, which is the MCP default and works with
76//! Claude Desktop and other MCP clients out of the box.
77
78use std::future::Future;
79
80use serde_json::Value;
81use turbomcp_core::error::{McpError, McpResult};
82
83// Re-export the unified McpHandler from core
84pub use turbomcp_core::handler::McpHandler;
85
86// Use the server's rich context for native transports
87use super::RequestContext;
88
89/// Extension trait for running McpHandler on various transports.
90///
91/// This trait provides simple, zero-config entry points for running MCP servers.
92/// For advanced configuration (rate limits, connection limits, etc.), use the
93/// builder pattern via `McpServerExt::builder()`.
94///
95/// # Design Philosophy
96///
97/// - **Simple**: `handler.run()` → runs with STDIO (Claude Desktop compatible)
98/// - **Direct transport**: `handler.run_http("...")` → specific transport, default config
99/// - **Configurable**: `handler.builder().transport(...).serve()` → full control
100///
101/// # Example
102///
103/// ```rust,ignore
104/// use turbomcp::prelude::*;
105///
106/// #[tokio::main]
107/// async fn main() {
108///     // Simplest: STDIO (default)
109///     MyServer.run().await?;
110///
111///     // Specific transport, default config
112///     MyServer.run_http("0.0.0.0:8080").await?;
113///
114///     // Full configuration via builder
115///     MyServer.builder()
116///         .transport(Transport::http("0.0.0.0:8080"))
117///         .with_rate_limit(100, Duration::from_secs(1))
118///         .serve()
119///         .await?;
120/// }
121/// ```
122pub trait McpHandlerExt: McpHandler {
123    /// Run with the default transport (STDIO).
124    ///
125    /// STDIO is the MCP standard transport, compatible with Claude Desktop
126    /// and other MCP clients. This is the recommended entry point for most servers.
127    #[cfg(feature = "stdio")]
128    fn run(&self) -> impl Future<Output = McpResult<()>> + Send;
129
130    /// Run on STDIO transport (explicit, equivalent to `run()`).
131    #[cfg(feature = "stdio")]
132    fn run_stdio(&self) -> impl Future<Output = McpResult<()>> + Send;
133
134    /// Run on HTTP transport (JSON-RPC over HTTP POST).
135    #[cfg(feature = "http")]
136    fn run_http(&self, addr: &str) -> impl Future<Output = McpResult<()>> + Send;
137
138    /// Run on WebSocket transport (bidirectional JSON-RPC).
139    #[cfg(feature = "websocket")]
140    fn run_websocket(&self, addr: &str) -> impl Future<Output = McpResult<()>> + Send;
141
142    /// Run on TCP transport (line-based JSON-RPC).
143    #[cfg(feature = "tcp")]
144    fn run_tcp(&self, addr: &str) -> impl Future<Output = McpResult<()>> + Send;
145
146    /// Run on Unix domain socket transport (line-based JSON-RPC).
147    #[cfg(feature = "unix")]
148    fn run_unix(&self, path: &str) -> impl Future<Output = McpResult<()>> + Send;
149
150    /// Handle a single JSON-RPC request (for serverless environments).
151    ///
152    /// Useful for AWS Lambda, Cloudflare Workers, and other serverless
153    /// environments where you process one request at a time.
154    fn handle_request(
155        &self,
156        request: Value,
157        ctx: RequestContext,
158    ) -> impl Future<Output = McpResult<Value>> + Send;
159}
160
161/// Blanket implementation of McpHandlerExt for all McpHandler types.
162///
163/// Each transport method delegates to the corresponding module in `super::transport`.
164impl<T: McpHandler> McpHandlerExt for T {
165    #[cfg(feature = "stdio")]
166    fn run(&self) -> impl Future<Output = McpResult<()>> + Send {
167        super::transport::stdio::run(self)
168    }
169
170    #[cfg(feature = "stdio")]
171    fn run_stdio(&self) -> impl Future<Output = McpResult<()>> + Send {
172        super::transport::stdio::run(self)
173    }
174
175    #[cfg(feature = "http")]
176    fn run_http(&self, addr: &str) -> impl Future<Output = McpResult<()>> + Send {
177        let addr = addr.to_string();
178        let handler = self.clone();
179        async move { super::transport::http::run(&handler, &addr).await }
180    }
181
182    #[cfg(feature = "websocket")]
183    fn run_websocket(&self, addr: &str) -> impl Future<Output = McpResult<()>> + Send {
184        let addr = addr.to_string();
185        let handler = self.clone();
186        async move { super::transport::websocket::run(&handler, &addr).await }
187    }
188
189    #[cfg(feature = "tcp")]
190    fn run_tcp(&self, addr: &str) -> impl Future<Output = McpResult<()>> + Send {
191        let addr = addr.to_string();
192        let handler = self.clone();
193        async move { super::transport::tcp::run(&handler, &addr).await }
194    }
195
196    #[cfg(feature = "unix")]
197    fn run_unix(&self, path: &str) -> impl Future<Output = McpResult<()>> + Send {
198        let path = path.to_string();
199        let handler = self.clone();
200        async move { super::transport::unix::run(&handler, &path).await }
201    }
202
203    fn handle_request(
204        &self,
205        request: Value,
206        ctx: RequestContext,
207    ) -> impl Future<Output = McpResult<Value>> + Send {
208        let handler = self.clone();
209        async move {
210            let request_str = serde_json::to_string(&request)
211                .map_err(|e| McpError::internal(format!("Failed to serialize request: {e}")))?;
212
213            let parsed = super::router::parse_request(&request_str)?;
214            let core_ctx = ctx.to_core_context();
215            let response = super::router::route_request(&handler, parsed, &core_ctx).await;
216
217            serde_json::to_value(&response)
218                .map_err(|e| McpError::internal(format!("Failed to serialize response: {e}")))
219        }
220    }
221}
222
223#[cfg(test)]
224mod tests {
225    use super::*;
226    use turbomcp_core::context::RequestContext as CoreRequestContext;
227    use turbomcp_types::{
228        Prompt, PromptResult, Resource, ResourceResult, ServerInfo, Tool, ToolResult,
229    };
230
231    #[derive(Clone)]
232    struct TestHandler;
233
234    impl McpHandler for TestHandler {
235        fn server_info(&self) -> ServerInfo {
236            ServerInfo::new("test", "1.0.0")
237        }
238
239        fn list_tools(&self) -> Vec<Tool> {
240            vec![Tool::new("test_tool", "A test tool")]
241        }
242
243        fn list_resources(&self) -> Vec<Resource> {
244            vec![]
245        }
246
247        fn list_prompts(&self) -> Vec<Prompt> {
248            vec![]
249        }
250
251        fn call_tool<'a>(
252            &'a self,
253            name: &'a str,
254            _args: Value,
255            _ctx: &'a CoreRequestContext,
256        ) -> impl std::future::Future<Output = McpResult<ToolResult>> + Send + 'a {
257            let name = name.to_string();
258            async move {
259                if name == "test_tool" {
260                    Ok(ToolResult::text("Tool executed"))
261                } else {
262                    Err(McpError::tool_not_found(&name))
263                }
264            }
265        }
266
267        fn read_resource<'a>(
268            &'a self,
269            uri: &'a str,
270            _ctx: &'a CoreRequestContext,
271        ) -> impl std::future::Future<Output = McpResult<ResourceResult>> + Send + 'a {
272            let uri = uri.to_string();
273            async move { Err(McpError::resource_not_found(&uri)) }
274        }
275
276        fn get_prompt<'a>(
277            &'a self,
278            name: &'a str,
279            _args: Option<Value>,
280            _ctx: &'a CoreRequestContext,
281        ) -> impl std::future::Future<Output = McpResult<PromptResult>> + Send + 'a {
282            let name = name.to_string();
283            async move { Err(McpError::prompt_not_found(&name)) }
284        }
285    }
286
287    #[tokio::test]
288    async fn test_handle_request() {
289        let handler = TestHandler;
290        let ctx = RequestContext::stdio();
291
292        let request = serde_json::json!({
293            "jsonrpc": "2.0",
294            "id": 1,
295            "method": "ping"
296        });
297
298        let response = handler.handle_request(request, ctx).await.unwrap();
299        assert!(response.get("result").is_some());
300    }
301
302    #[tokio::test]
303    async fn test_handle_request_tools_list() {
304        let handler = TestHandler;
305        let ctx = RequestContext::stdio();
306
307        let request = serde_json::json!({
308            "jsonrpc": "2.0",
309            "id": 1,
310            "method": "tools/list"
311        });
312
313        let response = handler.handle_request(request, ctx).await.unwrap();
314        let result = response.get("result").unwrap();
315        let tools = result.get("tools").unwrap().as_array().unwrap();
316        assert_eq!(tools.len(), 1);
317        assert_eq!(tools[0]["name"], "test_tool");
318    }
319}