use std::net::SocketAddr;
use crate::serve::ServeOptions;
#[derive(Debug, Clone, Default, PartialEq, Eq, clap::Args)]
pub struct ServeArgs {
#[arg(long, value_name = "ADDR")]
pub bind: Option<SocketAddr>,
#[arg(long, conflicts_with_all = ["bind", "allowed_hosts", "allowed_origins"])]
pub stdio: bool,
#[arg(long = "allowed-host", value_name = "HOST")]
pub allowed_hosts: Vec<String>,
#[arg(long = "allowed-origin", value_name = "ORIGIN")]
pub allowed_origins: Vec<String>,
}
impl ServeArgs {
pub fn bind_or(&self, default_bind: SocketAddr) -> SocketAddr {
self.bind.unwrap_or(default_bind)
}
pub fn serve_options(&self, default_bind: SocketAddr) -> ServeOptions {
ServeOptions::new(self.bind_or(default_bind))
.with_allowed_hosts(self.allowed_hosts.iter().cloned())
.with_allowed_origins(self.allowed_origins.iter().cloned())
}
}
pub fn parse_switch(value: &str) -> Result<bool, String> {
match value.trim().to_ascii_lowercase().as_str() {
"" | "false" | "0" | "no" | "off" | "n" | "f" => Ok(false),
"true" | "1" | "yes" | "on" | "y" | "t" => Ok(true),
other => Err(format!("expected true/false, 1/0 or yes/no, got {other:?}")),
}
}
#[cfg(test)]
mod tests {
use super::*;
use clap::Parser;
#[derive(Parser, Debug)]
struct Cli {
#[command(flatten)]
serve: ServeArgs,
}
fn parse(args: &[&str]) -> Result<ServeArgs, clap::Error> {
Cli::try_parse_from(std::iter::once("t").chain(args.iter().copied())).map(|c| c.serve)
}
#[test]
fn defaults_to_the_servers_own_bind() {
let a = parse(&[]).unwrap();
assert_eq!(a, ServeArgs::default());
let o = a.serve_options("127.0.0.1:9000".parse().unwrap());
assert_eq!(o.bind, "127.0.0.1:9000".parse().unwrap());
assert!(o.allowed_hosts.is_empty() && o.allowed_origins.is_empty());
}
#[test]
fn repeatable_hosts_and_origins() {
let a = parse(&[
"--bind",
"0.0.0.0:8100",
"--allowed-host",
"localhost",
"--allowed-host",
"mcp.example.com",
"--allowed-origin",
"https://app.example.com",
])
.unwrap();
let o = a.serve_options("127.0.0.1:9000".parse().unwrap());
assert_eq!(o.bind, "0.0.0.0:8100".parse().unwrap());
assert_eq!(o.allowed_hosts, ["localhost", "mcp.example.com"]);
assert_eq!(o.allowed_origins, ["https://app.example.com"]);
}
#[test]
fn stdio_excludes_http_flags() {
assert!(parse(&["--stdio"]).unwrap().stdio);
assert!(parse(&["--stdio", "--bind", "127.0.0.1:1"]).is_err());
assert!(parse(&["--stdio", "--allowed-host", "x"]).is_err());
assert!(parse(&["--bind", "not-an-address"]).is_err());
}
#[test]
fn switch_values() {
for off in ["", " ", "false", "0", "No", "OFF", "n", "f"] {
assert_eq!(parse_switch(off), Ok(false), "{off:?}");
}
for on in ["true", "1", "YES", " on ", "y", "T"] {
assert_eq!(parse_switch(on), Ok(true), "{on:?}");
}
assert!(parse_switch("maybe").is_err());
assert!(parse_switch("2").is_err());
}
}