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    /// Optional dynamic bearer-token authorizer.
40    ///
41    /// When present, this authorizer is consulted after the static
42    /// [`bearer_token`](Self::bearer_token) check. A request is accepted when
43    /// either configured mechanism accepts the supplied bearer token.
44    pub bearer_authorizer: Option<Arc<dyn BearerTokenAuthorizer>>,
45    /// IP filter controlling which client addresses are allowed
46    pub ip_filter: IpFilter,
47    /// Optional event handler for MCP server lifecycle and request events
48    pub event_handler: Option<Arc<dyn McpEventHandler>>,
49}
50
51impl Clone for HttpServerConfig {
52    fn clone(&self) -> Self {
53        Self {
54            addr: self.addr,
55            bearer_token: self.bearer_token.clone(),
56            bearer_authorizer: self.bearer_authorizer.clone(),
57            ip_filter: self.ip_filter.clone(),
58            event_handler: self.event_handler.clone(),
59        }
60    }
61}
62
63impl std::fmt::Debug for HttpServerConfig {
64    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
65        f.debug_struct("HttpServerConfig")
66            .field("addr", &self.addr)
67            .field("has_bearer_token", &self.bearer_token.is_some())
68            .field("has_bearer_authorizer", &self.bearer_authorizer.is_some())
69            .field("ip_filter", &self.ip_filter)
70            .field("has_event_handler", &self.event_handler.is_some())
71            .finish()
72    }
73}
74
75/// Per-request context passed to a dynamic bearer-token authorizer.
76///
77/// The MCP runtime builds this value before dispatching a request to the
78/// protocol implementation. Servers can inspect it to make authorization
79/// decisions that depend on more than the raw token string.
80///
81/// # Payload shape
82///
83/// `payload` is deliberately transport-shaped:
84///
85/// - streamable HTTP JSON-RPC requests receive the `params` object, such as
86///   `{ "name": "tool_name", "arguments": { ... } }` for `tools/call`.
87/// - legacy `/tools/execute` requests receive the full request body, usually
88///   `{ "tool": "tool_name", "parameters": { ... } }`.
89/// - metadata-style routes such as `/tools/list` and `/resources/list` receive
90///   `None` when there is no useful body to authorize.
91///
92/// Server crates keep ownership of policy. For example, a memory server can
93/// inspect tool arguments inside `payload` and allow a token for one chain while
94/// denying the same token for another.
95#[derive(Debug, Clone, PartialEq, Eq)]
96pub struct BearerAuthContext {
97    /// Client socket address reported by the HTTP framework.
98    pub client_addr: SocketAddr,
99    /// HTTP route that received the request.
100    pub route: String,
101    /// MCP method or legacy action being authorized.
102    pub action: String,
103    /// Parsed request payload or JSON-RPC params, when available.
104    ///
105    /// This value is cloned from the already-parsed request body so authorizers
106    /// do not need to parse JSON a second time.
107    pub payload: Option<serde_json::Value>,
108}
109
110/// Dynamic bearer-token authorization hook for MCP HTTP transports.
111///
112/// Implement this trait when a server needs revocable tokens, scoped access,
113/// or token lookup from durable storage instead of a single static configured
114/// secret.
115///
116/// # Examples
117///
118/// ```
119/// use mcp::{BearerAuthContext, BearerTokenAuthorizer};
120///
121/// struct ToolListOnly;
122///
123/// impl BearerTokenAuthorizer for ToolListOnly {
124///     fn authorize_bearer_token(&self, token: &str, context: &BearerAuthContext) -> bool {
125///         token == "good-token" && context.action == "tools/list"
126///     }
127/// }
128/// ```
129pub trait BearerTokenAuthorizer: Send + Sync {
130    /// Return `true` when a request without a bearer token should be allowed.
131    ///
132    /// The default is fail-closed. Override this only when the embedding server
133    /// has an explicit runtime mode where unauthenticated requests are expected.
134    fn allow_missing_bearer_token(&self, _context: &BearerAuthContext) -> bool {
135        false
136    }
137
138    /// Return `true` when `token` is allowed for `context`.
139    ///
140    /// Implementations should avoid logging raw token values. If comparing
141    /// against stored secrets, prefer constant-time hash comparison in the
142    /// server crate.
143    fn authorize_bearer_token(&self, token: &str, context: &BearerAuthContext) -> bool;
144}
145
146/// A running HTTP server instance
147pub struct HttpServerInstance {
148    /// Socket address the server is listening on
149    pub addr: SocketAddr,
150    /// Handle for shutting down the server
151    /// Type erased to allow different framework implementations
152    shutdown_handle: Box<dyn std::any::Any + Send + Sync>,
153}
154
155impl HttpServerInstance {
156    /// Create a new server instance with the given address and shutdown handle
157    pub fn new(addr: SocketAddr, shutdown_handle: Box<dyn std::any::Any + Send + Sync>) -> Self {
158        Self {
159            addr,
160            shutdown_handle,
161        }
162    }
163
164    /// Get the server's socket address
165    pub fn get_addr(&self) -> SocketAddr {
166        self.addr
167    }
168
169    /// Get mutable reference to the shutdown handle for advanced usage
170    pub fn shutdown_handle_mut(&mut self) -> &mut Box<dyn std::any::Any + Send + Sync> {
171        &mut self.shutdown_handle
172    }
173}
174
175/// Trait for HTTP server implementations
176///
177/// Implementations of this trait provide HTTP endpoints for MCP protocols.
178/// Different HTTP frameworks can be swapped by implementing this trait.
179#[async_trait::async_trait]
180pub trait HttpServerAdapter: Send + Sync {
181    /// Start the HTTP server with the given configuration and tool protocol
182    ///
183    /// # Arguments
184    ///
185    /// * `config` - Server configuration (address, auth, IP filtering)
186    /// * `protocol` - The ToolProtocol implementation to expose
187    ///
188    /// # Endpoints
189    ///
190    /// The server must provide the following endpoints:
191    /// - `POST /tools/list` - List all available tools from the protocol
192    /// - `POST /tools/execute` - Execute a tool with given parameters
193    /// - `POST /resources/list` - List all available resources (if protocol supports)
194    /// - `POST /resources/read` - Read a resource by URI (if protocol supports)
195    ///
196    /// # Returns
197    ///
198    /// A running server instance, or an error if startup fails
199    async fn start(
200        &self,
201        config: HttpServerConfig,
202        protocol: Arc<dyn ToolProtocol>,
203    ) -> Result<HttpServerInstance, Box<dyn Error + Send + Sync>>;
204
205    /// Get the name of this adapter (for logging/debugging)
206    fn name(&self) -> &str {
207        "unknown"
208    }
209}
210
211/// Build an Axum router that exposes a [`ToolProtocol`] over the shared HTTP MCP surface.
212///
213/// The returned router serves:
214/// - `POST /tools/list`
215/// - `POST /tools/execute`
216/// - `POST /resources/list`
217/// - `POST /resources/read`
218///
219/// This helper is useful when a crate wants to reuse the shared MCP transport
220/// but still compose extra routes of its own, such as a `/health` endpoint.
221#[cfg(feature = "server")]
222pub fn axum_router(config: &HttpServerConfig, protocol: Arc<dyn ToolProtocol>) -> Router {
223    use crate::events::McpEvent;
224    use axum::{
225        extract::ConnectInfo, http::HeaderMap, http::StatusCode, response::IntoResponse,
226        routing::post, Json, Router,
227    };
228    use serde_json::json;
229    use sha2::{Digest, Sha256};
230    use subtle::ConstantTimeEq;
231
232    fn bearer_from_headers(headers: &HeaderMap) -> Option<&str> {
233        headers
234            .get("Authorization")
235            .and_then(|v| v.to_str().ok())
236            .and_then(|v| v.strip_prefix("Bearer "))
237    }
238
239    /// Validate the Authorization header against static or dynamic bearer auth.
240    ///
241    /// Returns `true` when neither auth mechanism is configured (open server),
242    /// when the supplied `Bearer <token>` matches the static token, or when the
243    /// dynamic authorizer accepts it. Static comparison uses
244    /// `subtle::ConstantTimeEq` on SHA-256 digests so the compiler cannot
245    /// short-circuit the comparison and leak token length via timing.
246    fn check_auth(
247        expected_token: &Option<String>,
248        authorizer: &Option<Arc<dyn BearerTokenAuthorizer>>,
249        headers: &HeaderMap,
250        context: BearerAuthContext,
251    ) -> bool {
252        if expected_token.is_none() && authorizer.is_none() {
253            return true;
254        }
255
256        let Some(provided) = bearer_from_headers(headers) else {
257            return authorizer
258                .as_ref()
259                .is_some_and(|auth| auth.allow_missing_bearer_token(&context));
260        };
261
262        if let Some(expected) = expected_token.as_deref() {
263            let expected_hash = Sha256::digest(expected.as_bytes());
264            let provided_hash = Sha256::digest(provided.as_bytes());
265            if bool::from(expected_hash.ct_eq(&provided_hash)) {
266                return true;
267            }
268        }
269
270        authorizer
271            .as_ref()
272            .is_some_and(|auth| auth.authorize_bearer_token(provided, &context))
273    }
274
275    let bearer_token = Arc::new(config.bearer_token.clone());
276    let bearer_authorizer = Arc::new(config.bearer_authorizer.clone());
277    let ip_filter = Arc::new(config.ip_filter.clone());
278
279    let token_list = bearer_token.clone();
280    let authz_list = bearer_authorizer.clone();
281    let ips_list = ip_filter.clone();
282    let token_exec = bearer_token.clone();
283    let authz_exec = bearer_authorizer.clone();
284    let ips_exec = ip_filter.clone();
285    let token_res_list = bearer_token.clone();
286    let authz_res_list = bearer_authorizer.clone();
287    let ips_res_list = ip_filter.clone();
288    let token_res_read = bearer_token.clone();
289    let authz_res_read = bearer_authorizer.clone();
290    let ips_res_read = ip_filter.clone();
291
292    let eh_list = config.event_handler.clone();
293    let eh_exec = config.event_handler.clone();
294
295    let protocol_list = protocol.clone();
296    let protocol_exec = protocol.clone();
297    let protocol_res_list = protocol.clone();
298    let protocol_res_read = protocol.clone();
299
300    Router::new()
301        .route(
302            "/tools/list",
303            post(
304                move |ConnectInfo(addr): ConnectInfo<SocketAddr>, headers: HeaderMap| {
305                    let token = token_list.clone();
306                    let authz = authz_list.clone();
307                    let allowed = ips_list.clone();
308                    let proto = protocol_list.clone();
309                    let eh = eh_list.clone();
310                    async move {
311                        if !allowed.is_allowed(addr.ip()) {
312                            if let Some(ref handler) = eh {
313                                handler
314                                    .on_mcp_event(&McpEvent::RequestRejected {
315                                        client_addr: addr.ip().to_string(),
316                                        reason: "IP not allowed".to_string(),
317                                    })
318                                    .await;
319                            }
320                            return (
321                                StatusCode::FORBIDDEN,
322                                Json(json!({"error": "Access denied"})),
323                            )
324                                .into_response();
325                        }
326
327                        if !check_auth(
328                            &token,
329                            &authz,
330                            &headers,
331                            BearerAuthContext {
332                                client_addr: addr,
333                                route: "/tools/list".to_string(),
334                                action: "tools/list".to_string(),
335                                payload: None,
336                            },
337                        ) {
338                            return (
339                                StatusCode::UNAUTHORIZED,
340                                Json(json!({"error": "Unauthorized"})),
341                            )
342                                .into_response();
343                        }
344
345                        if let Some(ref handler) = eh {
346                            handler
347                                .on_mcp_event(&McpEvent::ToolListRequested {
348                                    client_addr: addr.ip().to_string(),
349                                })
350                                .await;
351                        }
352
353                        match proto.list_tools().await {
354                            Ok(tools) => {
355                                let tool_count = tools.len();
356                                if let Some(ref handler) = eh {
357                                    handler
358                                        .on_mcp_event(&McpEvent::ToolListReturned {
359                                            client_addr: addr.ip().to_string(),
360                                            tool_count,
361                                        })
362                                        .await;
363                                }
364                                (StatusCode::OK, Json(json!({"tools": tools}))).into_response()
365                            }
366                            Err(e) => (
367                                StatusCode::INTERNAL_SERVER_ERROR,
368                                Json(json!({"error": e.to_string()})),
369                            )
370                                .into_response(),
371                        }
372                    }
373                },
374            ),
375        )
376        .route(
377            "/tools/execute",
378            post(
379                move |ConnectInfo(addr): ConnectInfo<SocketAddr>,
380                      headers: HeaderMap,
381                      Json(payload): Json<serde_json::Value>| {
382                    let token = token_exec.clone();
383                    let authz = authz_exec.clone();
384                    let allowed = ips_exec.clone();
385                    let proto = protocol_exec.clone();
386                    let eh = eh_exec.clone();
387                    async move {
388                        if !allowed.is_allowed(addr.ip()) {
389                            if let Some(ref handler) = eh {
390                                handler
391                                    .on_mcp_event(&McpEvent::RequestRejected {
392                                        client_addr: addr.ip().to_string(),
393                                        reason: "IP not allowed".to_string(),
394                                    })
395                                    .await;
396                            }
397                            return (
398                                StatusCode::FORBIDDEN,
399                                Json(json!({"error": "Access denied"})),
400                            )
401                                .into_response();
402                        }
403
404                        if !check_auth(
405                            &token,
406                            &authz,
407                            &headers,
408                            BearerAuthContext {
409                                client_addr: addr,
410                                route: "/tools/execute".to_string(),
411                                action: "tools/execute".to_string(),
412                                payload: Some(payload.clone()),
413                            },
414                        ) {
415                            return (
416                                StatusCode::UNAUTHORIZED,
417                                Json(json!({"error": "Unauthorized"})),
418                            )
419                                .into_response();
420                        }
421
422                        let tool_name = payload["tool"].as_str().unwrap_or("").to_string();
423                        let params = payload["parameters"].clone();
424
425                        if let Some(ref handler) = eh {
426                            handler
427                                .on_mcp_event(&McpEvent::ToolCallReceived {
428                                    client_addr: addr.ip().to_string(),
429                                    tool_name: tool_name.clone(),
430                                    parameters: params.clone(),
431                                })
432                                .await;
433                        }
434
435                        let exec_start = std::time::Instant::now();
436                        match proto.execute(&tool_name, params).await {
437                            Ok(result) => {
438                                let duration_ms = exec_start.elapsed().as_millis() as u64;
439                                let success = result.success;
440                                let error = result.error.clone();
441                                if let Some(ref handler) = eh {
442                                    handler
443                                        .on_mcp_event(&McpEvent::ToolCallCompleted {
444                                            client_addr: addr.ip().to_string(),
445                                            tool_name: tool_name.clone(),
446                                            success,
447                                            error,
448                                            duration_ms,
449                                        })
450                                        .await;
451                                }
452                                (StatusCode::OK, Json(json!({"result": result}))).into_response()
453                            }
454                            Err(e) => {
455                                let duration_ms = exec_start.elapsed().as_millis() as u64;
456                                let err_msg = e.to_string();
457                                if let Some(ref handler) = eh {
458                                    handler
459                                        .on_mcp_event(&McpEvent::ToolError {
460                                            source: addr.ip().to_string(),
461                                            tool_name: tool_name.clone(),
462                                            error: err_msg.clone(),
463                                            duration_ms,
464                                        })
465                                        .await;
466                                }
467                                (StatusCode::BAD_REQUEST, Json(json!({"error": err_msg})))
468                                    .into_response()
469                            }
470                        }
471                    }
472                },
473            ),
474        )
475        .route(
476            "/resources/list",
477            post(
478                move |ConnectInfo(addr): ConnectInfo<SocketAddr>, headers: HeaderMap| {
479                    let token = token_res_list.clone();
480                    let authz = authz_res_list.clone();
481                    let allowed = ips_res_list.clone();
482                    let proto = protocol_res_list.clone();
483                    async move {
484                        if !allowed.is_allowed(addr.ip()) {
485                            return (
486                                StatusCode::FORBIDDEN,
487                                Json(json!({"error": "Access denied"})),
488                            )
489                                .into_response();
490                        }
491
492                        if !check_auth(
493                            &token,
494                            &authz,
495                            &headers,
496                            BearerAuthContext {
497                                client_addr: addr,
498                                route: "/resources/list".to_string(),
499                                action: "resources/list".to_string(),
500                                payload: None,
501                            },
502                        ) {
503                            return (
504                                StatusCode::UNAUTHORIZED,
505                                Json(json!({"error": "Unauthorized"})),
506                            )
507                                .into_response();
508                        }
509
510                        if !proto.supports_resources() {
511                            return (
512                                StatusCode::NOT_IMPLEMENTED,
513                                Json(json!({"error": "Resources not supported"})),
514                            )
515                                .into_response();
516                        }
517
518                        match proto.list_resources().await {
519                            Ok(resources) => {
520                                (StatusCode::OK, Json(json!({"resources": resources})))
521                                    .into_response()
522                            }
523                            Err(e) => (
524                                StatusCode::INTERNAL_SERVER_ERROR,
525                                Json(json!({"error": e.to_string()})),
526                            )
527                                .into_response(),
528                        }
529                    }
530                },
531            ),
532        )
533        .route(
534            "/resources/read",
535            post(
536                move |ConnectInfo(addr): ConnectInfo<SocketAddr>,
537                      headers: HeaderMap,
538                      Json(payload): Json<serde_json::Value>| {
539                    let token = token_res_read.clone();
540                    let authz = authz_res_read.clone();
541                    let allowed = ips_res_read.clone();
542                    let proto = protocol_res_read.clone();
543                    async move {
544                        if !allowed.is_allowed(addr.ip()) {
545                            return (
546                                StatusCode::FORBIDDEN,
547                                Json(json!({"error": "Access denied"})),
548                            )
549                                .into_response();
550                        }
551
552                        if !check_auth(
553                            &token,
554                            &authz,
555                            &headers,
556                            BearerAuthContext {
557                                client_addr: addr,
558                                route: "/resources/read".to_string(),
559                                action: "resources/read".to_string(),
560                                payload: Some(payload.clone()),
561                            },
562                        ) {
563                            return (
564                                StatusCode::UNAUTHORIZED,
565                                Json(json!({"error": "Unauthorized"})),
566                            )
567                                .into_response();
568                        }
569
570                        if !proto.supports_resources() {
571                            return (
572                                StatusCode::NOT_IMPLEMENTED,
573                                Json(json!({"error": "Resources not supported"})),
574                            )
575                                .into_response();
576                        }
577
578                        let uri = payload["uri"].as_str().unwrap_or("");
579
580                        match proto.read_resource(uri).await {
581                            Ok(content) => (
582                                StatusCode::OK,
583                                Json(json!({"uri": uri, "content": content})),
584                            )
585                                .into_response(),
586                            Err(e) => {
587                                (StatusCode::NOT_FOUND, Json(json!({"error": e.to_string()})))
588                                    .into_response()
589                            }
590                        }
591                    }
592                },
593            ),
594        )
595}
596
597/// Default Axum-based HTTP server adapter
598///
599/// Provides a full MCP-compatible HTTP server using the Axum framework.
600/// Only available when the `server` feature is enabled.
601#[cfg(feature = "server")]
602pub struct AxumHttpAdapter;
603
604#[cfg(feature = "server")]
605#[async_trait::async_trait]
606impl HttpServerAdapter for AxumHttpAdapter {
607    async fn start(
608        &self,
609        config: HttpServerConfig,
610        protocol: Arc<dyn ToolProtocol>,
611    ) -> Result<HttpServerInstance, Box<dyn Error + Send + Sync>> {
612        use crate::events::McpEvent;
613        use tokio::net::TcpListener;
614        let app =
615            axum_router(&config, protocol).into_make_service_with_connect_info::<SocketAddr>();
616
617        // Bind and start server
618        let listener = TcpListener::bind(config.addr).await?;
619        let addr = listener.local_addr()?;
620
621        // Fire ServerStarted event
622        if let Some(ref handler) = config.event_handler {
623            handler
624                .on_mcp_event(&McpEvent::ServerStarted {
625                    addr: addr.to_string(),
626                })
627                .await;
628        }
629
630        let server_handle = tokio::spawn(async move { axum::serve(listener, app).await });
631
632        Ok(HttpServerInstance::new(addr, Box::new(server_handle)))
633    }
634
635    fn name(&self) -> &str {
636        "axum"
637    }
638}