use std::net::SocketAddr;
use std::sync::Arc;
use atd_runtime::dispatch::ServerState;
use atd_runtime::registry::Registry;
use atd_runtime::secrets::BearerIdentity;
use axum::Json;
use axum::extract::{DefaultBodyLimit, State};
use axum::http::{HeaderMap, StatusCode};
use axum::response::IntoResponse;
use axum::routing::post;
use axum::{Router, serve};
use serde_json::{Value, json};
use tokio::net::TcpListener;
use tokio::sync::oneshot;
use crate::bearer::{BearerOutcome, resolve_bearer};
use crate::config::HttpServerConfig;
use crate::error::HttpServerError;
use crate::mcp::{
JsonRpcRequest, error_response, error_response_with_headers, handle_initialize,
handle_initialized_notification, handle_tools_call, handle_tools_list,
};
use crate::origin::origin_allowed;
#[derive(Clone)]
pub(crate) struct HttpAppState {
pub state: Arc<ServerState>,
pub extra_origins: Arc<Vec<String>>,
pub require_bearer: bool,
pub server_version: Arc<String>,
}
pub struct ServerBuilder {
registry: Registry,
config: HttpServerConfig,
tier_policy: atd_runtime::TierPolicy,
middleware: Vec<Arc<dyn atd_runtime::Middleware>>,
}
impl ServerBuilder {
pub fn config(mut self, cfg: HttpServerConfig) -> Self {
self.config = cfg;
self
}
pub fn tier_policy(mut self, policy: atd_runtime::TierPolicy) -> Self {
self.tier_policy = policy;
self
}
pub fn middleware(mut self, middleware: Vec<Arc<dyn atd_runtime::Middleware>>) -> Self {
self.middleware = middleware;
self
}
pub fn build(self) -> (Router, Server) {
let HttpServerConfig {
listen,
extra_origins,
require_bearer,
max_body_bytes,
shared,
} = self.config;
let server_version = Arc::new(shared.server_version.clone());
let cursor_signing_key = shared.cursor_signing_key;
let server_state = Arc::new(ServerState {
registry: self.registry,
config: Arc::try_unwrap(shared).unwrap_or_else(|arc| {
atd_runtime::dispatch::SharedServerConfig {
cwd: arc.cwd.clone(),
max_output_bytes: arc.max_output_bytes,
default_call_timeout_ms: arc.default_call_timeout_ms,
granted_capabilities: arc.granted_capabilities.clone(),
audit_sink: arc.audit_sink.clone(),
server_version: arc.server_version.clone(),
token_broker: arc.token_broker.clone(),
max_ucan_chain_depth: arc.max_ucan_chain_depth,
ucan_revocation_store: arc.ucan_revocation_store.clone(),
frame_deadline_active_ms: arc.frame_deadline_active_ms,
frame_deadline_handshake_ms: arc.frame_deadline_handshake_ms,
cursor_signing_key: arc.cursor_signing_key,
cursor_ttl_seconds: arc.cursor_ttl_seconds,
}
}),
tier_policy: self.tier_policy,
middleware: self.middleware,
metrics: Arc::new(atd_runtime::MetricsCounters::default()),
cursor_issuer: Arc::new(atd_runtime::cursor::CursorIssuer::new(cursor_signing_key)),
});
let app_state = HttpAppState {
state: server_state,
extra_origins: Arc::new(extra_origins),
require_bearer,
server_version,
};
let router = Router::new()
.route("/mcp", post(handle_mcp_post))
.layer(DefaultBodyLimit::max(max_body_bytes))
.with_state(app_state);
let server = Server {
listen,
shutdown_tx: None,
local_addr: None,
};
(router, server)
}
}
pub struct Server {
listen: SocketAddr,
shutdown_tx: Option<oneshot::Sender<()>>,
local_addr: Option<SocketAddr>,
}
impl Server {
pub fn builder(registry: Registry) -> ServerBuilder {
ServerBuilder {
registry,
config: HttpServerConfig::default(),
tier_policy: atd_runtime::TierPolicy::defaults(),
middleware: Vec::new(),
}
}
pub fn local_addr(&self) -> Option<SocketAddr> {
self.local_addr
}
pub fn shutdown(&mut self) -> bool {
match self.shutdown_tx.take() {
Some(tx) => tx.send(()).is_ok(),
None => false,
}
}
pub async fn serve(mut self, router: Router) -> Result<(), HttpServerError> {
let listener =
TcpListener::bind(self.listen)
.await
.map_err(|source| HttpServerError::Bind {
addr: self.listen,
source,
})?;
let local = listener
.local_addr()
.map_err(|source| HttpServerError::Bind {
addr: self.listen,
source,
})?;
self.local_addr = Some(local);
let (tx, rx) = oneshot::channel::<()>();
self.shutdown_tx = Some(tx);
serve(listener, router)
.with_graceful_shutdown(async move {
let _ = rx.await;
})
.await?;
Ok(())
}
pub async fn bind(&mut self) -> Result<TcpListener, HttpServerError> {
let listener =
TcpListener::bind(self.listen)
.await
.map_err(|source| HttpServerError::Bind {
addr: self.listen,
source,
})?;
let local = listener
.local_addr()
.map_err(|source| HttpServerError::Bind {
addr: self.listen,
source,
})?;
self.local_addr = Some(local);
Ok(listener)
}
pub async fn serve_with_listener(
mut self,
listener: TcpListener,
router: Router,
) -> Result<(), HttpServerError> {
let (tx, rx) = oneshot::channel::<()>();
self.shutdown_tx = Some(tx);
serve(listener, router)
.with_graceful_shutdown(async move {
let _ = rx.await;
})
.await?;
Ok(())
}
}
async fn handle_mcp_post(
State(app): State<HttpAppState>,
headers: HeaderMap,
body: Result<Json<JsonRpcRequest>, axum::extract::rejection::JsonRejection>,
) -> axum::response::Response {
let Json(req) = match body {
Ok(j) => j,
Err(rej) => {
let (status, code, msg) = body_rejection_status(&rej);
return error_response(status, None, code, msg);
}
};
if !origin_allowed(&headers, &app.extra_origins) {
return error_response(StatusCode::FORBIDDEN, req.id, -32001, "origin not allowed");
}
let outcome = resolve_bearer(
&headers,
app.state.config.token_broker.as_ref(),
app.require_bearer,
)
.await;
let identity: Option<BearerIdentity> = match outcome {
BearerOutcome::Anonymous => None,
BearerOutcome::Validated(id) => Some(id),
rejection => {
let status = rejection.http_status();
let message = rejection
.rejection_message()
.unwrap_or_else(|| "bearer auth failed".into());
let mut headers: Vec<(&str, String)> = Vec::new();
if let Some(www) = rejection.www_authenticate() {
headers.push(("WWW-Authenticate", www.to_string()));
}
if let Some(retry) = rejection.retry_after() {
headers.push(("Retry-After", retry.to_string()));
}
return error_response_with_headers(status, req.id, -32002, message, &headers);
}
};
match req.method.as_str() {
"initialize" => handle_initialize(
req.id,
&app.server_version,
app.state.config.token_broker.as_ref(),
),
"notifications/initialized" => handle_initialized_notification(req.id),
"tools/list" => handle_tools_list(req.id, &app.state),
"tools/call" => handle_tools_call(req.id, &app.state, identity.as_ref(), req.params).await,
other => error_response(
StatusCode::OK,
req.id,
-32601,
format!("method not found: {other}"),
),
}
}
fn body_rejection_status(
rej: &axum::extract::rejection::JsonRejection,
) -> (StatusCode, i32, String) {
use axum::extract::rejection::JsonRejection;
match rej {
JsonRejection::JsonDataError(e) => (StatusCode::BAD_REQUEST, -32600, e.to_string()),
JsonRejection::JsonSyntaxError(e) => (StatusCode::BAD_REQUEST, -32700, e.to_string()),
JsonRejection::MissingJsonContentType(_) => (
StatusCode::UNSUPPORTED_MEDIA_TYPE,
-32600,
"expected application/json".into(),
),
JsonRejection::BytesRejection(_) => (
StatusCode::PAYLOAD_TOO_LARGE,
-32600,
"body too large".into(),
),
other => (
StatusCode::BAD_REQUEST,
-32600,
format!("invalid request body: {other}"),
),
}
}
#[allow(dead_code)]
fn body_too_large_response(id: Option<Value>) -> axum::response::Response {
(
StatusCode::PAYLOAD_TOO_LARGE,
Json(json!({
"jsonrpc": "2.0",
"id": id,
"error": {"code": -32600, "message": "body too large"},
})),
)
.into_response()
}