use crate::protocol::*;
use crate::transport::{McpTransport, McpTransportError, TransportTypeId};
use async_trait::async_trait;
use serde_json::Value;
use std::sync::atomic::{AtomicI64, Ordering};
use std::sync::Arc;
use std::time::Duration;
pub struct HttpTransport {
endpoint: String,
client: reqwest::Client,
next_id: Arc<AtomicI64>,
}
impl HttpTransport {
pub fn new(endpoint: impl Into<String>) -> Self {
let client = reqwest::Client::builder()
.timeout(Duration::from_secs(30))
.build()
.expect("Failed to create HTTP client");
Self {
endpoint: endpoint.into(),
client,
next_id: Arc::new(AtomicI64::new(1)),
}
}
pub fn with_timeout(
endpoint: impl Into<String>,
timeout: Duration,
) -> Result<Self, McpTransportError> {
let client = reqwest::Client::builder()
.timeout(timeout)
.build()
.map_err(|e| {
McpTransportError::TransportError(format!("Failed to create HTTP client: {}", e))
})?;
Ok(Self {
endpoint: endpoint.into(),
client,
next_id: Arc::new(AtomicI64::new(1)),
})
}
pub async fn send_request(
&self,
method: &str,
params: Option<Value>,
) -> Result<Value, McpTransportError> {
let id = self.next_id.fetch_add(1, Ordering::SeqCst);
let request = JsonRpcRequest::new(JsonRpcId::Number(id), method, params);
let response = self
.client
.post(&self.endpoint)
.json(&request)
.send()
.await
.map_err(|e| {
McpTransportError::TransportError(format!("HTTP request failed: {}", e))
})?;
if !response.status().is_success() {
return Err(McpTransportError::TransportError(format!(
"HTTP error: {} - {}",
response.status(),
response.text().await.unwrap_or_default()
)));
}
let json_response: JsonRpcResponse = response.json().await.map_err(|e| {
McpTransportError::TransportError(format!("Failed to parse JSON response: {}", e))
})?;
match json_response.payload {
JsonRpcPayload::Success { result } => Ok(result),
JsonRpcPayload::Error { error } => Err(McpTransportError::ServerError(format!(
"MCP Error: {}",
error
))),
}
}
pub async fn health_check(&self) -> bool {
self.client
.head(&self.endpoint)
.send()
.await
.map(|r| r.status().is_success())
.unwrap_or(false)
}
pub fn endpoint(&self) -> &str {
&self.endpoint
}
}
pub struct HttpTransportAdapter {
inner: HttpTransport,
}
impl HttpTransportAdapter {
pub fn new(endpoint: impl Into<String>) -> Self {
Self {
inner: HttpTransport::new(endpoint),
}
}
pub fn with_timeout(
endpoint: impl Into<String>,
timeout: Duration,
) -> Result<Self, McpTransportError> {
Ok(Self {
inner: HttpTransport::with_timeout(endpoint, timeout)?,
})
}
}
#[async_trait]
impl McpTransport for HttpTransportAdapter {
async fn list_tools(&self) -> Result<Vec<McpToolDefinition>, McpTransportError> {
let result = self
.inner
.send_request("tools/list", Some(serde_json::json!({})))
.await?;
let list_result: ListToolsResult = serde_json::from_value(result)?;
Ok(list_result.tools)
}
async fn call_tool(&self, name: &str, args: Value) -> Result<Value, McpTransportError> {
let params = CallToolParams {
name: name.to_string(),
arguments: Some(args),
task: None,
meta: None,
};
let result = self
.inner
.send_request("tools/call", Some(serde_json::to_value(¶ms)?))
.await?;
let call_result: CallToolResult = serde_json::from_value(result)?;
if call_result.is_error == Some(true) {
let error_text = call_result
.content
.first()
.and_then(|c| c.as_text())
.unwrap_or("Unknown error");
return Err(McpTransportError::ServerError(error_text.to_string()));
}
let text = call_result
.content
.iter()
.filter_map(|c| c.as_text())
.collect::<Vec<_>>()
.join("\n");
Ok(Value::String(text))
}
async fn shutdown(&self) -> Result<(), McpTransportError> {
Ok(())
}
fn is_alive(&self) -> bool {
true
}
fn transport_type(&self) -> TransportTypeId {
TransportTypeId::Http
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_http_transport_creation() {
let transport = HttpTransport::new("http://localhost:8080/mcp");
assert_eq!(transport.endpoint(), "http://localhost:8080/mcp");
}
#[tokio::test]
async fn test_http_transport_with_timeout() {
let transport =
HttpTransport::with_timeout("http://localhost:8080/mcp", Duration::from_secs(10));
assert!(transport.is_ok());
}
#[test]
fn test_transport_type() {
assert_eq!(TransportTypeId::Http.to_string(), "http");
}
}