use std::path::PathBuf;
use std::process::Command;
use anyhow::{Context, Result};
use crate::cli::McpAction;
fn tsafe_mcp_filename() -> &'static str {
if cfg!(windows) {
"tsafe-mcp.exe"
} else {
"tsafe-mcp"
}
}
fn resolve_tsafe_mcp_binary() -> Result<PathBuf> {
let name = tsafe_mcp_filename();
if let Ok(path) = std::env::var("TSAFE_MCP_BIN") {
let candidate = PathBuf::from(path);
if candidate.is_file() {
return Ok(candidate);
}
}
if let Ok(exe) = std::env::current_exe() {
if let Some(dir) = exe.parent() {
let candidate = dir.join(name);
if candidate.is_file() {
return Ok(candidate);
}
}
}
if let Some(hit) = find_on_path(name) {
return Ok(PathBuf::from(hit));
}
let tsafe = std::env::current_exe()
.map(|p| p.display().to_string())
.unwrap_or_else(|_| "(could not read tsafe path)".to_string());
anyhow::bail!(
"tsafe-mcp binary not found.\n\
\n\
Looked at TSAFE_MCP_BIN, next to this tsafe ({tsafe}), and on your PATH for `{name}`.\n\
\n\
Install tsafe-mcp via the release channel or companion-binary path that ships it for your stack.\n\
That can be a bundle that places tsafe-mcp next to tsafe or a separate companion install that adds it to PATH.\n\
Development builds: `cargo build -p tsafe-mcp` places tsafe-mcp in the same\n\
target/*/debug (or release) directory as tsafe."
);
}
fn find_on_path(filename: &str) -> Option<String> {
std::env::var_os("PATH").and_then(|path_var| {
std::env::split_paths(&path_var).find_map(|dir| {
let candidate = dir.join(filename);
if candidate.is_file() {
candidate.to_str().map(|s| s.to_string())
} else {
None
}
})
})
}
pub(crate) fn cmd_mcp(profile: &str, action: McpAction) -> Result<()> {
let bin = resolve_tsafe_mcp_binary()?;
let status = match action {
McpAction::Serve {
allowed_keys,
denied_keys,
contract,
allow_reveal,
audit_source,
} => {
let mut cmd = Command::new(&bin);
cmd.arg("serve");
cmd.args(["--profile", profile]);
if let Some(k) = allowed_keys {
cmd.args(["--allowed-keys", &k]);
}
if let Some(k) = denied_keys {
cmd.args(["--denied-keys", &k]);
}
if let Some(c) = contract {
cmd.args(["--contract", &c]);
}
if allow_reveal {
cmd.arg("--allow-reveal");
}
if let Some(src) = audit_source {
cmd.args(["--audit-source", &src]);
}
cmd.status()
.with_context(|| format!("failed to start {}", bin.display()))?
}
McpAction::Install {
host,
name,
global,
project,
dry_run,
allowed_keys,
denied_keys,
contract,
allow_reveal,
} => {
let mut cmd = Command::new(&bin);
cmd.args(["install", &host, "--profile", profile]);
if let Some(n) = name {
cmd.args(["--name", &n]);
}
if global {
cmd.arg("--global");
}
if let Some(proj) = project {
cmd.args(["--project", &proj]);
}
if dry_run {
cmd.arg("--dry-run");
}
if let Some(k) = allowed_keys {
cmd.args(["--allowed-keys", &k]);
}
if let Some(k) = denied_keys {
cmd.args(["--denied-keys", &k]);
}
if let Some(c) = contract {
cmd.args(["--contract", &c]);
}
if allow_reveal {
cmd.arg("--allow-reveal");
}
cmd.status()
.with_context(|| format!("failed to start {}", bin.display()))?
}
McpAction::Uninstall { host, name } => {
let mut cmd = Command::new(&bin);
cmd.args(["uninstall", &host, "--profile", profile]);
if let Some(n) = name {
cmd.args(["--name", &n]);
}
cmd.status()
.with_context(|| format!("failed to start {}", bin.display()))?
}
McpAction::Status => Command::new(&bin)
.arg("status")
.status()
.with_context(|| format!("failed to start {}", bin.display()))?,
};
let code = status.code().unwrap_or(1);
if code != 0 {
std::process::exit(code);
}
Ok(())
}