systemprompt-mcp 0.2.4

Native Model Context Protocol (MCP) implementation for systemprompt.io. Orchestration, per-server OAuth2, RBAC middleware, and tool-call governance — the core of the AI governance pipeline.
Documentation
use crate::McpServerConfig;
use anyhow::{Context, Result};
use std::fs;
use std::path::Path;
use std::process::Command;
use systemprompt_models::{AppPaths, ProfileBootstrap, SecretsBootstrap};

const MAX_LOG_SIZE: u64 = 10 * 1024 * 1024;

fn rotate_log_if_needed(log_path: &Path) {
    if let Ok(metadata) = fs::metadata(log_path) {
        if metadata.len() > MAX_LOG_SIZE {
            let backup_path = log_path.with_extension("log.old");
            if let Err(e) = fs::rename(log_path, &backup_path) {
                tracing::warn!(
                    error = %e,
                    log_path = %log_path.display(),
                    backup_path = %backup_path.display(),
                    "Failed to rotate MCP log file"
                );
            }
        }
    }
}

pub fn spawn_server(config: &McpServerConfig) -> Result<u32> {
    let paths = AppPaths::get().map_err(|e| anyhow::anyhow!("{}", e))?;

    let binary_path = paths
        .build()
        .resolve_binary(&config.binary)
        .with_context(|| {
            format!(
                "Failed to find binary '{}' for {}",
                config.binary, config.name
            )
        })?;

    let config_global = systemprompt_models::Config::get()?;

    let log_dir = paths.system().logs();
    fs::create_dir_all(&log_dir)
        .with_context(|| format!("Failed to create logs directory: {}", log_dir.display()))?;

    let log_file_path = log_dir.join(format!("mcp-{}.log", config.name));
    rotate_log_if_needed(&log_file_path);

    let log_file = fs::OpenOptions::new()
        .create(true)
        .append(true)
        .open(&log_file_path)
        .with_context(|| format!("Failed to create log file: {}", log_file_path.display()))?;

    let tools_config_json =
        serde_json::to_string(&config.tools).context("Failed to serialize tools config")?;
    let server_model_config_json = serde_json::to_string(&config.model_config)
        .context("Failed to serialize server model config")?;

    let profile_path = ProfileBootstrap::get_path()
        .context("SYSTEMPROMPT_PROFILE not set - cannot spawn MCP server")?;
    let secrets =
        SecretsBootstrap::get().context("Secrets not available - cannot spawn MCP server")?;

    let mut child_command = Command::new(&binary_path);

    child_command
        .env("SYSTEMPROMPT_PROFILE", profile_path)
        .env("SYSTEMPROMPT_SUBPROCESS", "1")
        .env("JWT_SECRET", &secrets.jwt_secret)
        .env("DATABASE_URL", &secrets.database_url)
        .env("DATABASE_TYPE", &config_global.database_type)
        .env("MCP_SERVICE_ID", &config.name)
        .env("MCP_PORT", config.port.to_string())
        .env("MCP_TOOLS_CONFIG", &tools_config_json)
        .env("MCP_SERVER_MODEL_CONFIG", &server_model_config_json)
        .env("SYSTEM_PATH", paths.system().root());

    if let Some(key) = &secrets.gemini {
        child_command.env("GEMINI_API_KEY", key);
    }
    if let Some(key) = &secrets.anthropic {
        child_command.env("ANTHROPIC_API_KEY", key);
    }
    if let Some(key) = &secrets.openai {
        child_command.env("OPENAI_API_KEY", key);
    }
    if let Some(key) = &secrets.github {
        child_command.env("GITHUB_TOKEN", key);
    }

    if !secrets.custom.is_empty() {
        let uppercase_keys = secrets.custom_env_var_names();
        child_command.env("SYSTEMPROMPT_CUSTOM_SECRETS", uppercase_keys.join(","));
        for (env_name, value) in secrets.custom_env_vars() {
            child_command.env(env_name, value);
        }
    }

    for var_name in &config.env_vars {
        match std::env::var(var_name) {
            Ok(value) => {
                child_command.env(var_name, value);
            },
            Err(_) => {
                tracing::warn!(
                    var = %var_name,
                    service = %config.name,
                    "Optional env var not set for MCP server"
                );
            },
        }
    }

    let child = child_command
        .stdout(std::process::Stdio::null())
        .stderr(std::process::Stdio::from(log_file))
        .stdin(std::process::Stdio::null())
        .spawn()
        .with_context(|| format!("Failed to start detached {}", config.name))?;

    let pid = child.id();

    std::mem::forget(child);

    Ok(pid)
}

pub fn verify_binary(config: &McpServerConfig) -> Result<()> {
    let paths = AppPaths::get().map_err(|e| anyhow::anyhow!("{}", e))?;
    let binary_path = paths.build().resolve_binary(&config.binary)?;

    let metadata = fs::metadata(&binary_path)
        .with_context(|| format!("Binary not found: {}", binary_path.display()))?;

    tracing::debug!(
        service = %config.name,
        binary = %config.binary,
        path = %binary_path.display(),
        size = metadata.len(),
        "Binary verified"
    );
    Ok(())
}

pub fn build_server(config: &McpServerConfig) -> Result<()> {
    tracing::info!(service = %config.name, binary = %config.binary, "Building service (debug mode)");

    let output = Command::new("cargo")
        .args([
            "build",
            "--package",
            &config.binary,
            "--bin",
            &config.binary,
        ])
        .output()
        .with_context(|| {
            format!(
                "Failed to build {} (binary: {})",
                config.name, config.binary
            )
        })?;

    if output.status.success() {
        tracing::info!(service = %config.name, binary = %config.binary, "Build completed (debug)");
        Ok(())
    } else {
        let stderr = String::from_utf8_lossy(&output.stderr);
        tracing::error!(service = %config.name, binary = %config.binary, error = %stderr, "Build failed");
        Err(anyhow::anyhow!(
            "Build failed for {} (binary: {})",
            config.name,
            config.binary
        ))
    }
}