use crate::backend::SpawnSpec;
use anyhow::{Result, bail};
use onlyne_acp::{EnvVariable, McpServer};
use onlyne_config::layout::RoleWorkspace;
use std::path::{Path, PathBuf};
const TOOLS_NAME: &str = "onlyne";
const TOOLS_ARGS: [&str; 1] = ["mcp"];
pub(super) fn mount(spec: &SpawnSpec) -> Result<McpServer> {
if spec.tools_token.is_empty() {
bail!(
"acp: task {} was spawned without a tools token; the client mints one for every \
session it opens, and without it `onlyne mcp` could not speak for this one \
(`SpawnSpec.tools_token`)",
spec.task_id
);
}
let command = entrypoint(spec)?;
let socket = socket_of(spec);
Ok(McpServer::Stdio {
name: TOOLS_NAME.to_string(),
command: command.to_string_lossy().into_owned(),
args: TOOLS_ARGS.iter().map(|arg| arg.to_string()).collect(),
env: vec![
EnvVariable {
name: "ONLYNE_SOCKET".to_string(),
value: socket.to_string_lossy().into_owned(),
},
EnvVariable {
name: "ONLYNE_MCP_TOKEN".to_string(),
value: spec.tools_token.clone(),
},
],
})
}
fn socket_of(spec: &SpawnSpec) -> PathBuf {
match spec.env.get("ONLYNE_SOCKET") {
Some(served) if !served.is_empty() => PathBuf::from(served),
_ => RoleWorkspace::resolve(&spec.cwd).socket_path(),
}
}
fn entrypoint(spec: &SpawnSpec) -> Result<PathBuf> {
if let Some(found) = beside_exe() {
return Ok(found);
}
if let Some(found) = on_path(spec) {
return Ok(found);
}
bail!(
"{} — an ACP session is handed `onlyne mcp` as its tools mount, so the CLI has to sit \
beside this client's binary or on the PATH the agent runs with",
onlyne_proto::binary_not_found(TOOLS_NAME)
)
}
fn beside_exe() -> Option<PathBuf> {
let exe = std::env::current_exe().ok()?;
let dir = exe.parent()?;
candidates_in(dir, TOOLS_NAME)
.into_iter()
.find(|candidate| is_executable_file(candidate))
}
fn on_path(spec: &SpawnSpec) -> Option<PathBuf> {
let path = match spec.env.get("PATH") {
Some(path) => path.clone(),
None => std::env::var("PATH").ok()?,
};
for dir in std::env::split_paths(&path) {
if dir.as_os_str().is_empty() || !dir.is_dir() {
continue;
}
if let Some(found) = candidates_in(&dir, TOOLS_NAME)
.into_iter()
.find(|candidate| is_executable_file(candidate))
{
return Some(found);
}
}
None
}
fn is_executable_file(path: &Path) -> bool {
let Ok(meta) = path.metadata() else {
return false;
};
if !meta.is_file() {
return false;
}
#[cfg(unix)]
{
use std::os::unix::fs::MetadataExt;
meta.mode() & 0o111 != 0
}
#[cfg(not(unix))]
{
true
}
}
fn candidates_in(dir: &Path, name: &str) -> Vec<PathBuf> {
let mut names = vec![dir.join(name), dir.join(format!("{name}.exe"))];
if cfg!(windows) {
names.push(dir.join(format!("{name}.cmd")));
names.push(dir.join(format!("{name}.bat")));
}
names
}