//! MCP transport layer — manages child-process and HTTP-based transport //! for connecting to MCP servers. use std::{ io::{Read, Write}, process::{Child, ChildStdin, ChildStdout, Command, Stdio}, }; /// A running MCP server process connected via stdio. /// /// Holds the child process handle plus the piped stdin/stdout streams /// so callers can send JSON-RPC messages and read responses. pub struct McpTransport { process: Option, stdin: Option, stdout: Option, } impl McpTransport { /// Spawn a child process as an MCP server over stdio. /// /// The command is passed to `sh -c` so shell syntax (pipes, redirects, etc.) /// works naturally. Stderr is discarded to avoid corrupting a TUI that may /// be running in the same terminal. pub fn start_child_process(name: &str, command: &str) -> anyhow::Result { tracing::info!("starting MCP transport '{name}': {command}"); let mut child = Command::new("sh") .arg("-c") .arg(command) .stdin(Stdio::piped()) .stdout(Stdio::piped()) .stderr(Stdio::null()) .spawn()?; let stdin = child.stdin.take(); let stdout = child.stdout.take(); Ok(McpTransport { process: Some(child), stdin, stdout, }) } /// Write raw bytes to the child's stdin. pub fn send(&mut self, data: &[u8]) -> anyhow::Result<()> { if let Some(ref mut stdin) = self.stdin { stdin.write_all(data)?; stdin.flush()?; } Ok(()) } /// Read from the child's stdout into the provided buffer. /// /// Returns `Ok(Some(n))` with the number of bytes read, /// `Ok(None)` on EOF, or `Err` on I/O errors. pub fn receive(&mut self, buf: &mut [u8]) -> anyhow::Result> { match self.stdout.as_mut() { Some(stdout) => match stdout.read(buf) { Ok(0) => Ok(None), Ok(n) => Ok(Some(n)), Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock => Ok(None), Err(e) => Err(e.into()), }, None => Ok(None), } } /// Gracefully shut down the child by closing stdin (sending EOF) and /// then killing the process. pub fn kill(&mut self) { // Close stdin first to signal EOF to the MCP server. let _ = self.stdin.take(); if let Some(ref mut child) = self.process { let _ = child.kill(); let _ = child.wait(); } } /// Check whether the child process is still running. pub fn is_running(&mut self) -> bool { self.process .as_mut() .is_some_and(|c| matches!(c.try_wait(), Ok(None))) } /// Stop the child process. This is the public API alias for `kill`. pub fn stop(&mut self) -> anyhow::Result<()> { self.kill(); Ok(()) } } impl Drop for McpTransport { fn drop(&mut self) { self.kill(); } }