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_bytes: Vec<u8> = Vec::new();
loop {
let bytes = match read_line_capped(
&mut self.reader,
&mut line_bytes,
super::MAX_MCP_RESPONSE_BYTES,
)
.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 line = String::from_utf8_lossy(&line_bytes);
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);
}
}
async fn read_line_capped<R>(
reader: &mut R,
out: &mut Vec<u8>,
max: usize,
) -> std::io::Result<usize>
where
R: tokio::io::AsyncBufRead + Unpin,
{
use tokio::io::AsyncBufReadExt;
out.clear();
loop {
let (chunk, consumed, done) = {
let available = reader.fill_buf().await?;
if available.is_empty() {
(Vec::new(), 0usize, true)
} else if let Some(pos) = available.iter().position(|&b| b == b'\n') {
(available[..=pos].to_vec(), pos + 1, true)
} else {
(available.to_vec(), available.len(), false)
}
};
if consumed > 0 {
reader.consume(consumed);
}
out.extend_from_slice(&chunk);
if done {
break;
}
if out.len() > max {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!("MCP stdio line exceeded {max} bytes without a newline"),
));
}
}
Ok(out.len())
}
#[cfg(test)]
mod read_cap_tests {
use super::read_line_capped;
#[tokio::test]
async fn reads_a_line_and_reports_eof() {
let data = b"hello\nworld\n".to_vec();
let mut reader = tokio::io::BufReader::new(std::io::Cursor::new(data));
let mut out = Vec::new();
assert_eq!(
read_line_capped(&mut reader, &mut out, 1024).await.unwrap(),
6
);
assert_eq!(out, b"hello\n");
assert_eq!(
read_line_capped(&mut reader, &mut out, 1024).await.unwrap(),
6
);
assert_eq!(out, b"world\n");
assert_eq!(
read_line_capped(&mut reader, &mut out, 1024).await.unwrap(),
0
);
}
#[tokio::test]
async fn aborts_on_newline_free_line_over_cap() {
let data = vec![b'x'; 4096]; let mut reader = tokio::io::BufReader::new(std::io::Cursor::new(data));
let mut out = Vec::new();
let err = read_line_capped(&mut reader, &mut out, 1024)
.await
.unwrap_err();
assert_eq!(err.kind(), std::io::ErrorKind::InvalidData);
}
}