use std::io::Read;
use std::path::PathBuf;
use std::process::ExitCode;
use std::time::Duration;
use super::error::Error;
use crate::serve::cli::Config;
const CONFIG_BYTES: u64 = 1_048_576;
const USAGE: &str =
"usage: haematite mcp-http --config <absolute service.json> [--exit-on-stdin-close]";
pub fn run(args: impl Iterator<Item = String>) -> ExitCode {
match run_inner(args) {
Ok(()) => ExitCode::SUCCESS,
Err(error) => {
eprintln!("haematite mcp-http: {error}");
ExitCode::FAILURE
}
}
}
fn run_inner(mut args: impl Iterator<Item = String>) -> Result<(), Error> {
let mut path = None;
let mut exit_on_stdin_close = false;
while let Some(arg) = args.next() {
match arg.as_str() {
"--config" if path.is_none() => {
path = Some(PathBuf::from(
args.next().ok_or_else(|| Error::config(USAGE))?,
));
}
"--exit-on-stdin-close" if !exit_on_stdin_close => exit_on_stdin_close = true,
_ => return Err(Error::config(USAGE)),
}
}
let path = path
.filter(|path| path.is_absolute())
.ok_or_else(|| Error::config(USAGE))?;
let file = std::fs::File::open(path).map_err(|error| {
Error::config("service configuration could not be opened").with_io(&error)
})?;
let mut bytes = Vec::new();
file.take(CONFIG_BYTES + 1)
.read_to_end(&mut bytes)
.map_err(|error| {
Error::config("service configuration could not be read").with_io(&error)
})?;
if u64::try_from(bytes.len())
.map_err(|_| Error::config("configuration length is not representable"))?
> CONFIG_BYTES
{
return Err(Error::config(
"service configuration exceeds its byte limit",
));
}
let mut config: Config = serde_json::from_slice(&bytes)
.map_err(|_| Error::config("service configuration is not valid"))?;
let http = config
.mcp_http
.take()
.ok_or_else(|| Error::config("mcp_http must be explicitly configured"))?;
let address = http.listen;
let cap = usize::try_from(config.max_frame.get())
.map_err(|_| Error::config("frame cap is not representable"))?;
let socket = resolve_socket(&config)?;
let server = super::listener::Server::new(
http,
socket,
Duration::from_millis(config.timeout_ms.get()),
cap,
config.max_clients.get(),
)?;
let listener = std::net::TcpListener::bind(address)
.map_err(|error| Error::config("HTTP listener could not be bound").with_io(&error))?;
let address = listener
.local_addr()
.map_err(|error| Error::config("bound address could not be read").with_io(&error))?;
let watch = exit_on_stdin_close
.then(crate::serve::stdin_close::Watch::start)
.transpose()
.map_err(|error| {
Error::new(
500,
"mcp_http_stdin_watch_failed",
"stdin reader could not start",
)
.with_io(&error)
})?;
eprintln!("haematite mcp-http: ready on {address}");
if let Some(watch) = watch {
server.until_stdin(listener, watch)?;
} else {
server.serve(listener);
}
Ok(())
}
fn resolve_socket(config: &Config) -> Result<PathBuf, Error> {
config
.endpoint(false)
.map(|endpoint| endpoint.socket)
.map_err(|error| {
Error::new(
500,
error.code,
"service runtime endpoint could not be resolved",
)
})
}
#[cfg(test)]
mod runtime_endpoint_tests {
use crate::serve::cli::endpoint_tests::with_fixture;
#[test]
fn http_configuration_uses_the_services_default_endpoint()
-> Result<(), Box<dyn std::error::Error>> {
with_fixture(|fixture| {
let config = fixture.config()?;
assert_eq!(
super::resolve_socket(&config)?,
config.endpoint(false)?.socket
);
Ok(())
})
}
}