tower_mcp/client/
stdio.rs1use std::process::Stdio;
19
20use async_trait::async_trait;
21use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
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: BufReader<tokio::process::ChildStdout>,
37}
38
39impl StdioClientTransport {
40 pub async fn spawn(program: &str, args: &[&str]) -> Result<Self> {
47 let mut cmd = Command::new(program);
48 cmd.args(args);
49 Self::spawn_command(&mut cmd).await
50 }
51
52 pub async fn spawn_command(cmd: &mut Command) -> Result<Self> {
75 cmd.stdin(Stdio::piped()).stdout(Stdio::piped());
76
77 let mut child = cmd
78 .spawn()
79 .map_err(|e| Error::Transport(format!("Failed to spawn process: {}", e)))?;
80
81 let stdin = child
82 .stdin
83 .take()
84 .ok_or_else(|| Error::Transport("Failed to get child stdin".to_string()))?;
85 let stdout = child
86 .stdout
87 .take()
88 .ok_or_else(|| Error::Transport("Failed to get child stdout".to_string()))?;
89
90 tracing::info!("Spawned MCP server process");
91
92 Ok(Self {
93 child: Some(child),
94 stdin: Some(stdin),
95 stdout: BufReader::new(stdout),
96 })
97 }
98
99 pub fn take_stderr(&mut self) -> Option<tokio::process::ChildStderr> {
105 self.child.as_mut()?.stderr.take()
106 }
107
108 pub fn from_child(mut child: Child) -> Result<Self> {
112 let stdin = child
113 .stdin
114 .take()
115 .ok_or_else(|| Error::Transport("Failed to get child stdin".to_string()))?;
116 let stdout = child
117 .stdout
118 .take()
119 .ok_or_else(|| Error::Transport("Failed to get child stdout".to_string()))?;
120
121 Ok(Self {
122 child: Some(child),
123 stdin: Some(stdin),
124 stdout: BufReader::new(stdout),
125 })
126 }
127}
128
129#[async_trait]
130impl ClientTransport for StdioClientTransport {
131 async fn send(&mut self, message: &str) -> Result<()> {
132 let stdin = self
133 .stdin
134 .as_mut()
135 .ok_or_else(|| Error::Transport("Transport closed".to_string()))?;
136
137 stdin
138 .write_all(message.as_bytes())
139 .await
140 .map_err(|e| Error::Transport(format!("Failed to write: {}", e)))?;
141 stdin
142 .write_all(b"\n")
143 .await
144 .map_err(|e| Error::Transport(format!("Failed to write newline: {}", e)))?;
145 stdin
146 .flush()
147 .await
148 .map_err(|e| Error::Transport(format!("Failed to flush: {}", e)))?;
149 Ok(())
150 }
151
152 async fn recv(&mut self) -> Result<Option<String>> {
153 let mut line = String::new();
154 let bytes = self
155 .stdout
156 .read_line(&mut line)
157 .await
158 .map_err(|e| Error::Transport(format!("Failed to read: {}", e)))?;
159
160 if bytes == 0 {
161 return Ok(None); }
163
164 Ok(Some(line.trim().to_string()))
165 }
166
167 fn is_connected(&self) -> bool {
168 self.child.is_some() && self.stdin.is_some()
169 }
170
171 async fn close(&mut self) -> Result<()> {
172 self.stdin.take();
174
175 if let Some(mut child) = self.child.take() {
176 let result =
177 tokio::time::timeout(std::time::Duration::from_secs(5), child.wait()).await;
178
179 match result {
180 Ok(Ok(status)) => {
181 tracing::info!(status = ?status, "Child process exited");
182 }
183 Ok(Err(e)) => {
184 tracing::error!(error = %e, "Error waiting for child");
185 }
186 Err(_) => {
187 tracing::warn!("Timeout waiting for child, killing");
188 let _ = child.kill().await;
189 }
190 }
191 }
192
193 Ok(())
194 }
195}
196
197#[cfg(test)]
198mod tests {
199 use super::*;
200
201 #[tokio::test]
202 async fn test_spawn_nonexistent_program() {
203 let result = StdioClientTransport::spawn("nonexistent-program-xyz", &[]).await;
204 assert!(result.is_err());
205 }
206
207 #[tokio::test]
208 async fn test_send_and_recv_via_cat() {
209 let mut transport = StdioClientTransport::spawn("cat", &[]).await.unwrap();
211
212 assert!(transport.is_connected());
213
214 let msg = r#"{"jsonrpc":"2.0","id":1,"method":"test"}"#;
216 transport.send(msg).await.unwrap();
217
218 let received = transport.recv().await.unwrap();
220 assert_eq!(received.as_deref(), Some(msg));
221 }
222
223 #[tokio::test]
224 async fn test_close_signals_eof() {
225 let mut transport = StdioClientTransport::spawn("cat", &[]).await.unwrap();
226 assert!(transport.is_connected());
227
228 transport.close().await.unwrap();
229 assert!(!transport.is_connected());
230 }
231
232 #[tokio::test]
233 async fn test_recv_returns_none_on_eof() {
234 let mut transport = StdioClientTransport::spawn("true", &[]).await.unwrap();
236
237 let result = transport.recv().await.unwrap();
239 assert_eq!(result, None);
240 }
241
242 #[tokio::test]
243 async fn test_send_after_close_fails() {
244 let mut transport = StdioClientTransport::spawn("cat", &[]).await.unwrap();
245 transport.close().await.unwrap();
246
247 let result = transport.send("hello").await;
248 assert!(result.is_err());
249 }
250
251 #[tokio::test]
252 async fn test_spawn_command_with_env() {
253 let mut cmd = Command::new("sh");
254 cmd.args(["-c", "echo $TEST_VAR"]);
255 cmd.env("TEST_VAR", "hello_from_test");
256
257 let mut transport = StdioClientTransport::spawn_command(&mut cmd).await.unwrap();
258
259 let received = transport.recv().await.unwrap();
260 assert_eq!(received.as_deref(), Some("hello_from_test"));
261 }
262
263 #[tokio::test]
264 async fn test_spawn_command_preserves_piped_stderr() {
265 let mut cmd = Command::new("sh");
266 cmd.args(["-c", "echo diagnostic >&2"]);
267 cmd.stderr(Stdio::piped());
268
269 let mut transport = StdioClientTransport::spawn_command(&mut cmd).await.unwrap();
270 let stderr = transport
271 .take_stderr()
272 .expect("spawn_command must not replace piped stderr");
273 let mut stderr = BufReader::new(stderr);
274 let mut line = String::new();
275 stderr.read_line(&mut line).await.unwrap();
276
277 assert_eq!(line.trim(), "diagnostic");
278 assert!(transport.take_stderr().is_none());
279 }
280
281 #[tokio::test]
282 async fn test_multiple_send_recv_roundtrips() {
283 let mut transport = StdioClientTransport::spawn("cat", &[]).await.unwrap();
284
285 for i in 0..5 {
286 let msg = format!(r#"{{"id":{i},"msg":"test"}}"#);
287 transport.send(&msg).await.unwrap();
288 let received = transport.recv().await.unwrap();
289 assert_eq!(received.as_deref(), Some(msg.as_str()));
290 }
291
292 transport.close().await.unwrap();
293 }
294}