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