use core::num::NonZeroUsize;
use std::sync::Arc;
use serde_json::Value;
use crate::adapters::primary::transport::http::config::Streaming;
use crate::adapters::primary::transport::http::documents::{
answer_of, id_as_i64, method_of, params_of, refusal_document, refusal_document_from_refusal,
result_document,
};
use crate::adapters::primary::transport::http::exchange::{ExchangeTable, HttpExchange};
use crate::adapters::primary::transport::http::refusal::Refusal;
use crate::adapters::primary::transport::http::router::{
EventSource, Inbound, RequestHandler, SessionHandle, Unsessioned,
};
use crate::adapters::primary::transport::http::session_store::SessionStore;
use crate::adapters::primary::transport::http::stateless::{StatelessHeaders, StatelessRequest};
use crate::adapters::primary::transport::http::stream::{EventTail, Finishing};
use crate::application::server::context::{PeerSession, RequestContext};
use crate::application::server::dispatch::Dispatcher;
use crate::application::server::{ListKind, ListSubscription};
use crate::application::session::IdSource;
use crate::domain::feature::resolve;
use crate::domain::ir::JsonObject;
use crate::domain::jsonrpc::{Frame, WireId, decode};
use crate::domain::mcp::McpSessionId;
use crate::domain::session::{CancellationToken, ProgressToken, SessionId};
use crate::domain::{
CapabilitySet, EventId, Negotiation, ProtocolVersion, RequestId, TransportKind,
};
mod builder;
mod check;
mod sink;
#[cfg(test)]
mod tests;
use check::{check_header, declares_own_version};
pub(super) use sink::TailSink;
pub use builder::{HttpBuilder, ServeError, ServeReport};
#[cfg(test)]
pub(super) use builder::test_routes;
pub(super) type Tail = EventTail<Value>;
type Ids = Arc<dyn IdSource + Send + Sync>;
pub(super) const KEEP_ALIVE: core::time::Duration = core::time::Duration::ZERO;
pub(super) const TAIL_CAPACITY: NonZeroUsize = match NonZeroUsize::new(64) {
Some(capacity) => capacity,
None => NonZeroUsize::MIN,
};
#[derive(Debug)]
pub struct HttpSession {
peer: PeerSession,
tail: Tail,
exchange: ExchangeTable,
_lists: Option<ListSubscription>,
}
impl EventSource for HttpSession {
fn tail(&self) -> &Tail {
&self.tail
}
}
impl HttpSession {
fn peer_of(session: &Arc<Self>) -> PeerSession {
session.peer.clone()
}
pub fn peer(&self) -> PeerSession {
self.peer.clone()
}
}
pub struct HttpHandler {
pub(super) dispatcher: Arc<Dispatcher>,
pub(super) sessions: Arc<SessionStore<HttpSession>>,
ceiling: ProtocolVersion,
}
impl core::fmt::Debug for HttpHandler {
fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
formatter
.debug_struct("HttpHandler")
.field("ceiling", &self.ceiling)
.finish_non_exhaustive()
}
}
impl HttpHandler {
#[must_use]
pub const fn new(
dispatcher: Arc<Dispatcher>,
sessions: Arc<SessionStore<HttpSession>>,
ceiling: ProtocolVersion,
) -> Self {
Self {
dispatcher,
sessions,
ceiling,
}
}
#[must_use]
pub const fn ceiling(&self) -> ProtocolVersion {
self.ceiling
}
fn peer_for(&self, stored: Option<&Arc<HttpSession>>) -> PeerSession {
stored.map_or_else(|| self.seed().peer, HttpSession::peer_of)
}
fn seed(&self) -> HttpSession {
Self::session_for(self.ceiling, CapabilitySet::empty())
}
pub(super) fn session_for(version: ProtocolVersion, client_caps: CapabilitySet) -> HttpSession {
let profile = resolve(Negotiation {
version,
client_caps,
server_caps: CapabilitySet::all_for(version),
transport: TransportKind::StreamableHttp,
});
let tail = Tail::new(TAIL_CAPACITY);
let exchange = ExchangeTable::new();
let sink = Arc::new(TailSink { tail: tail.clone() });
HttpSession {
peer: PeerSession::new(
version,
profile,
SessionId::new(1),
TransportKind::StreamableHttp,
Arc::new(HttpExchange::new(exchange.clone(), tail.clone())),
sink,
),
tail,
exchange,
_lists: None,
}
}
fn respond_beside_stream(
&self,
request_peer: &PeerSession,
session: &Arc<HttpSession>,
id: Option<&WireId<'static>>,
progress: Option<ProgressToken>,
method: &str,
params: &JsonObject,
) -> Result<axum::response::Response, Refusal> {
let tail = session.tail.clone();
let since = session.tail.last().to_string();
let finished = Finishing::new();
let response = tail
.plan(Some(since.as_str()))
.into_finishable_response(finished.clone(), KEEP_ALIVE)
.map_err(|error| Refusal::Handler(error.to_string()))?;
let dispatcher = Arc::clone(&self.dispatcher);
let owned_peer = request_peer.clone();
let owned_id = id.map(WireId::to_owned);
let owned_method = method.to_owned();
let owned_params = params.clone();
let tail_for_result = tail;
let finished_for_task = finished;
let task = tokio::spawn(async move {
let peer = owned_peer;
let context = RequestContext::for_request_with_state(
&peer,
RequestId::from_wire(owned_id.as_ref()),
CancellationToken::new(),
progress,
dispatcher.state().clone(),
);
let document = match dispatcher
.dispatch(&context, &owned_method, &owned_params)
.await
{
Ok(result) => match result_document(owned_id.as_ref(), &result) {
Ok(document) => document,
Err(refusal) => refusal_document_from_refusal(owned_id.as_ref(), &refusal),
},
Err(error) => refusal_document(owned_id.as_ref(), &error),
};
tail_for_result.push(document);
finished_for_task.fire();
});
let _task = task;
Ok(response)
}
fn acknowledge_answer(
body: &[u8],
session: Option<&Arc<HttpSession>>,
id: Option<&WireId<'static>>,
) -> Result<axum::response::Response, Refusal> {
if let (Some(session), Some(id)) = (session, id)
&& let Some(numeric) = id_as_i64(id)
{
session.exchange.resolve(numeric, answer_of(body));
}
axum::response::Response::builder()
.status(axum::http::StatusCode::ACCEPTED)
.body(axum::body::Body::empty())
.map_err(|_| Refusal::Handler("the acknowledgement did not build".to_owned()))
}
}
impl RequestHandler<HttpSession> for HttpHandler {
fn unsessioned(&self, body: &[u8], headers: &axum::http::HeaderMap) -> Unsessioned {
if declares_own_version(body) || StatelessHeaders::names_stateless_revision(headers) {
return Unsessioned::Stateless;
}
match method_of(body) {
Some(method) if method == "initialize" => Unsessioned::Establishing,
_ => Unsessioned::NeedsSession,
}
}
fn dispatch(
&self,
call: Inbound<'_, HttpSession>,
) -> impl core::future::Future<Output = Result<axum::response::Response, Refusal>> + Send {
let body = call.body.to_vec();
let protocol = call.protocol;
let streaming = call.streaming;
let opening = matches!(call.session, SessionHandle::Establishing);
let stateless = matches!(call.session, SessionHandle::Stateless);
let stored = match &call.session {
SessionHandle::Establishing | SessionHandle::Stateless => None,
SessionHandle::Live { session, .. } => Some(Arc::clone(session)),
};
let auth = call.context.clone();
let peer = self.peer_for(stored.as_ref()).with_auth(auth.clone());
let stateless_headers = stateless.then(|| StatelessHeaders::read(call.headers));
async move {
let decoded = decode(&body).map_err(|error| Refusal::MalformedBody {
reason: error.to_string(),
})?;
let Frame::Single(envelope) = decoded else {
return Err(Refusal::MalformedBody {
reason: "an HTTP request body carries one message".to_owned(),
});
};
let cursor: Option<EventId> = stored.as_ref().map(|session| session.tail.last());
let id: Option<WireId<'static>> = envelope.id().map(WireId::to_owned);
if method_of(&body).is_none() {
return Self::acknowledge_answer(&body, stored.as_ref(), id.as_ref());
}
let params = params_of(&envelope)?;
let method = envelope.method_str().unwrap_or_default();
if let Some(headers) = stateless_headers {
return self
.serve_stateless(StatelessRequest {
id,
method: method.to_owned(),
params,
headers,
auth,
streaming,
})
.await;
}
let progress: Option<ProgressToken> =
crate::application::mcp::request_meta::progress_token(¶ms);
let peer_for_now = peer;
let context = RequestContext::for_request_with_state(
&peer_for_now,
RequestId::from_wire(id.as_ref()),
CancellationToken::new(),
progress.clone(),
Arc::clone(self.dispatcher.state()),
);
let (document, negotiated) = if method == "initialize" {
match self.dispatcher.handshake(&context, ¶ms) {
Ok((result, peer)) => (result_document(id.as_ref(), &result)?, Some(peer)),
Err(error) => (refusal_document(id.as_ref(), &error), None),
}
} else if let Some(session) = stored.as_ref() {
if streaming == Streaming::EventStream {
return self.respond_beside_stream(
&peer_for_now,
session,
id.as_ref(),
progress,
method,
¶ms,
);
}
match self.dispatcher.dispatch(&context, method, ¶ms).await {
Ok(result) => (result_document(id.as_ref(), &result)?, None),
Err(error) => (refusal_document(id.as_ref(), &error), None),
}
} else {
match self.dispatcher.dispatch(&context, method, ¶ms).await {
Ok(result) => (result_document(id.as_ref(), &result)?, None),
Err(error) => (refusal_document(id.as_ref(), &error), None),
}
};
if id.is_none() {
return Self::response(&Value::Null, None, None);
}
if let Some(agreed) = negotiated.as_ref().map(PeerSession::negotiated) {
check_header(protocol, agreed)?;
}
if opening {
return self.open_session(negotiated, stored.as_deref(), streaming, &document);
}
Self::response(
&document,
None,
if streaming == Streaming::EventStream {
stored.map(|session| (session.tail.clone(), cursor))
} else {
None
},
)
}
}
}
impl HttpHandler {
fn open_session(
&self,
negotiated: Option<PeerSession>,
stored: Option<&HttpSession>,
streaming: Streaming,
document: &Value,
) -> Result<axum::response::Response, Refusal> {
let peer = negotiated
.ok_or_else(|| Refusal::Handler("a session was opened without a handshake".to_owned()))?
.without_auth();
let readable = streaming == Streaming::EventStream;
let (tail, exchange) = stored.map_or_else(
|| {
let exchange = if readable {
ExchangeTable::readable()
} else {
ExchangeTable::new()
};
(Tail::new(TAIL_CAPACITY), exchange)
},
|session| (session.tail.clone(), session.exchange.clone()),
);
let sink: Arc<dyn crate::application::session::DynFrameSink> =
Arc::new(TailSink { tail: tail.clone() });
let lists = self.dispatcher.server().handle().subscribe(
&sink,
peer.profile(),
&ListKind::ALL,
None,
);
let peer = peer
.with_sink(sink)
.with_exchange(Arc::new(HttpExchange::new(exchange.clone(), tail.clone())));
let id_source = self.sessions.next_id();
let peer = peer.with_session(id_source);
let minted = self
.sessions
.open_with_id(
Arc::new(HttpSession {
peer,
tail,
exchange,
_lists: Some(lists),
}),
id_source,
)
.map_err(|error| Refusal::Handler(format!("no session: {error}")))?;
Self::response(document, Some(&minted), None)
}
fn response(
document: &Value,
id: Option<&McpSessionId>,
tail: Option<(Tail, Option<EventId>)>,
) -> Result<axum::response::Response, Refusal> {
if let Some((tail, cursor)) = tail {
tail.push(document.clone());
let since = cursor.map(|id| id.to_string());
return match tail
.plan(since.as_deref())
.into_draining_response(KEEP_ALIVE)
{
Ok(response) => {
let mut response = response;
if let Some(id) = id {
let value = axum::http::HeaderValue::from_str(&id.to_string()).map_err(
|error| Refusal::Handler(format!("the id was not a header: {error}")),
)?;
response.headers_mut().insert("mcp-session-id", value);
}
Ok(response)
}
Err(error) => Err(Refusal::Handler(error.to_string())),
};
}
let encoded = serde_json::to_vec(document)
.map_err(|error| Refusal::Handler(format!("the reply did not serialise: {error}")))?;
let mut response = axum::response::Response::new(axum::body::Body::from(encoded));
if let Some(id) = id {
let value = axum::http::HeaderValue::from_str(&id.to_string())
.map_err(|error| Refusal::Handler(format!("the id was not a header: {error}")))?;
response.headers_mut().insert("mcp-session-id", value);
}
Ok(response)
}
}