use async_trait::async_trait;
use serde_json::Value;
use std::collections::HashMap;
use url::Url;
use crate::error::{McpClientResult, TransportError};
pub mod http;
pub mod sse;
pub use http::HttpTransport;
#[allow(deprecated)] pub use sse::SseTransport;
#[derive(Debug, Clone, PartialEq)]
pub enum TransportType {
Http,
Sse,
}
impl std::fmt::Display for TransportType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
TransportType::Http => write!(f, "HTTP"),
TransportType::Sse => write!(f, "SSE"),
}
}
}
#[derive(Debug, Clone)]
pub struct TransportCapabilities {
pub streaming: bool,
pub bidirectional: bool,
pub server_events: bool,
pub max_message_size: Option<usize>,
pub persistent: bool,
}
#[derive(Debug, Clone)]
pub struct ConnectionInfo {
pub transport_type: TransportType,
pub endpoint: String,
pub connected: bool,
pub capabilities: TransportCapabilities,
pub metadata: Value,
}
#[derive(Debug, Clone)]
pub struct TransportResponse {
pub body: Value,
pub headers: HashMap<String, String>,
}
impl TransportResponse {
pub fn new(body: Value, headers: HashMap<String, String>) -> Self {
Self { body, headers }
}
pub fn body_only(body: Value) -> Self {
Self {
body,
headers: HashMap::new(),
}
}
}
#[async_trait]
pub trait Transport: Send + Sync {
fn transport_type(&self) -> TransportType;
fn capabilities(&self) -> TransportCapabilities;
async fn connect(&self) -> McpClientResult<()>;
async fn disconnect(&self) -> McpClientResult<()>;
fn is_connected(&self) -> bool;
async fn send_request(&self, request: Value) -> McpClientResult<Value>;
async fn send_request_streaming(
&self,
_request: Value,
) -> McpClientResult<tokio::sync::mpsc::UnboundedReceiver<Value>> {
Err(crate::error::TransportError::Http(
"this transport does not support streaming requests".to_string(),
)
.into())
}
async fn send_request_with_extra_headers(
&self,
request: Value,
_extra_headers: &[(String, String)],
) -> McpClientResult<Value> {
self.send_request(request).await
}
async fn send_request_with_headers(&self, request: Value)
-> McpClientResult<TransportResponse>;
async fn send_notification(&self, notification: Value) -> McpClientResult<()>;
async fn send_delete(&self, session_id: &str) -> McpClientResult<()>;
fn set_session_id(&self, session_id: String);
fn clear_session_id(&self);
async fn update_auth_header(&self, _value: Option<String>) {
}
fn set_protocol_version(&self, _version: &str) {}
async fn start_event_listener(&self) -> McpClientResult<EventReceiver>;
fn connection_info(&self) -> ConnectionInfo;
async fn health_check(&self) -> McpClientResult<bool> {
let ping_request = serde_json::json!({
"jsonrpc": "2.0",
"id": "health_check",
"method": "ping",
"params": {}
});
match self.send_request(ping_request).await {
Ok(_) => Ok(true),
Err(_) => Ok(false),
}
}
fn statistics(&self) -> TransportStatistics {
TransportStatistics::default()
}
}
pub type BoxedTransport = Box<dyn Transport>;
pub type EventReceiver = tokio::sync::mpsc::UnboundedReceiver<ServerEvent>;
#[derive(Debug, Clone)]
pub enum ServerEvent {
Notification(Value),
Request(Value),
Response(Value),
ConnectionLost,
Error(String),
Heartbeat,
}
#[derive(Debug, Clone, Default)]
pub struct TransportStatistics {
pub requests_sent: u64,
pub responses_received: u64,
pub notifications_sent: u64,
pub events_received: u64,
pub errors: u64,
pub avg_response_time_ms: f64,
pub last_error: Option<String>,
}
pub fn detect_transport_type(url_str: &str) -> McpClientResult<TransportType> {
let url = Url::parse(url_str)
.map_err(|e| TransportError::ConnectionFailed(format!("Invalid URL: {}", e)))?;
match url.scheme() {
"http" | "https" => {
if url.path().contains("/sse") || url.query().unwrap_or("").contains("transport=sse") {
Ok(TransportType::Sse)
} else {
Ok(TransportType::Http)
}
}
"stdio" | "file" => Err(TransportError::Unsupported(
"Stdio transport not yet implemented".to_string(),
)
.into()),
scheme => Err(TransportError::Unsupported(format!("Unknown scheme: {}", scheme)).into()),
}
}
pub struct TransportFactory;
impl TransportFactory {
pub fn from_url(url: &str) -> McpClientResult<BoxedTransport> {
let transport_type = detect_transport_type(url)?;
match transport_type {
TransportType::Http => Ok(Box::new(HttpTransport::new(url)?)),
#[allow(deprecated)] TransportType::Sse => Ok(Box::new(SseTransport::new(url)?)),
}
}
pub fn create(
transport_type: TransportType,
endpoint: &str,
) -> McpClientResult<BoxedTransport> {
match transport_type {
TransportType::Http => Ok(Box::new(HttpTransport::new(endpoint)?)),
#[allow(deprecated)] TransportType::Sse => Ok(Box::new(SseTransport::new(endpoint)?)),
}
}
pub fn available_transports() -> Vec<TransportType> {
vec![TransportType::Http, TransportType::Sse]
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_transport_type_detection() {
assert_eq!(
detect_transport_type("http://localhost:8080/mcp").unwrap(),
TransportType::Http
);
assert_eq!(
detect_transport_type("http://localhost:8080/mcp/sse").unwrap(),
TransportType::Sse
);
assert!(detect_transport_type("ftp://localhost:8080/mcp").is_err());
assert!(detect_transport_type("invalid://localhost").is_err());
}
#[test]
fn test_transport_factory() {
let transport = TransportFactory::from_url("http://localhost:8080/mcp").unwrap();
assert_eq!(transport.transport_type(), TransportType::Http);
let transports = TransportFactory::available_transports();
assert!(transports.contains(&TransportType::Http));
assert!(transports.contains(&TransportType::Sse));
}
}