use std::collections::VecDeque;
use std::sync::Arc;
use std::time::Duration;
use anyhow::{Context, Result};
use tokio::io::{AsyncBufReadExt, AsyncWriteExt};
use tokio::process::{Child, ChildStdin, ChildStdout};
use tokio::sync::Mutex as TokioMutex;
use super::{McpServerConfig, McpTransport};
use crate::child_env;
pub(super) struct StdioTransport {
pub(super) child: Child,
pub(super) stdin: ChildStdin,
pub(super) reader: tokio::io::BufReader<ChildStdout>,
pub(super) stderr_tail: Arc<StderrTail>,
}
pub(super) const STDIO_SHUTDOWN_GRACE: Duration = Duration::from_millis(2_000);
const STDERR_TAIL_CAPACITY: usize = 64;
#[derive(Default)]
pub(super) struct StderrTail {
lines: TokioMutex<VecDeque<String>>,
}
impl StderrTail {
pub(super) fn new() -> Arc<Self> {
Arc::new(Self {
lines: TokioMutex::new(VecDeque::with_capacity(STDERR_TAIL_CAPACITY)),
})
}
pub(super) async fn push(&self, line: String) {
let mut buf = self.lines.lock().await;
if buf.len() >= STDERR_TAIL_CAPACITY {
buf.pop_front();
}
buf.push_back(line);
}
async fn snapshot(&self) -> Vec<String> {
self.lines.lock().await.iter().cloned().collect()
}
}
impl StdioTransport {
pub(super) fn spawn(
server_name: &str,
command: &str,
config: &McpServerConfig,
) -> Result<Self> {
let mut cmd = tokio::process::Command::new(command);
crate::utils::suppress_tokio_console_window(&mut cmd);
cmd.args(&config.args)
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.kill_on_drop(true);
if let Some(cwd) = &config.cwd {
cmd.current_dir(cwd);
}
let expanded_env = super::expand_env_placeholders_map(&config.env, "env")
.with_context(|| format!("MCP server '{server_name}' env expansion failed"))?;
child_env::apply_to_tokio_command_mcp(&mut cmd, child_env::string_map_env(&expanded_env));
let mut child = cmd.spawn().with_context(|| {
let env_keys: Vec<&str> = expanded_env.keys().map(String::as_str).collect();
format!(
"MCP stdio spawn failed (transport=stdio server={server_name} cmd={command:?} args={:?} env_keys={env_keys:?})",
config.args,
)
})?;
let stdin = child.stdin.take().context("Failed to get MCP stdin")?;
let stdout = child.stdout.take().context("Failed to get MCP stdout")?;
let stderr = child.stderr.take().context("Failed to get MCP stderr")?;
let stderr_tail = StderrTail::new();
{
let tail = Arc::clone(&stderr_tail);
tokio::spawn(async move {
let mut lines = tokio::io::BufReader::new(stderr).lines();
while let Ok(Some(line)) = lines.next_line().await {
tail.push(line).await;
}
});
}
Ok(Self {
child,
stdin,
reader: tokio::io::BufReader::new(stdout),
stderr_tail,
})
}
}
async fn format_stderr_context(tail: &StderrTail) -> Option<String> {
let lines = tail.snapshot().await;
if lines.is_empty() {
return None;
}
Some(format!(
"MCP server stderr (last {} line{}):\n{}",
lines.len(),
if lines.len() == 1 { "" } else { "s" },
lines.join("\n"),
))
}
fn send_sigterm(child: &Child) -> bool {
#[cfg(unix)]
{
if let Some(pid) = child.id() {
unsafe {
let _ = libc::kill(pid as i32, libc::SIGTERM);
}
return true;
}
false
}
#[cfg(not(unix))]
{
let _ = child;
false
}
}
#[async_trait::async_trait]
impl McpTransport for StdioTransport {
async fn send(&mut self, mut msg: Vec<u8>) -> Result<()> {
msg.push(b'\n');
self.stdin.write_all(&msg).await?;
self.stdin.flush().await?;
Ok(())
}
async fn recv(&mut self) -> Result<Vec<u8>> {
let mut line = String::new();
loop {
line.clear();
let bytes = match self.reader.read_line(&mut line).await {
Ok(b) => b,
Err(err) => {
if let Some(stderr) = format_stderr_context(&self.stderr_tail).await {
anyhow::bail!("Stdio transport read error: {err}\n{stderr}");
}
return Err(err.into());
}
};
if bytes == 0 {
if let Some(stderr) = format_stderr_context(&self.stderr_tail).await {
anyhow::bail!("Stdio transport closed\n{stderr}");
}
anyhow::bail!("Stdio transport closed");
}
let trimmed = line.trim();
if trimmed.is_empty() {
continue;
}
return Ok(trimmed.as_bytes().to_vec());
}
}
async fn shutdown(&mut self) {
send_sigterm(&self.child);
let _ = tokio::time::timeout(STDIO_SHUTDOWN_GRACE, self.child.wait()).await;
}
}
impl Drop for StdioTransport {
fn drop(&mut self) {
send_sigterm(&self.child);
}
}