use std::collections::BTreeSet;
use std::net::SocketAddr;
use std::path::Path;
use std::sync::Arc;
use harn_serve::{
A2aHttpServeOptions, A2aServer, A2aServerConfig, ApiKeyAuthConfig, AuthMethodConfig,
AuthPolicy, DispatchCore, DispatchCoreConfig, ExportCatalog, ExportedCallableKind,
HmacAuthConfig, HttpTlsConfig, McpHttpServeOptions, McpServer, McpServerConfig,
};
use time::Duration;
use crate::cli::{A2aServeArgs, McpServeTransport, ServeAcpArgs, ServeMcpArgs, ServeTlsMode};
pub(crate) async fn run_acp_server(args: &ServeAcpArgs) -> Result<(), String> {
crate::acp::run_acp_server(Some(&args.file)).await;
Ok(())
}
pub(crate) async fn run_a2a_server(args: &A2aServeArgs) -> Result<(), String> {
let mut config = DispatchCoreConfig::for_script(&args.file);
config.auth_policy = build_auth_policy(&args.api_key, args.hmac_secret.as_ref());
let core = DispatchCore::new(config).map_err(|error| error.to_string())?;
let mut server_config = A2aServerConfig::new(core);
server_config.card_signing_secret = args.card_signing_secret.clone();
let server = Arc::new(A2aServer::new(server_config));
server
.run_http(A2aHttpServeOptions {
bind: SocketAddr::from(([0, 0, 0, 0], args.port)),
public_url: args.public_url.clone(),
tls: build_tls_config(args.tls, args.cert.as_ref(), args.key.as_ref())?,
})
.await
}
pub(crate) async fn run_mcp_server(args: &ServeMcpArgs) -> Result<(), String> {
if args.transport == McpServeTransport::Stdio
&& (!args.api_key.is_empty() || args.hmac_secret.is_some())
{
return Err("HTTP auth flags require `harn serve mcp --transport http`".to_string());
}
let catalog = ExportCatalog::from_path(Path::new(&args.file))
.map_err(|error| format!("failed to load script: {error}"))?;
let has_pub_fn_exports = catalog
.functions
.values()
.any(|function| function.kind == ExportedCallableKind::Function);
if !has_pub_fn_exports {
if args.transport != McpServeTransport::Stdio {
return Err(
"scripts using `mcp_tools(...)` are only served over stdio; \
either expose `pub fn` entrypoints or omit `--transport http`"
.to_string(),
);
}
crate::commands::run::run_file_mcp_serve(&args.file, args.card.as_deref()).await;
return Ok(());
}
if args.card.is_some() {
return Err(
"`--card` is only honored for legacy `mcp_tools(...)` scripts; \
attach card metadata directly to your `pub fn` exports instead"
.to_string(),
);
}
let mut config = DispatchCoreConfig::for_script(&args.file);
config.auth_policy = build_auth_policy(&args.api_key, args.hmac_secret.as_ref());
let core = DispatchCore::new(config).map_err(|error| error.to_string())?;
let server = Arc::new(McpServer::new(McpServerConfig::new(core)));
match args.transport {
McpServeTransport::Stdio => server.run_stdio().await,
McpServeTransport::Http => {
server
.run_http(McpHttpServeOptions {
bind: args.bind,
path: args.path.clone(),
sse_path: args.sse_path.clone(),
messages_path: args.messages_path.clone(),
tls: build_tls_config(args.tls, args.cert.as_ref(), args.key.as_ref())?,
})
.await
}
}
}
fn build_tls_config(
mode: ServeTlsMode,
cert: Option<&std::path::PathBuf>,
key: Option<&std::path::PathBuf>,
) -> Result<HttpTlsConfig, String> {
match (mode, cert, key) {
(ServeTlsMode::Plain, None, None) => Ok(HttpTlsConfig::plain()),
(ServeTlsMode::Plain, Some(cert), Some(key))
| (ServeTlsMode::Pem, Some(cert), Some(key)) => {
Ok(HttpTlsConfig::pem_files(cert.clone(), key.clone()))
}
(ServeTlsMode::Pem, None, None) => {
Err("`--tls pem` requires `--cert` and `--key`".to_string())
}
(_, Some(_), None) => Err("`--cert` requires `--key`".to_string()),
(_, None, Some(_)) => Err("`--key` requires `--cert`".to_string()),
(ServeTlsMode::Edge, None, None) => Ok(HttpTlsConfig::edge_terminated()),
(ServeTlsMode::SelfSignedDev, None, None) => Ok(HttpTlsConfig::self_signed_dev()),
(ServeTlsMode::Edge | ServeTlsMode::SelfSignedDev, Some(_), Some(_)) => Err(
"`--cert` and `--key` are only valid with `--tls pem` or default TLS mode".to_string(),
),
}
}
fn build_auth_policy(api_keys: &[String], hmac_secret: Option<&String>) -> AuthPolicy {
let mut methods = Vec::new();
if !api_keys.is_empty() {
methods.push(AuthMethodConfig::ApiKey(ApiKeyAuthConfig {
keys: api_keys.iter().cloned().collect::<BTreeSet<_>>(),
}));
}
if let Some(secret) = hmac_secret {
methods.push(AuthMethodConfig::Hmac(HmacAuthConfig {
shared_secret: secret.clone(),
provider: "harn-serve".to_string(),
timestamp_window: Duration::seconds(300),
}));
}
AuthPolicy { methods }
}