Skip to main content

mcp/
http.rs

1//! HTTP Server Adapter for MCP
2//!
3//! This module defines a pluggable interface for HTTP servers that expose MCP protocols.
4//! The adapter pattern allows different HTTP frameworks (axum, actix, warp, etc.) to be
5//! swapped without changing the MCPServerBuilder API.
6//!
7//! # Design
8//!
9//! ```text
10//! MCPServerBuilder
11//!        ↓
12//!   (configures)
13//!        ↓
14//! HttpServerAdapter (trait)
15//!        ↓ (implements)
16//!   ┌────┴────┬─────────────┐
17//!   ↓         ↓             ↓
18//! AxumAdapter ActixAdapter OtherAdapter
19//! ```
20//!
21//! This allows users to swap HTTP frameworks without changing their code.
22
23use crate::builder_utils::IpFilter;
24use crate::events::McpEventHandler;
25use crate::protocol::ToolProtocol;
26use std::error::Error;
27use std::net::SocketAddr;
28use std::sync::Arc;
29
30#[cfg(feature = "server")]
31use axum::Router;
32
33/// Configuration for an HTTP MCP server
34pub struct HttpServerConfig {
35    /// Socket address to bind to (e.g., "127.0.0.1:8080")
36    pub addr: SocketAddr,
37    /// Optional bearer token for authentication
38    pub bearer_token: Option<String>,
39    /// IP filter controlling which client addresses are allowed
40    pub ip_filter: IpFilter,
41    /// Optional event handler for MCP server lifecycle and request events
42    pub event_handler: Option<Arc<dyn McpEventHandler>>,
43}
44
45impl Clone for HttpServerConfig {
46    fn clone(&self) -> Self {
47        Self {
48            addr: self.addr,
49            bearer_token: self.bearer_token.clone(),
50            ip_filter: self.ip_filter.clone(),
51            event_handler: self.event_handler.clone(),
52        }
53    }
54}
55
56impl std::fmt::Debug for HttpServerConfig {
57    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
58        f.debug_struct("HttpServerConfig")
59            .field("addr", &self.addr)
60            .field("bearer_token", &self.bearer_token)
61            .field("ip_filter", &self.ip_filter)
62            .field("has_event_handler", &self.event_handler.is_some())
63            .finish()
64    }
65}
66
67/// A running HTTP server instance
68pub struct HttpServerInstance {
69    /// Socket address the server is listening on
70    pub addr: SocketAddr,
71    /// Handle for shutting down the server
72    /// Type erased to allow different framework implementations
73    shutdown_handle: Box<dyn std::any::Any + Send + Sync>,
74}
75
76impl HttpServerInstance {
77    /// Create a new server instance with the given address and shutdown handle
78    pub fn new(addr: SocketAddr, shutdown_handle: Box<dyn std::any::Any + Send + Sync>) -> Self {
79        Self {
80            addr,
81            shutdown_handle,
82        }
83    }
84
85    /// Get the server's socket address
86    pub fn get_addr(&self) -> SocketAddr {
87        self.addr
88    }
89
90    /// Get mutable reference to the shutdown handle for advanced usage
91    pub fn shutdown_handle_mut(&mut self) -> &mut Box<dyn std::any::Any + Send + Sync> {
92        &mut self.shutdown_handle
93    }
94}
95
96/// Trait for HTTP server implementations
97///
98/// Implementations of this trait provide HTTP endpoints for MCP protocols.
99/// Different HTTP frameworks can be swapped by implementing this trait.
100#[async_trait::async_trait]
101pub trait HttpServerAdapter: Send + Sync {
102    /// Start the HTTP server with the given configuration and tool protocol
103    ///
104    /// # Arguments
105    ///
106    /// * `config` - Server configuration (address, auth, IP filtering)
107    /// * `protocol` - The ToolProtocol implementation to expose
108    ///
109    /// # Endpoints
110    ///
111    /// The server must provide the following endpoints:
112    /// - `POST /tools/list` - List all available tools from the protocol
113    /// - `POST /tools/execute` - Execute a tool with given parameters
114    /// - `POST /resources/list` - List all available resources (if protocol supports)
115    /// - `POST /resources/read` - Read a resource by URI (if protocol supports)
116    ///
117    /// # Returns
118    ///
119    /// A running server instance, or an error if startup fails
120    async fn start(
121        &self,
122        config: HttpServerConfig,
123        protocol: Arc<dyn ToolProtocol>,
124    ) -> Result<HttpServerInstance, Box<dyn Error + Send + Sync>>;
125
126    /// Get the name of this adapter (for logging/debugging)
127    fn name(&self) -> &str {
128        "unknown"
129    }
130}
131
132/// Build an Axum router that exposes a [`ToolProtocol`] over the shared HTTP MCP surface.
133///
134/// The returned router serves:
135/// - `POST /tools/list`
136/// - `POST /tools/execute`
137/// - `POST /resources/list`
138/// - `POST /resources/read`
139///
140/// This helper is useful when a crate wants to reuse the shared MCP transport
141/// but still compose extra routes of its own, such as a `/health` endpoint.
142#[cfg(feature = "server")]
143pub fn axum_router(config: &HttpServerConfig, protocol: Arc<dyn ToolProtocol>) -> Router {
144    use crate::events::McpEvent;
145    use axum::{
146        extract::ConnectInfo, http::HeaderMap, http::StatusCode, response::IntoResponse,
147        routing::post, Json, Router,
148    };
149    use serde_json::json;
150    use sha2::{Digest, Sha256};
151    use subtle::ConstantTimeEq;
152
153    /// Validate the Authorization header against the configured bearer token.
154    ///
155    /// Returns `true` when no token is configured (open server) or when the
156    /// provided `Bearer <token>` matches the expected token. Uses
157    /// `subtle::ConstantTimeEq` on SHA-256 digests so the compiler cannot
158    /// short-circuit the comparison and leak token length via timing.
159    fn check_auth(expected_token: &Option<String>, headers: &HeaderMap) -> bool {
160        match expected_token.as_deref() {
161            None => true,
162            Some(expected) => {
163                let provided = headers
164                    .get("Authorization")
165                    .and_then(|v| v.to_str().ok())
166                    .and_then(|v| v.strip_prefix("Bearer "))
167                    .unwrap_or("");
168                let expected_hash = Sha256::digest(expected.as_bytes());
169                let provided_hash = Sha256::digest(provided.as_bytes());
170                expected_hash.ct_eq(&provided_hash).into()
171            }
172        }
173    }
174
175    let bearer_token = Arc::new(config.bearer_token.clone());
176    let ip_filter = Arc::new(config.ip_filter.clone());
177
178    let token_list = bearer_token.clone();
179    let ips_list = ip_filter.clone();
180    let token_exec = bearer_token.clone();
181    let ips_exec = ip_filter.clone();
182    let token_res_list = bearer_token.clone();
183    let ips_res_list = ip_filter.clone();
184    let token_res_read = bearer_token.clone();
185    let ips_res_read = ip_filter.clone();
186
187    let eh_list = config.event_handler.clone();
188    let eh_exec = config.event_handler.clone();
189
190    let protocol_list = protocol.clone();
191    let protocol_exec = protocol.clone();
192    let protocol_res_list = protocol.clone();
193    let protocol_res_read = protocol.clone();
194
195    Router::new()
196        .route(
197            "/tools/list",
198            post(
199                move |ConnectInfo(addr): ConnectInfo<SocketAddr>, headers: HeaderMap| {
200                    let token = token_list.clone();
201                    let allowed = ips_list.clone();
202                    let proto = protocol_list.clone();
203                    let eh = eh_list.clone();
204                    async move {
205                        if !allowed.is_allowed(addr.ip()) {
206                            if let Some(ref handler) = eh {
207                                handler
208                                    .on_mcp_event(&McpEvent::RequestRejected {
209                                        client_addr: addr.ip().to_string(),
210                                        reason: "IP not allowed".to_string(),
211                                    })
212                                    .await;
213                            }
214                            return (
215                                StatusCode::FORBIDDEN,
216                                Json(json!({"error": "Access denied"})),
217                            )
218                                .into_response();
219                        }
220
221                        if !check_auth(&token, &headers) {
222                            return (
223                                StatusCode::UNAUTHORIZED,
224                                Json(json!({"error": "Unauthorized"})),
225                            )
226                                .into_response();
227                        }
228
229                        if let Some(ref handler) = eh {
230                            handler
231                                .on_mcp_event(&McpEvent::ToolListRequested {
232                                    client_addr: addr.ip().to_string(),
233                                })
234                                .await;
235                        }
236
237                        match proto.list_tools().await {
238                            Ok(tools) => {
239                                let tool_count = tools.len();
240                                if let Some(ref handler) = eh {
241                                    handler
242                                        .on_mcp_event(&McpEvent::ToolListReturned {
243                                            client_addr: addr.ip().to_string(),
244                                            tool_count,
245                                        })
246                                        .await;
247                                }
248                                (StatusCode::OK, Json(json!({"tools": tools}))).into_response()
249                            }
250                            Err(e) => (
251                                StatusCode::INTERNAL_SERVER_ERROR,
252                                Json(json!({"error": e.to_string()})),
253                            )
254                                .into_response(),
255                        }
256                    }
257                },
258            ),
259        )
260        .route(
261            "/tools/execute",
262            post(
263                move |ConnectInfo(addr): ConnectInfo<SocketAddr>,
264                      headers: HeaderMap,
265                      Json(payload): Json<serde_json::Value>| {
266                    let token = token_exec.clone();
267                    let allowed = ips_exec.clone();
268                    let proto = protocol_exec.clone();
269                    let eh = eh_exec.clone();
270                    async move {
271                        if !allowed.is_allowed(addr.ip()) {
272                            if let Some(ref handler) = eh {
273                                handler
274                                    .on_mcp_event(&McpEvent::RequestRejected {
275                                        client_addr: addr.ip().to_string(),
276                                        reason: "IP not allowed".to_string(),
277                                    })
278                                    .await;
279                            }
280                            return (
281                                StatusCode::FORBIDDEN,
282                                Json(json!({"error": "Access denied"})),
283                            )
284                                .into_response();
285                        }
286
287                        if !check_auth(&token, &headers) {
288                            return (
289                                StatusCode::UNAUTHORIZED,
290                                Json(json!({"error": "Unauthorized"})),
291                            )
292                                .into_response();
293                        }
294
295                        let tool_name = payload["tool"].as_str().unwrap_or("").to_string();
296                        let params = payload["parameters"].clone();
297
298                        if let Some(ref handler) = eh {
299                            handler
300                                .on_mcp_event(&McpEvent::ToolCallReceived {
301                                    client_addr: addr.ip().to_string(),
302                                    tool_name: tool_name.clone(),
303                                    parameters: params.clone(),
304                                })
305                                .await;
306                        }
307
308                        let exec_start = std::time::Instant::now();
309                        match proto.execute(&tool_name, params).await {
310                            Ok(result) => {
311                                let duration_ms = exec_start.elapsed().as_millis() as u64;
312                                let success = result.success;
313                                let error = result.error.clone();
314                                if let Some(ref handler) = eh {
315                                    handler
316                                        .on_mcp_event(&McpEvent::ToolCallCompleted {
317                                            client_addr: addr.ip().to_string(),
318                                            tool_name: tool_name.clone(),
319                                            success,
320                                            error,
321                                            duration_ms,
322                                        })
323                                        .await;
324                                }
325                                (StatusCode::OK, Json(json!({"result": result}))).into_response()
326                            }
327                            Err(e) => {
328                                let duration_ms = exec_start.elapsed().as_millis() as u64;
329                                let err_msg = e.to_string();
330                                if let Some(ref handler) = eh {
331                                    handler
332                                        .on_mcp_event(&McpEvent::ToolError {
333                                            source: addr.ip().to_string(),
334                                            tool_name: tool_name.clone(),
335                                            error: err_msg.clone(),
336                                            duration_ms,
337                                        })
338                                        .await;
339                                }
340                                (StatusCode::BAD_REQUEST, Json(json!({"error": err_msg})))
341                                    .into_response()
342                            }
343                        }
344                    }
345                },
346            ),
347        )
348        .route(
349            "/resources/list",
350            post(
351                move |ConnectInfo(addr): ConnectInfo<SocketAddr>, headers: HeaderMap| {
352                    let token = token_res_list.clone();
353                    let allowed = ips_res_list.clone();
354                    let proto = protocol_res_list.clone();
355                    async move {
356                        if !allowed.is_allowed(addr.ip()) {
357                            return (
358                                StatusCode::FORBIDDEN,
359                                Json(json!({"error": "Access denied"})),
360                            )
361                                .into_response();
362                        }
363
364                        if !check_auth(&token, &headers) {
365                            return (
366                                StatusCode::UNAUTHORIZED,
367                                Json(json!({"error": "Unauthorized"})),
368                            )
369                                .into_response();
370                        }
371
372                        if !proto.supports_resources() {
373                            return (
374                                StatusCode::NOT_IMPLEMENTED,
375                                Json(json!({"error": "Resources not supported"})),
376                            )
377                                .into_response();
378                        }
379
380                        match proto.list_resources().await {
381                            Ok(resources) => {
382                                (StatusCode::OK, Json(json!({"resources": resources})))
383                                    .into_response()
384                            }
385                            Err(e) => (
386                                StatusCode::INTERNAL_SERVER_ERROR,
387                                Json(json!({"error": e.to_string()})),
388                            )
389                                .into_response(),
390                        }
391                    }
392                },
393            ),
394        )
395        .route(
396            "/resources/read",
397            post(
398                move |ConnectInfo(addr): ConnectInfo<SocketAddr>,
399                      headers: HeaderMap,
400                      Json(payload): Json<serde_json::Value>| {
401                    let token = token_res_read.clone();
402                    let allowed = ips_res_read.clone();
403                    let proto = protocol_res_read.clone();
404                    async move {
405                        if !allowed.is_allowed(addr.ip()) {
406                            return (
407                                StatusCode::FORBIDDEN,
408                                Json(json!({"error": "Access denied"})),
409                            )
410                                .into_response();
411                        }
412
413                        if !check_auth(&token, &headers) {
414                            return (
415                                StatusCode::UNAUTHORIZED,
416                                Json(json!({"error": "Unauthorized"})),
417                            )
418                                .into_response();
419                        }
420
421                        if !proto.supports_resources() {
422                            return (
423                                StatusCode::NOT_IMPLEMENTED,
424                                Json(json!({"error": "Resources not supported"})),
425                            )
426                                .into_response();
427                        }
428
429                        let uri = payload["uri"].as_str().unwrap_or("");
430
431                        match proto.read_resource(uri).await {
432                            Ok(content) => (
433                                StatusCode::OK,
434                                Json(json!({"uri": uri, "content": content})),
435                            )
436                                .into_response(),
437                            Err(e) => {
438                                (StatusCode::NOT_FOUND, Json(json!({"error": e.to_string()})))
439                                    .into_response()
440                            }
441                        }
442                    }
443                },
444            ),
445        )
446}
447
448/// Default Axum-based HTTP server adapter
449///
450/// Provides a full MCP-compatible HTTP server using the Axum framework.
451/// Only available when the `server` feature is enabled.
452#[cfg(feature = "server")]
453pub struct AxumHttpAdapter;
454
455#[cfg(feature = "server")]
456#[async_trait::async_trait]
457impl HttpServerAdapter for AxumHttpAdapter {
458    async fn start(
459        &self,
460        config: HttpServerConfig,
461        protocol: Arc<dyn ToolProtocol>,
462    ) -> Result<HttpServerInstance, Box<dyn Error + Send + Sync>> {
463        use crate::events::McpEvent;
464        use tokio::net::TcpListener;
465        let app =
466            axum_router(&config, protocol).into_make_service_with_connect_info::<SocketAddr>();
467
468        // Bind and start server
469        let listener = TcpListener::bind(config.addr).await?;
470        let addr = listener.local_addr()?;
471
472        // Fire ServerStarted event
473        if let Some(ref handler) = config.event_handler {
474            handler
475                .on_mcp_event(&McpEvent::ServerStarted {
476                    addr: addr.to_string(),
477                })
478                .await;
479        }
480
481        let server_handle = tokio::spawn(async move { axum::serve(listener, app).await });
482
483        Ok(HttpServerInstance::new(addr, Box::new(server_handle)))
484    }
485
486    fn name(&self) -> &str {
487        "axum"
488    }
489}