use std::io::{self, BufReader};
use std::process::{Command, Stdio};
use std::sync::{Arc, Mutex, PoisonError};
use anyhow::{Context as _, Result};
use super::gate::Gateway;
use crate::cli::McpProxyArgs;
use crate::exit::Code;
use crate::home::Home;
use crate::mcp::relay::{self, Relay, Save};
use crate::mcp::startup::{self, Launch, Startup};
use crate::mcp::tools::{self, ToolPins};
pub fn run(args: &McpProxyArgs) -> Result<Code> {
let home = Home::locate()?;
let cwd = std::env::current_dir().ok();
if let Startup::Refuse(why) = startup::check(&home, &Launch::of(args), cwd.as_deref()) {
return refuse(home, args, &why);
}
let path = tools::path(&home, &args.server);
let pins = ToolPins::load(&path)?;
let trust_first_sight = crate::integrity::refuse_drift(&home, "trust first sight").is_ok()
&& home
.load_policy()
.is_ok_and(|policy| policy.trust_mcp_first_sight());
let save: Save = Box::new(move |p: &ToolPins| p.save(&path));
let gateway = Gateway::new(home, &args.server)?;
let relay = Arc::new(Relay::new(
&args.server,
pins,
trust_first_sight,
Box::new(gateway),
save,
crate::time::now_ms,
));
let mut child = command(args)?
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::inherit())
.spawn()
.with_context(|| format!("starting MCP server `{}`", args.server))?;
let to_server = Arc::new(Mutex::new(Some(
child.stdin.take().context("no pipe to the server")?,
)));
let from_server = child.stdout.take().context("no pipe from the server")?;
let host = Arc::new(Mutex::new(Some(io::stdout())));
let (relay_in, server_in, host_in) = (
Arc::clone(&relay),
Arc::clone(&to_server),
Arc::clone(&host),
);
std::thread::spawn(move || {
let pumped = relay::pump_host(&relay_in, io::stdin().lock(), &*server_in, &*host_in, ¬e);
if let Err(e) = pumped {
note(&format!("{e:#}"));
}
server_in
.lock()
.unwrap_or_else(PoisonError::into_inner)
.take();
});
let pumped = relay::pump_server(
&relay,
BufReader::new(from_server),
&*to_server,
&*host,
¬e,
);
if pumped.is_err() {
let _ = child.kill();
}
let status = child.wait().context("waiting for the MCP server")?;
pumped?;
Ok(if status.success() {
Code::Ok
} else {
Code::Failed
})
}
fn refuse(home: Home, args: &McpProxyArgs, why: &str) -> Result<Code> {
let message = format!("OpenMoat did not start MCP server `{}`: {why}", args.server);
note(&message);
let recorded = Gateway::new(home, &args.server).and_then(|g| g.refused_start(&message));
if let Err(e) = recorded {
note(&format!(
"the refusal could not be recorded in the audit log: {e:#}"
));
}
let host = Mutex::new(Some(io::stdout()));
relay::refuse_all(io::stdin().lock(), &host, &crate::render::clean(&message))?;
Ok(Code::Failed)
}
fn note(why: &str) {
eprintln!("openmoat mcp proxy: {}", crate::render::clean(why));
}
fn command(args: &McpProxyArgs) -> Result<Command> {
let (program, rest) = args.command.split_first().context("no server command")?;
if !args.sandbox {
let mut command = Command::new(program);
command.args(rest);
return Ok(command);
}
let moat = std::env::current_exe().context("locating the moat binary")?;
let mut command = Command::new(moat);
command.arg("run");
for write in &args.writes {
command.arg("--write").arg(write);
}
command.arg("--").arg(program).args(rest);
Ok(command)
}