use std::path::{Path, PathBuf};
use anyhow::Result;
use openmoat_core::{Decision, Verdict};
use openmoat_hosts::Host;
use crate::home::Home;
use crate::install::HostConfig;
use crate::integrity::Lock;
pub mod outside;
pub mod pins;
pub mod relay;
pub mod servers;
pub mod startup;
pub mod tools;
pub mod wrap;
pub use pins::{Change, Pins};
pub const SERVER_RULE: &str = "mcp-server-changed";
pub fn set_up_hosts(home: &Home, binary: &Path) -> Vec<Host> {
Host::ALL
.into_iter()
.filter(|h| HostConfig::for_host(home, *h).is_ok_and(|c| c.set_up(binary)))
.collect()
}
#[derive(Debug, Default)]
pub struct Review {
pub pinned: bool,
pub pin_count: usize,
pub changes: Vec<Change>,
pub unreadable: Vec<(PathBuf, String)>,
}
pub fn review(home: &Home, lock: &Lock, hosts: &[Host], cwd: Option<&Path>) -> Result<Review> {
let mut sources = Vec::new();
for host in hosts {
sources.extend(servers::sources(home, *host, cwd)?);
}
let scan = servers::scan(&sources);
let empty = Pins::new();
let pins = lock.mcp_servers.as_ref().unwrap_or(&empty);
Ok(Review {
pinned: lock.mcp_servers.is_some(),
pin_count: pins.len(),
changes: pins::changes(pins, &scan),
unreadable: scan.unreadable,
})
}
pub fn violation(
home: &Home,
lock: &Lock,
host: Host,
cwd: Option<&Path>,
tool: &str,
) -> Result<Option<Decision>> {
if lock.mcp_servers.is_none() {
return Ok(None);
}
let found = review(home, lock, &[host], cwd)?;
let mut reasons: Vec<String> = found
.unreadable
.iter()
.map(|(path, why)| {
format!(
"{} cannot be read ({why}), so OpenMoat cannot tell which MCP servers it starts",
path.display()
)
})
.collect();
let mut names = Vec::new();
for change in found
.changes
.iter()
.filter(|c| c.blocks() && tool_of(host, tool, &c.name))
{
reasons.push(change.describe());
names.push(change.name.clone());
}
if reasons.is_empty() {
return Ok(None);
}
let accept = names.first().map_or_else(
|| "fix the file, then run `openmoat mcp accept` in a terminal".to_owned(),
|name| format!("a person must review it: run `openmoat mcp accept {name}` in a terminal"),
);
reasons.push(accept);
let mut decision = Decision::new(Verdict::Deny);
decision.rules.push(SERVER_RULE.to_owned());
decision.reasons = reasons;
Ok(Some(decision))
}
pub fn tool_of(host: Host, tool: &str, name: &str) -> bool {
pins::tool_of(tool, name)
|| host
.spec()
.mcp
.as_ref()
.is_some_and(|mcp| mcp.tool_names.tool_of(tool, name))
}
pub fn accept(home: &Home, changes: &[Change]) -> Result<()> {
let path = home.lock_path();
let mut lock = Lock::load(&path)?;
let pins = lock.mcp_servers.get_or_insert_with(Pins::new);
pins::apply(pins, changes);
lock.save(&path)
}
pub fn accept_ids(
home: &Home,
hosts: &[Host],
ids: &std::collections::BTreeSet<String>,
) -> Result<()> {
let lock = Lock::load(&home.lock_path())?;
if lock.mcp_servers.is_none() {
return Ok(());
}
let found = review(home, &lock, hosts, None)?;
let chosen: Vec<Change> = found
.changes
.into_iter()
.filter(|c| ids.contains(&c.id))
.collect();
accept(home, &chosen)
}
#[derive(Debug, Default)]
pub struct Pinned {
pub count: usize,
pub held: Vec<Change>,
}
pub fn pin_current(home: &Home, binary: &Path, cwd: Option<&Path>, hold: bool) -> Result<Pinned> {
pin(home, &set_up_hosts(home, binary), cwd, hold)
}
fn pin(home: &Home, hosts: &[Host], cwd: Option<&Path>, hold_new: bool) -> Result<Pinned> {
let lock = Lock::load(&home.lock_path())?;
let found = review(home, &lock, hosts, cwd)?;
let (held, accepted): (Vec<Change>, Vec<Change>) = found
.changes
.into_iter()
.partition(|change| hold_new && change.blocks());
accept(home, &accepted)?;
let count = Lock::load(&home.lock_path())?
.mcp_servers
.map_or(0, |p| p.len());
Ok(Pinned { count, held })
}
#[cfg(test)]
mod tests;