1use std::path::PathBuf;
5use std::sync::atomic::{AtomicBool, Ordering};
6use std::sync::{Arc, Mutex};
7use std::time::Duration;
8
9use serde_json::Value;
10use tokio::io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt};
11use tokio::process::ChildStdin;
12use tokio::sync::watch;
13use yapi_types::sync::lock;
14
15use super::jsonrpc::McpError;
16use super::transport::{Event, Events, MAX_MESSAGE_BYTES};
17
18const MAX_STDERR_BYTES: usize = 64 * 1024;
19const STDIN_CLOSE_GRACE: Duration = Duration::from_millis(500);
21const CLOSE_TIMEOUT: Duration = Duration::from_millis(2000);
23
24#[derive(Clone, Debug, Default, PartialEq)]
26pub struct StdioOptions {
27 pub command: String,
29 pub args: Vec<String>,
31 pub cwd: PathBuf,
33 pub env: Vec<(String, String)>,
35}
36
37pub struct StdioTransport {
39 options: StdioOptions,
40 stdin: tokio::sync::Mutex<Option<ChildStdin>>,
41 pid: Mutex<Option<u32>>,
42 exited: Mutex<Option<watch::Receiver<bool>>>,
43 stderr: Arc<Mutex<Vec<u8>>>,
44 closed: AtomicBool,
45}
46
47impl StdioTransport {
48 pub fn new(options: StdioOptions) -> StdioTransport {
50 StdioTransport {
51 options,
52 stdin: tokio::sync::Mutex::new(None),
53 pid: Mutex::new(None),
54 exited: Mutex::new(None),
55 stderr: Arc::new(Mutex::new(Vec::new())),
56 closed: AtomicBool::new(false),
57 }
58 }
59
60 pub fn stderr(&self) -> String {
62 String::from_utf8_lossy(&lock(&self.stderr)).into_owned()
63 }
64
65 pub async fn start(&self, events: Events) -> Result<(), McpError> {
67 if self.closed.load(Ordering::SeqCst) {
68 return Err(McpError::closed());
69 }
70 let mut command = tokio::process::Command::new(&self.options.command);
71 command
72 .args(&self.options.args)
73 .current_dir(&self.options.cwd)
74 .envs(crate::config::child_env())
75 .envs(self.options.env.iter().map(|(key, value)| (key, value)))
76 .stdin(std::process::Stdio::piped())
77 .stdout(std::process::Stdio::piped())
78 .stderr(std::process::Stdio::piped());
79 #[cfg(unix)]
81 command.process_group(0);
82 let mut child = command.spawn().map_err(|error| {
83 if error.kind() == std::io::ErrorKind::NotFound {
84 McpError::Other(format!("spawn {} ENOENT", self.options.command))
85 } else {
86 McpError::Other(error.to_string())
87 }
88 })?;
89 *lock(&self.pid) = child.id();
90 *self.stdin.lock().await = child.stdin.take();
91 let (exit_tx, exit_rx) = watch::channel(false);
92 *lock(&self.exited) = Some(exit_rx.clone());
93
94 if let Some(stderr) = child.stderr.take() {
95 let tail = Arc::clone(&self.stderr);
96 tokio::spawn(async move {
97 let mut stderr = stderr;
98 let mut buffer = [0u8; 4096];
99 while let Ok(count) = stderr.read(&mut buffer).await {
100 if count == 0 {
101 break;
102 }
103 let mut tail = lock(&tail);
104 tail.extend_from_slice(&buffer[..count]);
105 if tail.len() > MAX_STDERR_BYTES {
106 let excess = tail.len() - MAX_STDERR_BYTES;
107 tail.drain(..excess);
108 }
109 }
110 });
111 }
112 if let Some(stdout) = child.stdout.take() {
113 tokio::spawn(read_messages(stdout, events, exit_rx));
114 }
115 tokio::spawn(async move {
116 let _ = child.wait().await;
117 let _ = exit_tx.send(true);
118 });
119 Ok(())
120 }
121
122 pub async fn send(&self, message: &Value) -> Result<(), McpError> {
124 if self.closed.load(Ordering::SeqCst) {
125 return Err(McpError::closed());
126 }
127 let mut stdin = self.stdin.lock().await;
128 let stdin = stdin.as_mut().ok_or_else(McpError::closed)?;
129 let line = yapi_types::json::stringify(message) + "\n";
130 let failed = |error: std::io::Error| {
132 if error.kind() == std::io::ErrorKind::BrokenPipe {
133 McpError::Other("write EPIPE".into())
134 } else {
135 McpError::closed()
136 }
137 };
138 stdin.write_all(line.as_bytes()).await.map_err(failed)?;
139 stdin.flush().await.map_err(failed)
140 }
141
142 pub async fn close(&self) {
145 if self.closed.swap(true, Ordering::SeqCst) {
146 return;
147 }
148 let Some(mut exited) = lock(&self.exited).clone() else {
149 drop(self.stdin.lock().await.take());
150 return;
151 };
152 let pid = *lock(&self.pid);
153 let wait = async {
156 drop(self.stdin.lock().await.take());
157 let _ = exited.wait_for(|done| *done).await;
158 };
159 tokio::pin!(wait);
160 if tokio::time::timeout(STDIN_CLOSE_GRACE, &mut wait)
161 .await
162 .is_err()
163 {
164 #[cfg(unix)]
165 crate::tools::bash::signal_tree(pid, rustix::process::Signal::TERM);
166 if tokio::time::timeout(CLOSE_TIMEOUT, &mut wait)
167 .await
168 .is_err()
169 {
170 #[cfg(unix)]
171 crate::tools::bash::signal_tree(pid, rustix::process::Signal::KILL);
172 wait.await;
173 }
174 }
175 #[cfg(unix)]
176 crate::tools::bash::signal_tree(pid, rustix::process::Signal::TERM);
177 #[cfg(not(unix))]
178 let _ = pid;
179 }
180}
181
182async fn read_messages(
185 stdout: tokio::process::ChildStdout,
186 events: Events,
187 mut exited: watch::Receiver<bool>,
188) {
189 let mut reader = tokio::io::BufReader::new(stdout);
190 let mut line = Vec::new();
191 let mut incomplete = false;
192 let mut skipping = false;
194 loop {
195 line.clear();
196 let read = (&mut reader)
197 .take(MAX_MESSAGE_BYTES as u64 + 1)
198 .read_until(b'\n', &mut line)
199 .await;
200 match read {
201 Ok(0) | Err(_) => break,
202 Ok(_) => {}
203 }
204 if skipping {
205 skipping = !line.ends_with(b"\n");
206 continue;
207 }
208 if !line.ends_with(b"\n") {
209 if line.len() > MAX_MESSAGE_BYTES {
210 let _ = events.send(Event::Error(format!(
211 "MCP stdio message exceeds {MAX_MESSAGE_BYTES} bytes"
212 )));
213 skipping = true;
214 continue;
215 }
216 incomplete = !String::from_utf8_lossy(&line).trim().is_empty();
217 break;
218 }
219 let text = String::from_utf8_lossy(&line);
220 let text = text.trim_end_matches('\n').trim_end_matches('\r');
221 if text.trim().is_empty() {
222 continue;
223 }
224 let event = match serde_json::from_str::<Value>(text) {
225 Ok(message) if super::jsonrpc::classify(&message).is_some() => Event::Message(message),
226 Ok(_) => Event::Error("Invalid JSON-RPC message".into()),
227 Err(error) => Event::Error(error.to_string()),
228 };
229 let _ = events.send(event);
230 }
231 let _ = exited.wait_for(|done| *done).await;
232 if incomplete {
233 let _ = events.send(Event::Error(
234 "MCP stdio server closed with an incomplete JSON-RPC message".into(),
235 ));
236 }
237 let _ = events.send(Event::Closed);
238}