use crate::core::platform::container::arsenal::ArsenalError;
use crate::infrastructure::adapters::arsenal::mcp_protocol::{MCPMessage, MCPTransport};
use async_trait::async_trait;
use serde_json;
use std::process::Stdio;
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
use tokio::process::{Child, ChildStdin, ChildStdout, Command};
use tokio::sync::Mutex;
#[doc(hidden)]
pub struct MCPStdioAdapter {
command: String,
args: Vec<String>,
process: Option<Child>,
stdin: Option<Mutex<ChildStdin>>,
stdout: Option<Mutex<BufReader<ChildStdout>>>,
}
impl MCPStdioAdapter {
pub fn new(command: impl Into<String>, args: Vec<impl Into<String>>) -> Self {
Self {
command: command.into(),
args: args.into_iter().map(|a| a.into()).collect(),
process: None,
stdin: None,
stdout: None,
}
}
pub async fn connect(&mut self) -> Result<(), ArsenalError> {
let mut child = Command::new(&self.command)
.args(&self.args)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.kill_on_drop(true)
.spawn()
.map_err(|e| ArsenalError::TransportError(format!("Failed to spawn process: {}", e)))?;
let stdin = child
.stdin
.take()
.ok_or_else(|| ArsenalError::TransportError("Failed to capture stdin".to_string()))?;
let stdout = child
.stdout
.take()
.ok_or_else(|| ArsenalError::TransportError("Failed to capture stdout".to_string()))?;
self.stdin = Some(Mutex::new(stdin));
self.stdout = Some(Mutex::new(BufReader::new(stdout)));
self.process = Some(child);
Ok(())
}
pub fn is_connected(&self) -> bool {
self.process.is_some()
}
}
#[async_trait]
impl MCPTransport for MCPStdioAdapter {
async fn send(&mut self, message: &MCPMessage) -> Result<(), ArsenalError> {
let stdin = self.stdin.as_ref().ok_or_else(|| {
ArsenalError::TransportError("Not connected - call connect() first".to_string())
})?;
let json = serde_json::to_string(message).map_err(|e| {
ArsenalError::TransportError(format!("Failed to serialize message: {}", e))
})?;
let mut stdin_lock = stdin.lock().await;
stdin_lock.write_all(json.as_bytes()).await.map_err(|e| {
ArsenalError::TransportError(format!("Failed to write to stdin: {}", e))
})?;
stdin_lock
.write_all(b"\n")
.await
.map_err(|e| ArsenalError::TransportError(format!("Failed to write newline: {}", e)))?;
stdin_lock
.flush()
.await
.map_err(|e| ArsenalError::TransportError(format!("Failed to flush stdin: {}", e)))?;
Ok(())
}
async fn receive(&mut self) -> Result<MCPMessage, ArsenalError> {
let stdout = self.stdout.as_ref().ok_or_else(|| {
ArsenalError::TransportError("Not connected - call connect() first".to_string())
})?;
let mut stdout_lock = stdout.lock().await;
let mut line = String::new();
stdout_lock.read_line(&mut line).await.map_err(|e| {
ArsenalError::TransportError(format!("Failed to read from stdout: {}", e))
})?;
if line.is_empty() {
return Err(ArsenalError::TransportError(
"EOF reached - process terminated".to_string(),
));
}
let message: MCPMessage = serde_json::from_str(&line).map_err(|e| {
ArsenalError::TransportError(format!("Failed to deserialize message: {}", e))
})?;
Ok(message)
}
}
impl Drop for MCPStdioAdapter {
fn drop(&mut self) {
if let Some(mut process) = self.process.take() {
let _ = process.start_kill();
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_adapter_creation() {
let adapter = MCPStdioAdapter::new("echo", vec!["test"]);
assert_eq!(adapter.command, "echo");
assert_eq!(adapter.args, vec!["test"]);
assert!(!adapter.is_connected());
}
#[test]
fn test_adapter_not_connected() {
let adapter = MCPStdioAdapter::new("echo", vec!["test"]);
assert!(!adapter.is_connected());
}
}