use async_trait::async_trait;
use std::sync::Arc;
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
use tokio::process::{Child, ChildStdin, ChildStdout, Command};
use tokio::sync::Mutex;
use tokio::time::{timeout, Duration};
use super::protocol::{MCPError, MCPRequest, MCPResponse};
use super::types::MCPConfig;
const SSE_DISCOVER_TIMEOUT: Duration = Duration::from_secs(30);
#[async_trait]
pub trait MCPTransport: Send + Sync {
async fn request(&self, req: MCPRequest) -> Result<MCPResponse, MCPError>;
async fn notify(&self, method: &str, params: Option<serde_json::Value>)
-> Result<(), MCPError>;
async fn close(&self) -> Result<(), MCPError>;
}
pub struct StdioTransport {
stdin: Arc<Mutex<ChildStdin>>,
stdout: Arc<Mutex<BufReader<ChildStdout>>>,
child: Arc<Mutex<Child>>,
request_lock: Arc<Mutex<()>>,
}
impl StdioTransport {
pub async fn new(config: &MCPConfig) -> Result<Self, MCPError> {
let (command, args, env) = match config {
MCPConfig::Stdio { command, args, env } => (command, args, env),
_ => return Err(MCPError::new(-1, "StdioTransport 需要 Stdio 配置")),
};
let mut cmd = Command::new(command);
cmd.args(args);
for (k, v) in env {
cmd.env(k, v);
}
cmd.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped());
let mut child = cmd
.spawn()
.map_err(|e| MCPError::new(-1, format!("启动子进程失败: {}", e)))?;
let stdin = child
.stdin
.take()
.ok_or_else(|| MCPError::new(-1, "子进程无 stdin"))?;
let stdout = child
.stdout
.take()
.ok_or_else(|| MCPError::new(-1, "子进程无 stdout"))?;
if let Some(stderr) = child.stderr.take() {
tokio::spawn(async move {
let reader = BufReader::new(stderr);
let mut lines = reader.lines();
while let Ok(Some(line)) = lines.next_line().await {
eprintln!("[mcp-stdio-stderr] {}", line);
}
});
}
Ok(Self {
stdin: Arc::new(Mutex::new(stdin)),
stdout: Arc::new(Mutex::new(BufReader::new(stdout))),
child: Arc::new(Mutex::new(child)),
request_lock: Arc::new(Mutex::new(())),
})
}
}
#[async_trait]
impl MCPTransport for StdioTransport {
async fn request(&self, req: MCPRequest) -> Result<MCPResponse, MCPError> {
let _guard = self.request_lock.lock().await;
let json = serde_json::to_string(&req)
.map_err(|e| MCPError::new(-1, format!("序列化请求失败: {}", e)))?;
{
let mut stdin = self.stdin.lock().await;
stdin
.write_all(json.as_bytes())
.await
.map_err(|e| MCPError::new(-1, format!("写 stdin 失败: {}", e)))?;
stdin
.write_all(b"\n")
.await
.map_err(|e| MCPError::new(-1, format!("写换行失败: {}", e)))?;
stdin
.flush()
.await
.map_err(|e| MCPError::new(-1, format!("flush stdin 失败: {}", e)))?;
}
let mut line = String::new();
{
let mut stdout = self.stdout.lock().await;
loop {
line.clear();
let n = stdout
.read_line(&mut line)
.await
.map_err(|e| MCPError::new(-1, format!("读 stdout 失败: {}", e)))?;
if n == 0 {
return Err(MCPError::new(-1, "子进程 stdout 已关闭"));
}
if !line.trim().is_empty() {
break;
}
}
}
serde_json::from_str::<MCPResponse>(line.trim())
.map_err(|e| MCPError::new(-1, format!("解析响应失败: {} | 原文: {}", e, line)))
}
async fn close(&self) -> Result<(), MCPError> {
let mut child = self.child.lock().await;
let _ = child.kill().await;
Ok(())
}
async fn notify(
&self,
method: &str,
params: Option<serde_json::Value>,
) -> Result<(), MCPError> {
let notif = serde_json::json!({
"jsonrpc": "2.0",
"method": method,
});
let mut payload = notif;
if let Some(p) = params {
payload
.as_object_mut()
.unwrap()
.insert("params".to_string(), p);
}
let json = serde_json::to_string(&payload)
.map_err(|e| MCPError::new(-1, format!("序列化通知失败: {}", e)))?;
let mut stdin = self.stdin.lock().await;
stdin
.write_all(json.as_bytes())
.await
.map_err(|e| MCPError::new(-1, format!("写通知到 stdin 失败: {}", e)))?;
stdin
.write_all(b"\n")
.await
.map_err(|e| MCPError::new(-1, format!("写换行失败: {}", e)))?;
stdin
.flush()
.await
.map_err(|e| MCPError::new(-1, format!("flush stdin 失败: {}", e)))?;
Ok(())
}
}
pub struct SseTransport {
sse_url: String,
post_url: Arc<Mutex<Option<String>>>,
client: reqwest::Client,
}
impl SseTransport {
pub fn new(config: &MCPConfig) -> Result<Self, MCPError> {
let sse_url = match config {
MCPConfig::Sse { url } => url.clone(),
_ => return Err(MCPError::new(-1, "SseTransport requires SSE config")),
};
Ok(Self {
sse_url,
post_url: Arc::new(Mutex::new(None)),
client: reqwest::Client::new(),
})
}
async fn discover_post_url(&self) -> Result<String, MCPError> {
{
let post_url = self.post_url.lock().await;
if let Some(url) = &*post_url {
return Ok(url.clone());
}
}
let response = timeout(SSE_DISCOVER_TIMEOUT, async {
self.client
.get(&self.sse_url)
.header("Accept", "text/event-stream")
.send()
.await
.map_err(|e| MCPError::new(-1, format!("SSE connection failed: {}", e)))
})
.await
.map_err(|_| {
MCPError::new(
-1,
"SSE discover timed out: server did not respond within 30s",
)
})??;
let status = response.status();
if !status.is_success() {
return Err(MCPError::new(
-1,
format!("SSE endpoint returned HTTP {}", status),
));
}
let text = timeout(SSE_DISCOVER_TIMEOUT, async {
response
.text()
.await
.map_err(|e| MCPError::new(-1, format!("Failed to read SSE response: {}", e)))
})
.await
.map_err(|_| {
MCPError::new(
-1,
"SSE discover timed out: reading response body took longer than 30s",
)
})??;
let mut found_url = None;
let mut current_event = String::new();
for line in text.lines() {
if let Some(stripped) = line.strip_prefix("event:") {
current_event = stripped.trim().to_string();
} else if line.starts_with("data:") && current_event == "endpoint" {
found_url = Some(line[5..].trim().to_string());
break;
}
}
let url = found_url
.ok_or_else(|| MCPError::new(-1, "SSE stream did not send 'endpoint' event"))?;
{
let mut post_url = self.post_url.lock().await;
*post_url = Some(url.clone());
}
Ok(url)
}
}
#[async_trait]
impl MCPTransport for SseTransport {
async fn request(&self, req: MCPRequest) -> Result<MCPResponse, MCPError> {
let post_url = self.discover_post_url().await?;
let resp = self
.client
.post(&post_url)
.json(&req)
.send()
.await
.map_err(|e| MCPError::new(-1, format!("HTTP POST failed: {}", e)))?;
let status = resp.status();
if !status.is_success() {
return Err(MCPError::new(-1, format!("HTTP error: {}", status)));
}
let body = resp
.text()
.await
.map_err(|e| MCPError::new(-1, format!("Failed to read response: {}", e)))?;
if let Ok(mcp_resp) = serde_json::from_str::<MCPResponse>(&body) {
return Ok(mcp_resp);
}
for line in body.lines() {
if let Some(data) = line.strip_prefix("data:") {
let trimmed = data.trim();
if !trimmed.is_empty() {
if let Ok(mcp_resp) = serde_json::from_str::<MCPResponse>(trimmed) {
return Ok(mcp_resp);
}
}
}
}
Err(MCPError::new(
-1,
format!("Could not parse MCP response from: {}", body),
))
}
async fn close(&self) -> Result<(), MCPError> {
let mut post_url = self.post_url.lock().await;
*post_url = None;
Ok(())
}
async fn notify(
&self,
method: &str,
params: Option<serde_json::Value>,
) -> Result<(), MCPError> {
let post_url = self.discover_post_url().await?;
let mut payload = serde_json::json!({
"jsonrpc": "2.0",
"method": method,
});
if let Some(p) = params {
payload
.as_object_mut()
.unwrap()
.insert("params".to_string(), p);
}
self.client
.post(&post_url)
.json(&payload)
.send()
.await
.map_err(|e| MCPError::new(-1, format!("Failed to send notification: {}", e)))?;
Ok(())
}
}
#[cfg(test)]
#[allow(unused_imports)]
mod tests {
use super::*;
#[tokio::test]
async fn test_stdio_transport_new_invalid_command() {
let config = MCPConfig::stdio("nonexistent_command_xyz_zzz", vec![]);
let result = StdioTransport::new(&config).await;
assert!(result.is_err());
}
#[test]
fn test_sse_transport_new_wrong_config() {
let config = MCPConfig::stdio("npx", vec![]);
let result = SseTransport::new(&config);
assert!(result.is_err());
}
#[test]
fn test_sse_transport_new_ok() {
let config = MCPConfig::sse("http://localhost:3001/sse");
let transport = SseTransport::new(&config);
assert!(transport.is_ok());
}
}