use std::process::Stdio;
use std::sync::Arc;
use kvlar_audit::AuditLogger;
use kvlar_core::Engine;
use tokio::io::BufReader;
use tokio::process::Command;
use tokio::sync::Mutex;
use crate::handler;
pub struct StdioTransport {
engine: Arc<Mutex<Engine>>,
audit: Arc<Mutex<AuditLogger>>,
command: String,
args: Vec<String>,
fail_open: bool,
}
impl StdioTransport {
pub fn new(
engine: Engine,
audit: AuditLogger,
command: String,
args: Vec<String>,
fail_open: bool,
) -> Self {
Self {
engine: Arc::new(Mutex::new(engine)),
audit: Arc::new(Mutex::new(audit)),
command,
args,
fail_open,
}
}
pub async fn run(&self) -> Result<(), Box<dyn std::error::Error>> {
tracing::info!(
command = %self.command,
args = ?self.args,
"spawning upstream MCP server"
);
let mut child = Command::new(&self.command)
.args(&self.args)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::inherit()) .spawn()
.map_err(|e| format!("failed to spawn upstream command '{}': {}", self.command, e))?;
let child_stdin = child.stdin.take().ok_or("failed to capture child stdin")?;
let child_stdout = child
.stdout
.take()
.ok_or("failed to capture child stdout")?;
let client_reader = BufReader::new(tokio::io::stdin());
let client_writer = tokio::io::stdout();
let upstream_writer = child_stdin;
let upstream_reader = BufReader::new(child_stdout);
tracing::info!("stdio proxy running");
let result = handler::run_proxy_loop(
client_reader,
Arc::new(Mutex::new(client_writer)),
upstream_reader,
Arc::new(Mutex::new(upstream_writer)),
self.engine.clone(),
self.audit.clone(),
self.fail_open,
)
.await;
tracing::info!("proxy loop ended, waiting for child process");
let _ = child.kill().await;
let _ = child.wait().await;
result.map_err(|e| -> Box<dyn std::error::Error> { e })
}
}