tsafe-cli 1.0.28

Secrets runtime for developers โ€” inject credentials into processes via exec, never into shell history or .env files
Documentation
//! `tsafe mcp` command handlers โ€” opt-in under the `mcp` feature.
//!
//! Resolves the external `tsafe-mcp` binary at runtime and execs it. Mirrors
//! the resolution path used by `cmd_agent.rs` for `tsafe-agent`: env var,
//! sibling of `tsafe`, then PATH. Per ADR-006 / design ยง5.2, this module
//! does NOT link `tsafe-mcp` as a Cargo dependency.

use std::path::PathBuf;
use std::process::Command;

use anyhow::{Context, Result};

use crate::cli::McpAction;

/// Name of the MCP server executable for this platform.
fn tsafe_mcp_filename() -> &'static str {
    if cfg!(windows) {
        "tsafe-mcp.exe"
    } else {
        "tsafe-mcp"
    }
}

/// Resolve the `tsafe-mcp` binary path: `$TSAFE_MCP_BIN`, then sibling of
/// `tsafe`, then PATH.
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(())
}