codebase-graph 1.1.5

Native codebaseGraph CLI and MCP server for local code knowledge graphs.
use super::http::is_local_host;
use super::refresh::McpRefreshState;
use crate::cli::{format::mcp_help, graph::HealthOptions, util::required_arg};
use std::{env, net::TcpListener, path::PathBuf, sync::Arc};

#[derive(Clone, Debug)]
pub(in crate::cli) struct McpServeOptions {
    pub(in crate::cli) repo_root: Option<PathBuf>,
    pub(in crate::cli) config: Option<PathBuf>,
    pub(in crate::cli) db: Option<PathBuf>,
    pub(in crate::cli) manifest: Option<PathBuf>,
    pub(in crate::cli) refresh: Option<Arc<McpRefreshState>>,
}

impl McpServeOptions {
    pub(in crate::cli) fn parse(args: &[String]) -> Result<Self, String> {
        let mut options = Self {
            repo_root: None,
            config: None,
            db: None,
            manifest: None,
            refresh: None,
        };
        let mut index = 0;
        while index < args.len() {
            match args[index].as_str() {
                "--repo-root" => {
                    options.repo_root =
                        Some(PathBuf::from(required_arg(args, index, "--repo-root")?));
                    index += 2;
                }
                "--config" => {
                    options.config = Some(PathBuf::from(required_arg(args, index, "--config")?));
                    index += 2;
                }
                "--db" => {
                    options.db = Some(PathBuf::from(required_arg(args, index, "--db")?));
                    index += 2;
                }
                "--manifest" => {
                    options.manifest =
                        Some(PathBuf::from(required_arg(args, index, "--manifest")?));
                    index += 2;
                }
                other => {
                    return Err(format!(
                        "unknown mcp start option: {other}\n\n{}",
                        mcp_help()
                    ));
                }
            }
        }
        Ok(options)
    }

    pub(in crate::cli) fn health_options(&self) -> HealthOptions {
        HealthOptions {
            repo_root: self.repo_root.clone(),
            config: self.config.clone(),
            db: self.db.clone(),
            manifest: self.manifest.clone(),
            help: false,
            json: false,
        }
    }
}

#[derive(Clone, Debug)]
pub(in crate::cli) struct McpHttpOptions {
    pub(in crate::cli) serve: McpServeOptions,
    pub(in crate::cli) host: String,
    pub(in crate::cli) port: u16,
    pub(in crate::cli) endpoint_path: String,
    pub(in crate::cli) allow_remote: bool,
    pub(in crate::cli) auth_token: Option<String>,
}

impl McpHttpOptions {
    pub(in crate::cli) fn parse(args: &[String]) -> Result<Self, String> {
        let mut options = Self {
            serve: McpServeOptions {
                repo_root: None,
                config: None,
                db: None,
                manifest: None,
                refresh: None,
            },
            host: "127.0.0.1".to_string(),
            port: 8765,
            endpoint_path: "/mcp".to_string(),
            allow_remote: false,
            auth_token: None,
        };
        let mut index = 0;
        while index < args.len() {
            match args[index].as_str() {
                "--repo-root" => {
                    options.serve.repo_root =
                        Some(PathBuf::from(required_arg(args, index, "--repo-root")?));
                    index += 2;
                }
                "--config" => {
                    options.serve.config =
                        Some(PathBuf::from(required_arg(args, index, "--config")?));
                    index += 2;
                }
                "--db" => {
                    options.serve.db = Some(PathBuf::from(required_arg(args, index, "--db")?));
                    index += 2;
                }
                "--manifest" => {
                    options.serve.manifest =
                        Some(PathBuf::from(required_arg(args, index, "--manifest")?));
                    index += 2;
                }
                "--host" => {
                    options.host = required_arg(args, index, "--host")?.to_string();
                    index += 2;
                }
                "--port" => {
                    options.port = required_arg(args, index, "--port")?
                        .parse::<u16>()
                        .map_err(|_| "--port must be between 0 and 65535".to_string())?;
                    index += 2;
                }
                "--path" => {
                    options.endpoint_path = required_arg(args, index, "--path")?.to_string();
                    if !options.endpoint_path.starts_with('/') {
                        return Err("--path must start with /".to_string());
                    }
                    index += 2;
                }
                "--allow-remote" => {
                    options.allow_remote = true;
                    index += 1;
                }
                "--auth-token" => {
                    options.auth_token =
                        Some(required_arg(args, index, "--auth-token")?.to_string());
                    index += 2;
                }
                "--auth-token-env" => {
                    let name = required_arg(args, index, "--auth-token-env")?;
                    let value = env::var(name).map_err(|_| {
                        format!("Environment variable {name:?} must contain the HTTP bearer token")
                    })?;
                    options.auth_token = Some(value);
                    index += 2;
                }
                other => {
                    return Err(format!(
                        "unknown mcp http option: {other}\n\n{}",
                        mcp_help()
                    ));
                }
            }
        }
        options.validate()?;
        Ok(options)
    }

    pub(in crate::cli) fn validate(&self) -> Result<(), String> {
        if self
            .auth_token
            .as_deref()
            .is_some_and(|token| token.trim().is_empty())
        {
            return Err("MCP HTTP auth token must not be blank".to_string());
        }
        if self.allow_remote && self.auth_token.is_none() {
            return Err("MCP HTTP remote bind requires an auth token".to_string());
        }
        if !self.allow_remote && !is_local_host(&self.host) {
            return Err(
                "MCP HTTP transport may only bind to localhost unless allow_remote is enabled"
                    .to_string(),
            );
        }
        Ok(())
    }

    pub(in crate::cli) fn bind_listener(&self) -> Result<TcpListener, String> {
        self.validate()?;
        TcpListener::bind((self.host.as_str(), self.port))
            .map_err(|error| format!("failed to bind MCP HTTP server: {error}"))
    }
}