use std::path::Path;
use std::sync::mpsc::{self, Receiver, RecvTimeoutError};
use std::sync::atomic::{AtomicI64, Ordering};
use std::sync::Mutex;
use std::time::Duration;
use serde_json::{json, Value};
use cli_stream::{Command, Event, ProcessHandle, Stderr, Stdin};
use crate::node_cli::ResolveCli;
use super::client::McpConnection;
const REQUEST_TIMEOUT: Duration = Duration::from_secs(60);
pub(super) struct StdioConnection {
server: String,
handle: ProcessHandle,
inbox: Mutex<Receiver<Value>>,
next_id: AtomicI64,
}
impl StdioConnection {
pub(super) fn spawn(
server: &str,
command: &str,
args: &[String],
env: &[(String, String)],
cwd: &Path,
) -> Result<StdioConnection, String> {
let (tx, rx) = mpsc::channel();
let spawn = Command::new(command).cwd(cwd).run_id(format!("mcp-{server}"))
.args(args.iter().map(String::as_str))
.env(env.iter().map(|(k, v)| (k.as_str(), v.as_str())))
.stdin(Stdin::Piped)
.stderr(Stderr::Discarded);
let handle = spawn.resolve_cli().stream(move |event| {
if let Event::Stdout { line, .. } = event {
if let Ok(message) = serde_json::from_str::<Value>(line.trim()) {
let _ = tx.send(message);
}
}
})
.map_err(|e| format!("spawning `{command}`: {e}"))?;
Ok(StdioConnection {
server: server.to_owned(),
handle,
inbox: Mutex::new(rx),
next_id: AtomicI64::new(1),
})
}
fn send(&self, message: &Value) -> Result<(), String> {
let line = serde_json::to_string(message).map_err(|e| format!("{}: encoding: {e}", self.server))?;
self.handle.write_line(&line).map_err(|e| format!("{}: write failed: {e}", self.server))
}
}
impl McpConnection for StdioConnection {
fn request(&self, method: &str, params: Value) -> Result<Value, String> {
let id = self.next_id.fetch_add(1, Ordering::Relaxed);
let inbox = self.inbox.lock().map_err(|_| format!("{}: connection poisoned", self.server))?;
self.send(&json!({ "jsonrpc": "2.0", "id": id, "method": method, "params": params }))?;
loop {
match inbox.recv_timeout(REQUEST_TIMEOUT) {
Ok(message) => {
if message.get("id").and_then(Value::as_i64) != Some(id) {
continue; }
if let Some(err) = message.get("error") {
return Err(format!("{} {method}: {err}", self.server));
}
return Ok(message.get("result").cloned().unwrap_or(Value::Null));
}
Err(RecvTimeoutError::Timeout) => return Err(format!("{} {method}: timed out", self.server)),
Err(RecvTimeoutError::Disconnected) => return Err(format!("{} {method}: server closed", self.server)),
}
}
}
fn notify(&self, method: &str, params: Value) -> Result<(), String> {
self.send(&json!({ "jsonrpc": "2.0", "method": method, "params": params }))
}
}
impl Drop for StdioConnection {
fn drop(&mut self) {
let _ = self.handle.cancel();
}
}