ownserp 2.0.0

OwnSERP, an MCP server for the Serper.dev search API, over stdio and Streamable HTTP
use anyhow::Context;
use ownserp_core::{Settings, serve_http, serve_stdio};

use crate::cli::ServeArgs;

pub(crate) fn run(args: ServeArgs) -> anyhow::Result<()> {
    let bind_given = args.bind.is_some();
    let settings = Settings::load(args.into_overrides()).context("resolving settings")?;
    let http = settings.http();
    if bind_is_ignored(bind_given, http) {
        tracing::warn!(
            "--bind is ignored because the transport is stdio; pass --http to serve Streamable HTTP"
        );
    }
    let runtime = if http {
        tokio::runtime::Builder::new_multi_thread()
            .enable_all()
            .build()
            .context("building the multithread runtime")?
    } else {
        tokio::runtime::Builder::new_current_thread()
            .enable_all()
            .build()
            .context("building the current-thread runtime")?
    };
    block_on_and_stop(runtime, async move {
        if http {
            serve_http(settings).await?;
        } else {
            serve_stdio(settings).await?;
        }
        Ok(())
    })
}

fn block_on_and_stop<F>(runtime: tokio::runtime::Runtime, future: F) -> anyhow::Result<()>
where
    F: std::future::Future<Output = anyhow::Result<()>>,
{
    let outcome = runtime.block_on(future);
    runtime.shutdown_timeout(std::time::Duration::from_millis(500));
    outcome
}

const fn bind_is_ignored(bind_given: bool, http: bool) -> bool {
    bind_given && !http
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn a_blocking_input_read_does_not_hold_the_runtime_open() {
        let runtime = tokio::runtime::Builder::new_current_thread()
            .enable_all()
            .build()
            .expect("runtime");
        let _blocking =
            runtime.spawn_blocking(|| std::thread::sleep(std::time::Duration::from_secs(5)));
        let started = std::time::Instant::now();
        block_on_and_stop(runtime, async { Ok(()) }).expect("shutdown");
        assert!(started.elapsed() < std::time::Duration::from_secs(2));
    }

    #[test]
    fn the_bind_flag_is_reported_unused_only_for_stdio() {
        assert!(bind_is_ignored(true, false));
        assert!(!bind_is_ignored(true, true));
        assert!(!bind_is_ignored(false, false));
    }
}