tower_mcp/client/
stdio.rs1use std::process::Stdio;
19
20use async_trait::async_trait;
21use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader, Lines};
22use tokio::process::{Child, Command};
23
24use super::transport::ClientTransport;
25use crate::error::{Error, Result};
26
27pub struct StdioClientTransport {
34 child: Option<Child>,
35 stdin: Option<tokio::process::ChildStdin>,
36 stdout: Lines<BufReader<tokio::process::ChildStdout>>,
41}
42
43impl StdioClientTransport {
44 pub async fn spawn(program: &str, args: &[&str]) -> Result<Self> {
51 let mut cmd = Command::new(program);
52 cmd.args(args);
53 Self::spawn_command(&mut cmd).await
54 }
55
56 pub async fn spawn_command(cmd: &mut Command) -> Result<Self> {
79 cmd.stdin(Stdio::piped()).stdout(Stdio::piped());
80
81 let mut child = cmd
82 .spawn()
83 .map_err(|e| Error::Transport(format!("Failed to spawn process: {}", e)))?;
84
85 let stdin = child
86 .stdin
87 .take()
88 .ok_or_else(|| Error::Transport("Failed to get child stdin".to_string()))?;
89 let stdout = child
90 .stdout
91 .take()
92 .ok_or_else(|| Error::Transport("Failed to get child stdout".to_string()))?;
93
94 tracing::info!("Spawned MCP server process");
95
96 Ok(Self {
97 child: Some(child),
98 stdin: Some(stdin),
99 stdout: BufReader::new(stdout).lines(),
100 })
101 }
102
103 pub fn take_stderr(&mut self) -> Option<tokio::process::ChildStderr> {
109 self.child.as_mut()?.stderr.take()
110 }
111
112 pub fn from_child(mut child: Child) -> Result<Self> {
116 let stdin = child
117 .stdin
118 .take()
119 .ok_or_else(|| Error::Transport("Failed to get child stdin".to_string()))?;
120 let stdout = child
121 .stdout
122 .take()
123 .ok_or_else(|| Error::Transport("Failed to get child stdout".to_string()))?;
124
125 Ok(Self {
126 child: Some(child),
127 stdin: Some(stdin),
128 stdout: BufReader::new(stdout).lines(),
129 })
130 }
131}
132
133#[async_trait]
134impl ClientTransport for StdioClientTransport {
135 async fn send(&mut self, message: &str) -> Result<()> {
136 let stdin = self
137 .stdin
138 .as_mut()
139 .ok_or_else(|| Error::Transport("Transport closed".to_string()))?;
140
141 stdin
142 .write_all(message.as_bytes())
143 .await
144 .map_err(|e| Error::Transport(format!("Failed to write: {}", e)))?;
145 stdin
146 .write_all(b"\n")
147 .await
148 .map_err(|e| Error::Transport(format!("Failed to write newline: {}", e)))?;
149 stdin
150 .flush()
151 .await
152 .map_err(|e| Error::Transport(format!("Failed to flush: {}", e)))?;
153 Ok(())
154 }
155
156 async fn recv(&mut self) -> Result<Option<String>> {
157 let line = self
158 .stdout
159 .next_line()
160 .await
161 .map_err(|e| Error::Transport(format!("Failed to read: {}", e)))?;
162 Ok(line.map(|line| line.trim().to_string()))
163 }
164
165 fn is_connected(&self) -> bool {
166 self.child.is_some() && self.stdin.is_some()
167 }
168
169 async fn close(&mut self) -> Result<()> {
170 self.stdin.take();
172
173 if let Some(mut child) = self.child.take() {
174 let result =
175 tokio::time::timeout(std::time::Duration::from_secs(5), child.wait()).await;
176
177 match result {
178 Ok(Ok(status)) => {
179 tracing::info!(status = ?status, "Child process exited");
180 }
181 Ok(Err(e)) => {
182 tracing::error!(error = %e, "Error waiting for child");
183 }
184 Err(_) => {
185 tracing::warn!("Timeout waiting for child, killing");
186 let _ = child.kill().await;
187 }
188 }
189 }
190
191 Ok(())
192 }
193}
194
195#[cfg(test)]
196mod tests {
197 use super::*;
198
199 #[tokio::test]
200 async fn test_spawn_nonexistent_program() {
201 let result = StdioClientTransport::spawn("nonexistent-program-xyz", &[]).await;
202 assert!(result.is_err());
203 }
204
205 #[tokio::test]
206 async fn test_send_and_recv_via_cat() {
207 let mut transport = StdioClientTransport::spawn("cat", &[]).await.unwrap();
209
210 assert!(transport.is_connected());
211
212 let msg = r#"{"jsonrpc":"2.0","id":1,"method":"test"}"#;
214 transport.send(msg).await.unwrap();
215
216 let received = transport.recv().await.unwrap();
218 assert_eq!(received.as_deref(), Some(msg));
219 }
220
221 #[tokio::test]
222 async fn test_close_signals_eof() {
223 let mut transport = StdioClientTransport::spawn("cat", &[]).await.unwrap();
224 assert!(transport.is_connected());
225
226 transport.close().await.unwrap();
227 assert!(!transport.is_connected());
228 }
229
230 #[tokio::test]
231 async fn test_recv_returns_none_on_eof() {
232 let mut transport = StdioClientTransport::spawn("true", &[]).await.unwrap();
234
235 let result = transport.recv().await.unwrap();
237 assert_eq!(result, None);
238 }
239
240 #[tokio::test]
241 async fn line_reader_preserves_partial_frame_when_receive_is_cancelled() {
242 let (mut writer, reader) = tokio::io::duplex(256);
243 let mut lines = BufReader::new(reader).lines();
244 let frame = r#"{"jsonrpc":"2.0","id":2,"result":{"tools":[]}}"#;
245
246 writer.write_all(frame.as_bytes()).await.unwrap();
247 assert!(
248 tokio::time::timeout(std::time::Duration::from_millis(10), lines.next_line())
249 .await
250 .is_err(),
251 "a partial frame must remain pending until its newline arrives"
252 );
253
254 writer.write_all(b"\n").await.unwrap();
255 assert_eq!(lines.next_line().await.unwrap().as_deref(), Some(frame));
256 }
257
258 #[tokio::test]
259 async fn test_send_after_close_fails() {
260 let mut transport = StdioClientTransport::spawn("cat", &[]).await.unwrap();
261 transport.close().await.unwrap();
262
263 let result = transport.send("hello").await;
264 assert!(result.is_err());
265 }
266
267 #[tokio::test]
268 async fn test_spawn_command_with_env() {
269 let mut cmd = Command::new("sh");
270 cmd.args(["-c", "echo $TEST_VAR"]);
271 cmd.env("TEST_VAR", "hello_from_test");
272
273 let mut transport = StdioClientTransport::spawn_command(&mut cmd).await.unwrap();
274
275 let received = transport.recv().await.unwrap();
276 assert_eq!(received.as_deref(), Some("hello_from_test"));
277 }
278
279 #[tokio::test]
280 async fn test_spawn_command_preserves_piped_stderr() {
281 let mut cmd = Command::new("sh");
282 cmd.args(["-c", "echo diagnostic >&2"]);
283 cmd.stderr(Stdio::piped());
284
285 let mut transport = StdioClientTransport::spawn_command(&mut cmd).await.unwrap();
286 let stderr = transport
287 .take_stderr()
288 .expect("spawn_command must not replace piped stderr");
289 let mut stderr = BufReader::new(stderr);
290 let mut line = String::new();
291 stderr.read_line(&mut line).await.unwrap();
292
293 assert_eq!(line.trim(), "diagnostic");
294 assert!(transport.take_stderr().is_none());
295 }
296
297 #[tokio::test]
298 async fn test_multiple_send_recv_roundtrips() {
299 let mut transport = StdioClientTransport::spawn("cat", &[]).await.unwrap();
300
301 for i in 0..5 {
302 let msg = format!(r#"{{"id":{i},"msg":"test"}}"#);
303 transport.send(&msg).await.unwrap();
304 let received = transport.recv().await.unwrap();
305 assert_eq!(received.as_deref(), Some(msg.as_str()));
306 }
307
308 transport.close().await.unwrap();
309 }
310}