fnox 1.21.0

A flexible secret management tool supporting multiple providers and encryption methods
Documentation
use crate::commands::Cli;
use crate::config::Config;
use crate::error::Result;
use crate::mcp_server::FnoxMcpServer;
use crate::{env, error::FnoxError};
use clap::Args;
use rmcp::service::RunningService;
use rmcp::{RoleServer, ServiceExt};

#[derive(Debug, Args)]
pub struct McpCommand {}

impl McpCommand {
    pub async fn run(&self, cli: &Cli, config: Config) -> Result<()> {
        // MCP server must be non-interactive — provider stdin prompts would
        // corrupt the JSON-RPC stream on stdout.
        env::set_non_interactive(true);

        let profile = Config::get_profile(cli.profile.as_deref());
        let mcp_config = config.mcp.clone().unwrap_or_default();

        // Warn about allowlist entries that don't match any configured secret
        let all_secrets = config.get_secrets(&profile)?;
        if let Some(ref allowlist) = mcp_config.secrets {
            if allowlist.is_empty() {
                tracing::warn!(
                    "mcp.secrets is set to an empty list — no secrets will be available to the MCP server"
                );
            }
            let allowed_set: std::collections::HashSet<&str> =
                allowlist.iter().map(|s| s.as_str()).collect();
            let providers = config.get_providers(&profile);
            for name in allowlist {
                if !all_secrets.contains_key(name) {
                    tracing::warn!(
                        "mcp.secrets allowlist contains '{name}' which is not a configured secret"
                    );
                    continue;
                }
                // Warn if this secret's provider depends on another fnox secret
                // that is not in the allowlist (would cause silent auth failure).
                if let Some(sc) = all_secrets.get(name)
                    && let Some(provider_name) = sc.provider()
                    && let Some(pc) = providers.get(provider_name)
                {
                    for dep in pc.env_dependencies() {
                        if all_secrets.contains_key(*dep) && !allowed_set.contains(*dep) {
                            tracing::warn!(
                                "mcp.secrets: '{name}' uses provider '{provider_name}' which \
                                 depends on '{dep}' — add '{dep}' to mcp.secrets or its provider \
                                 may fail to authenticate"
                            );
                        }
                    }
                }
            }
        }

        // Apply MCP secret allowlist (no-op if not set)
        let profile_secrets = mcp_config.filter_secrets(all_secrets);

        if mcp_config.exec_timeout_secs == Some(0) {
            return Err(FnoxError::Config(
                "mcp.exec_timeout_secs must be >= 1; use a large value to effectively disable the timeout".into(),
            ));
        }

        let server = FnoxMcpServer::new(config, profile, mcp_config, profile_secrets);

        let service: RunningService<RoleServer, FnoxMcpServer> = server
            .serve(rmcp::transport::io::stdio())
            .await
            .map_err(|e| FnoxError::Config(format!("Failed to start MCP server: {e}")))?;

        service
            .waiting()
            .await
            .map_err(|e| FnoxError::Config(format!("MCP server error: {e}")))?;

        Ok(())
    }
}