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}